@staix/agent-hub 0.31.0 → 0.33.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/bus.md +3 -1
  4. package/docs/agent-notes/daemon.md +1 -0
  5. package/docs/agent-notes/tests.md +2 -2
  6. package/docs/design/bus.md +109 -4
  7. package/docs/dogfood.md +2 -0
  8. package/docs/memory.md +9 -3
  9. package/docs/operations.md +98 -17
  10. package/docs/quickstart.md +18 -2
  11. package/docs/security.md +52 -9
  12. package/docs/specs/2026-09-19-agent-hub-design.md +75 -5
  13. package/docs/specs/2026-09-20-upgrade-recovery-design.md +7 -3
  14. package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +55 -1
  15. package/docs/verification/2026-11-06-0.32.0.md +149 -0
  16. package/docs/verification/2026-11-06-0.33.0.md +222 -0
  17. package/docs/verification/2026-11-06-claude-implementer-2.1.319.md +57 -0
  18. package/docs/verification/2026-11-06-codex-0167-reviewer-isolation.md +56 -0
  19. package/docs/verification/2026-11-06-codex-0167-reviewer-tool-call.md +66 -0
  20. package/docs/verification/2026-11-06-kimi-251-reviewer-compat.md +88 -0
  21. package/docs/verification/2026-11-06-orca-1.4.250-cli-contract.md +50 -0
  22. package/docs/verification/2026-11-06-qwen-0268-acp.md +75 -0
  23. package/docs/verification/2026-11-06-qwen-0268-dual-output.md +44 -0
  24. package/docs/verification/claude-shell-wrapper-native.py +1 -1
  25. package/docs/verified.json +11 -11
  26. package/package.json +2 -2
  27. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  28. package/plugins/agent-hub/server.js +528 -206
  29. package/src/adapters/compat.ts +11 -11
  30. package/src/broker/protocol.ts +5 -0
  31. package/src/broker/server.ts +54 -4
  32. package/src/broker/store.ts +6 -3
  33. package/src/cli/console-state.ts +1 -1
  34. package/src/cli/output.ts +5 -1
  35. package/src/cli/room.ts +34 -12
  36. package/src/cli/status-lines.ts +8 -1
  37. package/src/cli/terminal-recovery.ts +37 -15
  38. package/src/hub/bus.ts +25 -0
  39. package/src/hub/daemon.ts +20 -9
  40. package/src/rooms/application.ts +15 -0
  41. package/src/rooms/commands.ts +135 -30
  42. package/src/rooms/health.ts +32 -0
  43. package/src/rooms/link.ts +394 -56
  44. package/src/rooms/liveness.ts +11 -0
  45. package/src/rooms/remote-peer.ts +18 -3
  46. package/src/rooms/service.ts +167 -28
  47. package/src/rooms/status.ts +65 -2
  48. package/src/rooms/store.ts +30 -1
  49. package/src/rooms/wire.ts +10 -8
  50. package/src/ui/index.html +5 -3
package/src/rooms/wire.ts CHANGED
@@ -23,7 +23,7 @@ const FP = /^[0-9a-f]{64}$/;
23
23
  export interface RoomMember extends PublicHubIdentity { label: string; exposedPeers: string[] }
24
24
  export interface FrameWire {
25
25
  v: 1; type: "frame"; room: string; epoch: string; sender: string;
26
- recipients: string[]; seq: string; ephemeral?: true; nonce: string; ciphertext: string; signature: string;
26
+ recipients: string[]; seq: string; ephemeral?: true; codecFeatures?: true; nonce: string; ciphertext: string; signature: string;
27
27
  }
