altis-claude-harness 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,8 +10,8 @@ Pre-requisitos: Node.js 18+, Python 3 no PATH, Git para Windows (git-bash), esta
10
10
 
11
11
  npx altis-claude-harness install
12
12
 
13
- Se a estacao nao estiver liberada, o comando mostra os MACs da maquina - envie ao gestor
14
- e rode de novo depois da liberacao.
13
+ Se a estacao nao estiver liberada, o comando mostra a(s) identidade(s) da maquina - envie
14
+ ao gestor e rode de novo depois da liberacao.
15
15
 
16
16
  Pronto. A cada abertura do Claude Code, uma checagem em background (no maximo 1 a cada 4h)
17
17
  compara a versao instalada com o registry npm e aplica atualizacoes sozinha. O conteudo
@@ -24,7 +24,7 @@ backup unico em `.claude.json.altis-bak`. As interacoes com o Claude Code (promp
24
24
 
25
25
  Comandos uteis:
26
26
 
27
- npx altis-claude-harness status # o que esta instalado + MACs da estacao
27
+ npx altis-claude-harness status # o que esta instalado + identidades da estacao
28
28
  npx altis-claude-harness check # ha atualizacao?
29
29
  npx altis-claude-harness update # forca sync agora
30
30
  npx altis-claude-harness uninstall # remove tudo que e gerenciado
@@ -35,10 +35,21 @@ Log do auto-update: `~/.claude/hooks/altis/update.log`.
35
35
 
36
36
  ## Liberacao de estacoes (gestor)
37
37
 
38
- 1. Dev roda `npx altis-claude-harness status` e envia os MACs exibidos.
39
- 2. Adicione a entrada em `payload/hooks/liberacoes.json`:
38
+ 1. Dev roda `npx altis-claude-harness status` e envia as identidades exibidas em
39
+ `identidades da estacao: ...` (MAC no Windows/Linux; no macOS tambem aparecem
40
+ `IOPlatformUUID`/`IOPlatformSerialNumber`, ja que o MAC e randomizado por rede e nao
41
+ identifica a maquina). Alternativa manual no macOS, sem instalar nada:
40
42
 
41
- { "liberadas": [ { "mac": "AA:BB:CC:DD:EE:FF", "id": "dev01" } ] }
43
+ ioreg -rd1 -c IOPlatformExpertDevice | grep -E "IOPlatformUUID|IOPlatformSerialNumber"
44
+
45
+ 2. Adicione a entrada em `payload/hooks/liberacoes.json`. Aceita `mac`, `maquina_id`
46
+ (o `IOPlatformUUID`) e `serial` (o `IOPlatformSerialNumber`) - pelo menos um dos tres e
47
+ obrigatorio, pode combinar mais de um na mesma entrada:
48
+
49
+ { "liberadas": [
50
+ { "mac": "AA:BB:CC:DD:EE:FF", "id": "dev01" },
51
+ { "maquina_id": "A1B2C3D4-1111-2222-3333-444455556666", "serial": "C02XL0THJGH5", "id": "dev02-macbook" }
52
+ ] }
42
53
 
43
54
  (use um apelido curto em `id` - o pacote e publico, nao exponha nome completo)
44
55
  3. `npm version patch` + `git push`. A Action publica e as estacoes recebem pelo
package/bin/cli.js CHANGED
@@ -45,12 +45,12 @@ function checkPython() {
45
45
  }
46
46
 
47
47
  function estaLiberada() {
48
- return license.isLiberated(license.readLiberacoes(), license.getStationMacs());
48
+ return license.resolveIdentidade(license.readLiberacoes()).ok;
49
49
  }
50
50
 
51
51
  function msgNaoLiberada() {
52
- const macs = license.getStationMacs();
53
- return `estacao nao liberada para o ${PKG_NAME}. Informe ao gestor o(s) MAC(s): ${macs.join(', ') || 'nenhum detectado'}`;
52
+ const ids = license.getStationIds();
53
+ return `estacao nao liberada para o ${PKG_NAME}. Informe ao gestor a(s) identidade(s): ${ids.join(', ') || 'nenhuma detectada'}`;
54
54
  }
55
55
 
56
56
  function runSync(target) {
@@ -121,7 +121,7 @@ function cmdCheck(target) {
121
121
  }
122
122
 
123
123
  function cmdStatus(target) {
124
- console.log(`macs da estacao: ${license.getStationMacs().join(', ') || 'nenhum detectado'}`);
124
+ console.log(`identidades da estacao: ${license.getStationIds().join(', ') || 'nenhuma detectada'}`);
125
125
  const m = readManifest(target);
126
126
  if (!m) {
127
127
  console.log(`nao instalado em ${target}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altis-claude-harness",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Padroes Claude Code da Altis Sistemas - instalador, auto-update e liberacao por estacao",
5
5
  "bin": {
6
6
  "altis-claude-harness": "bin/cli.js"
@@ -17,9 +17,7 @@
17
17
  { "mac": "50:BB:B5:7A:41:28", "id": "matheus", "funcionario_id": 35 },
18
18
 
19
19
  { "mac": "28:11:A8:83:BA:0D", "id": "nicolas", "funcionario_id": 28 },
20
-
21
- { "mac": "E8:B0:C5:37:CC:6A", "id": "luiz", "funcionario_id": 6 },
22
-
20
+
23
21
  { "mac": "5C:9B:A6:63:3C:74", "id": "vinicius-macbook", "funcionario_id": 7 },
24
22
  { "mac": "2A:10:61:A5:18:F0", "id": "vinicius-macbook", "funcionario_id": 7 },
25
23
  { "mac": "DE:50:D9:92:D5:8F", "id": "vinicius", "funcionario_id": 7 },
@@ -29,6 +27,8 @@
29
27
 
30
28
  { "mac": "A2:51:13:A5:DD:BF", "id": "kamusx2", "funcionario_id": 2 },
31
29
  { "mac": "12:57:E7:8E:6A:E3", "id": "kamusx2", "funcionario_id": 2 },
30
+ { "maquina_id": "F5E1932E-0A25-5226-9DE6-FD78A775B330", "id": "kamusx2-macbook", "funcionario_id": 2 },
31
+ { "serial": "J6C7P4J575", "id": "kamusx2-macbook", "funcionario_id": 2 },
32
32
 
33
33
  { "mac": "08:00:27:C6:34:7D", "id": "GeradorExe", "funcionario_id": 1 }
34
34
  ]
@@ -3,13 +3,18 @@
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
- const { execSync } = require('child_process');
6
+ const { execSync, execFileSync } = require('child_process');
7
7
 
8
8
  const TARGET_DIR = process.env.ALTIS_CLAUDE_TARGET || path.resolve(__dirname, '..', '..');
9
9
  const MANIFEST = path.join(TARGET_DIR, 'altis-claude.manifest.json');
10
10
  const LIBERACOES = process.env.ALTIS_CLAUDE_LIBERACOES || path.join(__dirname, 'liberacoes.json');
11
11
  const PKG_NAME = 'altis-claude-harness';
12
12
  const FRESH_MS = 24 * 60 * 60 * 1000;
13
+ const PREFIXO_MAC = 'MAC:';
14
+ const PREFIXO_UUID = 'UUID:';
15
+ const PREFIXO_SERIAL = 'SERIAL:';
16
+ const RE_UUID = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/;
17
+ const RE_SERIAL = /^[0-9A-Z]{8,16}$/;
13
18
 
14
19
  function normalizeMac(mac) {
15
20
  const hex = String(mac).replace(/[^0-9a-fA-F]/g, '').toUpperCase();
@@ -17,6 +22,20 @@ function normalizeMac(mac) {
17
22
  return hex.match(/.{2}/g).join(':');
18
23
  }
19
24
 
25
+ function texto(valor) {
26
+ return valor == null ? '' : String(valor).trim().toUpperCase();
27
+ }
28
+
29
+ function normalizeUuid(valor) {
30
+ const v = texto(valor);
31
+ return RE_UUID.test(v) ? v : null;
32
+ }
33
+
34
+ function normalizeSerial(valor) {
35
+ const v = texto(valor);
36
+ return RE_SERIAL.test(v) ? v : null;
37
+ }
38
+
20
39
  function getStationMacs() {
21
40
  const macs = new Set();
22
41
  for (const list of Object.values(os.networkInterfaces())) {
@@ -29,10 +48,60 @@ function getStationMacs() {
29
48
  return [...macs];
30
49
  }
31
50
 
51
+ const RE_IOREG_UUID = /"IOPlatformUUID"\s*=\s*"([^"]*)"/;
52
+ const RE_IOREG_SERIAL = /"IOPlatformSerialNumber"\s*=\s*"([^"]*)"/;
53
+
54
+ function execIoreg() {
55
+ return execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
56
+ encoding: 'utf8',
57
+ timeout: 3000,
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ windowsHide: true,
60
+ });
61
+ }
62
+
63
+ // macOS randomiza o MAC por SSID (Private Wi-Fi Address), entao o MAC nao identifica a
64
+ // estacao. IOPlatformUUID e IOPlatformSerialNumber sao estaveis, legiveis sem sudo e
65
+ // independem de rede. Fail-open: qualquer falha vira lista vazia e a decisao cai nos MACs.
66
+ function getMacIds(opts) {
67
+ const o = opts || {};
68
+ const platform = o.platform || process.platform;
69
+ if (platform !== 'darwin') return [];
70
+ let saida;
71
+ try {
72
+ saida = String((o.exec || execIoreg)());
73
+ } catch {
74
+ return [];
75
+ }
76
+ const ids = [];
77
+ const mUuid = RE_IOREG_UUID.exec(saida);
78
+ const uuid = mUuid && normalizeUuid(mUuid[1]);
79
+ if (uuid) ids.push(PREFIXO_UUID + uuid);
80
+ const mSerial = RE_IOREG_SERIAL.exec(saida);
81
+ const serial = mSerial && normalizeSerial(mSerial[1]);
82
+ if (serial) ids.push(PREFIXO_SERIAL + serial);
83
+ return ids;
84
+ }
85
+
86
+ const CAMPOS_IDENTIDADE = [
87
+ ['mac', normalizeMac, PREFIXO_MAC],
88
+ ['maquina_id', normalizeUuid, PREFIXO_UUID],
89
+ ['serial', normalizeSerial, PREFIXO_SERIAL],
90
+ ];
91
+
32
92
  function readLiberacoes(filePath = LIBERACOES) {
33
93
  try {
34
94
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
35
- return (data.liberadas || []).map(e => normalizeMac(e.mac)).filter(Boolean);
95
+ const ids = [];
96
+ for (const e of data.liberadas || []) {
97
+ if (!e || typeof e !== 'object') continue;
98
+ for (const [campo, normaliza, prefixo] of CAMPOS_IDENTIDADE) {
99
+ if (e[campo] === undefined) continue;
100
+ const v = normaliza(e[campo]);
101
+ if (v) ids.push(prefixo + v);
102
+ }
103
+ }
104
+ return ids;
36
105
  } catch {
37
106
  return [];
38
107
  }
@@ -43,6 +112,23 @@ function isLiberated(liberadas, stationMacs) {
43
112
  return stationMacs.some(m => set.has(m));
44
113
  }
45
114
 
115
+ function getStationIds(opts) {
116
+ return [
117
+ ...getStationMacs().map(m => PREFIXO_MAC + m),
118
+ ...getMacIds(opts),
119
+ ];
120
+ }
121
+
122
+ // Curto-circuito de performance: o guard roda a cada prompt /altis* e a cada Task/Skill
123
+ // gerenciada. Quando o MAC ja libera — o caso de toda estacao Windows e Linux — a chamada
124
+ // externa nem acontece.
125
+ function resolveIdentidade(liberadas, opts) {
126
+ const macs = getStationMacs().map(m => PREFIXO_MAC + m);
127
+ if (isLiberated(liberadas, macs)) return { ok: true, ids: macs };
128
+ const ids = [...macs, ...getMacIds(opts)];
129
+ return { ok: isLiberated(liberadas, ids), ids };
130
+ }
131
+
46
132
  function isFresh(manifest, nowMs) {
47
133
  if (!manifest || !manifest.lastSuccessAt) return false;
48
134
  const t = Date.parse(manifest.lastSuccessAt);
@@ -81,11 +167,11 @@ function tryRevalidate() {
81
167
  }
82
168
 
83
169
  function licenseDecision() {
84
- const macs = getStationMacs();
85
- if (!isLiberated(readLiberacoes(), macs)) {
170
+ const { ok, ids } = resolveIdentidade(readLiberacoes());
171
+ if (!ok) {
86
172
  return {
87
173
  ok: false,
88
- reason: `Estacao nao liberada para os comandos Altis. Informe ao gestor o(s) MAC(s): ${macs.join(', ') || 'nenhum detectado'}`,
174
+ reason: `Estacao nao liberada para os comandos Altis. Informe ao gestor a(s) identidade(s): ${ids.join(', ') || 'nenhuma detectada'}`,
89
175
  };
90
176
  }
91
177
  if (!isFresh(readManifest(), Date.now()) && !tryRevalidate()) {
@@ -149,5 +235,10 @@ if (require.main === module) {
149
235
  } catch {}
150
236
  process.exit(0);
151
237
  } else {
152
- module.exports = { normalizeMac, getStationMacs, readLiberacoes, isLiberated, isFresh, FRESH_MS };
238
+ module.exports = {
239
+ normalizeMac, normalizeUuid, normalizeSerial,
240
+ getStationMacs, getMacIds, getStationIds, resolveIdentidade,
241
+ readLiberacoes, isLiberated, isFresh, FRESH_MS,
242
+ PREFIXO_MAC, PREFIXO_UUID, PREFIXO_SERIAL,
243
+ };
153
244
  }
@@ -65,6 +65,26 @@ function macsDaEstacao() {
65
65
  return [...macs];
66
66
  }
67
67
 
68
+ // license_guard.cjs vive ao lado deste hook (~/.claude/hooks/altis/ em runtime, payload/hooks/
69
+ // no repositorio) e so executa seu main quando require.main === module, entao importa-lo aqui
70
+ // e seguro. Fail-open: qualquer falha no require deixa licenseGuard null e a identificacao cai
71
+ // de volta no comportamento historico (casar so por MAC).
72
+ let licenseGuard = null;
73
+ try {
74
+ licenseGuard = require('./license_guard.cjs');
75
+ } catch {
76
+ licenseGuard = null;
77
+ }
78
+
79
+ function idsExtrasDaEstacao(opts) {
80
+ if (!licenseGuard || typeof licenseGuard.getMacIds !== 'function') return [];
81
+ try {
82
+ return licenseGuard.getMacIds(opts);
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
68
88
  function lerLiberacoes() {
69
89
  try {
70
90
  const arquivo = path.join(__dirname, 'liberacoes.json');
@@ -75,12 +95,33 @@ function lerLiberacoes() {
75
95
  }
76
96
  }
77
97
 
78
- function identificarUsuario(macsEstacao, liberadas) {
98
+ // Casa a estacao com uma entrada de liberacoes.json por mac (historico) ou, se nao bater, por
99
+ // maquina_id/serial (macOS) — necessario porque o macOS randomiza o MAC por rede e a estacao
100
+ // pode estar cadastrada so por identidade de hardware. O campo "mac" do retorno e contrato de
101
+ // telemetria e sempre reflete o MAC da estacao (o primeiro, quando o match e por UUID/serial);
102
+ // quem identifica o usuario e a entrada casada, nao o campo mac.
103
+ function identificarUsuario(macsEstacao, liberadas, opts) {
104
+ const idsExtras = new Set(idsExtrasDaEstacao(opts));
79
105
  for (const entrada of liberadas) {
80
- const macEntrada = normalizarMac(entrada && entrada.mac);
81
- if (macEntrada && macsEstacao.includes(macEntrada)) {
106
+ if (!entrada || typeof entrada !== 'object') continue;
107
+ const macEntrada = normalizarMac(entrada.mac);
108
+ let macBate = macEntrada && macsEstacao.includes(macEntrada);
109
+
110
+ let idBate = false;
111
+ if (!macBate && licenseGuard && idsExtras.size > 0) {
112
+ if (entrada.maquina_id !== undefined) {
113
+ const uuid = licenseGuard.normalizeUuid(entrada.maquina_id);
114
+ if (uuid && idsExtras.has(licenseGuard.PREFIXO_UUID + uuid)) idBate = true;
115
+ }
116
+ if (!idBate && entrada.serial !== undefined) {
117
+ const serial = licenseGuard.normalizeSerial(entrada.serial);
118
+ if (serial && idsExtras.has(licenseGuard.PREFIXO_SERIAL + serial)) idBate = true;
119
+ }
120
+ }
121
+
122
+ if (macBate || idBate) {
82
123
  return {
83
- mac: macEntrada,
124
+ mac: macBate ? macEntrada : (macsEstacao[0] || null),
84
125
  nome_usuario: entrada.id ? String(entrada.id).slice(0, 60) : null,
85
126
  funcionario_id: Number.isInteger(entrada.funcionario_id) ? entrada.funcionario_id : null,
86
127
  };