auroq-os 2.5.7 → 2.5.9
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/.auroq-core/core-config.yaml +1 -1
- package/.claude/hooks/ahi/auth-config.cjs +3 -0
- package/.claude/hooks/ahi/runtime.cjs +226 -0
- package/.claude/hooks/ahi/security.cjs +162 -0
- package/.claude/hooks/ahi/worker.cjs +252 -0
- package/.claude/hooks/ahi-capture.cjs +32 -126
- package/.claude/settings.local.json +83 -6
- package/CHANGELOG.md +19 -0
- package/README.md +66 -4
- package/bin/auroq-os.js +38 -8
- package/installer/README.md +1 -1
- package/installer/install.ps1 +2 -2
- package/installer/install.sh +2 -2
- package/lib/ahi/client.mjs +134 -0
- package/lib/ahi/historico.mjs +10 -36
- package/lib/ahi/install.cjs +20 -0
- package/package.json +5 -3
- package/scripts/assert-mcp-pin.mjs +31 -0
- package/scripts/assert-native-release.mjs +55 -0
- package/scripts/build-pack.mjs +13 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const http = require('node:http');
|
|
6
|
+
const https = require('node:https');
|
|
7
|
+
const R = require('./runtime.cjs');
|
|
8
|
+
const S = require('./security.cjs');
|
|
9
|
+
function baseUrl() {
|
|
10
|
+
if (process.env.NODE_ENV === 'test' && process.env.AHI_TEST_MODE === '1' && process.env.AHI_TEST_URL) {
|
|
11
|
+
const url = new URL(process.env.AHI_TEST_URL);
|
|
12
|
+
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) throw new Error('UNSAFE_TEST_ENDPOINT');
|
|
13
|
+
return url.origin;
|
|
14
|
+
}
|
|
15
|
+
return 'https://arcane.arka.education';
|
|
16
|
+
}
|
|
17
|
+
function request(url, token, body, extraHeaders = {}) {
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
let finished = false; let timer;
|
|
20
|
+
const finish = value => { if (finished) return; finished = true; clearTimeout(timer); resolve(value); };
|
|
21
|
+
const target = new URL(url); const data = body === undefined ? null : JSON.stringify(body);
|
|
22
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
23
|
+
const req = transport.request(target, { method: data ? 'POST' : 'GET', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {}), ...extraHeaders }, timeout: 4000 }, res => {
|
|
24
|
+
let bytes = 0; let text = '';
|
|
25
|
+
res.on('data', chunk => { bytes += chunk.length; if (bytes > 128 * 1024) { req.destroy(); finish({ code: 0, error: 'RESPONSE_TOO_LARGE' }); } else text += chunk; });
|
|
26
|
+
res.on('end', () => { let json; try { json = JSON.parse(text); } catch {} const numericRetry = Number(res.headers['retry-after']); const retry = Number.isFinite(numericRetry) ? numericRetry * 1000 : Date.parse(res.headers['retry-after']) - Date.now(); finish({ code: res.statusCode, body: json, retryMs: Number.isFinite(retry) ? Math.min(300000, Math.max(0, retry)) : 0 }); });
|
|
27
|
+
});
|
|
28
|
+
req.on('error', () => finish({ code: 0, error: 'NETWORK_ERROR' }));
|
|
29
|
+
req.on('timeout', () => { req.destroy(); finish({ code: 0, error: 'NETWORK_TIMEOUT' }); });
|
|
30
|
+
timer = setTimeout(() => { req.destroy(); finish({ code: 0, error: 'NETWORK_TIMEOUT' }); }, 4000);
|
|
31
|
+
if (data) req.write(data); req.end();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function getCredential(ctx) {
|
|
35
|
+
let cred = R.credential();
|
|
36
|
+
if (!cred || cred.user.id !== ctx.authId) return null;
|
|
37
|
+
if (Number(cred.expires_at) > Date.now() / 1000 + 60) return cred;
|
|
38
|
+
if (!cred.refresh_token) return cred;
|
|
39
|
+
if (!fs.existsSync(ctx.accountDir)) S.protect(ctx.accountDir);
|
|
40
|
+
const release = R.lock({ ...ctx, dir: ctx.accountDir }, 'auth.lock', 0);
|
|
41
|
+
if (!release) return cred;
|
|
42
|
+
try {
|
|
43
|
+
cred = R.credential();
|
|
44
|
+
if (!cred || cred.user.id !== ctx.authId) return null;
|
|
45
|
+
if (Number(cred.expires_at) > Date.now() / 1000 + 60) return cred;
|
|
46
|
+
const auth = require('./auth-config.cjs');
|
|
47
|
+
const endpoint = process.env.NODE_ENV === 'test' && process.env.AHI_TEST_MODE === '1' ? `${baseUrl()}/auth/v1/token?grant_type=refresh_token` : `${auth.url}/auth/v1/token?grant_type=refresh_token`;
|
|
48
|
+
const response = await request(endpoint, null, { refresh_token: cred.refresh_token }, { apikey: auth.publicKey });
|
|
49
|
+
if (response.code !== 200 || typeof response.body?.access_token !== 'string' || typeof response.body?.refresh_token !== 'string' || response.body?.user?.id !== ctx.authId) return R.credential()?.user?.id === ctx.authId ? R.credential() : null;
|
|
50
|
+
// MCP can also refresh this shared login. Reread after network and preserve a newer token/account.
|
|
51
|
+
const current = R.credential();
|
|
52
|
+
if (!current || current.user.id !== ctx.authId) return null;
|
|
53
|
+
if (current.access_token !== cred.access_token || current.refresh_token !== cred.refresh_token) return current;
|
|
54
|
+
const updated = { ...current, access_token: response.body.access_token, refresh_token: response.body.refresh_token, expires_at: response.body.expires_at || Math.floor(Date.now() / 1000) + response.body.expires_in };
|
|
55
|
+
S.atomic(path.join(os.homedir(), '.arcane', 'credentials.json'), updated);
|
|
56
|
+
const saved = R.credential(); return saved?.user?.id === ctx.authId ? saved : null;
|
|
57
|
+
} finally { release(); }
|
|
58
|
+
}
|
|
59
|
+
function discard(ctx, files, reason) {
|
|
60
|
+
let count = 0;
|
|
61
|
+
for (const file of files) { try { S.safePath(file); fs.unlinkSync(file); R.removeOrder(ctx, path.basename(file, '.json')); count++; } catch {} }
|
|
62
|
+
if (count) R.record(ctx, 'discarded', reason, count);
|
|
63
|
+
}
|
|
64
|
+
async function lease(ctx, session) {
|
|
65
|
+
if (!R.configured(ctx.project, ctx.host)) return { stop: true, reason: 'MCP_DISABLED' };
|
|
66
|
+
const leaseFile = path.join(ctx.dir, `lease-${S.hash(`${ctx.host}:${session}`)}.json`);
|
|
67
|
+
const cred = await getCredential(ctx); if (!cred) { S.atomic(leaseFile, { enabled: false }); return { stop: true, reason: 'ACCOUNT_CHANGED' }; }
|
|
68
|
+
const response = await request(`${baseUrl()}/api/ahi/v1/capture-status?${new URLSearchParams({ project_key: ctx.projectKey, host: ctx.host, native_session_id: session, adapter_version: R.VERSION })}`, cred.access_token);
|
|
69
|
+
const body = response.body;
|
|
70
|
+
if (response.code === 200 && body?.enabled === true && body.auth_user_id === ctx.authId) {
|
|
71
|
+
const expiry = Date.parse(body.expires_at); const duration = Math.min(55000, expiry - Date.now());
|
|
72
|
+
if (!Number.isFinite(duration) || duration <= 0) return { retry: true, reason: 'INVALID_LEASE' };
|
|
73
|
+
const publicKey = S.read(path.join(ctx.dir, 'public-key.json'))?.key;
|
|
74
|
+
if (!publicKey) return { stop: true, reason: 'SECURE_STORAGE_UNAVAILABLE' };
|
|
75
|
+
S.atomic(leaseFile, { enabled: true, host: ctx.host, native_session_id: session, auth_user_id: ctx.authId, subject_id: body.subject_id, policy_version: body.policy_version, key_fingerprint: S.hash(publicKey), until: Date.now() + duration, uptime: os.uptime(), untilUptime: os.uptime() + duration / 1000, boot: R.boot() });
|
|
76
|
+
return { credential: cred };
|
|
77
|
+
}
|
|
78
|
+
S.atomic(leaseFile, { enabled: false });
|
|
79
|
+
if (response.code === 401) return { stop: true, reason: 'AUTH_REQUIRED' };
|
|
80
|
+
if (response.code === 403 || (response.code === 200 && body?.enabled === false)) {
|
|
81
|
+
const scoped = ['DELETED', 'REVOKED'].includes(body?.reason);
|
|
82
|
+
if (scoped) { const sf = path.join(ctx.dir, `session-${S.hash(`${ctx.host}:${session}`)}.json`); const sessionState = S.read(sf, {}); S.atomic(sf, { ...sessionState, active: false, revoked: true }); }
|
|
83
|
+
discard(ctx, R.queueFiles(ctx).filter(f => S.read(f)?.session_key === S.hash(`${ctx.host}:${session}`)), response.code === 403 ? 'LICENSE_REVOKED' : 'CAPTURE_DISABLED');
|
|
84
|
+
return { stop: !scoped, sessionBlocked: scoped, reason: response.code === 403 ? 'LICENSE_REVOKED' : body?.reason || 'CAPTURE_DISABLED' };
|
|
85
|
+
}
|
|
86
|
+
return { retry: true, reason: response.error || 'STATUS_UNAVAILABLE', retryMs: response.retryMs };
|
|
87
|
+
}
|
|
88
|
+
function markTerminal(ctx, file, eventId, kind, reason) {
|
|
89
|
+
// Write tombstone before deleting ciphertext: crash/restart will finish cleanup, not replay it.
|
|
90
|
+
S.atomic(path.join(ctx.dir, `terminal-${eventId}.json`), { kind, reason, at: new Date().toISOString() });
|
|
91
|
+
try { fs.unlinkSync(file); R.removeOrder(ctx, eventId); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
92
|
+
}
|
|
93
|
+
async function drain(ctx, cred) {
|
|
94
|
+
// Configuration is checked after selecting the queued host below.
|
|
95
|
+
const fresh = await getCredential(ctx); if (!fresh || (cred && fresh.user.id !== cred.user.id)) return { stop: true, reason: 'ACCOUNT_CHANGED' };
|
|
96
|
+
const files = R.queueFiles(ctx).slice(0, 50);
|
|
97
|
+
if (!files.length) return { empty: true };
|
|
98
|
+
const key = S.keyFor(ctx);
|
|
99
|
+
const rows = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const eventId = path.basename(file, '.json');
|
|
103
|
+
if (S.read(path.join(ctx.dir, `terminal-${eventId}.json`))) { fs.unlinkSync(file); R.removeOrder(ctx, eventId); continue; }
|
|
104
|
+
try {
|
|
105
|
+
const row = S.open(S.read(file), key, ctx.scope);
|
|
106
|
+
if (row.project_key !== ctx.projectKey || row.event.event_id !== eventId) throw new Error('QUEUE_IDENTITY_MISMATCH');
|
|
107
|
+
if (Date.now() - row.captured_at > R.TTL) {
|
|
108
|
+
const gapId = S.id(`${eventId}:ttl`);
|
|
109
|
+
const gap = { ...row, captured_at: Date.now(), event: { event_id: gapId, source_event_id: `CoverageGap:${S.hash(gapId)}`, type: 'coverage.gap', occurred_at: new Date().toISOString(), evidence_kind: 'observed', redacted: false, truncated: true, payload: { reason: 'QUEUE_TTL', excluded_count: 1 } } };
|
|
110
|
+
const publicKey = S.read(path.join(ctx.dir, 'public-key.json'))?.key;
|
|
111
|
+
if (publicKey) R.queueRow(ctx, gap, publicKey);
|
|
112
|
+
markTerminal(ctx, file, eventId, 'discarded', 'QUEUE_TTL'); R.record(ctx, 'discarded', 'QUEUE_TTL'); continue;
|
|
113
|
+
}
|
|
114
|
+
rows.push({ file, row });
|
|
115
|
+
} catch {
|
|
116
|
+
// Preserve corrupt ciphertext for manual diagnosis; stop repeated upload attempts.
|
|
117
|
+
fs.renameSync(file, `${file}.corrupt`); R.removeOrder(ctx, eventId); R.record(ctx, 'excluded', 'QUEUE_CORRUPT');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} finally { key.fill(0); }
|
|
121
|
+
rows.sort((a, b) => a.row.captured_at - b.row.captured_at || (a.row.capture_order || 0) - (b.row.capture_order || 0) || a.row.event.event_id.localeCompare(b.row.event.event_id));
|
|
122
|
+
if (!rows.length) return { empty: true };
|
|
123
|
+
const first = rows[0].row;
|
|
124
|
+
ctx.host = first.host;
|
|
125
|
+
if (!R.configured(ctx.project, first.host)) { discard(ctx, rows.filter(x => x.row.host === first.host).map(x => x.file), 'MCP_DISABLED'); return { sent: 0 }; }
|
|
126
|
+
if (!R.validLease(ctx, first.native_session_id)) { const check = await lease(ctx, first.native_session_id); if (check.stop || check.retry || check.sessionBlocked) return check.sessionBlocked ? { sent: 0 } : check; }
|
|
127
|
+
const selected = rows.filter(x => x.row.native_session_id === first.native_session_id && x.row.host === first.host).slice(0, 6);
|
|
128
|
+
const envelope = { schema_version: 1, host: first.host, adapter_version: R.VERSION, project_key: ctx.projectKey, native_session_id: first.native_session_id, events: selected.map(x => x.row.event) };
|
|
129
|
+
const response = await request(`${baseUrl()}/api/ahi/v1/ingest`, fresh.access_token, envelope);
|
|
130
|
+
if (response.code === 401) return { stop: true, reason: 'AUTH_REQUIRED' };
|
|
131
|
+
if (response.code === 403 || response.code === 410) {
|
|
132
|
+
discard(ctx, R.queueFiles(ctx).filter(f => S.read(f)?.session_key === S.hash(`${first.host}:${first.native_session_id}`)), 'REVOKED');
|
|
133
|
+
if (response.code === 410) { const sf = path.join(ctx.dir, `session-${S.hash(`${first.host}:${first.native_session_id}`)}.json`); S.atomic(sf, { ...S.read(sf, {}), active: false, revoked: true }); return { sent: 0 }; }
|
|
134
|
+
return { stop: true, reason: 'LICENSE_REVOKED' };
|
|
135
|
+
}
|
|
136
|
+
if ([400, 413, 422, 426].includes(response.code)) { discard(ctx, selected.map(x => x.file), response.code === 426 ? 'UPGRADE_REQUIRED' : 'INVALID_ENVELOPE'); return { stop: true, reason: response.code === 426 ? 'UPGRADE_REQUIRED' : 'INVALID_ENVELOPE' }; }
|
|
137
|
+
if (response.code < 200 || response.code >= 300 || !Array.isArray(response.body?.accepted_ids) || !Array.isArray(response.body?.duplicate_ids)) return { retry: true, reason: response.error || 'INGEST_RETRY', retryMs: response.retryMs };
|
|
138
|
+
const receipt = response.body;
|
|
139
|
+
const ack = new Set([...receipt.accepted_ids, ...receipt.duplicate_ids]);
|
|
140
|
+
const rejects = new Map((Array.isArray(receipt.rejected) ? receipt.rejected : []).map(x => [x.id, x]));
|
|
141
|
+
let accepted = 0; let rejected = 0; let pending = 0;
|
|
142
|
+
for (const item of selected) {
|
|
143
|
+
const eventId = item.row.event.event_id;
|
|
144
|
+
if (ack.has(eventId)) { markTerminal(ctx, item.file, eventId, 'acked', 'DURABLE_ACK'); accepted++; }
|
|
145
|
+
else {
|
|
146
|
+
const rejection = rejects.get(eventId);
|
|
147
|
+
if (rejection && ['CONFLICT', 'DELETED', 'REVOKED', 'INVALID', 'INVALID_EVENT', 'EVENT_ID_CONFLICT'].includes(rejection.code)) { markTerminal(ctx, item.file, eventId, 'discarded', rejection.code); rejected++; }
|
|
148
|
+
else pending++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (accepted) {
|
|
152
|
+
const release = R.lock(ctx, 'state.lock');
|
|
153
|
+
if (release) { try { const state = R.state(ctx); R.update(ctx, { acked: state.acked + accepted, last_ack_at: new Date().toISOString(), last_problem: null }); } finally { release(); } }
|
|
154
|
+
}
|
|
155
|
+
if (rejected) R.record(ctx, 'discarded', 'SERVER_TERMINAL_REJECTION', rejected);
|
|
156
|
+
return pending ? { retry: true, reason: 'MISSING_DURABLE_ACK' } : { sent: accepted };
|
|
157
|
+
}
|
|
158
|
+
function cleanup(ctx) {
|
|
159
|
+
// Only v2 ciphertext/metadata generated by this collector; never legacy JSONL or host history.
|
|
160
|
+
for (const name of fs.readdirSync(ctx.outbox)) {
|
|
161
|
+
if (!/\.(corrupt|tmp)$/.test(name)) continue;
|
|
162
|
+
const file = path.join(ctx.outbox, name); const stat = fs.lstatSync(file);
|
|
163
|
+
if (stat.isFile() && Date.now() - stat.mtimeMs > R.TTL) { fs.unlinkSync(file); R.record(ctx, 'discarded', 'QUARANTINE_TTL'); }
|
|
164
|
+
}
|
|
165
|
+
for (const name of fs.readdirSync(ctx.dir)) {
|
|
166
|
+
if (!/^terminal-[a-f0-9-]+\.json$/.test(name)) continue;
|
|
167
|
+
const file = path.join(ctx.dir, name); const receipt = S.read(file);
|
|
168
|
+
if (receipt && Date.now() - Date.parse(receipt.at) > 90 * 24 * 3600 * 1000) fs.unlinkSync(file);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function work(cwd, host, options = {}) {
|
|
172
|
+
const ctx = R.context(cwd, host, true); if (!ctx) return { reason: 'AUTH_REQUIRED' };
|
|
173
|
+
const release = R.lock(ctx, 'worker.lock', 0); if (!release) return { reason: 'WORKER_RUNNING' };
|
|
174
|
+
const started = Date.now(); let attempts = 0;
|
|
175
|
+
const wait = options.wait || (ms => new Promise(resolve => {
|
|
176
|
+
let timer; const finish = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', finish); resolve(); };
|
|
177
|
+
timer = setTimeout(finish, ms); options.signal?.addEventListener('abort', finish, { once: true });
|
|
178
|
+
if (options.signal?.aborted) finish();
|
|
179
|
+
}));
|
|
180
|
+
let nextAttemptAt = options.once ? 0 : Date.parse(R.state(ctx).next_retry_at || '') || 0; let leaseRetryAt = 0;
|
|
181
|
+
try {
|
|
182
|
+
// All OS ACL/keychain/DPAPI work is off the host's critical path.
|
|
183
|
+
S.protectMany([path.dirname(ctx.accountDir), ctx.accountDir, ctx.dir, ctx.outbox, ctx.orderDir]);
|
|
184
|
+
const seed = S.keyFor(ctx, true); const publicKey = S.publicKey(seed); seed.fill(0);
|
|
185
|
+
S.atomic(path.join(ctx.dir, 'public-key.json'), { algorithm: 'X25519-HKDF-SHA256-AES256GCM', key: publicKey });
|
|
186
|
+
cleanup(ctx);
|
|
187
|
+
// An active turn may outlive five minutes; idle/orphan deadlines, not process age, end production workers.
|
|
188
|
+
while (!options.signal?.aborted && (!options.maxRunMs || Date.now() - started < options.maxRunMs)) {
|
|
189
|
+
let result;
|
|
190
|
+
const sessions = fs.readdirSync(ctx.dir).filter(f => /^session-[a-f0-9]+\.json$/.test(f)).map(f => S.read(path.join(ctx.dir, f))).filter(s => s?.native_session_id && s.active);
|
|
191
|
+
for (const session of sessions) {
|
|
192
|
+
if (session.turn_active && Number(session.turn_deadline) <= Date.now()) {
|
|
193
|
+
const sf = path.join(ctx.dir, `session-${S.hash(`${session.host}:${session.native_session_id}`)}.json`);
|
|
194
|
+
const unlock = R.lock(ctx, 'capture.lock', 0);
|
|
195
|
+
if (unlock) {
|
|
196
|
+
try {
|
|
197
|
+
const currentSession = S.read(sf);
|
|
198
|
+
if (currentSession?.turn_active && Number(currentSession.turn_deadline) <= Date.now()) {
|
|
199
|
+
S.atomic(sf, { ...currentSession, turn_active: false, turn_expired: true });
|
|
200
|
+
R.record(ctx, 'excluded', 'ACTIVE_TURN_EXPIRED', 0);
|
|
201
|
+
}
|
|
202
|
+
} finally { unlock(); }
|
|
203
|
+
}
|
|
204
|
+
session.turn_active = false;
|
|
205
|
+
}
|
|
206
|
+
if (!(session.seen_mcp && session.turn_active) && Date.now() - (session.last_seen || started) > R.IDLE_TTL) continue;
|
|
207
|
+
const sessionCtx = { ...ctx, host: session.host };
|
|
208
|
+
if (!R.configured(cwd, session.host)) continue;
|
|
209
|
+
const current = R.validLease(sessionCtx, session.native_session_id);
|
|
210
|
+
if ((!current || current.until - Date.now() < 30000) && Date.now() >= leaseRetryAt) {
|
|
211
|
+
const checked = await lease(sessionCtx, session.native_session_id);
|
|
212
|
+
if (checked.stop || checked.retry) { result = { ...checked, leaseRetry: true }; break; }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const activity = S.read(path.join(ctx.dir, 'activity.json'), { at: started });
|
|
216
|
+
const activeTurn = sessions.some(session => session.active && session.seen_mcp && session.turn_active && Number(session.turn_deadline) > Date.now() && R.configured(cwd, session.host));
|
|
217
|
+
if (!options.once && !result?.stop && !activeTurn) {
|
|
218
|
+
if (!R.queueFiles(ctx).length && Date.now() - activity.at > R.IDLE_TTL) return { empty: true };
|
|
219
|
+
// A failed upload without a live turn gets a bounded grace period; ciphertext/retry state survive.
|
|
220
|
+
if (Date.now() - Math.max(started, activity.at) > 5 * 60 * 1000) return { reason: 'WORKER_IDLE_BUDGET' };
|
|
221
|
+
}
|
|
222
|
+
// Rate limiting uploads must not suspend license renewal for new educational turns.
|
|
223
|
+
if (!result && Date.now() < nextAttemptAt) {
|
|
224
|
+
await wait(Math.min(1000, nextAttemptAt - Date.now())); continue;
|
|
225
|
+
}
|
|
226
|
+
if (!result) result = await drain(ctx);
|
|
227
|
+
if (result?.stop) { R.record(ctx, 'excluded', result.reason, 0); return result; }
|
|
228
|
+
if (result?.retry) {
|
|
229
|
+
attempts++;
|
|
230
|
+
const retryMs = result.retryMs || Math.min(60000, 1000 * (2 ** Math.min(attempts, 6)));
|
|
231
|
+
nextAttemptAt = Date.now() + retryMs;
|
|
232
|
+
if (result.leaseRetry) leaseRetryAt = nextAttemptAt;
|
|
233
|
+
R.update(ctx, { next_retry_at: new Date(nextAttemptAt).toISOString(), last_problem: result.reason });
|
|
234
|
+
if (options.once) return result;
|
|
235
|
+
await wait(Math.min(1000, retryMs));
|
|
236
|
+
} else {
|
|
237
|
+
attempts = 0; nextAttemptAt = 0;
|
|
238
|
+
if (R.state(ctx).next_retry_at) R.update(ctx, { next_retry_at: null });
|
|
239
|
+
if (options.once) return result;
|
|
240
|
+
if (result?.empty && !activeTurn && Date.now() - activity.at > R.IDLE_TTL) return result;
|
|
241
|
+
await wait(result?.empty ? 1000 : 10);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { reason: options.signal?.aborted ? 'WORKER_CANCELLED' : 'WORKER_TIME_BUDGET' };
|
|
245
|
+
} catch (e) { R.record(ctx, 'excluded', /^(UNSUPPORTED|SECURE_|UNSAFE_)/.test(e.message) ? e.message : 'WORKER_FAILED', 0); return { stop: true, reason: 'WORKER_FAILED' }; }
|
|
246
|
+
finally { release(); }
|
|
247
|
+
}
|
|
248
|
+
if (require.main === module) {
|
|
249
|
+
process.on('uncaughtException', () => process.exit(0)); process.on('unhandledRejection', () => process.exit(0));
|
|
250
|
+
work(process.argv[2] || process.cwd(), process.argv[3] || 'claude-code').then(() => process.exit(0), () => process.exit(0));
|
|
251
|
+
}
|
|
252
|
+
module.exports = { baseUrl, request, getCredential, lease, drain, cleanup, work };
|
|
@@ -1,131 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Falha = exit 0. Sem stdout. Sem MCP Arcane neste negocio = no-op.
|
|
5
|
-
* Grava local e tenta enviar pra Arka. Se a nuvem falhar, o Claude segue.
|
|
6
|
-
*/
|
|
2
|
+
'use strict';
|
|
3
|
+
// Observer only. Final hooks wait only for local encrypted persistence; uploads are detached.
|
|
7
4
|
process.on('uncaughtException', () => process.exit(0));
|
|
8
5
|
process.on('unhandledRejection', () => process.exit(0));
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const R = require('./ahi/runtime.cjs');
|
|
9
|
+
const hostIndex = process.argv.indexOf('--host');
|
|
10
|
+
const host = hostIndex >= 0 ? process.argv[hostIndex + 1] : 'claude-code';
|
|
11
|
+
const project = fs.realpathSync(path.resolve(__dirname, '../..'));
|
|
12
|
+
const relative = path.relative(project, fs.realpathSync(process.cwd()));
|
|
13
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) process.exit(0);
|
|
14
|
+
let input = ''; let oversized = false;
|
|
16
15
|
process.stdin.setEncoding('utf8');
|
|
17
|
-
process.stdin.on('data', (chunk)
|
|
18
|
-
|
|
19
|
-
if (input.length > 512 * 1024) input = input.slice(0, 512 * 1024);
|
|
20
|
-
});
|
|
21
|
-
process.stdin.on('end', () => {
|
|
22
|
-
Promise.resolve()
|
|
23
|
-
.then(() => capture())
|
|
24
|
-
.catch(() => {})
|
|
25
|
-
.finally(done);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function redact(text) {
|
|
29
|
-
return String(text || '')
|
|
30
|
-
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED]')
|
|
31
|
-
.replace(/\bBearer\s+[A-Za-z0-9\-._~+/]+=*/gi, '[REDACTED]')
|
|
32
|
-
.replace(/\bsk-(?:live|test|proj)?[-_]?[A-Za-z0-9]{16,}\b/g, '[REDACTED]')
|
|
33
|
-
.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, '[REDACTED]')
|
|
34
|
-
.slice(0, 8000);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function mapType(name) {
|
|
38
|
-
if (name === 'UserPromptSubmit') return 'message.user';
|
|
39
|
-
if (name === 'Stop') return 'message.assistant';
|
|
40
|
-
if (name === 'PreToolUse') return 'tool.started';
|
|
41
|
-
if (name === 'PostToolUse') return 'tool.completed';
|
|
42
|
-
if (name === 'PostToolUseFailure') return 'tool.failed';
|
|
43
|
-
if (name === 'SessionStart') return 'session.started';
|
|
44
|
-
if (name === 'SessionEnd') return 'session.ended';
|
|
45
|
-
return 'work.checkpoint';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hostName() {
|
|
49
|
-
if (process.env.CODEX_HOME || process.env.CODEX_CLI || process.env.CODEX) return 'codex';
|
|
50
|
-
return 'claude-code';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function capture() {
|
|
54
|
-
const fs = require('node:fs');
|
|
55
|
-
const path = require('node:path');
|
|
56
|
-
const os = require('node:os');
|
|
57
|
-
const cwd = process.cwd();
|
|
58
|
-
let mcp = false;
|
|
16
|
+
process.stdin.on('data', chunk => { if (!oversized) { input += chunk; if (Buffer.byteLength(input) > R.MAX_INPUT) { oversized = true; input = ''; } } });
|
|
17
|
+
process.stdin.on('end', async () => {
|
|
59
18
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function ship(row) {
|
|
82
|
-
const fs = require('node:fs');
|
|
83
|
-
const path = require('node:path');
|
|
84
|
-
const os = require('node:os');
|
|
85
|
-
const http = require('node:http');
|
|
86
|
-
const https = require('node:https');
|
|
87
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
88
|
-
const credFile = path.join(home, '.arcane', 'credentials.json');
|
|
89
|
-
if (!fs.existsSync(credFile)) return Promise.resolve();
|
|
90
|
-
let token = '';
|
|
91
|
-
try {
|
|
92
|
-
token = JSON.parse(fs.readFileSync(credFile, 'utf8')).access_token || '';
|
|
93
|
-
} catch { return Promise.resolve(); }
|
|
94
|
-
if (!token) return Promise.resolve();
|
|
95
|
-
const target = process.env.AHI_INGEST_URL || 'https://arcane.arka.education/api/ahi/v1/ingest';
|
|
96
|
-
const url = new URL(target);
|
|
97
|
-
const payload = JSON.stringify({
|
|
98
|
-
schema_version: 1,
|
|
99
|
-
host: hostName(),
|
|
100
|
-
native_session_id: row.session_id || 'unknown',
|
|
101
|
-
events: [{
|
|
102
|
-
type: mapType(row.hook),
|
|
103
|
-
occurred_at: row.at,
|
|
104
|
-
source_event_id: `${row.hook}:${row.at}`,
|
|
105
|
-
evidence_kind: 'observed',
|
|
106
|
-
payload: { text: row.text, tool: row.tool, hook: row.hook },
|
|
107
|
-
}],
|
|
108
|
-
});
|
|
109
|
-
const lib = url.protocol === 'http:' ? http : https;
|
|
110
|
-
return new Promise((resolve) => {
|
|
111
|
-
const req = lib.request({
|
|
112
|
-
protocol: url.protocol,
|
|
113
|
-
hostname: url.hostname,
|
|
114
|
-
port: url.port || (url.protocol === 'http:' ? 80 : 443),
|
|
115
|
-
path: url.pathname,
|
|
116
|
-
method: 'POST',
|
|
117
|
-
headers: {
|
|
118
|
-
Authorization: `Bearer ${token}`,
|
|
119
|
-
'Content-Type': 'application/json',
|
|
120
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
121
|
-
},
|
|
122
|
-
timeout: 1500,
|
|
123
|
-
}, (res) => {
|
|
124
|
-
res.resume();
|
|
125
|
-
res.on('end', resolve);
|
|
126
|
-
});
|
|
127
|
-
req.on('error', resolve);
|
|
128
|
-
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
129
|
-
req.end(payload);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
19
|
+
if (!R.configured(project, host)) return;
|
|
20
|
+
if (oversized) { const ctx = R.context(project, host, true); if (ctx) R.record(ctx, 'excluded', 'HOOK_INPUT_TOO_LARGE'); return; }
|
|
21
|
+
let event;
|
|
22
|
+
try { event = JSON.parse(input); } catch { const ctx = R.context(project, host, true); if (ctx) R.record(ctx, 'excluded', 'INVALID_HOOK_JSON'); return; }
|
|
23
|
+
let result = R.capture(event, project, host);
|
|
24
|
+
// The first event after idle can wait in memory for a fresh lease. Never spool plaintext.
|
|
25
|
+
// Terminal hooks must finish local persistence before the host exits, with no lease/network wait.
|
|
26
|
+
const eventName = event.hook_event_name || event.hookEventName;
|
|
27
|
+
const terminal = R.TERMINAL.includes(eventName);
|
|
28
|
+
const deadline = Date.now() + (terminal ? 0 : eventName === 'Interrupt' ? 100 : 6000);
|
|
29
|
+
while (result.capture === 'waiting_lease' && Date.now() < deadline) {
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
31
|
+
result = R.capture(event, project, host);
|
|
32
|
+
}
|
|
33
|
+
if (result.capture === 'waiting_lease') { const ctx = R.context(project, host, true); if (ctx) { if (terminal) ctx.lockWaitMs = 500; R.record(ctx, 'excluded', 'NO_VALID_LICENSE_LEASE'); } }
|
|
34
|
+
} catch { try { const ctx = R.context(project, host, true); if (ctx) R.record(ctx, 'excluded', 'HOOK_FAILED'); } catch {} }
|
|
35
|
+
finally { process.exit(0); }
|
|
36
|
+
});
|
|
37
|
+
setTimeout(() => process.exit(0), 8000).unref();
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"hooks": [
|
|
24
24
|
{
|
|
25
25
|
"type": "command",
|
|
26
|
-
"command": "node .claude/hooks/ahi-capture.cjs",
|
|
27
|
-
"timeout":
|
|
26
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
27
|
+
"timeout": 10,
|
|
28
|
+
"async": true
|
|
28
29
|
}
|
|
29
30
|
]
|
|
30
31
|
}
|
|
@@ -35,8 +36,8 @@
|
|
|
35
36
|
"hooks": [
|
|
36
37
|
{
|
|
37
38
|
"type": "command",
|
|
38
|
-
"command": "node .claude/hooks/ahi-capture.cjs",
|
|
39
|
-
"timeout":
|
|
39
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
40
|
+
"timeout": 3
|
|
40
41
|
}
|
|
41
42
|
]
|
|
42
43
|
}
|
|
@@ -47,8 +48,9 @@
|
|
|
47
48
|
"hooks": [
|
|
48
49
|
{
|
|
49
50
|
"type": "command",
|
|
50
|
-
"command": "node .claude/hooks/ahi-capture.cjs",
|
|
51
|
-
"timeout":
|
|
51
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
52
|
+
"timeout": 10,
|
|
53
|
+
"async": true
|
|
52
54
|
}
|
|
53
55
|
]
|
|
54
56
|
}
|
|
@@ -64,6 +66,81 @@
|
|
|
64
66
|
}
|
|
65
67
|
]
|
|
66
68
|
}
|
|
69
|
+
],
|
|
70
|
+
"SessionStart": [
|
|
71
|
+
{
|
|
72
|
+
"matcher": "",
|
|
73
|
+
"hooks": [
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
77
|
+
"timeout": 10,
|
|
78
|
+
"async": true
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"PreToolUse": [
|
|
84
|
+
{
|
|
85
|
+
"matcher": "",
|
|
86
|
+
"hooks": [
|
|
87
|
+
{
|
|
88
|
+
"type": "command",
|
|
89
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
90
|
+
"timeout": 10,
|
|
91
|
+
"async": true
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"SubagentStop": [
|
|
97
|
+
{
|
|
98
|
+
"matcher": "",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "command",
|
|
102
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
103
|
+
"timeout": 3
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"SessionEnd": [
|
|
109
|
+
{
|
|
110
|
+
"matcher": "",
|
|
111
|
+
"hooks": [
|
|
112
|
+
{
|
|
113
|
+
"type": "command",
|
|
114
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
115
|
+
"timeout": 3
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"PostToolUseFailure": [
|
|
121
|
+
{
|
|
122
|
+
"matcher": "",
|
|
123
|
+
"hooks": [
|
|
124
|
+
{
|
|
125
|
+
"type": "command",
|
|
126
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
127
|
+
"timeout": 10,
|
|
128
|
+
"async": true
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"StopFailure": [
|
|
134
|
+
{
|
|
135
|
+
"matcher": "",
|
|
136
|
+
"hooks": [
|
|
137
|
+
{
|
|
138
|
+
"type": "command",
|
|
139
|
+
"command": "node .claude/hooks/ahi-capture.cjs --host claude-code",
|
|
140
|
+
"timeout": 3
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
67
144
|
]
|
|
68
145
|
},
|
|
69
146
|
"enableAllProjectMcpServers": true
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 2.5.9 — 2026-09-14
|
|
2
|
+
|
|
3
|
+
### Historico duravel do trabalho Arcane
|
|
4
|
+
|
|
5
|
+
- A fila local usa cifra e cofre nativo do Windows/macOS, filtra credenciais conhecidas e so remove eventos apos confirmacao duravel do servidor. Reenvios preservam a identidade do evento.
|
|
6
|
+
- Captura exige uso do MCP na sessao e licenca vigente. Respostas finais sao gravadas localmente antes de o host encerrar; o envio de rede continua separado.
|
|
7
|
+
- `historico` informa a cobertura parcial e permite consultar sessoes, exportar todas as paginas e solicitar exclusao autenticada. Nao importa transcricoes antigas nem captura historico global.
|
|
8
|
+
- Instalacao e manutencao atualizam os hooks de Claude Code e Codex. Instalacoes existentes precisam executar `npx -y auroq-os@latest manutencao` (Windows: `npx.cmd`) na pasta do negocio e reabrir o host. A publicacao nao atualiza maquinas automaticamente.
|
|
9
|
+
- A manutencao reconhece wrappers oficiais antigos tambem com quebras de linha CRLF do Windows e preserva wrappers personalizados.
|
|
10
|
+
- Mantem `arcane-mcp@0.5.0`. A cobertura depende dos eventos que cada host entrega; nao promete captura completa nem paridade nativa integral do Claude.
|
|
11
|
+
|
|
12
|
+
## 2.5.8 — 2026-09-14
|
|
13
|
+
|
|
14
|
+
### Doctor reescreve o pin MCP que nao existe no npm
|
|
15
|
+
|
|
16
|
+
- Quem ficou com `arcane-mcp@0.6.0` nas 2.5.4/2.5.5 (Connection closed) tem o registro reescrito pra `0.5.0` no `doctor`, nao so na manutencao/`register-mcp`.
|
|
17
|
+
- Publicacao recusa pin que o npm nao resolve, pra nao repetir o 0.6.0.
|
|
18
|
+
- Instaladores fixam esta versao.
|
|
19
|
+
|
|
1
20
|
## 2.5.7 — 2026-09-14
|
|
2
21
|
|
|
3
22
|
### Doctor do Codex sem falso negativo
|
package/README.md
CHANGED
|
@@ -116,6 +116,68 @@ personalizacoes ficam exatamente como estavam. O que se atualiza e so o framewor
|
|
|
116
116
|
O historico do que mudou em cada versao fica no arquivo `CHANGELOG.md`, dentro do
|
|
117
117
|
proprio pacote — o Ops resume pra voce antes de aplicar.
|
|
118
118
|
|
|
119
|
+
## Historico de uso (AHI)
|
|
120
|
+
|
|
121
|
+
`auroq-os historico status` mostra captura comprovada, fila pendente, ultimo ACK,
|
|
122
|
+
exclusoes e problemas. O registro comeca quando a sessao usa o MCP Arcane, com
|
|
123
|
+
conta autenticada, licenca validada pelo servidor e captura habilitada. Desligar
|
|
124
|
+
esse MCP interrompe a captura. Instalar ou apenas abrir o projeto nao prova captura.
|
|
125
|
+
|
|
126
|
+
O adaptador `ahi-hooks-v2` registra mensagens e entradas/saidas de ferramentas
|
|
127
|
+
que os hooks visiveis do Claude Code/Codex disponibilizam. A cobertura e parcial:
|
|
128
|
+
nao le raciocinio interno, transcripts globais, respostas intermediarias ausentes
|
|
129
|
+
nos hooks ou arquivos binarios. O Codex exige uma versao com hooks nativos e o
|
|
130
|
+
projeto precisa ter a confianca que o proprio host exige; o instalador nao altera
|
|
131
|
+
essa confianca. Grok e hosts sem esses hooks nao tem captura comprovada.
|
|
132
|
+
|
|
133
|
+
Na validacao desta versao, o Codex CLI 0.154.0 passou tres turnos completos e uma
|
|
134
|
+
resposta apos 125 s de espera. No Claude Code 2.1.270, a prova nativa chegou apenas
|
|
135
|
+
a `SessionStart`/`UserPromptSubmit`: o provedor recusou a execucao antes do MCP.
|
|
136
|
+
Os demais eventos Claude estao cobertos pelos testes automatizados do adaptador;
|
|
137
|
+
a prova nativa de ponta a ponta permanece incompleta.
|
|
138
|
+
|
|
139
|
+
Antes de gravar ou enviar, o coletor filtra formatos conhecidos de credenciais,
|
|
140
|
+
campos sensiveis, JSON aninhado e valores opacos longos. Nenhum filtro consegue
|
|
141
|
+
reconhecer toda senha possivel. A fila fica fora do Git, cifrada por evento, em
|
|
142
|
+
`~/.arcane/ahi-v2/`, separada por conta/projeto/sessao/host. A chave privada fica
|
|
143
|
+
no Keychain do macOS ou protegida pelo DPAPI do Windows. Cofre indisponivel ou
|
|
144
|
+
sistema sem suporte deixa o historico inativo, sem impedir o uso educacional.
|
|
145
|
+
|
|
146
|
+
O envio ocorre em processo separado. Hooks de fim (`Stop`, `SubagentStop`,
|
|
147
|
+
`StopFailure`, `SessionEnd`) aguardam apenas a gravacao cifrada local, com prazo
|
|
148
|
+
curto e sem esperar rede/licenca, para o fechamento do host nao cancelar o registro.
|
|
149
|
+
Os demais hooks de captura executam de forma assincrona. A licenca continua sendo
|
|
150
|
+
renovada em segundo plano durante um turno ativo, inclusive em respostas longas.
|
|
151
|
+
Sem atividade real por 60 min, o turno e considerado orfao (`ACTIVE_TURN_EXPIRED`);
|
|
152
|
+
heartbeats nao prolongam esse prazo. Sem turno ativo, o worker encerra apos 60s
|
|
153
|
+
de inatividade; se houver fila com falha de envio, tenta por ate cinco minutos
|
|
154
|
+
sem turno ativo e preserva a fila para a proxima atividade. `SubagentStop` nao encerra o turno principal. Cada evento permanece na fila ate o ACK
|
|
155
|
+
duravel individual; falhas temporarias tentam novamente com espera progressiva.
|
|
156
|
+
A fila tem limite de 100 MiB/10.000 eventos e sete dias. Exclusoes, truncamentos,
|
|
157
|
+
revogacoes e corrupcao ficam explicitos no status e em eventos de cobertura.
|
|
158
|
+
Logs locais antigos em `.auroq/ahi/events.jsonl` nao sao importados ou alterados.
|
|
159
|
+
|
|
160
|
+
Comandos de acesso ao proprio historico:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
auroq-os historico sessoes
|
|
164
|
+
auroq-os historico timeline --session UUID
|
|
165
|
+
auroq-os historico busca --query "termo"
|
|
166
|
+
auroq-os historico export --session UUID --arquivo
|
|
167
|
+
auroq-os historico excluir --session UUID --confirm-delete
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
As consultas indicam o cursor de continuacao. `export --arquivo` percorre todas
|
|
171
|
+
as paginas do mesmo recorte e grava um novo arquivo privado em
|
|
172
|
+
`~/.arcane/ahi-exports/`; uma falha nunca e apresentada como exportacao completa.
|
|
173
|
+
Direitos de consulta/exclusao continuam disponiveis com autenticacao mesmo sem
|
|
174
|
+
licenca de captura. Para apagar o historico da conta inteira, o escopo explicito
|
|
175
|
+
exigido e `excluir --account --confirm-delete`.
|
|
176
|
+
|
|
177
|
+
A atualizacao do coletor exige `npx auroq-os@latest manutencao` no projeto e reabrir
|
|
178
|
+
o host. O aviso de nova versao ao iniciar o Claude Code nao instala essa correcao.
|
|
179
|
+
A publicacao do pacote tambem nao comprova que cada aluno ja atualizou.
|
|
180
|
+
|
|
119
181
|
## Estrutura
|
|
120
182
|
|
|
121
183
|
```
|
|
@@ -168,12 +230,12 @@ agents/ → Seu exercito (companion, workers, minds, squads)
|
|
|
168
230
|
|
|
169
231
|
## Observabilidade do motor (candidato 2.5.2)
|
|
170
232
|
|
|
171
|
-
Apos publicacao, `npx -y auroq-os@2.5.
|
|
172
|
-
manutencao/diagnostico. `npx -y auroq-os@2.5.
|
|
233
|
+
Apos publicacao, `npx -y auroq-os@2.5.8 doctor` mostra a fila local segura e os comandos pinados de
|
|
234
|
+
manutencao/diagnostico. `npx -y auroq-os@2.5.8 manutencao` preserva o caminho existente
|
|
173
235
|
de consentimento legado, studentFiles, customizacoes e dados do negocio. Nao ha
|
|
174
236
|
migracao forcada. O registro MCP aponta para `arcane-mcp@0.5.0`, publicado e validado.
|
|
175
|
-
O hotfix 2.5.
|
|
176
|
-
Os instaladores oficiais devem fixar 2.5.
|
|
237
|
+
O hotfix 2.5.8 restaura a conexao enquanto o candidato 0.6.0 passa por validacao separada.
|
|
238
|
+
Os instaladores oficiais devem fixar 2.5.8 depois da publicacao e da verificacao do tarball.
|
|
177
239
|
|
|
178
240
|
O CLI inicia captura antes das dependencias pesadas e registra resultado ao sair.
|
|
179
241
|
Eventos com identidade ja existente podem seguir pela rota autenticada na proxima
|