@orkastery/cli 0.6.3 → 0.6.4

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.
@@ -2,7 +2,7 @@
2
2
  "id": "orkastery",
3
3
  "name": "Orkastery",
4
4
  "description": "Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool de projeto aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap; as maquinas da pessoa, de ork_network_status (sem projeto).",
5
- "version": "0.6.3",
5
+ "version": "0.6.4",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {},
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/openclaw-plugin",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Plugin Orkastery para OpenClaw: 32 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.RESERVA_DO_GRUPO = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE = void 0;
3
+ exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.RESERVA_DO_GRUPO = exports.ENV_CONTAINERS_DA_LANE = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = exports.ENV_CPUS_DA_LANE = void 0;
4
+ exports.registrarContainerDaLane = registrarContainerDaLane;
4
5
  exports.cargaDoProcLoadavg = cargaDoProcLoadavg;
5
6
  exports.medirCpuDoGrupo = medirCpuDoGrupo;
6
7
  exports.planejarCpuDoGrupo = planejarCpuDoGrupo;
@@ -16,12 +17,25 @@ exports.paredeAgileSimulada = paredeAgileSimulada;
16
17
  const node_fs_1 = require("node:fs");
17
18
  const steal_1 = require("./steal");
18
19
  exports.ENV_CPUS_DA_LANE = 'ORK_CHECK_CPUS_DA_LANE';
20
+ exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA';
19
21
  exports.ENV_CPUS_DA_LANE_APOS_RESERVA = 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA';
20
22
  exports.ENV_SINAL_DA_RESERVA = 'ORK_CHECK_RESERVA_DEVOLVIDA';
21
23
  exports.ENV_CPUS_DOCKER = 'ORK_CHECK_CPUS_DOCKER';
24
+ /** Registro privado da rodada: quem sobe um container na lane anota o nome para a sonda de CPU. */
25
+ exports.ENV_CONTAINERS_DA_LANE = 'ORK_CHECK_CONTAINERS_DA_LANE';
22
26
  exports.RESERVA_DO_GRUPO = 2;
23
27
  exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = 10;
24
28
  exports.JANELA_DA_SONDA_MS = 250;
29
+ /** Registra o container antes do `docker run`; falha de evidencia nunca impede a ferramenta de rodar. */
30
+ function registrarContainerDaLane(nome, ambiente = process.env) {
31
+ const arquivo = ambiente[exports.ENV_CONTAINERS_DA_LANE];
32
+ if (!arquivo || !nome)
33
+ return;
34
+ try {
35
+ (0, node_fs_1.appendFileSync)(arquivo, JSON.stringify({ nome, em: new Date().toISOString() }) + '\n', { mode: 0o600 });
36
+ }
37
+ catch { /* a medicao fica incompleta e o agendador conserva a reserva */ }
38
+ }
25
39
  const cpusValidas = (cpus) => Math.max(1, Math.floor(Number.isFinite(cpus) ? cpus : 1));
26
40
  /** A carga de um minuto de `/proc/loadavg`; entrada invalida ou ausencia nao inventam saturacao. */
