Conversation
hapi-ping-peer lands as role=user; work-graph ingest only scraped agent shapes, so A2A footers never became work_ad rows. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
-
[Major] Restrict user-role notify ingestion to identifiable peer deliveries - the new
isInboundUserMessagebranch accepts every ordinary user prompt, so a web/Telegram/CLI message ending in a valid footer is persisted as an append-only row with reservedAGENT_NOTIFY_SUMMARYprovenance and an agent principal. The realhapi ping-peerpath posts to the normal messages endpoint and is stampedsentFrom: "webapp"(cli/src/modules/pingPeer/pingPeer.ts:350,hub/src/web/routes/messages.ts:144), while the added test inventssentFrom: "peer"(hub/src/sync/workGraphNotifyIngest.test.ts:257). The implementation therefore cannot distinguish the intended peer traffic from pasted or authored user content. Evidence:hub/src/sync/workGraphNotifyIngest.ts:460.Suggested fix:
function isPeerNotifyDelivery(content: unknown): boolean { const record = asRecord(content) const meta = asRecord(record?.meta) return record?.role === "user" && meta?.notifySource === "peer" } if (isPeerNotifyDelivery(content)) { return extractInboundCauseText(content) }
Stamp
notifySource: "peer"only in a dedicated hub peer-delivery path, not from the ordinary message request body. Add a negative test proving asentFrom: "webapp"user footer is ignored and an integration test using the real ping-peer route.
Summary
Review mode: initial
One Major provenance issue found. The patch currently elevates arbitrary user-role footer text into agent-authored work-graph audit rows.
Testing
Not run (automation). GitHub integration check passed; main test check was pending during review. Missing coverage: ordinary web user footer rejection and real ping-peer route ingestion.
HAPI Bot
Restrict user-role AGENT_NOTIFY_SUMMARY elevation to deliveries marked meta.notifySource=peer so ordinary web/CLI prompts cannot mint work_ads. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed Major: user-role notify ingest is now gated on
Tip: will be on the next push after this comment. |
There was a problem hiding this comment.
Findings
-
[Major] Peer-stamped REST sends bypass the notify-ingest hook —
SyncEngine.sendMessage()forwards the marker toMessageService, but that service emits directly throughEventPublisher;ingestNotifySummaryFromMessage()is only called byhandleRealtimeEvent(), which receives CLI socket events. The stored peer user message therefore never becomes a work-graph row. Evidence:hub/src/sync/syncEngine.ts:1013, with surrounding flow athub/src/sync/messageService.ts:891andhub/src/sync/syncEngine.ts:490.Suggested fix:
const { actualSessionId, createdAt, message } = await this.messageService.sendMessage(sessionId, payload) this.ingestNotifyMessage(actualSessionId, message)
-
[Major]
notifySourceremains caller-controlled — the ordinary authenticated message schema accepts the marker and the route passes it through unchanged. Any web/API caller in the namespace can therefore create an append-onlyAGENT_NOTIFY_SUMMARYrow whose principal is recorded as an agent. Evidence:shared/src/apiTypes.ts:536,hub/src/web/routes/messages.ts:151.Suggested fix:
export const SendMessageRequestSchema = z.object({ text: z.string(), localId: z.string().min(1).optional(), attachments: z.array(AttachmentMetadataSchema).optional(), scheduledAt: z.number().int().positive().nullable().optional(), deliveryMode: MessageDeliveryModeSchema.optional() }) // Set notifySource only inside a trusted CLI socket/RPC peer-delivery handler. await engine.sendMessage(sessionId, { text, notifySource: 'peer' })
Questions
None.
Summary
Review mode: follow-up after new commits
Two Major issues remain: the actual ping-peer delivery path does not invoke notify ingestion, and the new provenance marker is exposed to ordinary message clients.
Testing
Not run (automation). Current checks: integration passed; main test pending. Missing coverage: an end-to-end engine.sendMessage(..., { notifySource: 'peer' }) assertion that a work-ad is inserted, plus rejection of a spoofed marker from the ordinary REST route.
HAPI Bot
REST peer deliveries now go through POST /peer-messages (hub stamps meta.notifySource=peer) and SyncEngine.sendMessage runs notify ingest. Ordinary /messages no longer accepts a client notifySource field. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the two new Majors on tip (pushing):
Will ping Ready YES after CI + fresh cold-review on this tip. |
There was a problem hiding this comment.
Findings
-
[Major] The peer endpoint does not establish or preserve a trusted sender identity — every
/api/*caller receives the same owner/namespace JWT authorization, so any browser or Telegram client in the namespace can call this new route and make the hub stampnotifySource: 'peer'. The request also carries no authenticated source session, socaptureNotifyFromMessage(actualSessionId, message)uses the recipient session and the work-ad builder records that recipient assource_refandprincipal.id. Reserved, append-onlyAGENT_NOTIFY_SUMMARYrows are therefore forgeable and attributed to the wrong agent. Evidence:hub/src/web/routes/messages.ts:160,hub/src/web/routes/messages.ts:182,hub/src/sync/syncEngine.ts:1007, with resulting attribution athub/src/sync/workGraphNotifyIngest.ts:430andhub/src/sync/workGraphNotifyIngest.ts:437.Suggested fix:
const source = requireSessionScopedPeerAuth(c, engine) if (source instanceof Response) return source await engine.sendPeerMessage({ sourceSessionId: source.sessionId, targetSessionId: sessionId, text: parsed.data.text }) // Build source_ref/principal from sourceSessionId; keep related_session_id as targetSessionId.
Questions
None.
Summary
Review mode: follow-up after new commits
The prior ingest-path gap is resolved. One Major provenance issue remains: the replacement endpoint is callable with an ordinary namespace JWT and cannot identify the sending session, so it can create forged, recipient-attributed audit rows.
Testing
Not run (automation; PR code was not executed). Current checks: integration passed; main test pending. Missing coverage: reject an ordinary web/Telegram JWT at peer-messages, and assert a successful peer delivery records the authenticated source session as the work-ad principal while retaining the recipient as the related session.
HAPI Bot
Move peer delivery to POST /cli/sessions/:source/peer-messages so web JWT cannot mint notifySource=peer, and attribute work_ad source_ref/principal to the sending session. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
-
[Major] Keep notify/cause history scoped to the authoring principal — the changed query now treats every notify work-ad related to the recipient as one history, including a peer-authored row whose
sourceRefis another session. A peer delivery is inserted as an already-invoked user row, but its own ingest excludes that row at the assistant-sequence boundary and records nocauseMessageId. On the recipient's next assistant summary, the legacy fallback sees the peer work-ad'smessageIdand marks that peer inbound consumed, so the response loses the peer handoff as its cause. Evidence:hub/src/sync/workGraphNotifyIngest.ts:260, with the consuming fallback athub/src/sync/workGraphNotifyIngest.ts:278and cause exclusion athub/src/sync/workGraphNotifyIngest.ts:325.Suggested fix:
function listPreviousWorkAds( store: Store, namespace: string, sessionId: string, principalSessionId: string ): WorkGraphEvent[] { return store.workGraph .listWorkAdsByRelatedSession(namespace, sessionId) .filter((event) => ( event.provenance === 'AGENT_NOTIFY_SUMMARY' && event.sourceRef === principalSessionId )) } const peerSourceSessionId = extractPeerSourceSessionId(input.content) const principalSessionId = peerSourceSessionId ?? input.sessionId const previousWorkAds = listPreviousWorkAds( input.store, input.namespace, input.sessionId, principalSessionId )
Questions
None.
Summary
Review mode: follow-up after new commits
One Major causality regression remains on the latest head: mixing peer-authored and recipient-authored work-ad history consumes the peer handoff before the recipient's response can cite it.
Testing
Not run (automation; PR code was not executed). GitHub status at review time: integration and drift-gate passed; test pending. Missing coverage: ingest a peer-stamped user footer, then a recipient assistant footer, and assert the second work-ad retains the peer message as causeMessageId.
HAPI Bot
Peer work_ads must not consume recipient cause history. Filter previous notify rows by sourceRef === principalSessionId and regression-test that a recipient assistant summary still cites the peer handoff as cause. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
-
[Major] Preserve standalone
hapi ping-peerdelivery —pingPeer()now requiresHAPI_SESSION_IDbefore authentication or delivery, so the documented CLI command fails from a normal shell even whenhapi auth loginsupplied valid hub credentials. Evidence:cli/src/modules/pingPeer/pingPeer.ts:490; documented standalone command context:cli/README.md:40.Suggested fix:
const sourceSessionId = resolveSourceSessionId(options.sourceSessionId) if (sourceSessionId) { if (matched.id === sourceSessionId) { throw new PingPeerError('bad_args', 'cannot ping the source session itself') } await sendPeerMessage(apiUrl, accessToken, sourceSessionId, matched.id, message, http) } else { await sendMessage(apiUrl, jwt, matched.id, message, http) }
Make
resolveSourceSessionIdreturnstring | null; attributed peer delivery remains used in-session, while the old JWT message path preserves standalone delivery without minting the peer stamp.
Questions
- None.
Summary
- Review mode: follow-up after new commits
- One Major CLI regression remains. The previous principal-history finding is addressed on the current head.
Testing
- Not run (automation; PR code was not executed). GitHub checks
test,drift-gate, andintegrationpassed. Missing coverage:pingPeerwithoutHAPI_SESSION_ID, plus route-boundary tests for invalid, self, inactive, and cross-namespace source/target sessions.
HAPI Bot
Attributed /cli peer-messages when a source session is known; otherwise fall back to ordinary /api messages so shell ping-peer still delivers. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
-
[Major] Trust peer provenance only from the dedicated route - the new parser treats persisted meta.notifySource/sourceSessionId as authoritative, but the CLI Socket.IO message payload is unknown and is stored verbatim after checking only the target sid. A namespace-authenticated socket client can therefore submit a peer-stamped user row with any source id, including a nonexistent or cross-namespace id, and mint an append-only agent principal that the peer route never authenticated. Evidence: hub/src/sync/workGraphNotifyIngest.ts:465; related socket path: hub/src/socket/handlers/cli/sessionHandlers.ts:106.
Suggested fix:
type NotifyIngestInput = { // ... trustedPeerSourceSessionId?: string } const peerSourceSessionId = input.trustedPeerSourceSessionId?.trim() || null const plainText = peerSourceSessionId ? extractInboundCauseText(input.content) : extractAgentNotifyText(input.content)
Pass trustedPeerSourceSessionId only from SyncEngine.sendMessage when the validated peer route supplied it; do not enable peer-user elevation from handleRealtimeEvent.
-
[Major] Do not assign recipient prompts as causes of peer-authored rows - after selecting the peer as principal, ingest still runs generic turn-cause selection over the recipient session. If the target has any unconsumed inbound before the peer delivery, that unrelated prompt becomes the peer work-ad causeMessageId/causeText in the append-only ledger. Evidence: hub/src/sync/workGraphNotifyIngest.ts:502, with the scan at hub/src/sync/workGraphNotifyIngest.ts:512.
Suggested fix:
const causeResult = peerSourceSessionId ? { cause: null, previousEventId: previousWorkAds.at(-1)?.id ?? null } : resolveWorkAdCause({ messages, previousWorkAds, assistantSeq }) const { cause, previousEventId } = causeResult
-
[Minor] Tag peer work-ads with the sender flavor - captureNotifyFromMessage resolves the recipient session and passes its flavor even when sourceRef/principal are changed to the peer source. A Claude-to-Codex delivery is therefore recorded with flavor:codex. Evidence: hub/src/sync/syncEngine.ts:1028.
Suggested fix:
const principalSession = peerSourceSessionId ? this.getSession(peerSourceSessionId) : session flavor: principalSession?.metadata?.flavor ?? null
Questions
- None.
Summary
Review mode: follow-up after new commits
Three issues remain in the latest peer-attribution implementation: untrusted socket metadata can mint peer provenance, peer rows can acquire unrelated recipient causes, and peer flavor tags use the recipient.
Testing
- Not run (automation; PR code was not executed).
- Missing coverage: forged peer metadata through the CLI socket, a prior recipient prompt before a peer footer, and different source/target flavors.
HAPI Bot
Elevate peer user footers only when SyncEngine passes trustedPeerSourceSessionId from the CLI peer route. Skip recipient cause attribution on peer rows and tag flavor from the source session. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
-
[Minor] Preserve the peer origin in
sentFrom— the new route stores an attributed peer delivery aswebapp, but later work-ad cause stamping readsmeta.sentFrom; a recipient agent's response is therefore recorded withcauseKind: "webapp"instead of"peer". Evidence:hub/src/web/routes/cli.ts:328; related handling:hub/src/sync/workGraphNotifyIngest.ts:351.Suggested fix:
// MessageService/SyncEngine payload type sentFrom?: 'telegram-bot' | 'webapp' | 'peer' // peer route await engine.sendMessage(target.sessionId, { text: parsed.data.text, sentFrom: 'peer', notifySource: 'peer', peerSourceSessionId: source.session.id })
Questions
- None.
Summary
Review mode: follow-up after new commits
One Minor attribution issue remains in the latest full diff. The three findings from the prior HAPI Bot review are addressed by the new commit.
Testing
- Not run (automation; PR code was not executed).
- Static check:
git diff --checkpassed. - GitHub checks observed:
drift-gateandintegrationpassed;testwas pending at review time. - Add a route-level regression asserting the persisted peer message uses
sentFrom: "peer", plus a recipient-summary assertion forcauseKind: "peer".
HAPI Bot
Keep work-ad causeKind as peer for recipient assistant summaries that cite an attributed peer handoff. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Findings
- None.
Questions
- None.
Summary
Review mode: follow-up after new commits
No reportable issues found in the latest full diff. The latest commit resolves the prior sentFrom / causeKind mismatch. Residual risk: the dedicated /cli/sessions/:id/peer-messages route has no direct route-level regression coverage; the added tests exercise the ping-peer client and SyncEngine ingest separately, so route authentication, namespace checks, self-target rejection, inactive-target rejection, and persisted peer origin are not covered together.
Testing
- Not run (automation; PR code was not executed).
- Static check:
git diff --check origin/main...HEADpassed. - GitHub checks:
drift-gate,test, andintegrationpassed. - Suggested coverage: route-level success asserting
sentFrom: "peer", plus cross-namespace, self-target, and inactive-target rejection cases.
HAPI Bot
Summary
ingestNotifySummaryFromMessagerejected non-agent message shapes, sohapi-ping-peer/ attributed peer deliveries (role=usertext with a trailingAGENT_NOTIFY_SUMMARY) never became work-graphwork_adrows.Test plan
bun test hub/src/sync/workGraphNotifyIngest.test.ts(includes new peer user-role case)AGENT_NOTIFY_SUMMARY→ work_ad / Session Log capture on a hub that has this tipIssues
Fixes #1737