@logicseek/hello-cc 0.1.6 → 0.1.7
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/CHANGELOG.md +55 -0
- package/README.md +21 -3
- package/README.zh-CN.md +21 -3
- package/assets/screenshots/web-console.png +0 -0
- package/bin/hcc.mjs +409 -96
- package/lib/cli-runtime.mjs +2 -1
- package/lib/core/coordination/tasks.mjs +17 -1
- package/lib/core/coordination/timeline.mjs +10 -1
- package/lib/core/peers/identity.mjs +10 -2
- package/lib/web/peer-actions.mjs +42 -6
- package/lib/web/ui-template.mjs +5 -3
- package/package.json +1 -1
- package/scripts/regression.mjs +422 -51
package/bin/hcc.mjs
CHANGED
|
@@ -291,6 +291,7 @@ const {
|
|
|
291
291
|
sendMessage
|
|
292
292
|
} = createMessageStore({ now, addEvent });
|
|
293
293
|
const {
|
|
294
|
+
assertTaskOwnerForMutation,
|
|
294
295
|
claimNextTasksForPeer,
|
|
295
296
|
claimTaskRowsForPeer,
|
|
296
297
|
queryOpenTasks,
|
|
@@ -325,6 +326,7 @@ const {
|
|
|
325
326
|
} = createWebPeerActions({
|
|
326
327
|
activePeerTtl: ACTIVE_PEER_TTL,
|
|
327
328
|
addEvent,
|
|
329
|
+
assertTaskOwnerForMutation,
|
|
328
330
|
claimNextTasksForPeer,
|
|
329
331
|
connect,
|
|
330
332
|
defaultLockTtl: DEFAULT_LOCK_TTL,
|
|
@@ -351,6 +353,15 @@ function iso(ts) {
|
|
|
351
353
|
return new Date(ts * 1000).toISOString();
|
|
352
354
|
}
|
|
353
355
|
|
|
356
|
+
function webErrorStatus(err) {
|
|
357
|
+
if (!(err instanceof CliError)) return 500;
|
|
358
|
+
if (['BAD_ARGS', 'BAD_REQUEST', 'PEER_IDENTITY_REQUIRED', 'REQUEST_TOO_LARGE'].includes(err.code)) return 400;
|
|
359
|
+
if (['PEER_IDENTITY_MISMATCH', 'TASK_OWNED', 'LOCK_OWNED'].includes(err.code)) return 403;
|
|
360
|
+
if (['NOT_FOUND'].includes(err.code)) return 404;
|
|
361
|
+
if (['LOCK_HELD', 'SESSION_NOT_RUNNING'].includes(err.code)) return 409;
|
|
362
|
+
return 500;
|
|
363
|
+
}
|
|
364
|
+
|
|
354
365
|
function autoPeerDefaults(ctx, kindHint = 'shell', status = 'working') {
|
|
355
366
|
const kind = autoPeerKind(kindHint);
|
|
356
367
|
const ancestor = readAncestorCliInfo();
|
|
@@ -369,6 +380,11 @@ function shellCommand(args) {
|
|
|
369
380
|
return shellCommandWithQuote(args, shellQuoteArg);
|
|
370
381
|
}
|
|
371
382
|
|
|
383
|
+
function resolveTargetPeer(ctx, opts = {}, key = 'peer', kindHint = 'shell') {
|
|
384
|
+
if (opts[key]) return { id: opts[key], auto: false, target: true };
|
|
385
|
+
return resolveCurrentPeer(ctx, opts, key, kindHint);
|
|
386
|
+
}
|
|
387
|
+
|
|
372
388
|
let projectMigrationFanoutDepth = 0;
|
|
373
389
|
const migratedRegisteredProjectDbs = new Set();
|
|
374
390
|
|
|
@@ -426,6 +442,22 @@ function addEvent(db, type, actor, taskId, payload) {
|
|
|
426
442
|
`).run(type, actor || null, taskId || null, JSON.stringify(payload || {}), now());
|
|
427
443
|
}
|
|
428
444
|
|
|
445
|
+
function auditPayload({ actor = null, target = null, source = 'cli', admin = false, ...extra } = {}) {
|
|
446
|
+
const payload = { ...extra, source };
|
|
447
|
+
if (actor) payload.actor_peer = actor;
|
|
448
|
+
if (target) payload.target_peer = target;
|
|
449
|
+
if (admin) payload.admin = true;
|
|
450
|
+
return payload;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function requestActorPeer(input = {}, fallback = 'web') {
|
|
454
|
+
return String(input.auditActorPeer || fallback || 'web').trim() || 'web';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function requestSource(input = {}, fallback = 'web') {
|
|
458
|
+
return String(input.auditSource || fallback || 'web').trim() || fallback;
|
|
459
|
+
}
|
|
460
|
+
|
|
429
461
|
function latestHookProviderSession(db, peer) {
|
|
430
462
|
if (!peer) return null;
|
|
431
463
|
try {
|
|
@@ -583,7 +615,8 @@ async function cmdInit(ctx, args) {
|
|
|
583
615
|
async function cmdRegister(ctx, args) {
|
|
584
616
|
registerProjectActivity(ctx);
|
|
585
617
|
const opts = parseOpts(args, { arrays: ['cap'] });
|
|
586
|
-
const
|
|
618
|
+
const identity = resolveCurrentPeer(ctx, opts, 'peer', opts.kind || 'shell');
|
|
619
|
+
const id = identity.id;
|
|
587
620
|
const db = connect(ctx);
|
|
588
621
|
const peer = {
|
|
589
622
|
id,
|
|
@@ -603,7 +636,7 @@ async function cmdRegister(ctx, args) {
|
|
|
603
636
|
async function cmdEnv(ctx, args) {
|
|
604
637
|
if (args[0] === '--help' || args[0] === '-h') return helpEnv();
|
|
605
638
|
const opts = parseOpts(args);
|
|
606
|
-
const peer =
|
|
639
|
+
const peer = resolveTargetPeer(ctx, opts, 'peer', 'shell').id;
|
|
607
640
|
const values = {
|
|
608
641
|
HCC_PEER: peer,
|
|
609
642
|
HCC_ROOT: ctx.root,
|
|
@@ -616,7 +649,8 @@ async function cmdJoin(ctx, args) {
|
|
|
616
649
|
if (args[0] === '--help' || args[0] === '-h') return helpJoin();
|
|
617
650
|
registerProjectActivity(ctx);
|
|
618
651
|
const opts = parseOpts(args, { arrays: ['cap'] });
|
|
619
|
-
const
|
|
652
|
+
const identity = resolveCurrentPeer(ctx, opts, 'peer', opts.kind || 'shell');
|
|
653
|
+
const id = identity.id;
|
|
620
654
|
const peer = {
|
|
621
655
|
id,
|
|
622
656
|
kind: opts.kind || 'other',
|
|
@@ -794,10 +828,39 @@ async function taskClaim(ctx, args) {
|
|
|
794
828
|
const ids = parseTaskIds(opts);
|
|
795
829
|
const db = connect(ctx);
|
|
796
830
|
touchCurrentPeer(db, ctx, identity, 'working', 'shell');
|
|
797
|
-
|
|
831
|
+
let tasks;
|
|
832
|
+
try {
|
|
833
|
+
tasks = claimTaskRowsForPeer(db, peer, ids, { force: Boolean(opts.force) });
|
|
834
|
+
} catch (err) {
|
|
835
|
+
notifyTaskOwnerConflict(ctx, err);
|
|
836
|
+
throw err;
|
|
837
|
+
}
|
|
798
838
|
printResult(ctx, ids.length === 1 ? tasks[0] : tasks, (data) => taskRowsText(Array.isArray(data) ? data : [data], 'claimed'));
|
|
799
839
|
}
|
|
800
840
|
|
|
841
|
+
function notifyTaskOwnerConflict(ctx, err) {
|
|
842
|
+
if (err?.code !== 'TASK_OWNED' || !err.extra?.notify_owner) return;
|
|
843
|
+
const { owner, task_id: taskId, attempted_by: attemptedBy, action } = err.extra;
|
|
844
|
+
if (!owner || !attemptedBy || owner === attemptedBy) return;
|
|
845
|
+
let db = null;
|
|
846
|
+
try {
|
|
847
|
+
db = connect(ctx);
|
|
848
|
+
sendMessage(
|
|
849
|
+
db,
|
|
850
|
+
attemptedBy,
|
|
851
|
+
owner,
|
|
852
|
+
taskId || null,
|
|
853
|
+
'task.owner-conflict',
|
|
854
|
+
`Task #${taskId} is owned by ${owner}; ${attemptedBy} attempted ${action || 'modify'} and hello-cc left ownership unchanged.`
|
|
855
|
+
);
|
|
856
|
+
err.extra.notified = true;
|
|
857
|
+
} catch {
|
|
858
|
+
err.extra.notified = false;
|
|
859
|
+
} finally {
|
|
860
|
+
try { db?.close(); } catch {}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
801
864
|
async function taskTakeover(ctx, args) {
|
|
802
865
|
const opts = parseOpts(args);
|
|
803
866
|
const identity = resolveCurrentPeer(ctx, opts, 'peer', 'shell');
|
|
@@ -842,25 +905,29 @@ async function taskUpdate(ctx, args) {
|
|
|
842
905
|
if (!id) throw new CliError('BAD_ARGS', 'Missing --id');
|
|
843
906
|
const db = connect(ctx);
|
|
844
907
|
touchCurrentPeer(db, ctx, identity, status === 'done' ? 'idle' : 'working', 'shell');
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
throw new CliError('
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
908
|
+
let task;
|
|
909
|
+
try {
|
|
910
|
+
task = tx(db, () => {
|
|
911
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
912
|
+
if (!row) throw new CliError('NOT_FOUND', `Task #${id} does not exist`);
|
|
913
|
+
assertTaskOwnerForMutation(db, peer, row, `update:${status}`);
|
|
914
|
+
const t = now();
|
|
915
|
+
const completedAt = status === 'done' ? t : row.completed_at;
|
|
916
|
+
db.prepare(`
|
|
917
|
+
UPDATE tasks
|
|
918
|
+
SET status = ?, completed_at = ?, updated_at = ?
|
|
919
|
+
WHERE id = ?
|
|
920
|
+
`).run(status, completedAt, t, id);
|
|
921
|
+
addEvent(db, `task.${status}`, peer, id, { summary: opts.summary || opts.reason || '' });
|
|
922
|
+
if (opts.body) {
|
|
923
|
+
sendMessage(db, peer, opts.to || 'all', id, 'task.update', opts.body);
|
|
924
|
+
}
|
|
925
|
+
return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
926
|
+
});
|
|
927
|
+
} catch (err) {
|
|
928
|
+
notifyTaskOwnerConflict(ctx, err);
|
|
929
|
+
throw err;
|
|
930
|
+
}
|
|
864
931
|
printResult(ctx, task, (data) => `task #${data.id} -> ${data.status}`);
|
|
865
932
|
}
|
|
866
933
|
|
|
@@ -1160,7 +1227,25 @@ async function cmdBroadcast(ctx, args) {
|
|
|
1160
1227
|
}
|
|
1161
1228
|
|
|
1162
1229
|
async function injectPeer(ctx, peer, text, enter = true, runtime = null) {
|
|
1163
|
-
|
|
1230
|
+
const actor = resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1231
|
+
const db = connect(ctx);
|
|
1232
|
+
try {
|
|
1233
|
+
addEvent(db, 'web.session.input.requested', actor, null, auditPayload({
|
|
1234
|
+
actor,
|
|
1235
|
+
target: peer,
|
|
1236
|
+
peer,
|
|
1237
|
+
source: 'cli',
|
|
1238
|
+
admin: actor !== peer,
|
|
1239
|
+
bytes: text.length,
|
|
1240
|
+
enter
|
|
1241
|
+
}));
|
|
1242
|
+
} finally {
|
|
1243
|
+
db.close();
|
|
1244
|
+
}
|
|
1245
|
+
return runtimeRequest(ctx, 'POST', `/api/sessions/${encodeURIComponent(peer)}/input`, {
|
|
1246
|
+
text,
|
|
1247
|
+
enter
|
|
1248
|
+
}, runtime);
|
|
1164
1249
|
}
|
|
1165
1250
|
|
|
1166
1251
|
async function cmdInject(ctx, args) {
|
|
@@ -1227,6 +1312,7 @@ async function peerStart(ctx, args) {
|
|
|
1227
1312
|
const optArgs = sep >= 0 ? args.slice(0, sep) : args;
|
|
1228
1313
|
const cmdArgs = sep >= 0 ? args.slice(sep + 1) : [];
|
|
1229
1314
|
const opts = parseOpts(optArgs, { booleans: ['last', 'continue', 'fork', 'force', 'restart-env'] });
|
|
1315
|
+
const actor = resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1230
1316
|
const id = opts.peer || opts._[0];
|
|
1231
1317
|
if (!id) throw new CliError('BAD_ARGS', 'Missing peer');
|
|
1232
1318
|
const firstCommand = cmdArgs[0] || '';
|
|
@@ -1269,6 +1355,17 @@ async function peerStart(ctx, args) {
|
|
|
1269
1355
|
status: 'starting',
|
|
1270
1356
|
capabilities: 'tmux'
|
|
1271
1357
|
});
|
|
1358
|
+
addEvent(db, 'peer.start.requested', actor, null, auditPayload({
|
|
1359
|
+
actor,
|
|
1360
|
+
target: id,
|
|
1361
|
+
peer: id,
|
|
1362
|
+
source: 'cli',
|
|
1363
|
+
admin: actor !== id,
|
|
1364
|
+
kind,
|
|
1365
|
+
role,
|
|
1366
|
+
command,
|
|
1367
|
+
cwd
|
|
1368
|
+
}));
|
|
1272
1369
|
});
|
|
1273
1370
|
} finally {
|
|
1274
1371
|
db.close();
|
|
@@ -1290,6 +1387,7 @@ async function peerStart(ctx, args) {
|
|
|
1290
1387
|
|
|
1291
1388
|
async function peerAttach(ctx, args) {
|
|
1292
1389
|
const opts = parseOpts(args, { booleans: ['force'] });
|
|
1390
|
+
const actor = resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1293
1391
|
const id = opts.peer || opts._[0];
|
|
1294
1392
|
if (!id) throw new CliError('BAD_ARGS', 'Missing peer');
|
|
1295
1393
|
const pane = opts.pane || process.env.TMUX_PANE || null;
|
|
@@ -1306,6 +1404,22 @@ async function peerAttach(ctx, args) {
|
|
|
1306
1404
|
throw e;
|
|
1307
1405
|
}
|
|
1308
1406
|
await runtimeRequest(ctx, 'GET', '/api/runtime', null, runtime);
|
|
1407
|
+
const db = connect(ctx);
|
|
1408
|
+
try {
|
|
1409
|
+
addEvent(db, 'peer.attach.requested', actor, null, auditPayload({
|
|
1410
|
+
actor,
|
|
1411
|
+
target: id,
|
|
1412
|
+
peer: id,
|
|
1413
|
+
source: 'cli',
|
|
1414
|
+
admin: actor !== id,
|
|
1415
|
+
pane,
|
|
1416
|
+
kind,
|
|
1417
|
+
role,
|
|
1418
|
+
cwd
|
|
1419
|
+
}));
|
|
1420
|
+
} finally {
|
|
1421
|
+
db.close();
|
|
1422
|
+
}
|
|
1309
1423
|
const data = await runtimeRequest(ctx, 'POST', '/api/sessions/attach', {
|
|
1310
1424
|
id,
|
|
1311
1425
|
kind,
|
|
@@ -1319,10 +1433,23 @@ async function peerAttach(ctx, args) {
|
|
|
1319
1433
|
|
|
1320
1434
|
async function peerStop(ctx, args) {
|
|
1321
1435
|
const opts = parseOpts(args);
|
|
1436
|
+
const actor = resolveCurrentPeer(ctx, {}, 'peer', 'shell').id;
|
|
1322
1437
|
const id = opts.peer || opts._[0];
|
|
1323
1438
|
if (!id) throw new CliError('BAD_ARGS', 'Missing peer');
|
|
1324
1439
|
let data;
|
|
1325
1440
|
try {
|
|
1441
|
+
const db = connect(ctx);
|
|
1442
|
+
try {
|
|
1443
|
+
addEvent(db, 'peer.stop.requested', actor, null, auditPayload({
|
|
1444
|
+
actor,
|
|
1445
|
+
target: id,
|
|
1446
|
+
peer: id,
|
|
1447
|
+
source: 'cli',
|
|
1448
|
+
admin: actor !== id
|
|
1449
|
+
}));
|
|
1450
|
+
} finally {
|
|
1451
|
+
db.close();
|
|
1452
|
+
}
|
|
1326
1453
|
data = await runtimeRequest(ctx, 'POST', `/api/sessions/${encodeURIComponent(id)}/stop`, {});
|
|
1327
1454
|
} catch (err) {
|
|
1328
1455
|
if (err instanceof CliError && err.code === 'RUNTIME_NOT_RUNNING') {
|
|
@@ -1330,7 +1457,12 @@ async function peerStop(ctx, args) {
|
|
|
1330
1457
|
const db = connect(ctx);
|
|
1331
1458
|
try {
|
|
1332
1459
|
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
|
|
1333
|
-
addEvent(db, 'peer.stopped',
|
|
1460
|
+
addEvent(db, 'peer.stopped', actor, null, auditPayload({
|
|
1461
|
+
actor,
|
|
1462
|
+
target: id,
|
|
1463
|
+
peer: id,
|
|
1464
|
+
admin: actor !== id
|
|
1465
|
+
}));
|
|
1334
1466
|
} finally { db.close(); }
|
|
1335
1467
|
printResult(ctx, { id, status: 'exited' }, (s) => `stopped ${s.id} (no server running — DB marked exited)`);
|
|
1336
1468
|
return;
|
|
@@ -1360,35 +1492,46 @@ async function lockAcquire(ctx, args) {
|
|
|
1360
1492
|
const reason = opts.reason || '';
|
|
1361
1493
|
const db = connect(ctx);
|
|
1362
1494
|
touchCurrentPeer(db, ctx, identity, 'working', 'shell');
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1495
|
+
let lock;
|
|
1496
|
+
try {
|
|
1497
|
+
lock = tx(db, () => {
|
|
1498
|
+
const t = now();
|
|
1499
|
+
if (taskId) {
|
|
1500
|
+
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
|
1501
|
+
if (!task) throw new CliError('NOT_FOUND', `Task #${taskId} does not exist`);
|
|
1502
|
+
assertTaskOwnerForMutation(db, peer, task, 'lock-acquire');
|
|
1503
|
+
}
|
|
1504
|
+
const activeLocks = db.prepare('SELECT * FROM locks WHERE expires_at > ?').all(t);
|
|
1505
|
+
const conflict = activeLocks.find((row) => locksConflict(row, requested) && row.owner !== peer);
|
|
1506
|
+
if (conflict) {
|
|
1507
|
+
throw new CliError('LOCK_HELD', `Resource ${lockLabel(requested)} conflicts with lock ${lockLabel(conflict)} held by ${conflict.owner}`, {
|
|
1508
|
+
resource: requested.base_resource,
|
|
1509
|
+
scope: requested.scope,
|
|
1510
|
+
lock_resource: conflict.resource,
|
|
1511
|
+
lock_scope: lockScope(conflict),
|
|
1512
|
+
owner: conflict.owner,
|
|
1513
|
+
expires_at: iso(conflict.expires_at)
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
const existing = db.prepare('SELECT * FROM locks WHERE resource = ?').get(requested.resource);
|
|
1517
|
+
db.prepare(`
|
|
1518
|
+
INSERT INTO locks(resource, base_resource, scope, owner, task_id, reason, expires_at, created_at)
|
|
1519
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1520
|
+
ON CONFLICT(resource) DO UPDATE SET
|
|
1521
|
+
base_resource = excluded.base_resource,
|
|
1522
|
+
scope = excluded.scope,
|
|
1523
|
+
owner = excluded.owner,
|
|
1524
|
+
task_id = excluded.task_id,
|
|
1525
|
+
reason = excluded.reason,
|
|
1526
|
+
expires_at = excluded.expires_at
|
|
1527
|
+
`).run(requested.resource, requested.base_resource, requested.scope, peer, taskId, reason, t + ttl, existing ? existing.created_at : t);
|
|
1528
|
+
addEvent(db, 'lock.acquired', peer, taskId, { resource: requested.base_resource, lock_resource: requested.resource, scope: requested.scope, ttl, previous_owner: existing ? existing.owner : null });
|
|
1529
|
+
return db.prepare('SELECT * FROM locks WHERE resource = ?').get(requested.resource);
|
|
1530
|
+
});
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
notifyTaskOwnerConflict(ctx, err);
|
|
1533
|
+
throw err;
|
|
1534
|
+
}
|
|
1392
1535
|
printResult(ctx, lock, (data) => `locked ${lockLabel(data)} by ${data.owner} until ${iso(data.expires_at)}`);
|
|
1393
1536
|
}
|
|
1394
1537
|
|
|
@@ -1523,7 +1666,7 @@ async function eventTail(ctx, args) {
|
|
|
1523
1666
|
|
|
1524
1667
|
async function cmdStatus(ctx, args) {
|
|
1525
1668
|
const opts = parseOpts(args);
|
|
1526
|
-
const identity =
|
|
1669
|
+
const identity = resolveTargetPeer(ctx, opts, 'peer', 'shell');
|
|
1527
1670
|
const peer = identity.id;
|
|
1528
1671
|
const data = statusSummary(ctx, peer, identity);
|
|
1529
1672
|
printResult(ctx, data, (s) => renderStatusSummary(s, peer));
|
|
@@ -1532,7 +1675,7 @@ async function cmdStatus(ctx, args) {
|
|
|
1532
1675
|
async function cmdState(ctx, args) {
|
|
1533
1676
|
if (wantsHelp(args)) return helpState();
|
|
1534
1677
|
const opts = parseOpts(args, { arrays: ['resource'] });
|
|
1535
|
-
const identity =
|
|
1678
|
+
const identity = resolveTargetPeer(ctx, opts, 'peer', 'shell');
|
|
1536
1679
|
const peer = identity.id;
|
|
1537
1680
|
const resources = normalizeStateResources(opts.resource || opts.resources || []);
|
|
1538
1681
|
const snapshot = statusSnapshot(ctx, peer, { resources, intent: opts.intent || null, scope: opts.scope || null });
|
|
@@ -1885,6 +2028,10 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
1885
2028
|
const projectContexts = new Map();
|
|
1886
2029
|
const prepared = await prepareLocalBus(ctx, opts);
|
|
1887
2030
|
|
|
2031
|
+
function newSessionActionToken() {
|
|
2032
|
+
return makeWebToken({ token: null });
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1888
2035
|
function rememberProject(projectCtx) {
|
|
1889
2036
|
const normalized = contextForProject(projectCtx.root, projectCtx.dbPath, { cwd: projectCtx.cwd, json: ctx.json });
|
|
1890
2037
|
projectContexts.set(normalized.root, normalized);
|
|
@@ -1936,6 +2083,38 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
1936
2083
|
return null;
|
|
1937
2084
|
}
|
|
1938
2085
|
|
|
2086
|
+
function readActionToken(input, req) {
|
|
2087
|
+
const headerToken = req.headers['x-hcc-session-token'];
|
|
2088
|
+
return String(input.action_token || input.actionToken || headerToken || '').trim();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function resolveWebActionSession(projectCtx, peer, input, req) {
|
|
2092
|
+
const db = connect(projectCtx);
|
|
2093
|
+
let session;
|
|
2094
|
+
try {
|
|
2095
|
+
session = getSession(projectCtx, peer, db);
|
|
2096
|
+
} finally {
|
|
2097
|
+
db.close();
|
|
2098
|
+
}
|
|
2099
|
+
if (!session || session.status !== 'running') {
|
|
2100
|
+
throw new CliError('PEER_IDENTITY_REQUIRED', `Web peer action requires a running managed session for ${peer}`, { peer });
|
|
2101
|
+
}
|
|
2102
|
+
const actorPeer = session.peerId || peer;
|
|
2103
|
+
if (actorPeer !== peer && session.id !== peer) {
|
|
2104
|
+
throw new CliError('PEER_IDENTITY_MISMATCH', `Web peer action target ${peer} does not match managed session ${actorPeer}`, {
|
|
2105
|
+
peer,
|
|
2106
|
+
actor_peer: actorPeer,
|
|
2107
|
+
session_id: session.id
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
const expected = session.actionToken || '';
|
|
2111
|
+
const provided = readActionToken(input, req);
|
|
2112
|
+
if (!expected || provided !== expected) {
|
|
2113
|
+
throw new CliError('PEER_IDENTITY_REQUIRED', `Web peer action for ${peer} requires the managed session action token`, { peer });
|
|
2114
|
+
}
|
|
2115
|
+
return actorPeer;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1939
2118
|
function knownPeerIds(projectCtx) {
|
|
1940
2119
|
const db = connect(projectCtx);
|
|
1941
2120
|
try {
|
|
@@ -1977,6 +2156,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
1977
2156
|
const session = {
|
|
1978
2157
|
id,
|
|
1979
2158
|
peerId: id,
|
|
2159
|
+
actionToken: newSessionActionToken(),
|
|
1980
2160
|
root: pctx.root,
|
|
1981
2161
|
ctx: pctx,
|
|
1982
2162
|
kind: meta.kind || 'external',
|
|
@@ -2187,7 +2367,9 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2187
2367
|
force: false,
|
|
2188
2368
|
projectCtx: ctx,
|
|
2189
2369
|
binding,
|
|
2190
|
-
autoAttach: true
|
|
2370
|
+
autoAttach: true,
|
|
2371
|
+
auditActorPeer: 'web-runtime',
|
|
2372
|
+
auditSource: 'runtime'
|
|
2191
2373
|
});
|
|
2192
2374
|
attached.peers.add(row.id);
|
|
2193
2375
|
if (session.peerId) attached.peers.add(session.peerId);
|
|
@@ -2331,6 +2513,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2331
2513
|
binding,
|
|
2332
2514
|
provider_session_known: Boolean(providerSessionLabel),
|
|
2333
2515
|
provider_session_label: providerSessionLabel,
|
|
2516
|
+
action_token: session.actionToken || null,
|
|
2334
2517
|
warning: session.warning || null
|
|
2335
2518
|
};
|
|
2336
2519
|
}
|
|
@@ -2376,10 +2559,13 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2376
2559
|
OR runtime_session_id = ?
|
|
2377
2560
|
OR (? IS NOT NULL AND runtime_target = ?)
|
|
2378
2561
|
`).run(t, session.id, peerId, session.id, session.pane || null, session.pane || null);
|
|
2379
|
-
addEvent(db, status === 'exited' ? 'tmux.session.exited' : 'tmux.session.detached', peerId, null, {
|
|
2562
|
+
addEvent(db, status === 'exited' ? 'tmux.session.exited' : 'tmux.session.detached', peerId, null, auditPayload({
|
|
2563
|
+
actor: peerId,
|
|
2564
|
+
target: peerId,
|
|
2565
|
+
source: session.auditSource || 'runtime',
|
|
2380
2566
|
runtime_session_id: session.id,
|
|
2381
2567
|
pane: session.pane
|
|
2382
|
-
});
|
|
2568
|
+
}));
|
|
2383
2569
|
} finally {
|
|
2384
2570
|
db.close();
|
|
2385
2571
|
}
|
|
@@ -2552,7 +2738,12 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2552
2738
|
|
|
2553
2739
|
function addRebindCleanupFailedEvent(db, actor, payload) {
|
|
2554
2740
|
if (!db) return;
|
|
2555
|
-
addEvent(db, 'tmux.session.rebind_cleanup_failed', actor, null,
|
|
2741
|
+
addEvent(db, 'tmux.session.rebind_cleanup_failed', actor, null, auditPayload({
|
|
2742
|
+
actor,
|
|
2743
|
+
target: payload.old_peer || payload.target_peer || null,
|
|
2744
|
+
admin: true,
|
|
2745
|
+
...payload
|
|
2746
|
+
}));
|
|
2556
2747
|
}
|
|
2557
2748
|
|
|
2558
2749
|
function oldTmuxRebindTarget(projectCtx, oldTarget, newTarget) {
|
|
@@ -2732,7 +2923,10 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2732
2923
|
});
|
|
2733
2924
|
}
|
|
2734
2925
|
if (db) {
|
|
2735
|
-
addEvent(db, 'tmux.session.rebind_cleanup_pending', actor, null, {
|
|
2926
|
+
addEvent(db, 'tmux.session.rebind_cleanup_pending', actor, null, auditPayload({
|
|
2927
|
+
actor,
|
|
2928
|
+
target: oldPeer,
|
|
2929
|
+
admin: true,
|
|
2736
2930
|
old_peer: oldPeer,
|
|
2737
2931
|
old_runtime_target: oldTarget,
|
|
2738
2932
|
new_runtime_target: newTarget,
|
|
@@ -2741,7 +2935,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2741
2935
|
expected_tmux_session: expectedSession,
|
|
2742
2936
|
allowed_tmux_session: allowedSession,
|
|
2743
2937
|
hcc_root: hccRoot || null
|
|
2744
|
-
});
|
|
2938
|
+
}));
|
|
2745
2939
|
}
|
|
2746
2940
|
return {
|
|
2747
2941
|
oldPeer,
|
|
@@ -2829,13 +3023,16 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2829
3023
|
throw err;
|
|
2830
3024
|
}
|
|
2831
3025
|
if (db) {
|
|
2832
|
-
addEvent(db, 'tmux.session.rebound', actor, null, {
|
|
3026
|
+
addEvent(db, 'tmux.session.rebound', actor, null, auditPayload({
|
|
3027
|
+
actor,
|
|
3028
|
+
target: oldPeer,
|
|
3029
|
+
admin: true,
|
|
2833
3030
|
old_peer: oldPeer,
|
|
2834
3031
|
old_runtime_target: oldTarget,
|
|
2835
3032
|
new_runtime_target: newTarget,
|
|
2836
3033
|
old_tmux_session: oldSessionName,
|
|
2837
3034
|
new_tmux_session: newSessionName || null
|
|
2838
|
-
});
|
|
3035
|
+
}));
|
|
2839
3036
|
}
|
|
2840
3037
|
return true;
|
|
2841
3038
|
}
|
|
@@ -2851,6 +3048,8 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2851
3048
|
const pctx = input.projectCtx || ctx;
|
|
2852
3049
|
const id = input.id;
|
|
2853
3050
|
if (!id) throw new CliError('BAD_REQUEST', 'id required');
|
|
3051
|
+
const actorPeer = requestActorPeer(input, id);
|
|
3052
|
+
const auditSource = requestSource(input, input.autoAttach ? 'runtime' : 'web');
|
|
2854
3053
|
const info = tmuxPaneInfo(input.pane || null);
|
|
2855
3054
|
if (info.dead) throw new CliError('TMUX_PANE_DEAD', `tmux pane is not running: ${info.pane}`);
|
|
2856
3055
|
|
|
@@ -2882,6 +3081,9 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2882
3081
|
const session = {
|
|
2883
3082
|
id,
|
|
2884
3083
|
peerId: id,
|
|
3084
|
+
actorPeer,
|
|
3085
|
+
auditSource,
|
|
3086
|
+
actionToken: newSessionActionToken(),
|
|
2885
3087
|
root: pctx.root,
|
|
2886
3088
|
ctx: pctx,
|
|
2887
3089
|
kind,
|
|
@@ -2968,7 +3170,16 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2968
3170
|
});
|
|
2969
3171
|
session.peerId = canonical.peer;
|
|
2970
3172
|
session.binding = { ...canonical.binding };
|
|
2971
|
-
addEvent(db, 'tmux.session.attached',
|
|
3173
|
+
addEvent(db, 'tmux.session.attached', actorPeer, null, auditPayload({
|
|
3174
|
+
actor: actorPeer,
|
|
3175
|
+
target: session.peerId || id,
|
|
3176
|
+
source: auditSource,
|
|
3177
|
+
admin: actorPeer !== (session.peerId || id),
|
|
3178
|
+
pane: info.pane,
|
|
3179
|
+
command,
|
|
3180
|
+
cwd,
|
|
3181
|
+
pid: info.pid
|
|
3182
|
+
}));
|
|
2972
3183
|
} catch (err) {
|
|
2973
3184
|
stopTmuxStream(session);
|
|
2974
3185
|
if (session.exitPoller) { clearInterval(session.exitPoller); session.exitPoller = null; }
|
|
@@ -2983,12 +3194,17 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2983
3194
|
try {
|
|
2984
3195
|
killOldTmuxForRebind(pctx, rebindOldPlan, session.peerId || id, eventDb);
|
|
2985
3196
|
if (rebindOldPeer && rebindOldPeer !== id) {
|
|
2986
|
-
|
|
3197
|
+
const actor = actorPeer || session.peerId || id;
|
|
3198
|
+
addEvent(eventDb, 'provider.session.rebound', actor, null, auditPayload({
|
|
3199
|
+
actor,
|
|
3200
|
+
target: rebindOldPeer,
|
|
3201
|
+
source: auditSource,
|
|
3202
|
+
admin: true,
|
|
2987
3203
|
from_peer: rebindOldPeer,
|
|
2988
3204
|
to_peer: id,
|
|
2989
3205
|
old_runtime_target: rebindOldTarget,
|
|
2990
3206
|
new_runtime_target: info.pane
|
|
2991
|
-
});
|
|
3207
|
+
}));
|
|
2992
3208
|
}
|
|
2993
3209
|
} finally {
|
|
2994
3210
|
eventDb.close();
|
|
@@ -3053,6 +3269,8 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3053
3269
|
const pctx = input.projectCtx || ctx;
|
|
3054
3270
|
const kind = input.kind || 'shell';
|
|
3055
3271
|
const id = input.id || nextProjectSessionId(pctx, kind);
|
|
3272
|
+
const actorPeer = requestActorPeer(input, id);
|
|
3273
|
+
const auditSource = requestSource(input, 'web');
|
|
3056
3274
|
const role = input.role || 'peer';
|
|
3057
3275
|
const command = input.command || defaultSessionCommand(kind);
|
|
3058
3276
|
const cwd = path.resolve(input.cwd || pctx.root);
|
|
@@ -3124,11 +3342,15 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3124
3342
|
hasSession = false;
|
|
3125
3343
|
const db = connect(pctx);
|
|
3126
3344
|
try {
|
|
3127
|
-
addEvent(db, 'tmux.session.restarted',
|
|
3345
|
+
addEvent(db, 'tmux.session.restarted', actorPeer, null, auditPayload({
|
|
3346
|
+
actor: actorPeer,
|
|
3347
|
+
target: id,
|
|
3348
|
+
source: auditSource,
|
|
3349
|
+
admin: true,
|
|
3128
3350
|
reason,
|
|
3129
3351
|
old_runtime_target: oldTarget,
|
|
3130
3352
|
old_tmux_session: parkedName
|
|
3131
|
-
});
|
|
3353
|
+
}));
|
|
3132
3354
|
} finally {
|
|
3133
3355
|
db.close();
|
|
3134
3356
|
}
|
|
@@ -3213,7 +3435,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3213
3435
|
try {
|
|
3214
3436
|
const eventDb = connect(pctx);
|
|
3215
3437
|
try {
|
|
3216
|
-
killOldTmuxForRebind(pctx, oldInfo.plan,
|
|
3438
|
+
killOldTmuxForRebind(pctx, oldInfo.plan, actorPeer, eventDb);
|
|
3217
3439
|
} finally {
|
|
3218
3440
|
eventDb.close();
|
|
3219
3441
|
}
|
|
@@ -3257,6 +3479,8 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3257
3479
|
pane: row.runtime_target,
|
|
3258
3480
|
projectCtx,
|
|
3259
3481
|
force: true,
|
|
3482
|
+
auditActorPeer: 'web-runtime',
|
|
3483
|
+
auditSource: 'runtime',
|
|
3260
3484
|
binding: {
|
|
3261
3485
|
provider: row.provider,
|
|
3262
3486
|
provider_session_id: row.provider_session_id,
|
|
@@ -3277,6 +3501,8 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3277
3501
|
const pctx = input.projectCtx || ctx;
|
|
3278
3502
|
const kind = input.kind || 'shell';
|
|
3279
3503
|
const id = input.id || nextProjectSessionId(pctx, kind);
|
|
3504
|
+
const actorPeer = requestActorPeer(input, id);
|
|
3505
|
+
const auditSource = requestSource(input, 'web');
|
|
3280
3506
|
const key = sessionKey(pctx, id);
|
|
3281
3507
|
if (sessions.has(key) && sessions.get(key).status === 'running') {
|
|
3282
3508
|
return sessions.get(key);
|
|
@@ -3303,6 +3529,9 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3303
3529
|
const session = {
|
|
3304
3530
|
id,
|
|
3305
3531
|
peerId: id,
|
|
3532
|
+
actorPeer,
|
|
3533
|
+
auditSource,
|
|
3534
|
+
actionToken: newSessionActionToken(),
|
|
3306
3535
|
root: pctx.root,
|
|
3307
3536
|
ctx: pctx,
|
|
3308
3537
|
kind,
|
|
@@ -3343,7 +3572,15 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3343
3572
|
}, Boolean(input.force));
|
|
3344
3573
|
session.peerId = canonical.peer;
|
|
3345
3574
|
session.binding = { ...canonical.binding };
|
|
3346
|
-
addEvent(db, 'web.session.started',
|
|
3575
|
+
addEvent(db, 'web.session.started', actorPeer, null, auditPayload({
|
|
3576
|
+
actor: actorPeer,
|
|
3577
|
+
target: session.peerId || id,
|
|
3578
|
+
source: auditSource,
|
|
3579
|
+
admin: actorPeer !== (session.peerId || id),
|
|
3580
|
+
command,
|
|
3581
|
+
cwd,
|
|
3582
|
+
pid: child.pid
|
|
3583
|
+
}));
|
|
3347
3584
|
} finally {
|
|
3348
3585
|
db.close();
|
|
3349
3586
|
}
|
|
@@ -3358,7 +3595,12 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3358
3595
|
const db = connect(pctx);
|
|
3359
3596
|
try {
|
|
3360
3597
|
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
|
|
3361
|
-
addEvent(db, 'web.session.exited', id, null,
|
|
3598
|
+
addEvent(db, 'web.session.exited', session.actorPeer || id, null, auditPayload({
|
|
3599
|
+
actor: session.actorPeer || id,
|
|
3600
|
+
target: session.peerId || id,
|
|
3601
|
+
source: session.auditSource || 'web',
|
|
3602
|
+
...event
|
|
3603
|
+
}));
|
|
3362
3604
|
} finally {
|
|
3363
3605
|
db.close();
|
|
3364
3606
|
}
|
|
@@ -3508,7 +3750,10 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3508
3750
|
resource: url.searchParams.getAll('resource')
|
|
3509
3751
|
}
|
|
3510
3752
|
: await readJsonRequest(req);
|
|
3511
|
-
|
|
3753
|
+
const actionInput = readOnly
|
|
3754
|
+
? input
|
|
3755
|
+
: { ...input, actorPeer: resolveWebActionSession(reqCtx, peer, input, req) };
|
|
3756
|
+
sendJson(res, 200, webPeerAction(reqCtx, peer, action, actionInput));
|
|
3512
3757
|
return;
|
|
3513
3758
|
}
|
|
3514
3759
|
// Detected sessions: peers registered via hooks/watcher but without PTY
|
|
@@ -3610,13 +3855,13 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3610
3855
|
}
|
|
3611
3856
|
if (req.method === 'POST' && url.pathname === '/api/sessions') {
|
|
3612
3857
|
const input = await readJsonRequest(req);
|
|
3613
|
-
const session = startSession({ ...input, projectCtx: reqCtx });
|
|
3858
|
+
const session = startSession({ ...input, projectCtx: reqCtx, auditActorPeer: 'web', auditSource: 'web' });
|
|
3614
3859
|
sendJson(res, 200, { session: serializeSession(session) });
|
|
3615
3860
|
return;
|
|
3616
3861
|
}
|
|
3617
3862
|
if (req.method === 'POST' && url.pathname === '/api/sessions/attach') {
|
|
3618
3863
|
const input = await readJsonRequest(req);
|
|
3619
|
-
const session = attachTmuxSession({ ...input, projectCtx: reqCtx });
|
|
3864
|
+
const session = attachTmuxSession({ ...input, projectCtx: reqCtx, auditActorPeer: 'web', auditSource: 'web' });
|
|
3620
3865
|
sendJson(res, 200, { session: serializeSession(session) });
|
|
3621
3866
|
return;
|
|
3622
3867
|
}
|
|
@@ -3644,7 +3889,16 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3644
3889
|
writeSessionInput(session, data);
|
|
3645
3890
|
const db = connect(session.ctx || reqCtx);
|
|
3646
3891
|
try {
|
|
3647
|
-
addEvent(db, 'web.session.input',
|
|
3892
|
+
addEvent(db, 'web.session.input', 'web', null, auditPayload({
|
|
3893
|
+
actor: 'web',
|
|
3894
|
+
target: session.peerId || id,
|
|
3895
|
+
source: 'web',
|
|
3896
|
+
admin: true,
|
|
3897
|
+
peer: session.peerId || id,
|
|
3898
|
+
runtime_session_id: session.id,
|
|
3899
|
+
bytes: data.length,
|
|
3900
|
+
enter: input.enter !== false
|
|
3901
|
+
}));
|
|
3648
3902
|
} finally {
|
|
3649
3903
|
db.close();
|
|
3650
3904
|
}
|
|
@@ -3654,6 +3908,8 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3654
3908
|
const stopMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/stop$/);
|
|
3655
3909
|
if (req.method === 'POST' && stopMatch) {
|
|
3656
3910
|
const id = decodeURIComponent(stopMatch[1]);
|
|
3911
|
+
let stopInput = {};
|
|
3912
|
+
try { stopInput = await readJsonRequest(req); } catch {}
|
|
3657
3913
|
const lookupDb = connect(reqCtx);
|
|
3658
3914
|
let session;
|
|
3659
3915
|
try {
|
|
@@ -3672,13 +3928,11 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3672
3928
|
if (session.wrapperPid) { try { process.kill(session.wrapperPid, 'SIGTERM'); } catch {} }
|
|
3673
3929
|
if (session.pid && session.pid !== session.wrapperPid) { try { process.kill(session.pid, 'SIGTERM'); } catch {} }
|
|
3674
3930
|
} else if (session.type === 'tmux') {
|
|
3675
|
-
let input = {};
|
|
3676
|
-
try { input = await readJsonRequest(req); } catch {}
|
|
3677
3931
|
let killPlan = null;
|
|
3678
3932
|
const stopDb = connect(reqCtx);
|
|
3679
3933
|
try {
|
|
3680
3934
|
const peerId = resolveSessionPeerId(stopDb, session) || session.peerId || session.id;
|
|
3681
|
-
if (
|
|
3935
|
+
if (stopInput.kill_tmux) {
|
|
3682
3936
|
killPlan = safeTmuxKillPlan(reqCtx, stopDb, peerId, session.pane || null);
|
|
3683
3937
|
}
|
|
3684
3938
|
} finally {
|
|
@@ -3690,6 +3944,21 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3690
3944
|
session.pty.kill();
|
|
3691
3945
|
}
|
|
3692
3946
|
}
|
|
3947
|
+
const eventDb = connect(reqCtx);
|
|
3948
|
+
try {
|
|
3949
|
+
const peerId = resolveSessionPeerId(eventDb, session) || session.peerId || id;
|
|
3950
|
+
addEvent(eventDb, 'web.session.stop_requested', 'web', null, auditPayload({
|
|
3951
|
+
actor: 'web',
|
|
3952
|
+
target: peerId,
|
|
3953
|
+
source: 'web',
|
|
3954
|
+
admin: true,
|
|
3955
|
+
peer: peerId,
|
|
3956
|
+
runtime_session_id: session.id,
|
|
3957
|
+
kill_tmux: Boolean(stopInput.kill_tmux)
|
|
3958
|
+
}));
|
|
3959
|
+
} finally {
|
|
3960
|
+
eventDb.close();
|
|
3961
|
+
}
|
|
3693
3962
|
sendJson(res, 200, { session: serializeSession(session) });
|
|
3694
3963
|
return;
|
|
3695
3964
|
}
|
|
@@ -3699,7 +3968,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3699
3968
|
const peerId = decodeURIComponent(detectedMsgMatch[1]);
|
|
3700
3969
|
const input = await readJsonRequest(req);
|
|
3701
3970
|
const body = String(input.body || '');
|
|
3702
|
-
const sender =
|
|
3971
|
+
const sender = 'web';
|
|
3703
3972
|
const taskId = input.task ? Number(input.task) : null;
|
|
3704
3973
|
if (!body) { sendJson(res, 400, { ok: false, error: { code: 'BAD_REQUEST', message: 'body required' } }); return; }
|
|
3705
3974
|
const db = connect(reqCtx);
|
|
@@ -3726,12 +3995,16 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3726
3995
|
}
|
|
3727
3996
|
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now_, peerId);
|
|
3728
3997
|
db.prepare('UPDATE peer_bindings SET runtime_target = NULL, updated_at = ? WHERE peer = ?').run(now_, peerId);
|
|
3729
|
-
addEvent(db, 'peer.stopped', 'web', null, {
|
|
3998
|
+
addEvent(db, 'peer.stopped', 'web', null, auditPayload({
|
|
3999
|
+
actor: 'web',
|
|
4000
|
+
target: peerId,
|
|
4001
|
+
source: 'web',
|
|
4002
|
+
admin: true,
|
|
3730
4003
|
peer: peerId,
|
|
3731
4004
|
kill_tmux: Boolean(killPlan),
|
|
3732
4005
|
tmux_session: killPlan?.session || null,
|
|
3733
4006
|
runtime_target: killPlan?.runtime_target || null
|
|
3734
|
-
});
|
|
4007
|
+
}));
|
|
3735
4008
|
} finally {
|
|
3736
4009
|
db.close();
|
|
3737
4010
|
}
|
|
@@ -3745,7 +4018,13 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3745
4018
|
try {
|
|
3746
4019
|
const now_ = now();
|
|
3747
4020
|
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('running', now_, peerId);
|
|
3748
|
-
addEvent(db, 'peer.restarted', 'web', null, {
|
|
4021
|
+
addEvent(db, 'peer.restarted', 'web', null, auditPayload({
|
|
4022
|
+
actor: 'web',
|
|
4023
|
+
target: peerId,
|
|
4024
|
+
source: 'web',
|
|
4025
|
+
admin: true,
|
|
4026
|
+
peer: peerId
|
|
4027
|
+
}));
|
|
3749
4028
|
} finally {
|
|
3750
4029
|
db.close();
|
|
3751
4030
|
}
|
|
@@ -3755,7 +4034,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3755
4034
|
sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: 'Route not found' } });
|
|
3756
4035
|
} catch (err) {
|
|
3757
4036
|
const detail = err instanceof CliError || process.env.HCC_DEBUG ? err.message : 'internal server error';
|
|
3758
|
-
sendJson(res,
|
|
4037
|
+
sendJson(res, webErrorStatus(err), { ok: false, error: { code: err.code || 'SERVER_ERROR', message: detail } });
|
|
3759
4038
|
}
|
|
3760
4039
|
});
|
|
3761
4040
|
|
|
@@ -3850,7 +4129,9 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3850
4129
|
registerProject(ctx);
|
|
3851
4130
|
const db = connect(ctx);
|
|
3852
4131
|
try {
|
|
3853
|
-
addEvent(db, startMeta.eventType || 'web.started', 'human', null, {
|
|
4132
|
+
addEvent(db, startMeta.eventType || 'web.started', 'human', null, auditPayload({
|
|
4133
|
+
actor: 'human',
|
|
4134
|
+
source: 'cli',
|
|
3854
4135
|
root: ctx.root,
|
|
3855
4136
|
db: ctx.dbPath,
|
|
3856
4137
|
host,
|
|
@@ -3858,7 +4139,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
3858
4139
|
requested_port: port,
|
|
3859
4140
|
guidance: startMeta.guidance || prepared.guidance || null,
|
|
3860
4141
|
runtime: runtimeFile
|
|
3861
|
-
});
|
|
4142
|
+
}));
|
|
3862
4143
|
} finally {
|
|
3863
4144
|
db.close();
|
|
3864
4145
|
}
|
|
@@ -3876,8 +4157,9 @@ async function cmdRun(ctx, args) {
|
|
|
3876
4157
|
const cmdArgs = sep >= 0 ? args.slice(sep + 1) : [];
|
|
3877
4158
|
const opts = parseOpts(optArgs, { booleans: ['force', 'web-managed'] });
|
|
3878
4159
|
validateOpts('run', opts, ['peer', 'kind', 'role', 'cwd', 'force']);
|
|
3879
|
-
const id = required(opts, 'peer', 'HCC_PEER');
|
|
3880
4160
|
const kind = opts.kind || 'other';
|
|
4161
|
+
const identity = resolveCurrentPeer(ctx, opts, 'peer', kind);
|
|
4162
|
+
const id = identity.id;
|
|
3881
4163
|
const role = opts.role || 'peer';
|
|
3882
4164
|
const cwd = path.resolve(opts.cwd || ctx.cwd);
|
|
3883
4165
|
const command = cmdArgs.length ? cmdArgs[0] : defaultSessionCommand(kind);
|
|
@@ -3895,7 +4177,12 @@ async function cmdRun(ctx, args) {
|
|
|
3895
4177
|
capabilities: 'run-wrapper'
|
|
3896
4178
|
});
|
|
3897
4179
|
upsertCanonicalPeerBinding(db, binding, Boolean(opts.force));
|
|
3898
|
-
addEvent(db, 'run.session.started', id, null, {
|
|
4180
|
+
addEvent(db, 'run.session.started', id, null, auditPayload({
|
|
4181
|
+
actor: id,
|
|
4182
|
+
target: id,
|
|
4183
|
+
command: [command, ...commandArgs].join(' '),
|
|
4184
|
+
cwd
|
|
4185
|
+
}));
|
|
3899
4186
|
} finally {
|
|
3900
4187
|
db.close();
|
|
3901
4188
|
}
|
|
@@ -3915,7 +4202,11 @@ async function cmdRun(ctx, args) {
|
|
|
3915
4202
|
const db2 = connect(ctx);
|
|
3916
4203
|
try {
|
|
3917
4204
|
db2.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
|
|
3918
|
-
addEvent(db2, 'run.session.exited', id, null,
|
|
4205
|
+
addEvent(db2, 'run.session.exited', id, null, auditPayload({
|
|
4206
|
+
actor: id,
|
|
4207
|
+
target: id,
|
|
4208
|
+
...exitCode
|
|
4209
|
+
}));
|
|
3919
4210
|
} finally {
|
|
3920
4211
|
db2.close();
|
|
3921
4212
|
}
|
|
@@ -3958,7 +4249,13 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
3958
4249
|
capabilities: 'run-pty'
|
|
3959
4250
|
});
|
|
3960
4251
|
upsertCanonicalPeerBinding(db, binding, force);
|
|
3961
|
-
addEvent(db, 'run.session.started', id, null, {
|
|
4252
|
+
addEvent(db, 'run.session.started', id, null, auditPayload({
|
|
4253
|
+
actor: id,
|
|
4254
|
+
target: id,
|
|
4255
|
+
command: [command, ...commandArgs].join(' '),
|
|
4256
|
+
cwd,
|
|
4257
|
+
webManaged: true
|
|
4258
|
+
}));
|
|
3962
4259
|
} finally {
|
|
3963
4260
|
db.close();
|
|
3964
4261
|
}
|
|
@@ -4075,7 +4372,11 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
4075
4372
|
const db2 = connect(ctx);
|
|
4076
4373
|
try {
|
|
4077
4374
|
db2.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
|
|
4078
|
-
addEvent(db2, 'run.session.exited', id, null,
|
|
4375
|
+
addEvent(db2, 'run.session.exited', id, null, auditPayload({
|
|
4376
|
+
actor: id,
|
|
4377
|
+
target: id,
|
|
4378
|
+
...exitCode
|
|
4379
|
+
}));
|
|
4079
4380
|
} finally {
|
|
4080
4381
|
db2.close();
|
|
4081
4382
|
}
|
|
@@ -4200,13 +4501,22 @@ async function cmdHook(ctx, args) {
|
|
|
4200
4501
|
SET last_seen_at = ?, status = COALESCE(?, status)
|
|
4201
4502
|
WHERE id = ?
|
|
4202
4503
|
`).run(now(), status, peerId);
|
|
4203
|
-
addEvent(db, 'provider.session.merged', peerId, null, {
|
|
4504
|
+
addEvent(db, 'provider.session.merged', peerId, null, auditPayload({
|
|
4505
|
+
actor: peerId,
|
|
4506
|
+
target: peerId,
|
|
4507
|
+
source: 'hook',
|
|
4204
4508
|
from_peer: previousPeer,
|
|
4205
4509
|
provider: kind,
|
|
4206
4510
|
session_id: sessionId || resumeId || null
|
|
4207
|
-
});
|
|
4511
|
+
}));
|
|
4208
4512
|
}
|
|
4209
|
-
addEvent(db, `hook.${hookKey}`, peerId, null, {
|
|
4513
|
+
addEvent(db, `hook.${hookKey}`, peerId, null, auditPayload({
|
|
4514
|
+
actor: peerId,
|
|
4515
|
+
target: peerId,
|
|
4516
|
+
source: 'hook',
|
|
4517
|
+
session_id: sessionId,
|
|
4518
|
+
cwd: hookCwd
|
|
4519
|
+
}));
|
|
4210
4520
|
try {
|
|
4211
4521
|
reconcileRunningPeerBindings(db, hookCtx, {
|
|
4212
4522
|
inspectProcess: inspectProviderProcess,
|
|
@@ -4830,13 +5140,16 @@ async function cmdTmux(ctx, args) {
|
|
|
4830
5140
|
`).run(t, candidate.peer, candidate.runtime_target);
|
|
4831
5141
|
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('detached', t, candidate.peer);
|
|
4832
5142
|
}
|
|
4833
|
-
addEvent(db, 'tmux.session.gc', actor, null, {
|
|
5143
|
+
addEvent(db, 'tmux.session.gc', actor, null, auditPayload({
|
|
5144
|
+
actor,
|
|
5145
|
+
target: candidate.peer,
|
|
5146
|
+
admin: true,
|
|
4834
5147
|
peer: candidate.peer,
|
|
4835
5148
|
tmux_session: candidate.session,
|
|
4836
5149
|
runtime_target: candidate.runtime_target,
|
|
4837
5150
|
reason: candidate.reason,
|
|
4838
5151
|
older_than_days: plan.older_than_days
|
|
4839
|
-
});
|
|
5152
|
+
}));
|
|
4840
5153
|
removed.push(candidate);
|
|
4841
5154
|
});
|
|
4842
5155
|
}
|