@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.
- package/CHANGELOG.md +33 -0
- package/README.md +2 -2
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +1 -0
- package/docs/agent-notes/tests.md +2 -2
- package/docs/design/bus.md +109 -4
- package/docs/dogfood.md +2 -0
- package/docs/memory.md +9 -3
- package/docs/operations.md +98 -17
- package/docs/quickstart.md +18 -2
- package/docs/security.md +52 -9
- package/docs/specs/2026-09-19-agent-hub-design.md +75 -5
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +7 -3
- package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +55 -1
- package/docs/verification/2026-11-06-0.32.0.md +149 -0
- package/docs/verification/2026-11-06-0.33.0.md +222 -0
- package/docs/verification/2026-11-06-claude-implementer-2.1.319.md +57 -0
- package/docs/verification/2026-11-06-codex-0167-reviewer-isolation.md +56 -0
- package/docs/verification/2026-11-06-codex-0167-reviewer-tool-call.md +66 -0
- package/docs/verification/2026-11-06-kimi-251-reviewer-compat.md +88 -0
- package/docs/verification/2026-11-06-orca-1.4.250-cli-contract.md +50 -0
- package/docs/verification/2026-11-06-qwen-0268-acp.md +75 -0
- package/docs/verification/2026-11-06-qwen-0268-dual-output.md +44 -0
- package/docs/verification/claude-shell-wrapper-native.py +1 -1
- package/docs/verified.json +11 -11
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +528 -206
- package/src/adapters/compat.ts +11 -11
- package/src/broker/protocol.ts +5 -0
- package/src/broker/server.ts +54 -4
- package/src/broker/store.ts +6 -3
- package/src/cli/console-state.ts +1 -1
- package/src/cli/output.ts +5 -1
- package/src/cli/room.ts +34 -12
- package/src/cli/status-lines.ts +8 -1
- package/src/cli/terminal-recovery.ts +37 -15
- package/src/hub/bus.ts +25 -0
- package/src/hub/daemon.ts +20 -9
- package/src/rooms/application.ts +15 -0
- package/src/rooms/commands.ts +135 -30
- package/src/rooms/health.ts +32 -0
- package/src/rooms/link.ts +394 -56
- package/src/rooms/liveness.ts +11 -0
- package/src/rooms/remote-peer.ts +18 -3
- package/src/rooms/service.ts +167 -28
- package/src/rooms/status.ts +65 -2
- package/src/rooms/store.ts +30 -1
- package/src/rooms/wire.ts +10 -8
- 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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
1678
|
-
|
|
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
|
|
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' : '';
|