28
28
  export interface RosterWire {
29
29
  v: 1; type: "roster"; room: string; epoch: string; owner: string;
@@ -108,9 +108,10 @@ function wrap(x: unknown): WrappedEpochKey {
108
108
  }
109
109
  function parseFrame(x: unknown): FrameWire {
110
110
  const ephemeral = typeof x === "object" && x !== null && Object.hasOwn(x, "ephemeral");
111
- const o = object(x, ["v", "type", "room", "epoch", "sender", "recipients", "seq", "nonce", "ciphertext", "signature", ...(ephemeral ? ["ephemeral"] : [])]); base(o, "frame");
112
- if (ephemeral && o.ephemeral !== true) fail();
113
- return { v: 1, type: "frame", room: b64(o.room, 32), epoch: positiveU64(o.epoch), sender: fingerprint(o.sender), recipients: sorted(array(o.recipients, 64, fingerprint, true), x => x), seq: positiveU64(o.seq), ...(ephemeral ? { ephemeral: true as const } : {}), nonce: b64(o.nonce, 12), ciphertext: ciphertext(o.ciphertext, 65552), signature: b64(o.signature, 64) };
111
+ const codecFeatures = typeof x === "object" && x !== null && Object.hasOwn(x, "codecFeatures");
112
+ const o = object(x, ["v", "type", "room", "epoch", "sender", "recipients", "seq", "nonce", "ciphertext", "signature", ...(ephemeral ? ["ephemeral"] : []), ...(codecFeatures ? ["codecFeatures"] : [])]); base(o, "frame");
113
+ if (ephemeral && o.ephemeral !== true || codecFeatures && (o.codecFeatures !== true || !ephemeral)) fail();
114
+ return { v: 1, type: "frame", room: b64(o.room, 32), epoch: positiveU64(o.epoch), sender: fingerprint(o.sender), recipients: sorted(array(o.recipients, 64, fingerprint, true), x => x), seq: positiveU64(o.seq), ...(ephemeral ? { ephemeral: true as const } : {}), ...(codecFeatures ? { codecFeatures: true as const } : {}), nonce: b64(o.nonce, 12), ciphertext: ciphertext(o.ciphertext, 65552), signature: b64(o.signature, 64) };
114
115
  }
115
116
  function parseRoster(x: unknown): RosterWire {
116
117
  const o = object(x, ["v", "type", "room", "epoch", "owner", "members", "wraps", "privateNonce", "privateCiphertext", "signature"]); base(o, "roster");
@@ -231,6 +232,7 @@ export interface VerifiedPublicFrame {
231
232
  readonly rosterDigest: string;
232
233
  readonly recipients: readonly string[];
233
234
  readonly ephemeral?: true;
235
+ readonly codecFeatures?: true;
234
236
  }
235
237
  interface PublicFrameState { wire: Uint8Array }
236
238
  const publicFrames = new WeakMap<VerifiedPublicFrame, PublicFrameState>();
@@ -255,7 +257,7 @@ export async function verifyPublicFrame(input: { wire: Uint8Array; roster: Verif
255
257
  await verifyFrameSignature(frame, sender);
256
258
  const frameDigest = await digest(canonicalBytes(frame));
257
259
  const proof = Object.freeze({ room: frame.room, epoch: frame.epoch, sender: frame.sender, seq: frame.seq,
258
- digest: frameDigest, rosterDigest: roster.digest, ...(frame.ephemeral ? { ephemeral: true as const } : {}), recipients: Object.freeze([...frame.recipients]) });
260
+ digest: frameDigest, rosterDigest: roster.digest, ...(frame.ephemeral ? { ephemeral: true as const } : {}), ...(frame.codecFeatures ? { codecFeatures: true as const } : {}), recipients: Object.freeze([...frame.recipients]) });
259
261
  publicFrames.set(proof, { wire });
260
262
  return proof;
261
263
  }
@@ -333,10 +335,10 @@ function frameAad(frame: Pick<FrameWire, "room" | "epoch" | "sender" | "seq">):
333
335
  return enc.encode(`${frame.room}|${frame.epoch}|${frame.sender}|${frame.seq}`);
334
336
  }
