@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 +3 -0
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/dist/adapters/codex-controller-sensor.js +40 -13
- package/dist/index.js +2 -0
- package/dist/preflight.js +13 -0
- package/dist/session-watcher.js +11 -4
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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 {
|
package/dist/session-watcher.js
CHANGED
|
@@ -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),
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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",
|