@orkastery/cli 0.4.1 → 0.4.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
@@ -13,6 +13,9 @@ append-only ledger.
13
13
  it again on an independent runner before the merge.
14
14
  - **You choose how often you are called.** One mode per #TAG, in the request itself, decides
15
15
  where the cycle pauses. Verification is the same in all of them.
16
+ - **Decisions sized for a full day.** Every question `ork` brings you is short, with alternatives
17
+ and one recommendation, so a person running dozens of things at once, including people with
18
+ ADHD, decides in seconds.
16
19
  - **Subscription, never pay-per-token.** Dispatch uses the runtime CLI's own login. `ork` never
17
20
  reads credentials, and the `subscription-only` policy blocks paid providers.
18
21
 
@@ -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.",
5
- "version": "0.4.1",
5
+ "version": "0.4.2",
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.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Plugin Orkastery para OpenClaw: 25 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,
@@ -70,23 +70,47 @@ function conferirDiretorioDaFonte(dir, dirSessoes, uid) {
70
70
  }
71
71
  return real;
72
72
  }
73
+ /**
74
+ * Quantas vezes a conferência lstat/abertura se repete quando o inode muda entre as duas. O worker
75
+ * grava o state.json por arquivo temporário e rename: cada gravação é um inode novo, e sob carga
76
+ * ela cai entre o lstat e o open. Só se aceita descritor cujo inode é o conferido; o limite impede
77
+ * laço sem fim contra quem troca o arquivo sem parar.
78
+ */
79
+ const TENTATIVAS_DE_INODE = 5;
73
80
  /** Abre sem seguir link, confere descritor/inode, modo, dono e limite antes de decodificar. */