335
337
  export async function sealFrame(input: {
336
- epoch: VerifiedEpoch; identity: HubIdentity; recipients: string[]; plaintext: Uint8Array; sequence: SequencePort; ephemeral?: true;
338
+ epoch: VerifiedEpoch; identity: HubIdentity; recipients: string[]; plaintext: Uint8Array; sequence: SequencePort; ephemeral?: true; codecFeatures?: true;
337
339
  }): Promise<Uint8Array> {
338
340
  await assertRoomCryptoReady();
339
- if (input.ephemeral !== undefined && input.ephemeral !== true) fail();
341
+ if (input.ephemeral !== undefined && input.ephemeral !== true || input.codecFeatures !== undefined && (input.codecFeatures !== true || input.ephemeral !== true)) fail();
340
342
  const epoch = input.epoch, identity = input.identity, sequence = input.sequence;
341
343
  const current = state(epoch), sender = identity.public.fingerprint;
342
344
  const member = current.roster.members.find(m => m.fingerprint === sender);
@@ -348,7 +350,7 @@ export async function sealFrame(input: {
348
350
  if (!sequence || typeof sequence.reserve !== "function" || typeof sequence.isCurrent !== "function") fail("state_unavailable");
349
351
  const binding = { room: epoch.room, sender, epoch: epoch.epoch, rosterDigest: epoch.digest };
350
352
  const seq = positiveU64(await portCall(() => sequence.reserve({ ...binding })));
351
- const header = { v: 1 as const, type: "frame" as const, room: epoch.room, epoch: epoch.epoch, sender, recipients, seq, ...(input.ephemeral ? { ephemeral: true as const } : {}) };
353
+ const header = { v: 1 as const, type: "frame" as const, room: epoch.room, epoch: epoch.epoch, sender, recipients, seq, ...(input.ephemeral ? { ephemeral: true as const } : {}), ...(input.codecFeatures ? { codecFeatures: true as const } : {}) };
352
354
  const nonce = randomBytes(12);
353
355
  const encrypted = await encryptEpoch({ key: current.key, purpose: "frame", context: frameContext(epoch, sender), aad: frameAad(header), nonce, plaintext });
354
356
  const body = { ...header, nonce: encodeBase64url(nonce), ciphertext: encodeBase64url(encrypted) };
package/src/ui/index.html CHANGED
@@ -1674,8 +1674,10 @@ function renderRooms(rooms) {
1674
1674
  const peers = el('div', undefined, 'peer-summary');
1675
1675
  for (const peer of room.remotePeers) {
1676
1676
  const id = roomMetadataText(peer.id, /^[a-z][a-z0-9-]{0,31}@[a-z][a-z0-9-]{0,31}$/, 65);
1677
- const state = ['idle', 'busy', 'paused', 'offline'].includes(peer.state) ? peer.state : 'unknown';
1678
- peers.append(el('span', id + ': ' + state, 'meta'));
1677
+ const observed = peer.presence ?? peer.state;
1678
+ const state = ['idle', 'busy', 'paused', 'offline', 'unknown'].includes(observed) ? observed : 'unknown';
1679
+ const note = peer.liveness === 'untracked' ? '; liveness not tracked (broker without ephemeral); upgrade the broker to track liveness' : '';
1680
+ peers.append(el('span', id + ': ' + state + note, 'meta'));
1679
1681
  }
1680
1682
  row.append(peers);
1681
1683
  }
@@ -2675,7 +2677,7 @@ function memorySyncLine(sync) {
2675
2677
  const cause = causes.includes(sync.cause) ? sync.cause : 'unknown';
2676
2678
  if (cause === 'declined' && sync.placementActive === false) {
2677
2679
  const declined = Number.isSafeInteger(sync.declinedCount) && sync.declinedCount >= 0 ? String(sync.declinedCount) : 'unknown';
2678
- return 'machine sync blocked: ' + declined + ' declined rows; per-row placement is not active on this store (agent-mem has no activation command yet, agent-mem#281). Inspect with `ahub memory declined`.';
2680
+ return 'machine sync blocked: ' + declined + ' declined rows; per-row placement is not active on this store. Activation is person-only: preview with `amem migrate --sync-placement --dry-run` and declare every writer via --writer-root. The hub does not yet supply the fresh placementAttestation required for active-placement sync (#1173). Inspect with `ahub memory declined`.';
2679
2681
  }
2680
2682
  const count = Number.isSafeInteger(sync.declinedCount) && sync.declinedCount >= 0 ? ' / ' + sync.declinedCount + ' declined' : '';
2681
2683
  const ids = cause === 'declined' && sync.declinedIdsKnown === false ? ' / declined ID set unknown' : '';