@orkastery/cli 0.2.0 → 0.4.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.
Files changed (257) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -65
  3. package/adapters/README.md +30 -15
  4. package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
  5. package/adapters/claude-code/README.md +110 -8
  6. package/adapters/claude-code/commands/check.md +4 -3
  7. package/adapters/claude-code/commands/goal.md +3 -2
  8. package/adapters/claude-code/commands/master.md +3 -2
  9. package/adapters/claude-code/commands/onboarding.md +21 -0
  10. package/adapters/claude-code/commands/ork.md +99 -27
  11. package/adapters/claude-code/hooks/hooks.json +6 -1
  12. package/adapters/claude-code/hooks/ork-guard.js +1 -1
  13. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  14. package/adapters/codex/skills/ork/SKILL.md +57 -0
  15. package/adapters/hermes/README.md +161 -0
  16. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  17. package/adapters/hermes/bin/ork-brain.sh +4 -0
  18. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  19. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  20. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  21. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  22. package/adapters/hermes/hermes.plugin.json +24 -4
  23. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  24. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  25. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  26. package/adapters/openclaw/README.md +136 -27
  27. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  28. package/adapters/openclaw/construir.sh +19 -0
  29. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  30. package/adapters/openclaw/dist/index.js +437 -0
  31. package/adapters/openclaw/openclaw.plugin.json +39 -134
  32. package/adapters/openclaw/package.json +26 -0
  33. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  34. package/adapters/openclaw/src/index.ts +460 -0
  35. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  36. package/adapters/openclaw/tsconfig.json +15 -0
  37. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  38. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  39. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  40. package/assets/docs/produto/README.md +14 -0
  41. package/assets/docs/produto/_modelo-feature.md +59 -0
  42. package/assets/docs/roadmap/README.md +14 -0
  43. package/assets/docs/roadmap/_modelo-item.md +73 -0
  44. package/assets/orkmind-native-schema.json +32 -0
  45. package/assets/orkmind_bridge.py +357 -0
  46. package/assets/orkmind_fixture.py +67 -0
  47. package/assets/orkmind_prospective.py +138 -0
  48. package/assets/reference-tariffs-i07.json +23 -0
  49. package/dist/adapters/claude-bg.js +488 -39
  50. package/dist/adapters/codex-controller-sensor.js +449 -0
  51. package/dist/adapters/codex-controller-worker.js +361 -0
  52. package/dist/adapters/codex-controller.js +252 -0
  53. package/dist/adapters/codex-events.js +205 -0
  54. package/dist/adapters/codex-question.js +26 -0
  55. package/dist/adapters/codex-runner.js +133 -0
  56. package/dist/adapters/codex.js +394 -0
  57. package/dist/agents-md.js +84 -0
  58. package/dist/auditoria.js +12 -9
  59. package/dist/auditrun.js +5 -4
  60. package/dist/board.js +174 -23
  61. package/dist/branch-de-estado.js +136 -0
  62. package/dist/canarios-hitl.js +177 -0
  63. package/dist/canarios-i43.js +543 -0
  64. package/dist/canarios-pulse.js +147 -0
  65. package/dist/canarios-sensores.js +129 -0
  66. package/dist/canarios.js +111 -2
  67. package/dist/catalogo.js +9 -0
  68. package/dist/ci.js +223 -0
  69. package/dist/ciclos.js +2 -1
  70. package/dist/claim-lint.js +64 -0
  71. package/dist/claims.js +60 -0
  72. package/dist/company-brain-capture.js +195 -0
  73. package/dist/company-brain-cli.js +123 -0
  74. package/dist/company-brain-client.js +61 -0
  75. package/dist/company-brain-contract.js +166 -0
  76. package/dist/company-brain-journal.js +192 -0
  77. package/dist/company-brain-mcp.js +33 -0
  78. package/dist/company-brain-migration.js +60 -0
  79. package/dist/company-brain-source.js +177 -0
  80. package/dist/company-brain-worker.js +18 -0
  81. package/dist/conducao-texto.js +61 -0
  82. package/dist/conducao.js +876 -0
  83. package/dist/contrato-publico.js +39 -0
  84. package/dist/creation-operation-store.js +251 -0
  85. package/dist/creation-operation.js +148 -0
  86. package/dist/decisao-autonoma.js +183 -0
  87. package/dist/delegation.js +79 -0
  88. package/dist/demo.js +120 -0
  89. package/dist/docs.js +828 -0
  90. package/dist/doctor.js +184 -25
  91. package/dist/entrega-pr.js +117 -0
  92. package/dist/escopo-escrita.js +71 -0
  93. package/dist/estado-thread.js +222 -0
  94. package/dist/evalrunner.js +12 -0
  95. package/dist/fabrica-estado.js +329 -0
  96. package/dist/fabrica-publicar.js +76 -0
  97. package/dist/fix.js +22 -1
  98. package/dist/gates.js +59 -11
  99. package/dist/handoff.js +39 -23
  100. package/dist/hitl-canais.js +333 -0
  101. package/dist/hitl-classificacao.js +131 -0
  102. package/dist/hitl-contract.js +465 -0
  103. package/dist/hitl-estado.js +136 -0
  104. package/dist/hitl-gates.js +554 -0
  105. package/dist/hitl-ingress-receipt.js +382 -0
  106. package/dist/hitl-local-atestado.js +57 -0
  107. package/dist/hitl-local-receipt.js +328 -0
  108. package/dist/hitl-local.js +143 -0
  109. package/dist/hitl-lock.js +139 -0
  110. package/dist/hitl-lote.js +223 -0
  111. package/dist/hitl-native-offer.js +97 -0
  112. package/dist/hitl-native.js +65 -0
  113. package/dist/hitl-presentation.js +209 -0
  114. package/dist/hitl-public-receipt.js +176 -0
  115. package/dist/hitl-resumo.js +209 -0
  116. package/dist/hitl-sessions.js +475 -0
  117. package/dist/hitl.js +711 -0
  118. package/dist/horario.js +269 -0
  119. package/dist/hosts.js +198 -22
  120. package/dist/index.js +1741 -94
  121. package/dist/indice.js +99 -0
  122. package/dist/init.js +28 -3
  123. package/dist/integracoes-locais.js +17 -0
  124. package/dist/leases.js +65 -21
  125. package/dist/ledger-stats.js +272 -0
  126. package/dist/ledger.js +109 -2
  127. package/dist/licoes.js +221 -0
  128. package/dist/liveness.js +218 -0
  129. package/dist/maestro-actions.js +51 -0
  130. package/dist/maestro-authority.js +152 -0
  131. package/dist/maestro-cli.js +97 -0
  132. package/dist/maestro-contract.js +55 -0
  133. package/dist/maestro-discovery.js +132 -0
  134. package/dist/maestro-runtime.js +268 -0
  135. package/dist/maestro-snapshot.js +86 -0
  136. package/dist/maestro-sources.js +224 -0
  137. package/dist/manifest.js +165 -6
  138. package/dist/maquina.js +102 -0
  139. package/dist/master-audit.js +101 -0
  140. package/dist/master-batch.js +43 -0
  141. package/dist/master-digest.js +149 -0
  142. package/dist/master-migracao.js +155 -0
  143. package/dist/master.js +361 -77
  144. package/dist/mcp-artifacts.js +234 -0
  145. package/dist/mcp-git.js +433 -0
  146. package/dist/mcp-install.js +310 -0
  147. package/dist/mcp-maestro.js +25 -0
  148. package/dist/mcp-server.js +469 -0
  149. package/dist/mcp-ship.js +436 -0
  150. package/dist/mcp-verify.js +173 -0
  151. package/dist/memoria-humana.js +207 -0
  152. package/dist/memoria.js +314 -78
  153. package/dist/memory-migration.js +279 -0
  154. package/dist/memory-prospective.js +148 -0
  155. package/dist/modos-migracao.js +191 -0
  156. package/dist/modos.js +151 -15
  157. package/dist/monitor-lock.js +110 -0
  158. package/dist/objective.js +449 -0
  159. package/dist/ocupacao.js +267 -0
  160. package/dist/onboarding.js +303 -0
  161. package/dist/orkmind.js +389 -90
  162. package/dist/orquestracao.js +96 -69
  163. package/dist/phase.js +612 -151
  164. package/dist/playbook-capabilities.js +150 -0
  165. package/dist/playbook-contracts.js +188 -0
  166. package/dist/policies.js +72 -0
  167. package/dist/portfolio-context.js +61 -0
  168. package/dist/portfolio.js +180 -0
  169. package/dist/preflight.js +207 -0
  170. package/dist/process-audit.js +112 -0
  171. package/dist/project-state.js +93 -0
  172. package/dist/prompts.js +25 -5
  173. package/dist/prova-minima.js +112 -0
  174. package/dist/pulse-cadencia.js +163 -0
  175. package/dist/pulse-consentimento.js +260 -0
  176. package/dist/pulse-delivery.js +323 -0
  177. package/dist/pulse-resposta.js +542 -0
  178. package/dist/pulse.js +262 -0
  179. package/dist/ratelimit.js +35 -2
  180. package/dist/recall.js +80 -3
  181. package/dist/redacao-saida.js +45 -0
  182. package/dist/redacao-url.js +66 -0
  183. package/dist/retry.js +640 -118
  184. package/dist/roadmap-reservas.js +243 -0
  185. package/dist/runtime-ambiente.js +38 -0
  186. package/dist/runtime-context.js +215 -0
  187. package/dist/runtime-profiles.js +869 -0
  188. package/dist/runtimes.js +103 -0
  189. package/dist/sandbox.js +9 -0
  190. package/dist/session-events.js +203 -0
  191. package/dist/session-watcher-claude.js +637 -0
  192. package/dist/session-watcher.js +700 -0
  193. package/dist/sessoes-adopt.js +154 -0
  194. package/dist/sessoes-inventario.js +159 -0
  195. package/dist/sessoes.js +16 -35
  196. package/dist/setup.js +691 -0
  197. package/dist/ship.js +168 -19
  198. package/dist/slug.js +1 -1
  199. package/dist/thread-close.js +115 -0
  200. package/dist/thread.js +142 -28
  201. package/dist/tokens.js +1 -1
  202. package/dist/util.js +4 -2
  203. package/dist/verify-sandbox.js +265 -0
  204. package/dist/verify.js +270 -25
  205. package/dist/versao.js +60 -0
  206. package/dist/worktree.js +31 -2
  207. package/dist/write-activation.js +291 -0
  208. package/dist/yaml.js +5 -1
  209. package/eval/casos/master-metrics.json +1 -1
  210. package/eval/casos/onboarding.json +102 -0
  211. package/eval/casos/orkastery-bootstrap.json +37 -3
  212. package/eval/casos/scope-check-capability-map.json +45 -7
  213. package/eval/casos/ship-release.json +3 -3
  214. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  215. package/eval/fixtures/b2-master-log/caso.json +1 -1
  216. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  217. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  218. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  219. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  220. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  221. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  222. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  223. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  224. package/eval/fixtures/fx-happy/caso.json +6 -4
  225. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  226. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  227. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  228. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  229. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  230. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  231. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  232. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  233. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  234. package/monitor/company-brain.cjs +17 -0
  235. package/monitor/pulse-scope.cjs +45 -0
  236. package/monitor/pulse.cron +18 -0
  237. package/monitor/varredura-pulse.sh +27 -0
  238. package/package.json +28 -7
  239. package/references/definition-of-done.md +2 -2
  240. package/schemas/claims.schema.json +25 -0
  241. package/schemas/company-brain.schema.json +1158 -0
  242. package/schemas/creation-operation.schema.json +365 -0
  243. package/schemas/maestro-snapshot.schema.json +2716 -0
  244. package/skills/README.md +3 -3
  245. package/skills/core/onboarding/SKILL.md +50 -0
  246. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  247. package/skills/core/thread-state/SKILL.md +3 -1
  248. package/skills/governance/decision-triage/SKILL.md +4 -4
  249. package/skills/governance/narrative-guardian/SKILL.md +1 -1
  250. package/skills/governance/roadmap-keeper/SKILL.md +1 -1
  251. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
  252. package/skills/observability/thread-tracing/SKILL.md +1 -1
  253. package/skills/phases/check-quality/SKILL.md +2 -2
  254. package/skills/phases/goal-definition/SKILL.md +1 -1
  255. package/skills/phases/master-metrics/SKILL.md +9 -7
  256. package/skills/phases/plan-specification/SKILL.md +1 -1
  257. package/skills/phases/ship-release/SKILL.md +1 -1
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.gravarEvidenciaLocal = gravarEvidenciaLocal;
37
+ exports.gravarEvidenciaLocalSessao = gravarEvidenciaLocalSessao;
38
+ exports.validarEvidenciaLocalSessao = validarEvidenciaLocalSessao;
39
+ exports.validarEvidenciaLocal = validarEvidenciaLocal;
40
+ /** Recibo durável do callback MCP local, autenticado por chave fora das threads. */
41
+ const node_crypto_1 = require("node:crypto");
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const estado_thread_1 = require("./estado-thread");
45
+ const thread_1 = require("./thread");
46
+ const ledger_1 = require("./ledger");
47
+ const hitl_contract_1 = require("./hitl-contract");
48
+ const hitl_local_atestado_1 = require("./hitl-local-atestado");
49
+ const sha = (v) => (0, node_crypto_1.createHash)('sha256').update(v).digest('hex');
50
+ const uid = () => {
51
+ const atual = process.getuid?.();
52
+ if (atual === undefined)
53
+ throw new Error('hitl.local.evidencia-invalida');
54
+ return atual;
55
+ };
56
+ function validarDiretorio(file, modo) {
57
+ const st = fs.lstatSync(file);
58
+ if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== uid() || (st.mode & 0o777) !== modo ||
59
+ fs.realpathSync(file) !== file)
60
+ throw new Error('hitl.local.evidencia-invalida');
61
+ return st;
62
+ }
63
+ function diretorioPrivado(file) {
64
+ try {
65
+ fs.mkdirSync(file, { mode: 0o700 });
66
+ }
67
+ catch (e) {
68
+ if (e.code !== 'EEXIST')
69
+ throw e;
70
+ }
71
+ return validarDiretorio(file, 0o700);
72
+ }
73
+ function chaveLocal(raiz, criar) {
74
+ const estado = path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery');
75
+ const stEstado = fs.lstatSync(estado);
76
+ if (!stEstado.isDirectory() || stEstado.isSymbolicLink() || fs.realpathSync(estado) !== estado) {
77
+ throw new Error('hitl.local.chave-invalida');
78
+ }
79
+ const pasta = path.join(estado, 'private');
80
+ const pastaSt = criar ? diretorioPrivado(pasta) : validarDiretorio(pasta, 0o700);
81
+ const dfd = fs.openSync(pasta, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
82
+ const file = `/proc/self/fd/${dfd}/hitl-local-signing.key`;
83
+ try {
84
+ if (criar) {
85
+ const temp = `/proc/self/fd/${dfd}/.${(0, node_crypto_1.randomUUID)()}.tmp`;
86
+ try {
87
+ const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT |
88
+ fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
89
+ try {
90
+ fs.writeFileSync(fd, (0, node_crypto_1.randomBytes)(32).toString('hex') + '\n');
91
+ fs.fsyncSync(fd);
92
+ }
93
+ finally {
94
+ fs.closeSync(fd);
95
+ }
96
+ try {
97
+ fs.linkSync(temp, file);
98
+ }
99
+ catch (e) {
100
+ if (e.code !== 'EEXIST')
101
+ throw e;
102
+ }
103
+ finally {
104
+ fs.unlinkSync(temp);
105
+ }
106
+ fs.fsyncSync(dfd);
107
+ }
108
+ catch (e) {
109
+ if (e.code !== 'EEXIST')
110
+ throw e;
111
+ }
112
+ }
113
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
114
+ try {
115
+ const st = fs.fstatSync(fd), atual = fs.lstatSync(pasta);
116
+ if (!st.isFile() || st.nlink !== 1 || st.uid !== uid() || (st.mode & 0o777) !== 0o600 ||
117
+ atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino)
118
+ throw new Error('hitl.local.chave-invalida');
119
+ const raw = fs.readFileSync(fd, 'utf8');
120
+ if (!/^[a-f0-9]{64}\n$/.test(raw))
121
+ throw new Error('hitl.local.chave-invalida');
122
+ return Buffer.from(raw.trim(), 'hex');
123
+ }
124
+ finally {
125
+ fs.closeSync(fd);
126
+ }
127
+ }
128
+ finally {
129
+ fs.closeSync(dfd);
130
+ }
131
+ }
132
+ const assinatura = (chave, dados) => (0, node_crypto_1.createHmac)('sha256', chave).update(JSON.stringify(dados)).digest('hex');
133
+ function referencia(thread, nome) {
134
+ return `.orkastery/threads/${thread}/hitl-ingress-local/${nome}`;
135
+ }
136
+ function lerRecibo(raiz, thread, nome) {
137
+ const pasta = path.join((0, thread_1.dirThread)(raiz, thread), 'hitl-ingress-local');
138
+ const pastaSt = validarDiretorio(pasta, 0o700);
139
+ const dfd = fs.openSync(pasta, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
140
+ try {
141
+ const fd = fs.openSync(`/proc/self/fd/${dfd}/${nome}`, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
142
+ try {
143
+ const st = fs.fstatSync(fd), atual = fs.lstatSync(pasta);
144
+ if (!st.isFile() || st.nlink !== 1 || st.uid !== uid() || (st.mode & 0o777) !== 0o600 ||
145
+ atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino)
146
+ throw new Error('hitl.local.evidencia-invalida');
147
+ const bytes = fs.readFileSync(fd);
148
+ const recibo = JSON.parse(bytes.toString('utf8'));
149
+ if (!recibo.dados || !/^[a-f0-9]{64}$/.test(recibo.assinatura ?? '') ||
150
+ Object.keys(recibo).sort().join(',') !== 'assinatura,dados')
151
+ throw new Error('hitl.local.evidencia-invalida');
152
+ return { bytes, dados: recibo.dados, assinatura: recibo.assinatura };
153
+ }
154
+ finally {
155
+ fs.closeSync(fd);
156
+ }
157
+ }
158
+ finally {
159
+ fs.closeSync(dfd);
160
+ }
161
+ }
162
+ /** O nome do recibo e determinado pela conexao e pela solicitacao, nunca pelo conteudo. */
163
+ const nomeDoRecibo = (a) => sha(JSON.stringify([a.connectionId, a.requestId])) + '.json';
164
+ /**
165
+ * Escrita atomica e conferida do recibo assinado. Extraida porque gate e sessao gravam a
166
+ * mesma coisa em lugares diferentes do contrato: duplicar aqui seria duplicar o cofre.
167
+ */
168
+ function gravarRecibo(raiz, thread, nome, dados) {
169
+ const chave = chaveLocal(raiz, true);
170
+ const pasta = path.join((0, thread_1.dirThread)(raiz, thread), 'hitl-ingress-local');
171
+ const pastaSt = diretorioPrivado(pasta);
172
+ const sig = assinatura(chave, dados), bytes = Buffer.from(JSON.stringify({ dados, assinatura: sig }) + '\n');
173
+ const dfd = fs.openSync(pasta, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
174
+ try {
175
+ const file = `/proc/self/fd/${dfd}/${nome}`;
176
+ const temp = `/proc/self/fd/${dfd}/.${(0, node_crypto_1.randomUUID)()}.tmp`;
177
+ try {
178
+ const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT |
179
+ fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
180
+ try {
181
+ fs.writeFileSync(fd, bytes);
182
+ fs.fsyncSync(fd);
183
+ }
184
+ finally {
185
+ fs.closeSync(fd);
186
+ }
187
+ try {
188
+ fs.linkSync(temp, file);
189
+ }
190
+ catch (e) {
191
+ if (e.code !== 'EEXIST')
192
+ throw e;
193
+ }
194
+ finally {
195
+ fs.unlinkSync(temp);
196
+ }
197
+ fs.fsyncSync(dfd);
198
+ }
199
+ catch (e) {
200
+ if (e.code !== 'EEXIST')
201
+ throw e;
202
+ }
203
+ const atual = fs.lstatSync(pasta);
204
+ if (atual.dev !== pastaSt.dev || atual.ino !== pastaSt.ino)
205
+ throw new Error('hitl.local.evidencia-invalida');
206
+ }
207
+ finally {
208
+ fs.closeSync(dfd);
209
+ }
210
+ const lido = lerRecibo(raiz, thread, nome);
211
+ if (!lido.bytes.equals(bytes))
212
+ throw new Error('hitl.local.evidencia-invalida');
213
+ return { arquivo: referencia(thread, nome), sha256: sha(bytes), recibo: sha(sig) };
214
+ }
215
+ /** Persiste a decisão normalizada que já entra no ledger; nunca o texto bruto respondido. */
216
+ function gravarEvidenciaLocal(atestado, veredito) {
217
+ const dados = { contrato: 'ork.hitl-local-ingress/v1', thread: atestado.thread,
218
+ pedidoId: atestado.pedidoId, contexto: atestado.contexto, pedidoSha256: atestado.pedidoSha256,
219
+ host: atestado.host, connectionId: atestado.connectionId, requestId: atestado.requestId,
220
+ recebidoEm: atestado.recebidoEm, ...veredito };
221
+ return gravarRecibo(atestado.raiz, atestado.thread, nomeDoRecibo(atestado), dados);
222
+ }
223
+ function gravarEvidenciaLocalSessao(atestado, entrega) {
224
+ const dados = { contrato: 'ork.hitl-local-session/v1', thread: atestado.thread,
225
+ pedidoId: atestado.pedidoId, contexto: atestado.contexto, pedidoSha256: atestado.pedidoSha256,
226
+ host: atestado.host, connectionId: atestado.connectionId, requestId: atestado.requestId,
227
+ recebidoEm: atestado.recebidoEm, ...entrega, estado: 'entregue', respostaSha256: sha(atestado.opcao),
228
+ reciboEntrega: (0, hitl_local_atestado_1.reciboDoMaterial)(atestado.connectionId, atestado.requestId, atestado.pedidoSha256, sha(atestado.opcao)) };
229
+ return gravarRecibo(atestado.raiz, atestado.thread, nomeDoRecibo(atestado), dados);
230
+ }
231
+ /** Revalida no sync o recibo de uma resposta de sessao entregue pelo canal MCP local. */
232
+ function validarEvidenciaLocalSessao(raiz, thread, evento) {
233
+ try {
234
+ if (evento.tipo !== 'session_answered' || evento.origem !== 'mcp-local' ||
235
+ !['mcp-local:codex', 'mcp-local:claude-code'].includes(String(evento.autorizadoPor)) ||
236
+ typeof evento.pedidoId !== 'string' || typeof evento.contexto !== 'string' ||
237
+ typeof evento.pedidoSha256 !== 'string' || typeof evento.conexao !== 'string' ||
238
+ typeof evento.solicitacao !== 'string' || typeof evento.recebidoEm !== 'string' ||
239
+ typeof evento.envioId !== 'string' || typeof evento.evidencia !== 'string' ||
240
+ !/^[a-f0-9]{64}$/.test(String(evento.evidenciaSha256 ?? '')))
241
+ return false;
242
+ const host = String(evento.autorizadoPor).slice('mcp-local:'.length);
243
+ // `canal` e `contratoResposta` sao campos publicos do evento: se nao forem conferidos
244
+ // contra o host que assinou, da para adulterar o canal do recibo sem reprovar nada.
245
+ if (evento.canal !== host || evento.contratoResposta !== 'ork.hitl-local-session/v1')
246
+ return false;
247
+ const nome = sha(JSON.stringify([evento.conexao, evento.solicitacao])) + '.json';
248
+ if (evento.evidencia !== referencia(thread, nome))
249
+ return false;
250
+ const lido = lerRecibo(raiz, thread, nome);
251
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread));
252
+ const solicitados = eventos.filter(e => e.tipo === 'hitl_requested' && e.pedido?.id === evento.pedidoId);
253
+ const respostas = eventos.filter(e => e.tipo === 'session_answered' && e.pedidoId === evento.pedidoId);
254
+ if (solicitados.length !== 1 || respostas.length !== 1 || JSON.stringify(respostas[0]) !== JSON.stringify(evento))
255
+ return false;
256
+ const pedido = solicitados[0].pedido;
257
+ (0, hitl_contract_1.validarPedidoHitl)(pedido);
258
+ if (pedido.thread !== thread || pedido.alvo.tipo !== 'session' ||
259
+ sha(JSON.stringify(pedido)) !== evento.pedidoSha256 ||
260
+ pedido.alvo.sessionId !== evento.sessionId || pedido.alvo.runtime !== evento.runtime)
261
+ return false;
262
+ const esperado = { contrato: 'ork.hitl-local-session/v1', thread,
263
+ pedidoId: evento.pedidoId, contexto: evento.contexto, pedidoSha256: evento.pedidoSha256,
264
+ host, connectionId: evento.conexao, requestId: evento.solicitacao, recebidoEm: evento.recebidoEm,
265
+ fase: String(evento.fase), sessionId: String(evento.sessionId), runtime: String(evento.runtime),
266
+ instancia: String(evento.instancia), bloqueio: String(evento.bloqueio), envioId: evento.envioId,
267
+ estado: 'entregue', respostaSha256: String(lido.dados.respostaSha256),
268
+ reciboEntrega: (0, hitl_local_atestado_1.reciboDoMaterial)(String(evento.conexao), String(evento.solicitacao), String(evento.pedidoSha256), String(lido.dados.respostaSha256)) };
269
+ // O recibo confirmado pelo receptor tem de derivar do MESMO hash de resposta: sem isso
270
+ // o recibo provaria so que alguem respondeu daquela conexao, nunca o que ele recebeu.
271
+ if (evento.reciboEntrega !== esperado.reciboEntrega)
272
+ return false;
273
+ const sig = assinatura(chaveLocal(raiz, false), esperado);
274
+ return JSON.stringify(lido.dados) === JSON.stringify(esperado) &&
275
+ (0, node_crypto_1.timingSafeEqual)(Buffer.from(lido.assinatura, 'hex'), Buffer.from(sig, 'hex')) &&
276
+ evento.evidenciaSha256 === sha(lido.bytes) && evento.recibo === sha(lido.assinatura);
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
282
+ /** Revalida no sync a prova criptográfica e o vínculo exato entre recibo e ledger. */
283
+ function validarEvidenciaLocal(raiz, thread, evento) {
284
+ try {
285
+ if (evento.origem !== 'mcp-local' || evento.proveniencia !== 'ork.mcp-elicitation/v1' ||
286
+ !['mcp-local:codex', 'mcp-local:claude-code'].includes(String(evento.autorizadoPor)) ||
287
+ typeof evento.pedidoId !== 'string' || typeof evento.contexto !== 'string' ||
288
+ typeof evento.pedidoSha256 !== 'string' || typeof evento.conexao !== 'string' ||
289
+ typeof evento.solicitacao !== 'string' || typeof evento.recebidoEm !== 'string' ||
290
+ !Number.isInteger(evento.opcao) || typeof evento.evidencia !== 'string' ||
291
+ !/^[a-f0-9]{64}$/.test(String(evento.evidenciaSha256 ?? '')) ||
292
+ !/^[a-f0-9]{64}$/.test(String(evento.recibo ?? '')))
293
+ return false;
294
+ const host = String(evento.autorizadoPor).slice('mcp-local:'.length);
295
+ if (evento.canal !== host || evento.contratoResposta !== 'ork.mcp-elicitation/v1')
296
+ return false;
297
+ const nome = sha(JSON.stringify([evento.conexao, evento.solicitacao])) + '.json';
298
+ if (evento.evidencia !== referencia(thread, nome))
299
+ return false;
300
+ const lido = lerRecibo(raiz, thread, nome);
301
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread));
302
+ const solicitados = eventos.filter(e => e.tipo === 'hitl_requested' && e.pedido?.id === evento.pedidoId);
303
+ const gates = eventos.filter(e => e.tipo === 'human_gate' && e.pedidoId === evento.pedidoId);
304
+ if (solicitados.length !== 1 || gates.length !== 1 || JSON.stringify(gates[0]) !== JSON.stringify(evento) || Object.hasOwn(evento, 'observacao'))
305
+ return false;
306
+ const pedido = solicitados[0].pedido;
307
+ (0, hitl_contract_1.validarPedidoHitl)(pedido);
308
+ const escolhas = (0, hitl_contract_1.escolhasDoPedido)(pedido);
309
+ if (pedido.thread !== thread || (0, hitl_contract_1.alvoDoPedido)(pedido)?.tipo !== 'gate' || sha(JSON.stringify(pedido)) !== evento.pedidoSha256 ||
310
+ escolhas.every(o => o.numero !== evento.opcao))
311
+ return false;
312
+ const opcao = escolhas.find(o => o.numero === evento.opcao);
313
+ const veredito = (0, hitl_contract_1.vereditoDoGate)(pedido, opcao);
314
+ if (evento.fase !== veredito.fase || evento.sobre !== veredito.sobre || evento.estado !== veredito.estado)
315
+ return false;
316
+ const esperado = { contrato: 'ork.hitl-local-ingress/v1', thread,
317
+ pedidoId: evento.pedidoId, contexto: evento.contexto, pedidoSha256: evento.pedidoSha256,
318
+ host, connectionId: evento.conexao, requestId: evento.solicitacao,
319
+ recebidoEm: evento.recebidoEm, ...veredito };
320
+ const sig = assinatura(chaveLocal(raiz, false), esperado);
321
+ return JSON.stringify(lido.dados) === JSON.stringify(esperado) &&
322
+ (0, node_crypto_1.timingSafeEqual)(Buffer.from(lido.assinatura, 'hex'), Buffer.from(sig, 'hex')) &&
323
+ evento.evidenciaSha256 === sha(lido.bytes) && evento.recibo === sha(lido.assinatura);
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reciboDoAtestado = exports.hashPedidoLocal = exports.consumirAtestadoLocal = void 0;
4
+ exports.criarIngressoLocal = criarIngressoLocal;
5
+ /** Ingresso do cliente MCP confiável. Não é uma API de resposta do modelo.
6
+ * A instalação/perfil do cliente pertence à fronteira de confiança: não autentica
7
+ * uma pessoa contra comprometimento do próprio cliente ou da conta local.
8
+ */
9
+ const node_crypto_1 = require("node:crypto");
10
+ const hitl_contract_1 = require("./hitl-contract");
11
+ const hitl_gates_1 = require("./hitl-gates");
12
+ const hitl_sessions_1 = require("./hitl-sessions");
13
+ const ledger_1 = require("./ledger");
14
+ const hitl_canais_1 = require("./hitl-canais");
15
+ const hitl_lock_1 = require("./hitl-lock");
16
+ const thread_1 = require("./thread");
17
+ const hitl_local_atestado_1 = require("./hitl-local-atestado");
18
+ var hitl_local_atestado_2 = require("./hitl-local-atestado");
19
+ Object.defineProperty(exports, "consumirAtestadoLocal", { enumerable: true, get: function () { return hitl_local_atestado_2.consumirAtestadoLocal; } });
20
+ Object.defineProperty(exports, "hashPedidoLocal", { enumerable: true, get: function () { return hitl_local_atestado_2.hashPedidoLocal; } });
21
+ Object.defineProperty(exports, "reciboDoAtestado", { enumerable: true, get: function () { return hitl_local_atestado_2.reciboDoAtestado; } });
22
+ const idValido = (s) => typeof s === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(s);
23
+ /** O callback é fornecido pelo servidor, nunca pelos argumentos de tools/call. */
24
+ function criarIngressoLocal(raiz, origem, elicitar,
25
+ /** Vinculo do adapter para resposta de SESSAO, fornecido pelo servidor. E o mesmo ponto
26
+ * de injecao que `responderSessao` ja tem; ausente, vale o controle nativo do runtime. */
27
+ opcoes = {}) {
28
+ if (!['claude-code', 'codex'].includes(origem.host) || !idValido(origem.connectionId))
29
+ throw new Error('hitl.local.origem-invalida');
30
+ const conexao = { ...origem }, ativos = new Map();
31
+ let fechada = false;
32
+ return {
33
+ fechar() { fechada = true; for (const c of ativos.values())
34
+ c.abort(); },
35
+ async solicitar(threadId, pedidoId, signal) {
36
+ if (fechada)
37
+ throw new Error('hitl.local.conexao-fechada');
38
+ if (!idValido(threadId) || !idValido(pedidoId))
39
+ throw new Error('hitl.local.identificador-invalido');
40
+ if (signal?.aborted)
41
+ return { ok: false, pedidoId, estado: 'pendente', repetida: false };
42
+ const key = `${threadId}:${pedidoId}`;
43
+ if (ativos.has(key))
44
+ throw new Error('hitl.local.solicitacao-em-curso');
45
+ (0, thread_1.lerThread)(raiz, threadId);
46
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId));
47
+ const evento = eventos.find(e => e.tipo === 'hitl_requested' && e.pedido?.id === pedidoId);
48
+ (0, hitl_contract_1.validarPedidoHitl)(evento?.pedido);
49
+ const pedido = evento.pedido;
50
+ if (pedido.thread !== threadId)
51
+ throw new Error('hitl.local.pedido-de-outra-thread');
52
+ // D12: gate e sessão são os dois alvos do contrato HITL, e os dois atendem aqui.
53
+ const alvo = pedido.alvo.tipo;
54
+ const respondido = alvo === 'gate'
55
+ ? eventos.find(e => e.tipo === 'human_gate' && e.pedidoId === pedidoId)
56
+ : eventos.find(e => e.tipo === 'session_answered' && e.pedidoId === pedidoId);
57
+ if (respondido) {
58
+ return alvo === 'gate'
59
+ ? { ok: true, pedidoId, estado: String(respondido.estado), repetida: true }
60
+ : { ok: true, pedidoId, sessionId: String(respondido.sessionId), estado: 'entregue', repetida: true };
61
+ }
62
+ const contexto = (0, hitl_gates_1.contextoHitl)(raiz, threadId);
63
+ if (evento.contexto !== contexto)
64
+ throw new Error('hitl.local.pedido-antigo');
65
+ if ((0, hitl_contract_1.estadoDoPedido)(pedido) !== 'aberto')
66
+ throw new Error('hitl.local.pedido-expirado');
67
+ const pedidoSha256 = (0, hitl_local_atestado_1.hashPedidoLocal)(pedido), requestId = (0, node_crypto_1.randomUUID)(), controller = new AbortController();
68
+ let liberarReserva;
69
+ (0, hitl_gates_1.comLockHitl)(raiz, threadId, () => {
70
+ const current = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId));
71
+ if (current.some(e => e.tipo === 'session_answer_sending' && e.pedidoId === pedidoId))
72
+ throw Error('hitl.channel.delivery-uncertain');
73
+ const selected = (0, hitl_canais_1.selecaoDeCanal)(current, pedidoId);
74
+ if (selected) {
75
+ if (selected.transporte !== 'mcp-local' || selected.reserva !== 'ork.hitl-dialogue/v1' ||
76
+ typeof selected.requestId !== 'string' || typeof selected.connectionId !== 'string')
77
+ throw Error('hitl.channel.in-use: seleção sem prova de abandono');
78
+ const release = (0, hitl_lock_1.reservarDialogoHitl)((0, thread_1.dirThread)(raiz, threadId), selected.requestId, selected.connectionId, true);
79
+ try {
80
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, threadId), threadId, 'hitl_channel_released', { pedidoId, requestId: selected.requestId,
81
+ reason: 'abandoned-dialogue', evidence: 'kernel-exclusive-lock', connectionId: conexao.connectionId });
82
+ }
83
+ finally {
84
+ release();
85
+ }
86
+ }
87
+ liberarReserva = (0, hitl_lock_1.reservarDialogoHitl)((0, thread_1.dirThread)(raiz, threadId), requestId, conexao.connectionId);
88
+ try {
89
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, threadId), threadId, 'hitl_channel_selected', { pedidoId,
90
+ canal: conexao.host, transporte: 'mcp-local', connectionId: conexao.connectionId, requestId, contexto, pedidoSha256,
91
+ reserva: 'ork.hitl-dialogue/v1' });
92
+ }
93
+ catch (e) {
94
+ liberarReserva();
95
+ throw e;
96
+ }
97
+ });
98
+ ativos.set(key, controller);
99
+ const abortar = () => controller.abort();
100
+ signal?.addEventListener('abort', abortar, { once: true });
101
+ const cancelar = new Promise(resolve => controller.signal.addEventListener('abort', () => resolve({ action: 'cancel' }), { once: true }));
102
+ const timer = setTimeout(() => controller.abort(), Math.min(300000, Date.parse(pedido.prazo) - Date.now()));
103
+ try {
104
+ // Copia isolada: o cliente não altera o snapshot usado na conferência final.
105
+ const resposta = await Promise.race([elicitar(JSON.parse(JSON.stringify(pedido)), controller.signal), cancelar]);
106
+ if (fechada || controller.signal.aborted || resposta?.action === 'cancel' || resposta?.action === 'decline') {
107
+ return { ok: false, pedidoId, estado: 'pendente', repetida: false };
108
+ }
109
+ if (resposta?.action !== 'accept' || !resposta.content ||
110
+ Object.keys(resposta.content).length !== 1 || typeof resposta.content.opcao !== 'string' ||
111
+ resposta.content.opcao.length > pedido.respostaAceita.maxCaracteres) {
112
+ throw new Error('hitl.local.resposta-invalida');
113
+ }
114
+ const token = Object.freeze({});
115
+ (0, hitl_local_atestado_1.registrarAtestadoLocal)(token, { raiz, thread: threadId, pedidoId, contexto, pedidoSha256, ...conexao,
116
+ requestId, opcao: resposta.content.opcao, recebidoEm: new Date().toISOString(), alvo });
117
+ try {
118
+ return alvo === 'gate'
119
+ ? (0, hitl_gates_1.responderGateLocal)(raiz, threadId, pedidoId, token)
120
+ : (0, hitl_sessions_1.responderSessaoLocal)(raiz, threadId, pedidoId, token, opcoes.controle);
121
+ }
122
+ finally {
123
+ (0, hitl_local_atestado_1.descartarAtestadoLocal)(token);
124
+ }
125
+ }
126
+ finally {
127
+ clearTimeout(timer);
128
+ signal?.removeEventListener('abort', abortar);
129
+ ativos.delete(key);
130
+ try {
131
+ (0, hitl_gates_1.comLockHitl)(raiz, threadId, () => {
132
+ const current = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, threadId)), selected = (0, hitl_canais_1.selecaoDeCanal)(current, pedidoId);
133
+ if (selected?.requestId === requestId && !current.some(e => ['human_gate', 'session_answer_sending', 'session_answered'].includes(e.tipo) && e.pedidoId === pedidoId))
134
+ (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, threadId), threadId, 'hitl_channel_released', { pedidoId, requestId, reason: 'dialogue-ended-without-effect' });
135
+ });
136
+ }
137
+ finally {
138
+ liberarReserva?.();
139
+ }
140
+ }
141
+ }
142
+ };
143
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.reservarDialogoHitl = reservarDialogoHitl;
37
+ exports.comLockInspecionavel = comLockInspecionavel;
38
+ /** Linux flock(2): o descritor do dono mantém a exclusão, inclusive após o helper sair. */
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const node_crypto_1 = require("node:crypto");
43
+ /** Reserva por apresentação. Só o kernel prova abandono; PID e prazo não liberam. */
44
+ function reservarDialogoHitl(dir, requestId, connectionId, recuperar = false) {
45
+ if (process.platform !== 'linux' || !/^[a-f0-9-]{36}$/.test(requestId))
46
+ throw Error('hitl.channel.recovery-unproven');
47
+ const file = path.join(dir, `.hitl-dialogue-${requestId}.lock`);
48
+ const flags = fs.constants.O_RDWR | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK |
49
+ (recuperar ? 0 : fs.constants.O_CREAT | fs.constants.O_EXCL);
50
+ let fd;
51
+ try {
52
+ fd = fs.openSync(file, flags, 0o600);
53
+ }
54
+ catch {
55
+ throw Error('hitl.channel.recovery-unproven');
56
+ }
57
+ try {
58
+ const st = fs.fstatSync(fd);
59
+ if (!st.isFile() || st.uid !== process.getuid?.() || (st.mode & 0o777) !== 0o600 || st.nlink !== 1 || st.size > 1024)
60
+ throw Error('hitl.channel.recovery-unproven');
61
+ const r = (0, node_child_process_1.spawnSync)('/usr/bin/flock', ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'pipe', 'pipe', fd], timeout: 2000 });
62
+ if (r.status !== 0 || r.error)
63
+ throw Error('hitl.channel.in-use: apresentação ainda reservada pelo kernel');
64
+ const current = fs.lstatSync(file);
65
+ if (current.ino !== st.ino || current.dev !== st.dev || current.isSymbolicLink())
66
+ throw Error('hitl.channel.recovery-unproven');
67
+ const metadata = JSON.stringify({ contrato: 'ork.hitl-dialogue/v1', requestId, connectionId });
68
+ if (recuperar) {
69
+ if (fs.readFileSync(fd, 'utf8') !== metadata)
70
+ throw Error('hitl.channel.recovery-unproven');
71
+ }
72
+ else {
73
+ fs.writeFileSync(fd, metadata);
74
+ fs.fsyncSync(fd);
75
+ }
76
+ return () => fs.closeSync(fd); // o inode permanece; nunca libera uma reserva por unlink
77
+ }
78
+ catch (e) {
79
+ fs.closeSync(fd);
80
+ throw e;
81
+ }
82
+ }
83
+ function comLockInspecionavel(dir, operacao, executar) {
84
+ if (process.platform !== 'linux')
85
+ throw new Error('runtime.unavailable: lock HITL exige flock Linux');
86
+ // Mesmo inode/nome do protocolo legado: writer wx antigo também recusa enquanto existir.
87
+ const file = path.join(dir, '.hitl.lock');
88
+ let fd, criado = false;
89
+ try {
90
+ fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
91
+ criado = true;
92
+ }
93
+ catch (e) {
94
+ if (e.code !== 'EEXIST')
95
+ throw e;
96
+ fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);
97
+ }
98
+ try {
99
+ const st = fs.fstatSync(fd);
100
+ if (!st.isFile() || st.uid !== process.getuid?.() || st.mode & 0o077 || st.nlink !== 1)
101
+ throw new Error('HITL lock exige arquivo privado do uid');
102
+ const r = (0, node_child_process_1.spawnSync)('/usr/bin/flock', ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'pipe', 'pipe', fd], timeout: 2000 });
103
+ if (r.status !== 0)
104
+ throw new Error('HITL ocupado; exclusão do kernel não adquirida');
105
+ // fd 3 no helper é dup do mesmo open file description. Fechar o helper não libera fd no dono.
106
+ const atual = fs.lstatSync(file);
107
+ if (atual.ino !== st.ino || atual.dev !== st.dev || atual.isSymbolicLink())
108
+ throw new Error('HITL lock mudou durante aquisição');
109
+ let anterior = null;
110
+ const raw = fs.readFileSync(fd, 'utf8');
111
+ if (!criado) {
112
+ let v;
113
+ try {
114
+ v = JSON.parse(raw);
115
+ }
116
+ catch { /* legado ambíguo permanece preservado */ }
117
+ if (v?.contrato !== 'ork.hitl-lock/v1')
118
+ throw new Error('HITL ocupado; lock legado exige inspeção e migração coordenada');
119
+ anterior = { token: v.token, dono: v.dono, estado: v.estado };
120
+ }
121
+ const stat = fs.readFileSync('/proc/self/stat', 'utf8');
122
+ const info = { contrato: 'ork.hitl-lock/v1', token: (0, node_crypto_1.randomUUID)(), operacao, estado: 'ocupado', adquiridoEm: new Date().toISOString(),
123
+ dono: { pid: process.pid, uid: process.getuid(), bootId: fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(),
124
+ inicio: stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19], executavel: fs.readlinkSync('/proc/self/exe'), cwd: process.cwd() },
125
+ anterior, evidencia: 'flock exclusivo adquirido pelo kernel; PID/TTL são somente metadados' };
126
+ const salvar = () => { fs.ftruncateSync(fd, 0); fs.writeSync(fd, JSON.stringify(info), 0, 'utf8'); fs.fsyncSync(fd); };
127
+ salvar();
128
+ try {
129
+ return executar();
130
+ }
131
+ finally {
132
+ info.estado = 'liberado';
133
+ salvar();
134
+ }
135
+ }
136
+ finally {
137
+ fs.closeSync(fd);
138
+ } // não unlink, não sinal, não TTL
139
+ }