sentinelayer-cli 0.4.5 → 0.8.0
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 +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- package/src/ui/command-hints.js +13 -0
|
@@ -6,11 +6,89 @@ import process from "node:process";
|
|
|
6
6
|
|
|
7
7
|
const CREDENTIALS_VERSION = 1;
|
|
8
8
|
const KEYRING_SERVICE = "sentinelayer-cli";
|
|
9
|
+
const SESSION_WARNING_PREFIX = "sentinelayer.auth.session";
|
|
10
|
+
const SESSION_WARNING_REDACT_KEYS = /token|secret|password|key|authorization/i;
|
|
11
|
+
const SESSION_WARNING_MAX_VALUE_LENGTH = 200;
|
|
12
|
+
const SESSION_WARNING_ALLOWED_FIELDS = new Set([
|
|
13
|
+
"reason",
|
|
14
|
+
"source",
|
|
15
|
+
"operation",
|
|
16
|
+
"storage",
|
|
17
|
+
"codeHint",
|
|
18
|
+
"requestIdHash",
|
|
19
|
+
]);
|
|
9
20
|
|
|
10
21
|
function nowIso() {
|
|
11
22
|
return new Date().toISOString();
|
|
12
23
|
}
|
|
13
24
|
|
|
25
|
+
function createSessionWarningId() {
|
|
26
|
+
try {
|
|
27
|
+
return crypto.randomUUID();
|
|
28
|
+
} catch {
|
|
29
|
+
return `session-${Date.now().toString(36)}-${crypto.randomBytes(8).toString("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeSessionWarningValue(value, depth = 0) {
|
|
34
|
+
if (value === null || value === undefined) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (depth > 3) {
|
|
38
|
+
return "[TRUNCATED]";
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((entry) => sanitizeSessionWarningValue(entry, depth + 1));
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "object") {
|
|
44
|
+
const sanitized = {};
|
|
45
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
46
|
+
if (SESSION_WARNING_REDACT_KEYS.test(key)) {
|
|
47
|
+
sanitized[key] = "[REDACTED]";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
sanitized[key] = sanitizeSessionWarningValue(entry, depth + 1);
|
|
51
|
+
}
|
|
52
|
+
return sanitized;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
if (value.length > SESSION_WARNING_MAX_VALUE_LENGTH) {
|
|
56
|
+
return `${value.slice(0, SESSION_WARNING_MAX_VALUE_LENGTH)}…`;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sanitizeSessionWarningDetails(details) {
|
|
64
|
+
if (!details || typeof details !== "object") {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
return sanitizeSessionWarningValue(details, 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function emitSessionWarning(code, details = {}) {
|
|
71
|
+
const sanitizedDetails = sanitizeSessionWarningDetails(details);
|
|
72
|
+
const payload = {
|
|
73
|
+
level: "warn",
|
|
74
|
+
code: String(code || "SESSION_WARNING").toUpperCase(),
|
|
75
|
+
warningId: createSessionWarningId(),
|
|
76
|
+
timestamp: nowIso(),
|
|
77
|
+
};
|
|
78
|
+
for (const [key, value] of Object.entries(sanitizedDetails)) {
|
|
79
|
+
if (SESSION_WARNING_ALLOWED_FIELDS.has(key)) {
|
|
80
|
+
payload[key] = value;
|
|
81
|
+
} else {
|
|
82
|
+
payload[key] = "[OMITTED]";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
console.warn(`${SESSION_WARNING_PREFIX} ${JSON.stringify(payload)}`);
|
|
87
|
+
} catch {
|
|
88
|
+
console.warn(`${SESSION_WARNING_PREFIX} ${payload.code}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
14
92
|
function resolveHomeDir(homeDir) {
|
|
15
93
|
return path.resolve(String(homeDir || os.homedir()));
|
|
16
94
|
}
|
|
@@ -26,6 +104,96 @@ export function resolveCredentialsFilePath({ homeDir } = {}) {
|
|
|
26
104
|
return path.join(resolvedHome, ".sentinelayer", "credentials.json");
|
|
27
105
|
}
|
|
28
106
|
|
|
107
|
+
function resolveCredentialsKeyPath({ homeDir } = {}) {
|
|
108
|
+
const resolvedHome = resolveHomeDir(homeDir);
|
|
109
|
+
return path.join(resolvedHome, ".sentinelayer", "keys", "credentials.key");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveLegacyCredentialsKeyPath({ homeDir } = {}) {
|
|
113
|
+
const resolvedHome = resolveHomeDir(homeDir);
|
|
114
|
+
return path.join(resolvedHome, ".sentinelayer", "credentials.key");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadOrCreateFileKey({ homeDir } = {}) {
|
|
118
|
+
const keyPath = resolveCredentialsKeyPath({ homeDir });
|
|
119
|
+
const legacyKeyPath = resolveLegacyCredentialsKeyPath({ homeDir });
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fsp.readFile(keyPath, "utf-8");
|
|
122
|
+
const key = Buffer.from(String(raw || "").trim(), "base64");
|
|
123
|
+
if (key.length === 32) {
|
|
124
|
+
return key;
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const legacyRaw = await fsp.readFile(legacyKeyPath, "utf-8");
|
|
134
|
+
const legacyKey = Buffer.from(String(legacyRaw || "").trim(), "base64");
|
|
135
|
+
if (legacyKey.length === 32) {
|
|
136
|
+
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
137
|
+
await fsp.writeFile(keyPath, legacyKey.toString("base64"), { encoding: "utf-8", mode: 0o600 });
|
|
138
|
+
try {
|
|
139
|
+
await fsp.chmod(keyPath, 0o600);
|
|
140
|
+
} catch {
|
|
141
|
+
// Windows does not reliably support POSIX chmod semantics.
|
|
142
|
+
}
|
|
143
|
+
let verified = false;
|
|
144
|
+
try {
|
|
145
|
+
const verifyRaw = await fsp.readFile(keyPath, "utf-8");
|
|
146
|
+
const verifyKey = Buffer.from(String(verifyRaw || "").trim(), "base64");
|
|
147
|
+
verified = verifyKey.length === 32 && verifyKey.equals(legacyKey);
|
|
148
|
+
} catch {
|
|
149
|
+
verified = false;
|
|
150
|
+
}
|
|
151
|
+
if (verified) {
|
|
152
|
+
await fsp.rm(legacyKeyPath, { force: true });
|
|
153
|
+
}
|
|
154
|
+
return legacyKey;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = crypto.randomBytes(32);
|
|
163
|
+
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
164
|
+
await fsp.writeFile(keyPath, key.toString("base64"), { encoding: "utf-8", mode: 0o600 });
|
|
165
|
+
try {
|
|
166
|
+
await fsp.chmod(keyPath, 0o600);
|
|
167
|
+
} catch {
|
|
168
|
+
// Windows does not reliably support POSIX chmod semantics.
|
|
169
|
+
}
|
|
170
|
+
return key;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function encryptToken(token, key) {
|
|
174
|
+
const iv = crypto.randomBytes(12);
|
|
175
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
176
|
+
const ciphertext = Buffer.concat([cipher.update(token, "utf-8"), cipher.final()]);
|
|
177
|
+
const tag = cipher.getAuthTag();
|
|
178
|
+
return {
|
|
179
|
+
tokenEncrypted: ciphertext.toString("base64"),
|
|
180
|
+
tokenIv: iv.toString("base64"),
|
|
181
|
+
tokenTag: tag.toString("base64"),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function decryptToken({ tokenEncrypted, tokenIv, tokenTag }, key) {
|
|
186
|
+
const iv = Buffer.from(tokenIv, "base64");
|
|
187
|
+
const tag = Buffer.from(tokenTag, "base64");
|
|
188
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
189
|
+
decipher.setAuthTag(tag);
|
|
190
|
+
const plaintext = Buffer.concat([
|
|
191
|
+
decipher.update(Buffer.from(tokenEncrypted, "base64")),
|
|
192
|
+
decipher.final(),
|
|
193
|
+
]);
|
|
194
|
+
return plaintext.toString("utf-8");
|
|
195
|
+
}
|
|
196
|
+
|
|
29
197
|
function buildKeyringAccountName(apiUrl) {
|
|
30
198
|
const digest = crypto
|
|
31
199
|
.createHash("sha256")
|
|
@@ -72,10 +240,122 @@ function normalizeMetadata(raw = {}) {
|
|
|
72
240
|
updatedAt: String(raw.updatedAt || "").trim() || nowIso(),
|
|
73
241
|
user: normalizeUser(raw.user),
|
|
74
242
|
token: String(raw.token || "").trim() || null,
|
|
243
|
+
tokenEncrypted: String(raw.tokenEncrypted || "").trim() || null,
|
|
244
|
+
tokenIv: String(raw.tokenIv || "").trim() || null,
|
|
245
|
+
tokenTag: String(raw.tokenTag || "").trim() || null,
|
|
75
246
|
aidenid: normalizeAidenId(raw.aidenid),
|
|
76
247
|
};
|
|
77
248
|
}
|
|
78
249
|
|
|
250
|
+
function isRetriableRenameError(error) {
|
|
251
|
+
if (!error || typeof error !== "object") {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return (
|
|
255
|
+
error.code === "EPERM" ||
|
|
256
|
+
error.code === "EACCES" ||
|
|
257
|
+
error.code === "EEXIST" ||
|
|
258
|
+
error.code === "EBUSY"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function delayMilliseconds(ms) {
|
|
263
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function renameWithRetry(sourcePath, destinationPath, { attempts = 3, baseDelayMs = 25 } = {}) {
|
|
267
|
+
let lastError = null;
|
|
268
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
269
|
+
try {
|
|
270
|
+
await fsp.rename(sourcePath, destinationPath);
|
|
271
|
+
return;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
lastError = error;
|
|
274
|
+
if (!isRetriableRenameError(error) || attempt >= attempts) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
await delayMilliseconds(baseDelayMs * attempt);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
throw lastError;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function syncFile(filePath) {
|
|
284
|
+
const normalizedPath = String(filePath || "").trim();
|
|
285
|
+
if (!normalizedPath) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
let handle = null;
|
|
289
|
+
try {
|
|
290
|
+
handle = await fsp.open(normalizedPath, "r");
|
|
291
|
+
await handle.sync();
|
|
292
|
+
} catch {
|
|
293
|
+
// Best effort only. Some filesystems/runtimes do not support explicit fsync.
|
|
294
|
+
} finally {
|
|
295
|
+
if (handle) {
|
|
296
|
+
await handle.close().catch(() => {});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function syncDirectory(directoryPath) {
|
|
302
|
+
const normalizedPath = String(directoryPath || "").trim();
|
|
303
|
+
if (!normalizedPath) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let handle = null;
|
|
307
|
+
try {
|
|
308
|
+
handle = await fsp.open(normalizedPath, "r");
|
|
309
|
+
await handle.sync();
|
|
310
|
+
} catch {
|
|
311
|
+
// Directory fsync support is platform-dependent; ignore when unsupported.
|
|
312
|
+
} finally {
|
|
313
|
+
if (handle) {
|
|
314
|
+
await handle.close().catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function replaceWithBackup(tmpPath, filePath) {
|
|
320
|
+
const directory = path.dirname(filePath);
|
|
321
|
+
const backupPath = path.join(
|
|
322
|
+
directory,
|
|
323
|
+
`.credentials.backup.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString("hex")}.bak`
|
|
324
|
+
);
|
|
325
|
+
let backupCreated = false;
|
|
326
|
+
try {
|
|
327
|
+
try {
|
|
328
|
+
await fsp.rename(filePath, backupPath);
|
|
329
|
+
backupCreated = true;
|
|
330
|
+
await syncDirectory(directory);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await renameWithRetry(tmpPath, filePath, { attempts: 3, baseDelayMs: 40 });
|
|
338
|
+
await syncFile(filePath);
|
|
339
|
+
await syncDirectory(directory);
|
|
340
|
+
|
|
341
|
+
if (backupCreated) {
|
|
342
|
+
await fsp.rm(backupPath, { force: true }).catch(() => {});
|
|
343
|
+
await syncDirectory(directory);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (backupCreated) {
|
|
347
|
+
try {
|
|
348
|
+
await fsp.rename(backupPath, filePath);
|
|
349
|
+
await syncFile(filePath);
|
|
350
|
+
await syncDirectory(directory);
|
|
351
|
+
} catch {
|
|
352
|
+
// Best-effort restore; keep original error as the primary failure.
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
79
359
|
async function loadKeytarClient() {
|
|
80
360
|
const disableKeyring = String(process.env.SENTINELAYER_DISABLE_KEYRING || "")
|
|
81
361
|
.trim()
|
|
@@ -83,6 +363,18 @@ async function loadKeytarClient() {
|
|
|
83
363
|
if (disableKeyring === "1" || disableKeyring === "true" || disableKeyring === "yes" || disableKeyring === "on") {
|
|
84
364
|
return null;
|
|
85
365
|
}
|
|
366
|
+
const keyringMode = String(process.env.SENTINELAYER_KEYRING_MODE || "")
|
|
367
|
+
.trim()
|
|
368
|
+
.toLowerCase();
|
|
369
|
+
const enableKeyring =
|
|
370
|
+
keyringMode === "keyring" ||
|
|
371
|
+
keyringMode === "enabled" ||
|
|
372
|
+
keyringMode === "on" ||
|
|
373
|
+
keyringMode === "true" ||
|
|
374
|
+
keyringMode === "1";
|
|
375
|
+
if (!enableKeyring) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
86
378
|
try {
|
|
87
379
|
const mod = await import("keytar");
|
|
88
380
|
const client = mod && typeof mod === "object" ? mod.default || mod : null;
|
|
@@ -120,16 +412,117 @@ async function readMetadata({ homeDir } = {}) {
|
|
|
120
412
|
}
|
|
121
413
|
|
|
122
414
|
async function writeMetadata(filePath, metadata) {
|
|
123
|
-
|
|
124
|
-
await fsp.
|
|
125
|
-
encoding: "utf-8",
|
|
126
|
-
mode: 0o600,
|
|
127
|
-
});
|
|
415
|
+
const directory = path.dirname(filePath);
|
|
416
|
+
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
128
417
|
try {
|
|
129
|
-
await fsp.chmod(
|
|
418
|
+
await fsp.chmod(directory, 0o700);
|
|
130
419
|
} catch {
|
|
131
420
|
// Windows does not reliably support POSIX chmod semantics.
|
|
132
421
|
}
|
|
422
|
+
const tmpPath = path.join(
|
|
423
|
+
directory,
|
|
424
|
+
`.credentials.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`
|
|
425
|
+
);
|
|
426
|
+
const payload = `${JSON.stringify(metadata, null, 2)}\n`;
|
|
427
|
+
try {
|
|
428
|
+
let tmpHandle = null;
|
|
429
|
+
try {
|
|
430
|
+
tmpHandle = await fsp.open(tmpPath, "w", 0o600);
|
|
431
|
+
await tmpHandle.writeFile(payload, { encoding: "utf-8" });
|
|
432
|
+
await tmpHandle.sync();
|
|
433
|
+
} finally {
|
|
434
|
+
if (tmpHandle) {
|
|
435
|
+
await tmpHandle.close().catch(() => {});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
await fsp.chmod(tmpPath, 0o600);
|
|
440
|
+
} catch {
|
|
441
|
+
// Windows does not reliably support POSIX chmod semantics.
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await renameWithRetry(tmpPath, filePath, { attempts: 3, baseDelayMs: 25 });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (isRetriableRenameError(error)) {
|
|
447
|
+
await replaceWithBackup(tmpPath, filePath);
|
|
448
|
+
} else {
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
await syncFile(filePath);
|
|
453
|
+
await syncDirectory(directory);
|
|
454
|
+
} finally {
|
|
455
|
+
await fsp.rm(tmpPath, { force: true }).catch(() => {});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir } = {}) {
|
|
460
|
+
if (!metadata || !metadata.token) {
|
|
461
|
+
return { metadata, token: null, migrated: false };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const plaintextToken = String(metadata.token || "").trim();
|
|
465
|
+
if (!plaintextToken) {
|
|
466
|
+
return { metadata: { ...metadata, token: null }, token: null, migrated: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const updatedAt = nowIso();
|
|
470
|
+
const nextMetadata = normalizeMetadata({
|
|
471
|
+
...metadata,
|
|
472
|
+
token: null,
|
|
473
|
+
updatedAt,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const keytar = await loadKeytarClient();
|
|
477
|
+
if (keytar) {
|
|
478
|
+
const keyringAccount = metadata.keyringAccount || buildKeyringAccountName(metadata.apiUrl);
|
|
479
|
+
await keytar.setPassword(KEYRING_SERVICE, keyringAccount, plaintextToken);
|
|
480
|
+
nextMetadata.storage = "keyring";
|
|
481
|
+
nextMetadata.keyringService = KEYRING_SERVICE;
|
|
482
|
+
nextMetadata.keyringAccount = keyringAccount;
|
|
483
|
+
const key = await loadOrCreateFileKey({ homeDir });
|
|
484
|
+
const encrypted = encryptToken(plaintextToken, key);
|
|
485
|
+
nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
|
|
486
|
+
nextMetadata.tokenIv = encrypted.tokenIv;
|
|
487
|
+
nextMetadata.tokenTag = encrypted.tokenTag;
|
|
488
|
+
} else {
|
|
489
|
+
const key = await loadOrCreateFileKey({ homeDir });
|
|
490
|
+
const encrypted = encryptToken(plaintextToken, key);
|
|
491
|
+
nextMetadata.storage = "file";
|
|
492
|
+
nextMetadata.keyringService = KEYRING_SERVICE;
|
|
493
|
+
nextMetadata.keyringAccount = "";
|
|
494
|
+
nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
|
|
495
|
+
nextMetadata.tokenIv = encrypted.tokenIv;
|
|
496
|
+
nextMetadata.tokenTag = encrypted.tokenTag;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await writeMetadata(filePath, nextMetadata);
|
|
500
|
+
const { metadata: verify } = await readMetadata({ homeDir });
|
|
501
|
+
if (verify && verify.token) {
|
|
502
|
+
throw new Error("Plaintext token migration failed: token field persisted.");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { metadata: nextMetadata, token: plaintextToken, migrated: true };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
509
|
+
if (!metadata || !metadata.tokenEncrypted || !metadata.tokenIv || !metadata.tokenTag) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const key = await loadOrCreateFileKey({ homeDir });
|
|
514
|
+
const token = decryptToken(
|
|
515
|
+
{
|
|
516
|
+
tokenEncrypted: metadata.tokenEncrypted,
|
|
517
|
+
tokenIv: metadata.tokenIv,
|
|
518
|
+
tokenTag: metadata.tokenTag,
|
|
519
|
+
},
|
|
520
|
+
key
|
|
521
|
+
);
|
|
522
|
+
return token || null;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
133
526
|
}
|
|
134
527
|
|
|
135
528
|
/**
|
|
@@ -164,27 +557,75 @@ export async function readStoredSession({ homeDir } = {}) {
|
|
|
164
557
|
return null;
|
|
165
558
|
}
|
|
166
559
|
|
|
560
|
+
if (metadata.token) {
|
|
561
|
+
const migrated = await migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir });
|
|
562
|
+
if (migrated.migrated) {
|
|
563
|
+
return {
|
|
564
|
+
...migrated.metadata,
|
|
565
|
+
filePath,
|
|
566
|
+
token: migrated.token,
|
|
567
|
+
storage: migrated.metadata.storage || "file",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
167
572
|
if (metadata.storage === "keyring") {
|
|
168
573
|
const keytar = await loadKeytarClient();
|
|
169
|
-
|
|
170
|
-
|
|
574
|
+
let keyringError = null;
|
|
575
|
+
if (keytar && metadata.keyringAccount) {
|
|
576
|
+
try {
|
|
577
|
+
const token = await keytar.getPassword(
|
|
578
|
+
metadata.keyringService || KEYRING_SERVICE,
|
|
579
|
+
metadata.keyringAccount
|
|
580
|
+
);
|
|
581
|
+
if (token) {
|
|
582
|
+
return {
|
|
583
|
+
...metadata,
|
|
584
|
+
filePath,
|
|
585
|
+
token,
|
|
586
|
+
storage: "keyring",
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
keyringError = new Error("Keyring token not found");
|
|
590
|
+
} catch (error) {
|
|
591
|
+
keyringError = error instanceof Error ? error : new Error("Keyring access failed");
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
keyringError = new Error("Keyring unavailable");
|
|
171
595
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
596
|
+
const fallbackToken = await tryDecryptFileToken({ metadata, homeDir });
|
|
597
|
+
if (fallbackToken) {
|
|
598
|
+
emitSessionWarning("KEYRING_FALLBACK_USED", {
|
|
599
|
+
reason: keyringError ? String(keyringError.message || keyringError) : "unknown",
|
|
600
|
+
source: "file-encrypted",
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
...metadata,
|
|
604
|
+
filePath,
|
|
605
|
+
token: fallbackToken,
|
|
606
|
+
storage: "file",
|
|
607
|
+
};
|
|
178
608
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
};
|
|
609
|
+
emitSessionWarning("KEYRING_FALLBACK_UNAVAILABLE", {
|
|
610
|
+
reason: keyringError ? String(keyringError.message || keyringError) : "unknown",
|
|
611
|
+
source: "keyring",
|
|
612
|
+
});
|
|
613
|
+
return null;
|
|
185
614
|
}
|
|
186
615
|
|
|
187
616
|
if (!metadata.token) {
|
|
617
|
+
if (metadata.tokenEncrypted && metadata.tokenIv && metadata.tokenTag) {
|
|
618
|
+
const token = await tryDecryptFileToken({ metadata, homeDir });
|
|
619
|
+
if (!token) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
...metadata,
|
|
624
|
+
filePath,
|
|
625
|
+
token,
|
|
626
|
+
storage: "file",
|
|
627
|
+
};
|
|
628
|
+
}
|
|
188
629
|
return null;
|
|
189
630
|
}
|
|
190
631
|
return {
|
|
@@ -226,6 +667,14 @@ export async function readStoredSessionMetadata({ homeDir } = {}) {
|
|
|
226
667
|
if (!metadata) {
|
|
227
668
|
return null;
|
|
228
669
|
}
|
|
670
|
+
if (metadata.token) {
|
|
671
|
+
const migrated = await migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir });
|
|
672
|
+
return {
|
|
673
|
+
...migrated.metadata,
|
|
674
|
+
filePath,
|
|
675
|
+
token: null,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
229
678
|
return {
|
|
230
679
|
...metadata,
|
|
231
680
|
filePath,
|
|
@@ -319,7 +768,12 @@ export async function writeStoredSession(
|
|
|
319
768
|
nextMetadata.storage = "file";
|
|
320
769
|
nextMetadata.keyringService = KEYRING_SERVICE;
|
|
321
770
|
nextMetadata.keyringAccount = "";
|
|
322
|
-
|
|
771
|
+
const key = await loadOrCreateFileKey({ homeDir });
|
|
772
|
+
const encrypted = encryptToken(normalizedToken, key);
|
|
773
|
+
nextMetadata.token = null;
|
|
774
|
+
nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
|
|
775
|
+
nextMetadata.tokenIv = encrypted.tokenIv;
|
|
776
|
+
nextMetadata.tokenTag = encrypted.tokenTag;
|
|
323
777
|
}
|
|
324
778
|
|
|
325
779
|
await writeMetadata(filePath, nextMetadata);
|
package/src/cli.js
CHANGED
|
@@ -119,6 +119,11 @@ const COMMAND_REGISTRARS = {
|
|
|
119
119
|
exportName: "registerDaemonCommand",
|
|
120
120
|
needsLegacy: false,
|
|
121
121
|
},
|
|
122
|
+
session: {
|
|
123
|
+
loader: () => import("./commands/session.js"),
|
|
124
|
+
exportName: "registerSessionCommand",
|
|
125
|
+
needsLegacy: false,
|
|
126
|
+
},
|
|
122
127
|
};
|
|
123
128
|
|
|
124
129
|
const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
|
package/src/commands/audit.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadAuditRunReport, resolveAuditRunDirectory, writeDdPackage } from "..
|
|
|
8
8
|
import { writeAuditComparisonArtifact } from "../audit/replay.js";
|
|
9
9
|
import { loadAuditRegistry, selectAuditAgents } from "../audit/registry.js";
|
|
10
10
|
import { resolveOutputRoot } from "../config/service.js";
|
|
11
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
11
12
|
import { buildLegacyArgs } from "./legacy-args.js";
|
|
12
13
|
|
|
13
14
|
function shouldEmitJson(options, command) {
|
|
@@ -939,12 +940,16 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
939
940
|
const reconciliation = reconcileWithBaseline(julesFindings, omarBaseline);
|
|
940
941
|
|
|
941
942
|
if (onEvent && reconciliation.summary) {
|
|
942
|
-
onEvent({
|
|
943
|
-
|
|
944
|
-
agent: {
|
|
945
|
-
|
|
943
|
+
onEvent(createAgentEvent({
|
|
944
|
+
event: "reconciliation_complete",
|
|
945
|
+
agent: {
|
|
946
|
+
id: JULES_DEFINITION.id,
|
|
947
|
+
persona: JULES_DEFINITION.persona,
|
|
948
|
+
color: JULES_DEFINITION.color,
|
|
949
|
+
avatar: JULES_DEFINITION.avatar,
|
|
950
|
+
},
|
|
946
951
|
payload: reconciliation.summary,
|
|
947
|
-
});
|
|
952
|
+
}));
|
|
948
953
|
}
|
|
949
954
|
|
|
950
955
|
// ── [9] FINAL REPORT ──────────────────────────────────────────
|
|
@@ -1157,10 +1162,10 @@ function buildEventHandler(emitStream, emitJson, def) {
|
|
|
1157
1162
|
|
|
1158
1163
|
function emitProgress(onEvent, def, message) {
|
|
1159
1164
|
if (onEvent) {
|
|
1160
|
-
onEvent({
|
|
1161
|
-
|
|
1165
|
+
onEvent(createAgentEvent({
|
|
1166
|
+
event: "progress",
|
|
1162
1167
|
agent: { id: def.id, persona: def.persona, color: def.color, avatar: def.avatar },
|
|
1163
1168
|
payload: { phase: "setup", message },
|
|
1164
|
-
});
|
|
1169
|
+
}));
|
|
1165
1170
|
}
|
|
1166
1171
|
}
|