@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/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 id = required(opts, 'peer', 'HCC_PEER');
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 = required(opts, 'peer', 'HCC_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 id = required(opts, 'peer', 'HCC_PEER');
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
- const tasks = claimTaskRowsForPeer(db, peer, ids, { force: Boolean(opts.force) });
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
- const task = tx(db, () => {
846
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
847
- if (!row) throw new CliError('NOT_FOUND', `Task #${id} does not exist`);
848
- if (row.owner && row.owner !== peer && !opts.force) {
849
- throw new CliError('TASK_OWNED', `Task #${id} is owned by ${row.owner}`, { owner: row.owner });
850
- }
851
- const t = now();
852
- const completedAt = status === 'done' ? t : row.completed_at;
853
- db.prepare(`
854
- UPDATE tasks
855
- SET status = ?, completed_at = ?, updated_at = ?
856
- WHERE id = ?
857
- `).run(status, completedAt, t, id);
858
- addEvent(db, `task.${status}`, peer, id, { summary: opts.summary || opts.reason || '' });
859
- if (opts.body) {
860
- sendMessage(db, peer, opts.to || 'all', id, 'task.update', opts.body);
861
- }
862
- return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
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
- return runtimeRequest(ctx, 'POST', `/api/sessions/${encodeURIComponent(peer)}/input`, { text, enter }, runtime);
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', process.env.HCC_PEER || 'human', null, { peer: id });
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
- const lock = tx(db, () => {
1364
- const t = now();
1365
- const activeLocks = db.prepare('SELECT * FROM locks WHERE expires_at > ?').all(t);
1366
- const conflict = activeLocks.find((row) => locksConflict(row, requested) && row.owner !== peer);
1367
- if (conflict) {
1368
- throw new CliError('LOCK_HELD', `Resource ${lockLabel(requested)} conflicts with lock ${lockLabel(conflict)} held by ${conflict.owner}`, {
1369
- resource: requested.base_resource,
1370
- scope: requested.scope,
1371
- lock_resource: conflict.resource,
1372
- lock_scope: lockScope(conflict),
1373
- owner: conflict.owner,
1374
- expires_at: iso(conflict.expires_at)
1375
- });
1376
- }
1377
- const existing = db.prepare('SELECT * FROM locks WHERE resource = ?').get(requested.resource);
1378
- db.prepare(`
1379
- INSERT INTO locks(resource, base_resource, scope, owner, task_id, reason, expires_at, created_at)
1380
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1381
- ON CONFLICT(resource) DO UPDATE SET
1382
- base_resource = excluded.base_resource,
1383
- scope = excluded.scope,
1384
- owner = excluded.owner,
1385
- task_id = excluded.task_id,
1386
- reason = excluded.reason,
1387
- expires_at = excluded.expires_at
1388
- `).run(requested.resource, requested.base_resource, requested.scope, peer, taskId, reason, t + ttl, existing ? existing.created_at : t);
1389
- addEvent(db, 'lock.acquired', peer, taskId, { resource: requested.base_resource, lock_resource: requested.resource, scope: requested.scope, ttl, previous_owner: existing ? existing.owner : null });
1390
- return db.prepare('SELECT * FROM locks WHERE resource = ?').get(requested.resource);
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 = resolveCurrentPeer(ctx, opts, 'peer', 'shell');
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 = resolveCurrentPeer(ctx, opts, 'peer', 'shell');
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, payload);
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', id, null, { pane: info.pane, command, cwd, pid: info.pid });
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
- addEvent(eventDb, 'provider.session.rebound', session.peerId || id, null, {
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', id, null, {
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, session.peerId || id, eventDb);
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', id, null, { command, cwd, pid: child.pid });
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, event);
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
- sendJson(res, 200, webPeerAction(reqCtx, peer, action, input));
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', id, null, { bytes: data.length, enter: input.enter !== false });
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 (input.kill_tmux) {
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 = String(input.from || 'web');
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, { peer: peerId });
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, 500, { ok: false, error: { code: err.code || 'SERVER_ERROR', message: detail } });
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, { command: [command, ...commandArgs].join(' '), cwd });
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, exitCode);
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, { command: [command, ...commandArgs].join(' '), cwd, webManaged: true });
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, exitCode);
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, { session_id: sessionId, cwd: hookCwd });
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
  }