27
41
  function cargaDoProcLoadavg(conteudo) {
@@ -68,14 +82,23 @@ function planejarCpuDoGrupo(cpusInformadas, medicao) {
68
82
  const saturado = motivos.length > 0;
69
83
  const fatia = saturado ? classic : Math.min(exports.RESERVA_DO_GRUPO, cpus - 1);
70
84
  const ferramentas = Math.max(1, Math.floor(fatia));
85
+ const medida = medicao.cpusIntegracao;
86
+ const cpusIntegracao = saturado ? ferramentas : Math.max(1, Math.min(ferramentas, Number.isFinite(medida) && medida > 0 ? Math.ceil(medida) : ferramentas));
87
+ const medidaCompativel = medicao.fonteCpusIntegracao === 'medida' && medida !== null && medida !== undefined;
88
+ const integracaoEmParalelo = !saturado && medidaCompativel && cpus - fatia - cpusIntegracao >= 1;
71
89
  return {
72
90
  cpus,
73
91
  ordem: saturado ? 'classic' : 'paralela',
74
92
  saturado,
75
- motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas ao grupo`,
76
- cpusHermetica: saturado ? classic : Math.max(1, cpus - fatia),
93
+ motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas a varredura; ` +
94
+ (integracaoEmParalelo ? `integracao medida cabe em paralelo com ${cpusIntegracao} CPU(s)` : 'integracao permanece encadeada'),
95
+ cpusHermetica: saturado ? classic : Math.max(1, cpus - fatia - (integracaoEmParalelo ? cpusIntegracao : 0)),
96
+ cpusHermeticaAposVarredura: saturado ? classic : Math.max(1, cpus - cpusIntegracao),
77
97
  cpusHermeticaAposReserva: saturado ? classic : cpus,
78
- cpusIntegracao: ferramentas,
98
+ cpusIntegracao,
99
+ fonteCpusIntegracao: saturado ? 'classic' : medidaCompativel
100
+ ? 'medida' : 'conservadora',
101
+ integracaoEmParalelo,
79
102
  cpusDaFatia: fatia,
80
103
  ferramentasDaVarredura: ferramentas,
81
104
  cpusPorContainer: Math.max(0.01, Math.floor((fatia / ferramentas) * 100) / 100),
@@ -91,10 +114,23 @@ function paredeAgileSimulada(plano, trabalho) {
91
114
  return trabalho.hermetica / plano.cpusHermetica + trabalho.varredura / plano.cpusDaFatia
92
115
  + trabalho.integracao / plano.cpusIntegracao;
93
116
  }
94
- const paredeDaReserva = trabalho.varredura / plano.cpusDaFatia + trabalho.integracao / plano.cpusIntegracao;
95
- const feitoAntesDaDevolucao = Math.min(trabalho.hermetica, paredeDaReserva * plano.cpusHermetica);
96
- const paredeDaHermetica = feitoAntesDaDevolucao === trabalho.hermetica
97
- ? trabalho.hermetica / plano.cpusHermetica
98
- : paredeDaReserva + (trabalho.hermetica - feitoAntesDaDevolucao) / plano.cpusHermeticaAposReserva;
99
- return Math.max(paredeDaHermetica, paredeDaReserva);
117
+ const paredeVarredura = trabalho.varredura / plano.cpusDaFatia;
118
+ const paredeIntegracao = trabalho.integracao / plano.cpusIntegracao;
119
+ if (plano.integracaoEmParalelo) {
120
+ let instante = 0, restante = trabalho.hermetica, capacidade = plano.cpusHermetica;
121
+ for (const evento of [{ em: paredeVarredura, libera: plano.cpusDaFatia },
122
+ { em: paredeIntegracao, libera: plano.cpusIntegracao }].sort((a, b) => a.em - b.em)) {
123
+ restante = Math.max(0, restante - (evento.em - instante) * capacidade);
124
+ instante = evento.em;
125
+ capacidade += evento.libera;
126
+ }
127
+ return Math.max(paredeVarredura, paredeIntegracao, instante + restante / capacidade);
128
+ }
129
+ const feitoNaVarredura = Math.min(trabalho.hermetica, paredeVarredura * plano.cpusHermetica);
130
+ const restanteAposVarredura = trabalho.hermetica - feitoNaVarredura;
131
+ const feitoNaIntegracao = Math.min(restanteAposVarredura, paredeIntegracao * plano.cpusHermeticaAposVarredura);
132
+ const restanteFinal = restanteAposVarredura - feitoNaIntegracao;
133
+ const paredeDaHermetica = (feitoNaVarredura ? paredeVarredura : 0)
134
+ + (feitoNaIntegracao ? paredeIntegracao : 0) + restanteFinal / plano.cpusHermeticaAposReserva;
135
+ return Math.max(paredeDaHermetica, paredeVarredura + paredeIntegracao);
100
136
  }
@@ -47,10 +47,10 @@ const estado_thread_1 = require("./estado-thread");
47
47
  const catalogo_1 = require("./catalogo");
48
48
  const linha = (e) => JSON.stringify(e) + '\n';
49
49
  function esperar(teste) {
50
- const limite = Date.now() + 5000;
50
+ const limite = Date.now() + 15000;
51
51
  while (!teste()) {
52
52
  if (Date.now() >= limite)
53
- throw new Error('subprocesso do cenário não terminou em 5000 ms');
53
+ throw new Error('subprocesso do cenário não terminou no prazo de transporte');
54
54
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
55
55
  }
56
56
  }
@@ -71,7 +71,8 @@ exports.CENARIO_SENSORES = {
71
71
  const hook = path.join(catalogo, 'adapters/claude-code/hooks/ork-sensor.js');
72
72
  const cli = (0, catalogo_1.cliDoCatalogo)(catalogo);
73
73
  const start = Date.now();
74
- const h = (0, node_child_process_1.spawnSync)(process.execPath, [hook], { cwd: wt, encoding: 'utf8', timeout: 5000,
74
+ // O teto de transporte absorve a fila do Classic; a fixture continua exigindo a latência funcional abaixo de 5 s.
75
+ const h = (0, node_child_process_1.spawnSync)(process.execPath, [hook], { cwd: wt, encoding: 'utf8', timeout: 15000,
75
76
  input: JSON.stringify({ cwd: wt, session_id: 'fixture-claude', hook_event_name: 'PermissionRequest', tool_use_id: 'fixture-tool' }),
76
77
  env: { ...process.env, ORK_SENSOR_CLI: cli } });
77
78
  if (h.status !== 0 || h.stderr || h.stdout)
@@ -98,7 +99,7 @@ exports.CENARIO_SENSORES = {
98
99
  }
99
100
  // A identidade vem de um processo realmente executado e encerrado, sem recibo.
100
101
  const runner = require.resolve('./adapters/codex-runner');
101
- const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 5000 });
102
+ const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 15000 });
102
103
  if (morto.status !== 0)
103
104
  throw new Error('processo de morte do cenário indisponível');
104
105
  const filho = JSON.parse(morto.stdout);
@@ -627,8 +627,8 @@ function resultadosDasLanes(linhas) {
627
627
  /** Hash do ambiente funcional; transporte do CHECK, npm e bookkeeping do shell nao mudam o teste. */
628
628
  function assinaturaDoAmbiente(env) {
629
629
  const ignorada = (nome) => nome.startsWith('npm_') || nome.startsWith('NPM_')
630
- || ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA', 'ORK_CHECK_RESERVA_DEVOLVIDA',
631
- 'ORK_CHECK_CPUS_DOCKER',
630
+ || ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA',
631
+ 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA', 'ORK_CHECK_RESERVA_DEVOLVIDA', 'ORK_CHECK_CONTAINERS_DA_LANE', 'ORK_CHECK_CPUS_DOCKER',
632
632
  'NODE_TEST_CONTEXT', 'NODE_COMPILE_CACHE', 'PWD', 'OLDPWD', 'SHLVL', '_', 'INIT_CWD'].includes(nome);
633
633
  const pares = Object.entries(env).filter(([nome, valor]) => valor !== undefined && !ignorada(nome))
634
634
  .sort(([a], [b]) => a.localeCompare(b));
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.argvDoItem = argvDoItem;
37
+ exports.chaveDoPerfilIntegracao = chaveDoPerfilIntegracao;
38
+ exports.picoCpuPorFonte = picoCpuPorFonte;
37
39
  exports.esperaDoGrupo = esperaDoGrupo;
38
40
  exports.rodarEmParalelo = rodarEmParalelo;
39
41
  /**
@@ -48,12 +50,17 @@ exports.rodarEmParalelo = rodarEmParalelo;
48
50
  * de integracao depois da varredura), e `maxParalelo` limita quantos rodam ao mesmo tempo.
49
51
  */
50
52
  const node_child_process_1 = require("node:child_process");
53
+ const node_crypto_1 = require("node:crypto");
51
54
  const fs = __importStar(require("node:fs"));
52
55
  const os = __importStar(require("node:os"));
53
56
  const path = __importStar(require("node:path"));
54
57
  const agendador_cpu_1 = require("./agendador-cpu");
58
+ const integracoes_locais_1 = require("./integracoes-locais");
55
59
  const TIMEOUT_BIN = '/usr/bin/timeout';
56
60
  const NICE_BIN = '/usr/bin/nice';
61
+ const TIME_BIN = '/usr/bin/time';
62
+ const CONTRATO_PERFIL_INTEGRACAO = 'ork.check.cpu-integracao/v1';
63
+ const INTERVALO_CPU_MS = 100;
57
64
  /** O argv de um item: `nice` (se pedido e existir), `timeout(1)` (se existir) e o `argv` do item ou `bash -c`. */
58
65
  function argvDoItem(item, comGrupo = fs.existsSync(TIMEOUT_BIN), comNice = fs.existsSync(NICE_BIN)) {
59
66
  const programa = item.argv?.length ? item.argv : ['bash', '-c', item.comando];
@@ -77,6 +84,243 @@ function vagas(max) {
77
84
  livres++; },
78
85
  };
79
86
  }
87
+ function arquivoDoPerfilIntegracao() {
88
+ const base = process.env.XDG_CACHE_HOME && path.isAbsolute(process.env.XDG_CACHE_HOME)
89
+ ? process.env.XDG_CACHE_HOME : path.join(os.homedir(), '.cache');
90
+ return path.join(base, 'orkastery', 'check', 'cpu-integracao.json');
91
+ }
92
+ /** Os arquivos da lane de integracao, pela mesma regra do `test-lanes.cjs`: a lista do ambiente ou a do nucleo. */
93
+ function arquivosDaIntegracao(env = process.env) {
94
+ const lista = env.ORK_LANES_INTEGRACAO !== undefined
95
+ ? env.ORK_LANES_INTEGRACAO.split(',').map((n) => n.trim()).filter(Boolean) : [...integracoes_locais_1.TESTES_DE_INTEGRACAO_LOCAL];
96
+ return [...new Set(lista)].sort();
97
+ }
98
+ /**
99
+ * A medida so atravessa rodadas quando comando, capacidade e entradas que definem a lane continuam iguais: o script das
100
+ * lanes, a lista de integracao e o conteudo compilado de cada teste dela. Teste novo, mudado ou ausente troca a chave.
101
+ */
102
+ function chaveDoPerfilIntegracao(itens, cpus, arquivos = arquivosDaIntegracao()) {
103
+ const integracao = itens.filter((i) => i.papelCpu === 'integracao');
104
+ if (!integracao.length)
105
+ return null;
106
+ const h = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({ cpus, node: process.version,
107
+ comandos: integracao.map((i) => i.comando), arquivos }));
108
+ const cwd = integracao[0].cwd;
109
+ const relativos = ['core/dist/integracoes-locais.js', 'core/scripts/test-lanes.cjs',
110
+ ...arquivos.map((nome) => path.join('core', 'dist-test', 'test', nome))];
111
+ for (const relativo of relativos) {
112
+ try {
113
+ h.update(relativo).update(fs.readFileSync(path.join(cwd, relativo)));
114
+ }
115
+ catch {
116
+ h.update(`${relativo}:ausente`);
117
+ }
118
+ }
119
+ return h.digest('hex');
120
+ }
121
+ function lerPerfilIntegracao(chave, cpus) {
122
+ if (!chave)
123
+ return null;
124
+ try {
125
+ const p = JSON.parse(fs.readFileSync(arquivoDoPerfilIntegracao(), 'utf8'));
126
+ return p?.contrato === CONTRATO_PERFIL_INTEGRACAO && p.chave === chave && p.cpus === cpus && p.completa
127
+ && Number.isFinite(p.picoCpus) && p.picoCpus > 0 && p.picoCpus <= cpus ? p : null;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function gravarPerfilIntegracao(chave, cpus, medicao) {
134
+ if (!chave || !medicao.completa || medicao.anomalia || medicao.picoCpus === null
135
+ || !(medicao.picoCpus > 0) || medicao.picoCpus > cpus)
136
+ return;
137
+ const arquivo = arquivoDoPerfilIntegracao();
138
+ try {
139
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
140
+ const temporario = `${arquivo}.${process.pid}.tmp`;
141
+ const perfil = { contrato: CONTRATO_PERFIL_INTEGRACAO, chave, cpus,
142
+ picoCpus: medicao.picoCpus, completa: true, medidoEm: new Date().toISOString() };
143
+ fs.writeFileSync(temporario, JSON.stringify(perfil) + '\n', { mode: 0o600 });
144
+ fs.renameSync(temporario, arquivo);
145
+ }
146
+ catch { /* cache de desempenho ausente conserva a reserva; nunca derruba o CHECK */ }
147
+ }
148
+ const CLK_TCK = (() => {
149
+ const r = (0, node_child_process_1.spawnSync)('getconf', ['CLK_TCK'], { encoding: 'utf8', timeout: 1000 });
150
+ const n = Number(r.stdout?.trim());
151
+ return Number.isFinite(n) && n > 0 ? n : 100;
152
+ })();
153
+ function processosCpu() {
154
+ const tabela = new Map();
155
+ let nomes = [];
156
+ try {
157
+ nomes = fs.readdirSync('/proc');
158
+ }
159
+ catch {
160
+ return tabela;
161
+ }
162
+ for (const nome of nomes) {
163
+ if (!/^\d+$/.test(nome))
164
+ continue;
165
+ try {
166
+ const stat = fs.readFileSync(`/proc/${nome}/stat`, 'utf8');
167
+ const depois = stat.slice(stat.lastIndexOf(')') + 2).trim().split(/\s+/);
168
+ tabela.set(Number(nome), { ppid: Number(depois[1]), cpuMs: (Number(depois[11]) + Number(depois[12])) * 1000 / CLK_TCK,
169
+ inicio: depois[19] });
170
+ }
171
+ catch { /* acabou entre a lista e a leitura */ }
172
+ }
173
+ return tabela;
174
+ }
175
+ function arvoreDeProcessos(raiz, tabela) {
176
+ const filhos = new Map();
177
+ for (const [pid, p] of tabela) {
178
+ const lista = filhos.get(p.ppid) ?? [];
179
+ lista.push(pid);
180
+ filhos.set(p.ppid, lista);
181
+ }
182
+ const vistos = new Set(), pilha = [raiz];
183
+ while (pilha.length) {
184
+ const pid = pilha.pop();
185
+ if (vistos.has(pid))
186
+ continue;
187
+ vistos.add(pid);
188
+ pilha.push(...(filhos.get(pid) ?? []));
189
+ }
190
+ return [...vistos];
191
+ }
192
+ function nomesDosContainers(arquivo) {
193
+ try {
194
+ return [...new Set(fs.readFileSync(arquivo, 'utf8').split('\n').filter(Boolean).flatMap((linha) => {
195
+ try {
196
+ const r = JSON.parse(linha);
197
+ return typeof r.nome === 'string' ? [r.nome] : [];
198
+ }
199
+ catch {
200
+ return [];
201
+ }
202
+ }))];
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ }
208
+ function arquivoCpuDoContainer(nome) {
209
+ const r = (0, node_child_process_1.spawnSync)('docker', ['inspect', '--format', '{{.Id}} {{.State.Pid}}', nome], { encoding: 'utf8', timeout: 1000 });
210
+ const [id = '', pidTexto = ''] = r.stdout?.trim().split(/\s+/) ?? [];
211
+ const pid = Number(pidTexto);
212
+ if (r.status !== 0)
213
+ return { fonte: null, motivo: 'docker inspect nao encontrou o conteiner' };
214
+ if (!/^[a-f0-9]{12,64}$/.test(id) || !Number.isInteger(pid) || pid <= 0) {
215
+ return { fonte: null, motivo: 'PID ou identificador do conteiner indisponivel' };
216
+ }
217
+ let linhas = '';
218
+ try {
219
+ linhas = fs.readFileSync(`/proc/${pid}/cgroup`, 'utf8');
220
+ }
221
+ catch {
222
+ return { fonte: null, motivo: 'cgroup do PID do conteiner indisponivel' };
223
+ }
224
+ const unificado = /^0::(.+)$/m.exec(linhas)?.[1];
225
+ const candidatos = [
226
+ ...(unificado ? [{ arquivo: path.join('/sys/fs/cgroup', unificado, 'cpu.stat'), divisor: 1000 }] : []),
227
+ { arquivo: path.join('/sys/fs/cgroup/system.slice', `docker-${id}.scope`, 'cpu.stat'), divisor: 1000 },
228
+ { arquivo: path.join('/sys/fs/cgroup/docker', id, 'cpu.stat'), divisor: 1000 },
229
+ ];
230
+ const legado = /^(?:\d+):[^\n]*(?:cpuacct|cpu)[^\n]*:(.+)$/m.exec(linhas)?.[1];
231
+ if (legado)
232
+ candidatos.push({ arquivo: path.join('/sys/fs/cgroup/cpuacct', legado, 'cpuacct.usage'), divisor: 1_000_000 });
233
+ const fonte = candidatos.find((x) => fs.existsSync(x.arquivo)) ?? null;
234
+ return fonte ? { fonte, motivo: null }
235
+ : { fonte: null, motivo: 'cpu.stat ausente no cgroup v2, no escopo do systemd e no caminho legado' };
236
+ }
237
+ function lerCpuDoCgroup(fonte) {
238
+ try {
239
+ const texto = fs.readFileSync(fonte.arquivo, 'utf8');
240
+ const bruto = fonte.arquivo.endsWith('cpu.stat') ? Number(/^usage_usec\s+(\d+)$/m.exec(texto)?.[1]) : Number(texto.trim());
241
+ return Number.isFinite(bruto) && bruto >= 0 ? bruto / fonte.divisor : null;
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ /**
248
+ * Calcula o pico por fonte. A primeira leitura vira baseline; uma fonte que nasce depois dela contribui com o acumulado
249
+ * desde seu nascimento, sem despejar na janela o tempo anterior de processos ou conteineres que ja existiam.
250
+ */
251
+ function picoCpuPorFonte(amostras, cpus) {
252
+ let pico = 0;
253
+ for (let i = 1; i < amostras.length; i++) {
254
+ const anterior = amostras[i - 1], atual = amostras[i];
255
+ if (!(atual.em > anterior.em))
256
+ continue;
257
+ let delta = 0;
258
+ for (const [fonte, acumulado] of Object.entries(atual.fontes)) {
259
+ const base = anterior.fontes[fonte];
260
+ delta += Math.max(0, acumulado - (base ?? 0));
261
+ }
262
+ pico = Math.max(pico, delta / (atual.em - anterior.em));
263
+ }
264
+ if (pico > cpus + 1e-9) {
265
+ return { picoCpus: null, anomalia: `pico medido de ${pico.toFixed(2)} CPU excede a capacidade de ${cpus} CPU` };
266
+ }
267
+ return { picoCpus: pico > 0 ? Math.ceil(pico * 100) / 100 : null, anomalia: null };
268
+ }
269
+ /** CPU acumulada e pico por janela da integracao; nomes registrados mas nunca observados tornam a medida incompleta. */
270
+ function sondarCpuDaIntegracao(pid, registro, cpus) {
271
+ const fontes = new Map();
272
+ const motivos = new Map();
273
+ const amostras = [];
274
+ const primeiraCpuDosContainers = new Map();
275
+ const ultimaCpuDosContainers = new Map();
276
+ const amostrar = () => {
277
+ const tabela = processosCpu();
278
+ const atuais = {};
279
+ for (const p of arvoreDeProcessos(pid, tabela)) {
280
+ const x = tabela.get(p);
281
+ if (x)
282
+ atuais[`processo:${p}:${x.inicio}`] = x.cpuMs;
283
+ }
284
+ for (const nome of nomesDosContainers(registro)) {
285
+ let fonte = fontes.get(nome);
286
+ if (!fonte) {
287
+ const busca = arquivoCpuDoContainer(nome);
288
+ if (busca.fonte) {
289
+ fonte = busca.fonte;
290
+ fontes.set(nome, fonte);
291
+ motivos.delete(nome);
292
+ }
293
+ else if (busca.motivo)
294
+ motivos.set(nome, busca.motivo);
295
+ }
296
+ const cpu = fonte ? lerCpuDoCgroup(fonte) : null;
297
+ if (cpu !== null) {
298
+ atuais[`conteiner:${nome}`] = cpu;
299
+ if (!primeiraCpuDosContainers.has(nome))
300
+ primeiraCpuDosContainers.set(nome, amostras.length === 0 ? cpu : 0);
301
+ ultimaCpuDosContainers.set(nome, cpu);
302
+ motivos.delete(nome);
303
+ }
304
+ else if (fonte)
305
+ motivos.set(nome, 'contador de CPU do cgroup ilegivel');
306
+ }
307
+ amostras.push({ em: Date.now(), fontes: atuais });
308
+ };
309
+ return {
310
+ amostrar,
311
+ fechar: (arvoreMs, paredeMs) => {
312
+ amostrar();
313
+ const nomes = nomesDosContainers(registro);
314
+ const containersMs = nomes.reduce((total, nome) => total + Math.max(0, (ultimaCpuDosContainers.get(nome) ?? 0) - (primeiraCpuDosContainers.get(nome) ?? 0)), 0);
315
+ const ausentes = nomes.filter((nome) => !ultimaCpuDosContainers.has(nome));
316
+ const pico = picoCpuPorFonte(amostras, cpus);
317
+ return { arvoreMs, containersMs, totalMs: arvoreMs + containersMs, paredeMs,
318
+ picoCpus: pico.picoCpus, completa: ausentes.length === 0 && !pico.anomalia,
319
+ containers: nomes, ausentes, motivosDosAusentes: ausentes.map((container) => ({ container,
320
+ motivo: motivos.get(container) ?? 'conteiner terminou antes de uma amostra completa' })), anomalia: pico.anomalia };
321
+ },
322
+ };
323
+ }
80
324
  /**
81
325
  * O lado do auxiliar: lanca cada item quando os de `apos` terminam e ha vaga, espera todos e grava `resultado.json` ao
82
326
  * lado das saidas.
@@ -84,7 +328,15 @@ function vagas(max) {
84
328
  async function rodarEspecificacao(esp) {
85
329
  const comGrupo = fs.existsSync(TIMEOUT_BIN);
86
330
  const usaAgendador = esp.itens.some((i) => i.papelCpu === 'hermetica');
87
- const plano = usaAgendador ? (0, agendador_cpu_1.planejarCpuDoGrupo)(esp.cpus ?? os.availableParallelism(), esp.medicaoCpu ?? await (0, agendador_cpu_1.medirCpuDoGrupo)()) : undefined;
331
+ const cpus = esp.cpus ?? os.availableParallelism();
332
+ const chaveDoPerfil = usaAgendador ? chaveDoPerfilIntegracao(esp.itens, cpus) : null;
333
+ const perfil = esp.medicaoCpu?.cpusIntegracao === undefined ? lerPerfilIntegracao(chaveDoPerfil, cpus) : null;
334
+ const medicao = esp.medicaoCpu ?? await (0, agendador_cpu_1.medirCpuDoGrupo)();
335
+ if (perfil)
336
+ Object.assign(medicao, { cpusIntegracao: perfil.picoCpus, fonteCpusIntegracao: 'medida' });
337
+ else if (medicao.cpusIntegracao === undefined)
338
+ Object.assign(medicao, { cpusIntegracao: null, fonteCpusIntegracao: 'conservadora' });
339
+ const plano = usaAgendador ? (0, agendador_cpu_1.planejarCpuDoGrupo)(cpus, medicao) : undefined;
88
340
  const sinalDaReserva = plano && !plano.saturado ? path.join(esp.dir, 'reserva-devolvida.json') : undefined;
89
341
  const itens = plano ? aplicarPlano(esp.itens, plano, sinalDaReserva) : esp.itens;
90
342
  const limite = vagas(esp.maxParalelo && esp.maxParalelo > 0 ? esp.maxParalelo : itens.length);
@@ -119,29 +371,86 @@ async function rodarEspecificacao(esp) {
119
371
  if (!sinalDaReserva || !reservados.some((item) => item.papelCpu === 'varredura')
120
372
  || !reservados.some((item) => item.papelCpu === 'integracao'))
121
373
  return;
122
- await Promise.all(reservados.map((item) => vezDoItem(item, indice.get(item.nome))));
123
- fs.writeFileSync(sinalDaReserva, JSON.stringify({ em: new Date().toISOString(), vagas: plano.cpusHermeticaAposReserva }), { mode: 0o600 });
374
+ const varreduras = reservados.filter((item) => item.papelCpu === 'varredura');
375
+ const integracoes = reservados.filter((item) => item.papelCpu === 'integracao');
376
+ const degrau = async (origem, pecas, vagas, antes) => {
377
+ await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
378
+ fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
379
+ em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
380
+ };
381
+ if (!plano.integracaoEmParalelo) {
382
+ await degrau('varredura', varreduras, plano.cpusHermeticaAposVarredura, plano.cpusHermetica);
383
+ await degrau('integracao', integracoes, plano.cpusHermeticaAposReserva, plano.cpusHermeticaAposVarredura);
384
+ }
385
+ else {
386
+ // As promessas escrevem em série no event loop; o primeiro término recebe somente a reserva da própria peça.
387
+ let vagas = plano.cpusHermetica;
388
+ const aoTerminar = async (origem, pecas, libera) => {
389
+ await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
390
+ const antes = vagas;
391
+ vagas = Math.min(plano.cpus, vagas + libera);
392
+ fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
393
+ em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
394
+ };
395
+ await Promise.all([aoTerminar('varredura', varreduras, plano.cpusDaFatia),
396
+ aoTerminar('integracao', integracoes, plano.cpusIntegracao)]);
397
+ }
124
398
  };
125
399
  const [, terminados] = await Promise.all([devolverReserva(), todos]);
400
+ const medidaDaIntegracao = terminados.find((t) => t.medicaoCpu)?.medicaoCpu;
401
+ if (plano && medidaDaIntegracao) {
402
+ plano.medicaoCpuIntegracao = medidaDaIntegracao;
403
+ gravarPerfilIntegracao(chaveDoPerfil, cpus, medidaDaIntegracao);
404
+ }
126
405
  fs.writeFileSync(path.join(esp.dir, 'resultado.json'), JSON.stringify({ terminados, ...(plano ? { agendamentoCpu: plano } : {}) }), { mode: 0o600 });
127
406
  function lancar(item, i) {
128
407
  return new Promise((resolve) => {
129
408
  const saida = fs.openSync(path.join(esp.dir, `${i}.out`), 'w', 0o600);
130
409
  const erro = fs.openSync(path.join(esp.dir, `${i}.err`), 'w', 0o600);
131
410
  const inicio = Date.now();
132
- const [bin, ...args] = argvDoItem(item, comGrupo);
411
+ const medirIntegracao = item.papelCpu === 'integracao';
412
+ const registro = path.join(esp.dir, `${i}.containers.jsonl`);
413
+ const tempo = path.join(esp.dir, `${i}.tempo`);
414
+ if (medirIntegracao)
415
+ fs.writeFileSync(registro, '', { mode: 0o600 });
416
+ const base = argvDoItem(item, comGrupo);
417
+ const medido = medirIntegracao && fs.existsSync(TIME_BIN)
418
+ ? [TIME_BIN, '-f', '%U %S', '-o', tempo, ...base] : base;
419
+ const [bin, ...args] = medido;
133
420
  let fim = null;
421
+ let sonda = null;
422
+ let relogio = null;
134
423
  const encerrar = (t) => {
135
424
  if (fim)
136
425
  return;
137
- fim = t;
426
+ if (relogio)
427
+ clearInterval(relogio);
428
+ let arvoreMs = 0;
429
+ if (medirIntegracao) {
430
+ try {
431
+ const [user, sys] = fs.readFileSync(tempo, 'utf8').trim().split(/\s+/).map(Number);
432
+ arvoreMs = (user + sys) * 1000;
433
+ }
434
+ catch { /* medida incompleta abaixo */ }
435
+ }
436
+ const medicaoCpu = medirIntegracao && sonda ? sonda.fechar(arvoreMs, t.duracaoMs) : undefined;
437
+ if (medicaoCpu && !(arvoreMs > 0))
438
+ medicaoCpu.completa = false;
439
+ fim = { ...t, ...(medicaoCpu ? { medicaoCpu } : {}) };
138
440
  fs.closeSync(saida);
139
441
  fs.closeSync(erro);
140
- resolve(t);
442
+ resolve(fim);
141
443
  };
142
444
  // Sem o `timeout(1)`, o proprio auxiliar mata pelo prazo, como o `spawnSync` do `exec` faria.
143
- const filho = (0, node_child_process_1.spawn)(bin, args, { cwd: item.cwd, env: { ...process.env, ...item.ambiente }, stdio: ['ignore', saida, erro],
445
+ const ambiente = { ...process.env, ...item.ambiente,
446
+ ...(medirIntegracao ? { [agendador_cpu_1.ENV_CONTAINERS_DA_LANE]: registro } : {}) };
447
+ const filho = (0, node_child_process_1.spawn)(bin, args, { cwd: item.cwd, env: ambiente, stdio: ['ignore', saida, erro],
144
448
  ...(comGrupo ? {} : { timeout: item.prazoMs, killSignal: 'SIGTERM' }) });
449
+ if (medirIntegracao) {
450
+ sonda = sondarCpuDaIntegracao(filho.pid, registro, cpus);
451
+ sonda.amostrar();
452
+ relogio = setInterval(sonda.amostrar, INTERVALO_CPU_MS);
453
+ }
145
454
  filho.on('error', (e) => encerrar({ nome: item.nome, code: null, signal: null,
146
455
  error: e.code ?? e.message, duracaoMs: Date.now() - inicio, comGrupo }));
147
456
  filho.on('close', (code, signal) => encerrar({ nome: item.nome, code, signal,
@@ -170,24 +479,25 @@ function aplicarPlano(originais, plano, sinalDaReserva) {
170
479
  const cpusContainer = String(plano.cpusPorContainer);
171
480
  ambiente(hermetica, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusHermetica), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
172
481
  ...(sinalDaReserva && varredura && integracoes.length > 0 ? {
482
+ [agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_VARREDURA]: String(plano.cpusHermeticaAposVarredura),
173
483
  [agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_RESERVA]: String(plano.cpusHermeticaAposReserva), [agendador_cpu_1.ENV_SINAL_DA_RESERVA]: sinalDaReserva,
174
484
  } : {}) });
175
485
  ambiente(varredura, {
176
486
  ORK_CHECK_VARREDURA_PARALELA: String(plano.ferramentasDaVarredura),
177
487
  [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
178
488
  });
179
- let anterior = varredura?.nome;
489
+ let anteriorIntegracao;
180
490
  for (const item of integracoes) {
181
491
  ambiente(item, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusIntegracao), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer });
182
- depender(item, [anterior]);
183
- anterior = item.nome;
492
+ depender(item, [anteriorIntegracao ?? (plano.integracaoEmParalelo ? undefined : varredura?.nome)]);
493
+ anteriorIntegracao = item.nome;
184
494
  }
185
495
  if (plano.saturado)
186
496
  depender(varredura, [hermetica?.nome]);
187
497
  // Comandos sem papel conhecido nao disputam CPU com as tres pecas: fecham a cadeia, um por vez.
188
498
  for (const item of outros) {
189
- depender(item, [hermetica?.nome, anterior]);
190
- anterior = item.nome;
499
+ depender(item, [hermetica?.nome, varredura?.nome, anteriorIntegracao]);
500
+ anteriorIntegracao = item.nome;
191
501
  }
192
502
  return itens;
193
503
  }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S = exports.SEMGREP_SEM_CACHE = void 0;
36
+ exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S = void 0;
37
37
  exports.especificacaoDoAgile = especificacaoDoAgile;
38
38
  exports.prazoDoBuildDaImagem = prazoDoBuildDaImagem;
39
39
  exports.estourouNoAgile = estourouNoAgile;
@@ -48,9 +48,9 @@ exports.runScanAgil = runScanAgil;
48
48
  * ferramentas do catalogo e a mesma classificacao contra a base. O que muda e o empacotamento: a arvore da base sai uma
49
49
  * vez, antes, e cada ferramenta roda num trabalhador proprio (`trabalhador.ts`), ao mesmo tempo, chamando o mesmo
50
50
  * `scanTool` do Classic; o relatorio sai na ordem do catalogo. As ferramentas do Trivy montam um diretorio persistente
51
- * por ferramenta no `/tmp/trivy-cache` do catalogo, para o banco nao descer a cada rodada (cache de banco, nunca de
52
- * resultado: o HEAD e sempre varrido de novo, e a base usa o cache por SHA que o `scanBase` ja tem, com a chave da
53
- * especificacao do Agile). Cada peca do Agile que nao pode rodar cai para o equivalente do Classic e vai ao relatorio
51
+ * por ferramenta no `/tmp/trivy-cache` do catalogo. O resultado normalizado do Semgrep e das ferramentas de dependência
52
+ * fica no cache da máquina, por versão, regras e conteúdo; o Classic continua varrendo toda a árvore. Cada peça do Agile
53
+ * que não pode rodar cai para o equivalente do Classic e vai ao relatório
54
54
  * (`check.pecas`) e ao `ORK_CHECK_RELATORIO` do verify.
55
55
  */
56
56
  const fs = __importStar(require("node:fs"));
@@ -63,6 +63,7 @@ const redacao_url_1 = require("../redacao-url");
63
63
  const compare_1 = require("./compare");
64
64
  const config_1 = require("./config");
65
65
  const guard_1 = require("./guard");
66
+ const incremental_1 = require("./incremental");
66
67
  const run_1 = require("./run");
67
68
  const executor_1 = require("./executor");
68
69
  const imagem_de_lint_1 = require("./imagem-de-lint");
@@ -71,12 +72,9 @@ const TRABALHADOR = path.join(__dirname, 'trabalhador.js');
71
72
  /** Sem prazo da rodada, o trabalhador ganha o maior prazo do catalogo para a HEAD e para a base, com folga. */
72
73
  const FOLGA_MS = 60_000;
73
74
  /**
74
- * O Semgrep baixa as regras do registro (`p/typescript`, ...) a cada rodada, e nao tem cache delas. Regra lida de arquivo
75
- * local ganha outro `check_id` (o prefixo do caminho), o que muda a impressao digital de todo achado: o cache de regras
76
- * fica de fora, declarado como peca em fallback.
75
+ * O cache guarda os achados normalizados junto da identidade das regras do registro. Assim uma rodada morna devolve o
76
+ * `check_id` original, em vez de reler uma regra por arquivo local e reescrever a impressão digital.
77
77
  */
78
- exports.SEMGREP_SEM_CACHE = { peca: 'cache-semgrep', estado: 'fallback',
79
- motivo: 'regra lida de arquivo local troca o check_id e a impressao digital', fallback: 'regras do registro a cada rodada, como no Classic' };
80
78
  /**
81
79
  * O prazo por regra e arquivo do Semgrep no Agile, em segundos. O padrao dele e 5 s; com as ferramentas juntas (e, no
82
80
  * verify, junto das lanes de teste) a disputa de CPU fazia uma regra estourar (`partial-timeout` medido na primeira
@@ -284,8 +282,8 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
284
282
  return report;
285
283
  }
286
284
  report.base = base;
287
- if (tools.some(t => t.name === 'semgrep'))
288
- pecas.push(exports.SEMGREP_SEM_CACHE);
285
+ pecas.push({ peca: 'varredura-incremental', estado: 'agile',
286
+ motivo: 'arquivo pela versao e regras da ferramenta; dependencias pelo lockfile; demais ferramentas rodam inteiras' });
289
287
  const count = (0, run_1.git)(root, ['rev-list', '--count', `${base}..${head}`]);
290
288
  const deadline = opts.deadlineMs === undefined ? null : Date.now() + opts.deadlineMs;
291
289
  // A arvore da base sai uma vez, aqui, e os trabalhadores a dividem (cada um varre a sua ferramenta nela).
@@ -298,6 +296,19 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
298
296
  }
299
297
  const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-security-scan-agil-')));
300
298
  guard.add('outDirs', dir);
299
+ // A peça do cache já fazia parte do contrato ordenado do relatório antes das peças de executor. Resolver aqui mantém
300
+ // essa ordem pública; o mesmo resultado segue no trabalho que será entregue ao processo da ferramenta.
301
+ const regrasSemgrepPorIndice = new Map();
302
+ tools.forEach((t, i) => {
303
+ if (t.name !== 'semgrep')
304
+ return;
305
+ const regrasSemgrep = (0, incremental_1.resolverRegrasSemgrep)(t, (0, run_1.unitsOf)(t, pairs));
306
+ regrasSemgrepPorIndice.set(i, regrasSemgrep);
307
+ pecas.push(regrasSemgrep.reutilizavel
308
+ ? { peca: 'cache-semgrep', estado: 'agile', motivo: 'hash do conteúdo das regras resolvidas; check_id preservado' }
309
+ : { peca: 'cache-semgrep', estado: 'fallback', motivo: regrasSemgrep.motivo,
310
+ fallback: 'varredura integral, como no Classic' });
311
+ });
301
312
  // RM-072 (D12): quem tem o binario nativo na versao do catalogo roda sem o Docker; a peca diz quem faltou.
302
313
  const probe = opts.probe ?? (0, executor_1.defaultProbe)();
303
314
  const comNativo = tools.filter(t => t.native && !t.install);
@@ -310,7 +321,12 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
310
321
  }
311
322
  const montados = tools.map((t, i) => {
312
323
  const restante = deadline === null ? t.timeoutMs : Math.min(t.timeoutMs, deadline - Date.now());
313
- const trabalho = { tool: eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas), units: (0, run_1.unitsOf)(t, pairs), config,
324
+ const units = (0, run_1.unitsOf)(t, pairs);
325
+ const tool = eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas);
326
+ const regrasSemgrep = regrasSemgrepPorIndice.get(i);
327
+ const trabalho = { tool, units, config,
328
+ incremental: true,
329
+ ...(regrasSemgrep ? { regrasSemgrep } : {}),
314
330
  ...(nativos.has(t.name) ? { nativoPrimeiro: true } : {}),
315
331
  ctx: { headTree, mainRoot, base, head, rangeCommits: count === null ? null : Number(count), deadline, ...(baseTree ? { baseTree } : {}) } };
316
332
  const arquivo = path.join(dir, `${i}.json`);
@@ -160,6 +160,7 @@ function runDocker(inv) {
160
160
  ...Object.entries(tool.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]),
161
161
  '--entrypoint', tool.containerBin ?? inv.argv[0], tool.image,
162
162
  ...inv.argv.slice(1).map(a => expand(a, values))];
163
+ (0, agendador_cpu_1.registrarContainerDaLane)(name);
163
164
  inv.guard?.add('containers', name);
164
165
  const r = (0, node_child_process_1.spawnSync)('docker', args, { encoding: 'utf8', timeout: inv.timeoutMs, killSignal: 'SIGKILL', maxBuffer: MAX_BUFFER, env: process.env });
165
166
  const timedOut = r.error?.code === 'ETIMEDOUT';
@@ -0,0 +1,289 @@
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.lerCacheIncremental = void 0;
37
+ exports.resolverRegrasSemgrep = resolverRegrasSemgrep;
38
+ exports.consultarCacheIncremental = consultarCacheIncremental;
39
+ exports.gravarCacheIncremental = gravarCacheIncremental;
40
+ /** Cache de resultado normalizado do Agile, por conteúdo e fora do repositório. */
41
+ const crypto = __importStar(require("node:crypto"));
42
+ const node_child_process_1 = require("node:child_process");
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const run_1 = require("./run");
46
+ const CONTRATO = 'ork.security-scan.incremental/v1';
47
+ const LOCKFILES = new Set(['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'poetry.lock',
48
+ 'Pipfile.lock', 'go.mod', 'go.sum']);
49
+ const sha256 = (valor) => crypto.createHash('sha256').update(valor).digest('hex');
50
+ const SCRIPT_DO_REGISTRO = String.raw `
51
+ # ORK_SEMGREP_RESOLVE_V1
52
+ import sys, urllib.request
53
+ pedido = urllib.request.Request(sys.argv[1], headers={"Accept": "application/json", "User-Agent": "orkastery-security-scan"})
54
+ with urllib.request.urlopen(pedido, timeout=20) as resposta:
55
+ if resposta.status != 200:
56
+ raise RuntimeError("HTTP " + str(resposta.status))
57
+ sys.stdout.buffer.write(resposta.read())
58
+ `;
59
+ /** O mesmo endpoint público para o qual o resolvedor do Semgrep mapeia um id do registro. */
60
+ function urlDoRegistro(referencia) {
61
+ if (/^[prs]\/[A-Za-z0-9._/-]+$/.test(referencia))
62
+ return `https://semgrep.dev/c/${referencia}`;
63
+ if (/^https:\/\//.test(referencia))
64
+ return referencia;
65
+ return null;
66
+ }
67
+ function baixarDoRegistro(ferramenta, url) {
68
+ const uid = process.getuid?.() ?? 65534;
69
+ const gid = process.getgid?.() ?? 65534;
70
+ const nome = `ork-security-scan-semgrep-registry-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
71
+ const r = (0, node_child_process_1.spawnSync)('docker', ['run', '--rm', '--name', nome, '--user', `${uid === 0 ? 65534 : uid}:${uid === 0 ? 65534 : gid}`,
72
+ '--network', 'default', '--entrypoint', 'python3', ferramenta.image, '-c', SCRIPT_DO_REGISTRO, url], { encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024, killSignal: 'SIGKILL' });
73
+ if (r.error || r.status !== 0)
74
+ (0, node_child_process_1.spawnSync)('docker', ['rm', '-f', nome], { stdio: 'ignore', timeout: 10_000 });
75
+ return !r.error && r.status === 0 && r.stdout.trim() ? r.stdout : null;
76
+ }
77
+ /** O conteúdo que o próprio Semgrep extrai da resposta do registro (`rules` direto ou envelope `rule_config`). */
78
+ function conteudoResolvido(resposta) {
79
+ try {
80
+ const json = JSON.parse(resposta);
81
+ const regras = Object.prototype.hasOwnProperty.call(json, 'rules') ? json : json.rule_config;
82
+ return typeof regras === 'string' ? regras : JSON.stringify(regras);
83
+ }
84
+ catch {
85
+ return resposta;
86
+ }
87
+ }
88
+ const MARCAS_ENTRE_ARQUIVOS = ['join', 'interfile', 'crossfile', 'multifile'];
89
+ const MARCA_NA_LINHA = /(?:^|[\s{,])(?:mode\s*:\s*)?(?:join|inter[-_ ]?file|cross[-_ ]?file|multi[-_ ]?file)(?:\s*:|\s*$)/i;
90
+ const CHAVE_DA_LINHA = /^(\s*)(-\s+)?([A-Za-z_][\w-]*)\s*:(?:\s+(.*))?$/;
91
+ const normalizar = (chave) => chave.toLowerCase().replace(/[-_]/g, '');
92
+ /**
93
+ * Se a marca cruza arquivos de fato. `join` cruza em qualquer motor. `interfile` em `options` só cruza no motor Pro do
94
+ * Semgrep; em `metadata` é rótulo do registro, que o motor ignora. Em qualquer outro lugar, cruza (conservador).
95
+ */
96
+ function marcaCruzaArquivos(marca, pai, pro) {
97
+ if (marca === 'join')
98
+ return true;
99
+ if (pai === 'metadata')
100
+ return false;
101
+ if (pai === 'options')
102
+ return pro;
103
+ return true;
104
+ }
105
+ /**
106
+ * O registro responde em YAML. Toda linha com marca cruza arquivos, menos a chave da própria marca, em bloco e com valor
107
+ * booleano, cujo pai pela indentação a torna inócua no motor da rodada. Linha de texto que parece chave nunca relaxa nada.
108
+ */
109
+ function marcaNoYaml(conteudo, pro) {
110
+ const pais = [];
111
+ for (const linha of conteudo.split(/\r?\n/)) {
112
+ const m = CHAVE_DA_LINHA.exec(linha);
113
+ const coluna = m ? m[1].length + (m[2]?.length ?? 0) : 0;
114
+ if (m)
115
+ while (pais.length && pais[pais.length - 1].coluna >= coluna)
116
+ pais.pop();
117
+ if (MARCA_NA_LINHA.test(linha)) {
118
+ const chave = m ? normalizar(m[3]) : null;
119
+ const booleano = !!m && /^(?:true|false|yes|no|on|off)?\s*(?:#.*)?$/i.test(m[4] ?? '');
120
+ if (!chave || !MARCAS_ENTRE_ARQUIVOS.includes(chave) || !booleano
121
+ || marcaCruzaArquivos(chave, pais[pais.length - 1]?.chave ?? null, pro))
122
+ return true;
123
+ }
124
+ if (m)
125
+ pais.push({ coluna, chave: normalizar(m[3]) });
126
+ }
127
+ return false;
128
+ }
129
+ function regraEntreArquivos(conteudo, pro) {
130
+ let valor;
131
+ try {
132
+ valor = JSON.parse(conteudo);
133
+ }
134
+ catch {
135
+ return marcaNoYaml(conteudo, pro);
136
+ }
137
+ if (typeof valor === 'string')
138
+ return marcaNoYaml(valor, pro);
139
+ const visitar = (atual, pai) => {
140
+ if (Array.isArray(atual))
141
+ return atual.some((item) => visitar(item, pai));
142
+ if (!atual || typeof atual !== 'object')
143
+ return false;
144
+ return Object.entries(atual).some(([chave, filho]) => {
145
+ const normal = normalizar(chave);
146
+ if (normal === 'mode' && typeof filho === 'string' && /^(join|interfile|crossfile|multifile)$/i.test(filho))
147
+ return true;
148
+ if (MARCAS_ENTRE_ARQUIVOS.includes(normal) && filho !== false && filho !== null
149
+ && marcaCruzaArquivos(normal, pai, pro))
150
+ return true;
151
+ return visitar(filho, normal);
152
+ });
153
+ };
154
+ return visitar(valor, null);
155
+ }
156
+ /** O Semgrep da rodada usa o motor Pro, o único que cruza arquivos nas regras `interfile`: `--pro*`, `semgrep ci` ou token. */
157
+ function motorPro(ferramenta, unidades) {
158
+ const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
159
+ const todos = [ferramenta.args, ...[...stacks].map((s) => ferramenta.argsByStack?.[s] ?? [])];
160
+ return todos.some((args) => args.some((a) => a.startsWith('--pro')) || args.includes('ci'))
161
+ || Object.keys(ferramenta.env ?? {}).some((k) => /^SEMGREP_APP_TOKEN$/i.test(k));
162
+ }
163
+ function referenciasDasRegras(ferramenta, unidades) {
164
+ const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
165
+ const referencias = new Set();
166
+ for (const stack of stacks) {
167
+ const args = ferramenta.argsByStack?.[stack] ?? [];
168
+ for (let i = 0; i < args.length - 1; i++)
169
+ if (args[i] === '--config' || args[i] === '-f')
170
+ referencias.add(args[++i]);
171
+ }
172
+ return [...referencias].sort();
173
+ }
174
+ function resolverRegrasSemgrep(ferramenta, unidades, baixar = baixarDoRegistro) {
175
+ const referencias = referenciasDasRegras(ferramenta, unidades);
176
+ if (referencias.length === 0)
177
+ return { reutilizavel: false, hash: null, motivo: 'regras-sem-referencia', referencias };
178
+ const resolvidas = [];
179
+ const pro = motorPro(ferramenta, unidades);
180
+ for (const referencia of referencias) {
181
+ const url = urlDoRegistro(referencia);
182
+ if (!url)
183
+ return { reutilizavel: false, hash: null, motivo: 'referencia-de-regra-nao-resolvivel', referencias };
184
+ const resposta = baixar(ferramenta, url);
185
+ if (!resposta)
186
+ return { reutilizavel: false, hash: null, motivo: 'registro-indisponivel', referencias };
187
+ const conteudo = conteudoResolvido(resposta);
188
+ if (regraEntreArquivos(conteudo, pro))
189
+ return { reutilizavel: false, hash: null, motivo: 'regra-entre-arquivos', referencias };
190
+ resolvidas.push([referencia, conteudo]);
191
+ }
192
+ return { reutilizavel: true, hash: sha256(JSON.stringify(resolvidas)), referencias };
193
+ }
194
+ /** Só habilita o que tem garantia explícita: Semgrep por arquivo e dependências por lockfile. */
195
+ function escopoDaFerramenta(t, trabalho) {
196
+ if (t.name === 'semgrep') {
197
+ // A API direta antecede o trabalhador e continua útil para conferir o retrato por arquivo isoladamente. No caminho
198
+ // real do Agile, `incremental: true` torna obrigatória a identidade das regras resolvidas: ausência, registro
199
+ // indisponível ou regra entre arquivos nunca cai nesta compatibilidade.
200
+ if (trabalho.incremental !== true)
201
+ return 'arquivo';
202
+ return trabalho.regrasSemgrep?.reutilizavel && trabalho.regrasSemgrep.hash ? 'arquivo' : null;
203
+ }
204
+ if (t.category === 'dependency')
205
+ return 'lockfile';
206
+ return null;
207
+ }
208
+ function arquivos(root, escopo) {
209
+ const saida = {};
210
+ const visitar = (dir) => {
211
+ let nomes;
212
+ try {
213
+ nomes = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
214
+ }
215
+ catch {
216
+ return;
217
+ }
218
+ for (const nome of nomes) {
219
+ if (nome.name === '.git')
220
+ continue;
221
+ const absoluto = path.join(dir, nome.name);
222
+ if (nome.isDirectory()) {
223
+ visitar(absoluto);
224
+ continue;
225
+ }
226
+ if (!nome.isFile())
227
+ continue;
228
+ const relativo = path.relative(root, absoluto).split(path.sep).join('/');
229
+ const lockfile = LOCKFILES.has(nome.name) || /^requirements(?:\.[^.]+)?\.txt$/.test(nome.name);
230
+ if (escopo === 'lockfile' && !lockfile)
231
+ continue;
232
+ try {
233
+ saida[relativo] = sha256(fs.readFileSync(absoluto));
234
+ }
235
+ catch { /* arquivo mudou durante a leitura: nao entra */ }
236
+ }
237
+ };
238
+ visitar(root);
239
+ return saida;
240
+ }
241
+ function identidade(trabalho, ferramenta) {
242
+ return sha256(JSON.stringify({ ferramenta, unidades: trabalho.units, configuracao: trabalho.config, base: trabalho.ctx.base,
243
+ regrasSemgrep: trabalho.regrasSemgrep?.hash ?? null }));
244
+ }
245
+ function localDaEntrada(chave) {
246
+ return path.join((0, run_1.cacheRoot)(), 'incremental', `${chave}.json`);
247
+ }
248
+ function consultarCacheIncremental(trabalho, ferramenta) {
249
+ const escopo = escopoDaFerramenta(ferramenta, trabalho);
250
+ if (!escopo)
251
+ return { escopo: null, atual: null, anterior: null, alterados: [], reutilizado: null };
252
+ const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
253
+ if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0) {
254
+ return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
255
+ }
256
+ const chave = identidade(trabalho, ferramenta);
257
+ try {
258
+ const entrada = JSON.parse(fs.readFileSync(localDaEntrada(chave), 'utf8'));
259
+ if (entrada.contrato !== CONTRATO || entrada.chave !== chave || entrada.resultado?.run?.state !== 'ran') {
260
+ return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
261
+ }
262
+ const nomes = new Set([...Object.keys(entrada.retrato.arquivos), ...Object.keys(retrato.arquivos)]);
263
+ const alterados = [...nomes].filter((nome) => entrada.retrato.arquivos[nome] !== retrato.arquivos[nome]).sort();
264
+ return { escopo, atual: retrato, anterior: entrada.resultado, alterados,
265
+ reutilizado: alterados.length === 0 ? entrada.resultado : null };
266
+ }
267
+ catch {
268
+ return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
269
+ }
270
+ }
271
+ const lerCacheIncremental = (trabalho, ferramenta) => consultarCacheIncremental(trabalho, ferramenta).reutilizado;
272
+ exports.lerCacheIncremental = lerCacheIncremental;
273
+ function gravarCacheIncremental(trabalho, ferramenta, resultado) {
274
+ const escopo = escopoDaFerramenta(ferramenta, trabalho);
275
+ if (!escopo || resultado.run.state !== 'ran')
276
+ return;
277
+ const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
278
+ if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0)
279
+ return;
280
+ const chave = identidade(trabalho, ferramenta), arquivo = localDaEntrada(chave);
281
+ try {
282
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true, mode: 0o700 });
283
+ const temporario = `${arquivo}.${process.pid}.tmp`;
284
+ const entrada = { contrato: CONTRATO, chave, retrato, resultado };
285
+ fs.writeFileSync(temporario, JSON.stringify(entrada), { mode: 0o600 });
286
+ fs.renameSync(temporario, arquivo);
287
+ }
288
+ catch { /* cache ausente custa CPU, mas nunca muda o veredito */ }
289
+ }
@@ -41,9 +41,11 @@ exports.comCacheNoHost = comCacheNoHost;
41
41
  * `runScanAgil`, um trabalhador por ferramenta, ao mesmo tempo.
42
42
  */
43
43
  const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
44
45
  const executor_1 = require("./executor");
45
46
  const guard_1 = require("./guard");
46
47
  const run_1 = require("./run");
48
+ const incremental_1 = require("./incremental");
47
49
  /**
48
50
  * S6 do CHECK: o binario nativo usa o cache por usuario que o Docker monta (o caminho do host no lugar do caminho do
49
51
  * container), e nao o `/tmp` compartilhado do catalogo, onde outro usuario da maquina deixaria um banco do Trivy.
@@ -51,10 +53,49 @@ const run_1 = require("./run");
51
53
  function comCacheNoHost(t) {
52
54
  if (!t.cacheMounts?.length)
53
55
  return t;
54
- let args = t.args;
55
- for (const m of t.cacheMounts)
56
- args = args.map((a) => a.split(m.container).join(m.host));
57
- return { ...t, args };
56
+ const trocar = (args) => {
57
+ let resposta = args;
58
+ for (const m of t.cacheMounts ?? [])
59
+ resposta = resposta.map((a) => a.split(m.container).join(m.host));
60
+ return resposta;
61
+ };
62
+ return { ...t, args: trocar(t.args), ...(t.argsByStack
63
+ ? { argsByStack: Object.fromEntries(Object.entries(t.argsByStack).map(([stack, args]) => [stack, trocar(args ?? [])])) }
64
+ : {}) };
65
+ }
66
+ /** Semgrep é a garantia por arquivo: roda apenas conteúdo mudado e conserva os achados dos demais arquivos. */
67
+ function semgrepIncremental(trabalho, ferramenta, consulta, executar) {
68
+ if (ferramenta.name !== 'semgrep' || !consulta.anterior || consulta.escopo !== 'arquivo' || !ferramenta.argsByStack)
69
+ return null;
70
+ if (new Set(trabalho.units.map((u) => u.stack)).size !== trabalho.units.length)
71
+ return null;
72
+ const existentes = consulta.alterados.filter((arquivo) => {
73
+ try {
74
+ return fs.statSync(path.join(trabalho.ctx.headTree, arquivo)).isFile();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ });
80
+ const argsByStack = {};
81
+ const unidades = trabalho.units.filter((unidade) => {
82
+ const prefixo = unidade.dir === '.' ? '' : `${unidade.dir.replace(/\\/g, '/')}/`;
83
+ const alvos = existentes.filter((arquivo) => !prefixo || arquivo.startsWith(prefixo));
84
+ const base = unidade.stack ? ferramenta.argsByStack?.[unidade.stack] : undefined;
85
+ if (!base?.length || alvos.length === 0 || !unidade.stack)
86
+ return false;
87
+ argsByStack[unidade.stack] = [...base.slice(0, -1), ...alvos.map((arquivo) => `{src}/${arquivo}`)];
88
+ return true;
89
+ });
90
+ const mudaram = new Set(consulta.alterados);
91
+ const preservados = consulta.anterior.head.filter((achado) => !mudaram.has(achado.file));
92
+ if (unidades.length === 0)
93
+ return { ...consulta.anterior, head: preservados };
94
+ const parcial = executar({ ...ferramenta, argsByStack }, unidades, true);
95
+ if (parcial.run.state !== 'ran')
96
+ return parcial;
97
+ const novos = parcial.head.filter((achado) => mudaram.has(achado.file));
98
+ return { ...parcial, head: [...preservados, ...novos], base: consulta.anterior.base };
58
99
  }
59
100
  if (require.main === module) {
60
101
  const trabalho = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
@@ -65,7 +106,14 @@ if (require.main === module) {
65
106
  // chooseExecutor escolher o nativo); sem ele, o Docker, como no Classic.
66
107
  const sonda = (0, executor_1.defaultProbe)();
67
108
  const probe = trabalho.nativoPrimeiro ? { ...sonda, dockerCli: () => false, dockerDaemon: () => false } : sonda;
68
- const r = (0, run_1.scanTool)(trabalho.nativoPrimeiro ? comCacheNoHost(trabalho.tool) : trabalho.tool, trabalho.units, trabalho.config, { ...resto, deadline: deadline ?? Infinity, probe, guard, images: new Map() });
109
+ const ferramenta = trabalho.nativoPrimeiro ? comCacheNoHost(trabalho.tool) : trabalho.tool;
110
+ const executar = (tool, units, somenteHead = false) => (0, run_1.scanTool)(tool, units, trabalho.config, { ...resto, ...(somenteHead ? { base: resto.head, baseTree: resto.headTree, rangeCommits: 0 } : {}),
111
+ deadline: deadline ?? Infinity, probe, guard, images: new Map() });
112
+ const consulta = trabalho.incremental ? (0, incremental_1.consultarCacheIncremental)(trabalho, ferramenta) : null;
113
+ const r = consulta?.reutilizado ?? (consulta ? semgrepIncremental(trabalho, ferramenta, consulta, executar) : null)
114
+ ?? executar(ferramenta, trabalho.units);
115
+ if (trabalho.incremental && !consulta?.reutilizado)
116
+ (0, incremental_1.gravarCacheIncremental)(trabalho, ferramenta, r);
69
117
  process.stdout.write(JSON.stringify(r));
70
118
  }
71
119
  finally {
package/dist/verify.js CHANGED
@@ -448,10 +448,21 @@ function executorDoGrupo(manifesto, ambienteDaRodada, instabilidade, leitor, con
448
448
  const processos = (0, check_paralelo_1.rodarEmParalelo)(paraOAuxiliar, ambienteDoVerify(), medicaoCpu ? { medicaoCpu } : {});
449
449
  if (processos.agendamentoCpu) {
450
450
  const p = processos.agendamentoCpu;
451
+ const medida = p.medicaoCpuIntegracao;
452
+ const evidencia = medida
453
+ ? `; CPU da integracao: arvore ${Math.round(medida.arvoreMs)} ms + conteineres ${Math.round(medida.containersMs)} ms` +
454
+ ` = ${Math.round(medida.totalMs)} ms, pico ${medida.picoCpus ?? 'indisponivel'} CPU, ` +
455
+ (medida.completa ? 'medida completa' : medida.anomalia
456
+ ? `medida anomala (${medida.anomalia})`
457
+ : `medida incompleta (${medida.motivosDosAusentes.map((x) => `${x.container}: ${x.motivo}`).join('; ')})`)
458
+ : '';
451
459
  executar.pecas = [{ peca: 'agendador-cpu', estado: p.saturado ? 'fallback' : 'agile',
452
460
  motivo: p.saturado
453
461
  ? `${p.motivo}; hermetica com ${p.cpusHermetica}/${p.cpus} CPUs, varredura e integracao depois dela`
454
- : `${p.cpusHermetica}/${p.cpus} CPUs na hermetica e ${p.cpusDaFatia} reservadas; conteineres com ${p.cpusPorContainer} CPU`,
462
+ : `${p.cpusHermetica}/${p.cpus} CPUs na hermetica; depois da varredura ${p.cpusHermeticaAposVarredura}` +
463
+ ` e depois da integracao ${p.cpusHermeticaAposReserva}; reserva da integracao ${p.fonteCpusIntegracao}` +
464
+ ` (${p.cpusIntegracao} CPU), ${p.integracaoEmParalelo ? 'em paralelo com a varredura' : 'encadeada depois da varredura'}` +
465
+ `; conteineres com ${p.cpusPorContainer} CPU${evidencia}`,
455
466
  ...(p.saturado ? { fallback: 'ordem do Classic para hermetica, varredura e integracao' } : {}) }];
456
467
  }
457
468
  const stealPct = janela.fechar();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "A software factory of AI agents that proves its work: parallel threads, verified results, and short decisions only when they matter.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/orkastery/orkastery#readme",