74
81
  function lerMetadado(dir, nome, uid, obrigatorio = true) {
75
82
  const file = path.join(dir, nome);
76
- let antes;
77
- try {
78
- antes = fs.lstatSync(file);
79
- }
80
- catch (e) {
81
- if (e.code === 'ENOENT' && !obrigatorio)
82
- return null;
83
- return recusar(`metadado ${nome} de controller ausente`);
83
+ for (let tentativa = 1;; tentativa++) {
84
+ let antes;
85
+ try {
86
+ antes = fs.lstatSync(file);
87
+ }
88
+ catch (e) {
89
+ if (e.code === 'ENOENT' && !obrigatorio)
90
+ return null;
91
+ return recusar(`metadado ${nome} de controller ausente`);
92
+ }
93
+ if (!antes.isFile() || antes.isSymbolicLink())
94
+ recusar(`metadado ${nome} de controller não é arquivo regular`);
95
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
96
+ let st;
97
+ try {
98
+ st = fs.fstatSync(fd);
99
+ }
100
+ catch (e) {
101
+ fs.closeSync(fd);
102
+ throw e;
103
+ }
104
+ if (st.isFile() && (st.ino !== antes.ino || st.dev !== antes.dev) && tentativa < TENTATIVAS_DE_INODE) {
105
+ // Substituído por rename entre o lstat e o open: confere e abre de novo, do zero.
106
+ fs.closeSync(fd);
107
+ continue;
108
+ }
109
+ return lerDescritorConferido(fd, st, antes, nome, uid);
84
110
  }
85
- if (!antes.isFile() || antes.isSymbolicLink())
86
- recusar(`metadado ${nome} de controller não é arquivo regular`);
87
- const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
111
+ }
112
+ function lerDescritorConferido(fd, st, antes, nome, uid) {
88
113
  try {
89
- const st = fs.fstatSync(fd);
90
114
  if (!st.isFile() || st.ino !== antes.ino || st.dev !== antes.dev)
91
115
  recusar(`descritor de ${nome} divergente do inode conferido`);
92
116
  if (st.uid !== uid)
@@ -395,11 +419,14 @@ function registrarFonteController(dir, esperado) {
395
419
  }
396
420
  /** Releitura da fonte fixada. Nunca descobre outro processo, nem aceita override de rollout. */
397
421
  function lerSnapshotController(fonte, opcoes = {}) {
398
- const agora = opcoes.agoraMs ?? Date.now();
399
422
  const real = conferirDiretorioDaFonte(fonte.dir, path.dirname(fonte.dir), fonte.uid);
400
423
  if (real !== fonte.dir)
401
424
  recusar('fonte de controller mudou de caminho real');
402
425
  const estado = conferirEstado(lerMetadado(real, 'state.json', fonte.uid), fonte);
426
+ // O relógio de referência é o do fim da leitura. O worker grava o close quando o runtime fecha,
427
+ // e isso pode acontecer entre o início desta releitura e a leitura do state.json: esse close não
428
+ // está no futuro. Com o relógio lido antes, ele era recusado e o watcher morria sem phase_result.
429
+ const agora = opcoes.agoraMs ?? Date.now();
403
430
  if (!mesmaIdentidade(identidadeValida(estado.processoController, fonte.uid), fonte.processoController))
404
431
  recusar('identidade do controller divergente da registrada');
405
432
  if (!mesmaIdentidade(identidadeValida(estado.processoRuntime, fonte.uid), fonte.processoRuntime))
package/dist/index.js CHANGED
@@ -144,6 +144,8 @@ const licoes_1 = require("./licoes");
144
144
  const demo_1 = require("./demo");
145
145
  const entrega_pr_1 = require("./entrega-pr");
146
146
  /** A versao publicada em `@orkastery/cli`, lida do package.json (`versao.ts`). */
147
+ // Antes de qualquer arquivo ou despacho: sem escrita de grupo nem de outros, que o sensor recusaria.
148
+ (0, preflight_1.endurecerUmask)();
147
149
  const VERSAO = versao_1.VERSAO_DO_ORK;
148
150
  /** Parse simples de argumentos: `--chave valor`, `--chave=valor` e flags booleanas. */
149
151
  function parseArgs(argv) {
package/dist/preflight.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.endurecerUmask = endurecerUmask;
36
37
  exports.umaskDoProcesso = umaskDoProcesso;
37
38
  exports.sondasDeAmbiente = sondasDeAmbiente;
38
39
  exports.checkDeContas = checkDeContas;
@@ -61,6 +62,18 @@ const sensoresPadrao = {
61
62
  sandboxCodex: () => (0, codex_2.sondarSandbox)().ok,
62
63
  auth: authPadrao,
63
64
  };
65
+ /**
66
+ * O sensor recusa fonte com escrita de grupo ou de outros (bits 0o022). O ork acrescenta esses bits
67
+ * ao umask do proprio processo, sem nunca afrouxar um umask mais restrito (077 continua 077): tudo o
68
+ * que ele cria, e tudo o que ele despacha (claude, codex), nasce sem escrita alheia, qualquer que
69
+ * seja o umask do shell de quem roda. Devolve o umask anterior e o que ficou valendo.
70
+ */
71
+ function endurecerUmask() {
72
+ const anterior = process.umask(0o077);
73
+ const atual = anterior | 0o022;
74
+ process.umask(atual);
75
+ return { anterior, atual };
76
+ }
64
77
  /** umask efetivo do processo, lido de /proc (Linux) para nao usar o getter deprecado. */
65
78
  function umaskDoProcesso() {
66
79
  try {
@@ -183,6 +183,11 @@ function comLockWatcher(dir, action) {
183
183
  }
184
184
  }
185
185
  }
186
+ /**
187
+ * `agoraMs` só vem quando o relógio é injetado. Sem ele, a referência de "evento do futuro" é o
188
+ * relógio depois da leitura: a linha que o runtime anexou durante a observação não é futura, e
189
+ * descartá-la perdia o terminal do turno.
190
+ */
186
191
  function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
187
192
  const fd = fs.openSync(file, fixada
188
193
  ? fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK : 'r');
@@ -206,6 +211,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
206
211
  let offset = mesmo ? anterior.offset : 0;
207
212
  const buffer = Buffer.alloc(Math.min(exports.BLOCO_WATCH_BYTES, stat.size - offset));
208
213
  const n = fs.readSync(fd, buffer, 0, buffer.length, offset);
214
+ const agora = agoraMs ?? Date.now();
209
215
  const fim = buffer.subarray(0, n).lastIndexOf(10);
210
216
  let terminal = mesmo ? anterior.terminal : null;
211
217
  if (fim >= 0) {
@@ -221,7 +227,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
221
227
  (e?.type === 'session_meta' && e.payload?.id !== sessionId))
222
228
  throw new Error('log/rollout pertence a outra sessão');
223
229
  }
224
- for (const t of parser.push(buffer.subarray(0, fim + 1), agoraMs))
230
+ for (const t of parser.push(buffer.subarray(0, fim + 1), agora))
225
231
  terminal = t;
226
232
  offset += fim + 1;
227
233
  }
@@ -235,7 +241,7 @@ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
235
241
  const parcial = n < exports.BLOCO_WATCH_BYTES && stat.size > offset;
236
242
  return { cursor: { file, ino: stat.ino, tamanho: stat.size, offset, parser: parser.estado, terminal, parcial },
237
243
  cresceu: parser.eventosValidos > 0,
238
- quando: Math.min(agoraMs, stat.mtimeMs), bytes: n, temMais: stat.size > offset };
244
+ quando: Math.min(agora, stat.mtimeMs), bytes: n, temMais: stat.size > offset };
239
245
  }
240
246
  finally {
241
247
  fs.closeSync(fd);
@@ -352,7 +358,8 @@ function observarSessao(carregado, sessionId, opcoes = {}) {
352
358
  fixacao: path.join(dir, 'sessoes', `watcher-source-${chave}.json`),
353
359
  vinculo: { thread: thread.id, fase: sessao.fase, promptSha256: sessao.promptSha256 }
354
360
  });
355
- snapshot = (0, codex_controller_sensor_1.lerSnapshotController)(fonteControlador, { agoraMs: now });
361
+ // Relógio injetado vale como está; o real é lido pelo sensor depois do state.json.
362
+ snapshot = (0, codex_controller_sensor_1.lerSnapshotController)(fonteControlador, opcoes.agoraMs === undefined ? {} : { agoraMs: now });
356
363
  }
357
364
  const file = path.join(pasta, 'cursor.json');
358
365
  const cursor = lerJson(file) ?? { schema: 'ork.session-cursor/v1', sessionId,
@@ -370,7 +377,7 @@ function observarSessao(carregado, sessionId, opcoes = {}) {
370
377
  continue;
371
378
  let leitura;
372
379
  try {
373
- leitura = lerFonte(fonte, cursor.fontes[nome], now, sessionId, nome === 'rollout' ? fonteControlador ?? undefined : undefined);
380
+ leitura = lerFonte(fonte, cursor.fontes[nome], opcoes.agoraMs === undefined ? undefined : now, sessionId, nome === 'rollout' ? fonteControlador ?? undefined : undefined);
374
381
  }
375
382
  catch (e) {
376
383
  if (nome !== 'rollout' || e.message !== 'log/rollout pertence a outra sessão')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",