Skip to content

[cold-review] feat(peer): ping_peer delivery provenance (#1203) - #118

Draft
heavygee wants to merge 148 commits into
mainfrom
feat/a2a-p05-peer-provenance
Draft

[cold-review] feat(peer): ping_peer delivery provenance (#1203)#118
heavygee wants to merge 148 commits into
mainfrom
feat/a2a-p05-peer-provenance

Conversation

@heavygee

@heavygee heavygee commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Fork-side cold-review stage for A2A Layer 0.1 / P0.5 peer delivery provenance (not for merge).

Implements trusted attribution for ping_peer / hapi ping-peer so peer nudges stop looking like operator keystrokes (sentFrom: webapp ghost-user bug).

Tip: 02e754f4d on feat/a2a-p05-peer-provenance (off upstream/main).

Local gates: bun typecheck clean; focused hub/cli/shared/web tests green. Full bun run test hit one environmental runner integration flake (live driver runner at 0.27.2) unrelated to this diff.

Local Opus cold (claude-opus-5-thinking-high) already run once; Blocker (UserMessage selector loop) + Majors (header trust docstring, dead HAPI_SESSION_NAME, hub name lookup, fail-open) addressed on tip before push.

Test plan

  • Forge: POST /messages with peer body and no X-Hapi-Peer-Delivery stays sentFrom: webapp
  • With header + HAPI_SESSION_ID of a real same-namespace session: stored meta.sentFrom=peer + store-derived sourceName
  • Outside session (header, empty peer): sentFrom=peer, no invented source id
  • Web badge links to /sessions/<sourceSessionId> when present
  • Unknown/out-of-namespace source id dropped (still peer, no forge link)

Issues

Ref tiann#1203 (upstream Fixes lands on the tiann PR after full court press)

A2A note

Layer 0.1 / P0.5 - not a P2 work-contract. See Discussion tiann#1332.

@heavygee
heavygee force-pushed the feat/a2a-p05-peer-provenance branch from 6444d0a to b8c4c0e Compare August 9, 2026 20:56
@heavygee heavygee added the cold-review-clean Fork-side bot review is satisfactory; safe to promote to upstream PR label Aug 9, 2026
heavygee and others added 28 commits August 10, 2026 04:02
Stop ghost user messages from peer nudges (tiann#1203 / A2A Layer 0.1): CLI
stamps X-Hapi-Peer-Delivery from HAPI_SESSION_ID, hub stores sentFrom=peer
with store-validated source session, and web badges the source link.

Co-authored-by: Cursor <cursoragent@cursor.com>
Match rich-composer @mention chrome for who sent a peer nudge so
provenance reads like an @ reference, not a separate prose badge.

Co-authored-by: Cursor <cursoragent@cursor.com>
Close cold-pass-1 B1/M1: web JWT sends never trust body sourceSessionId;
attributed delivery goes through POST /cli/sessions/:source/peer-messages.
Receiving agents get a From: /sessions/<id> prefix for reply targeting.

Co-authored-by: Cursor <cursoragent@cursor.com>
…te Pi

Close Sol pass-2 B1/M1: peer-messages needs an HMAC capability minted at
CLI create/load (hub JWT secret), so path+CLI-token alone cannot forge
source. Pi preparePiUserMessage now suffixes From: /sessions/<id>.

Co-authored-by: Cursor <cursoragent@cursor.com>
…sume

HappyUserMessage now selects Object.is-stable primitives for peer chrome.
Session-scoped CLI sockets receive hub-minted peer-capability so resumed
wrappers attribute ping_peer without exposing mint on namespace-token GET.

Co-authored-by: Cursor <cursoragent@cursor.com>
Socket peer-capability now requires the create-time session tag so a
namespace-token sibling cannot steal another session's mint. Persist tag
plus capability under HAPI_HOME for resume, await readiness in MCP
ping_peer, and refuse silent unattributed downgrade when a source id is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove HAPI_HOME peer-session credential files that same-UID siblings could
read. Keep the bearer in the session parent and expose delivery through a
descendant-gated unix broker. Resume injects the create-time tag via runner
env for tag-gated socket mint; wrapped hapi ping-peer fails closed without
the broker instead of silently sending unattributed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hand resume mint tag via stdio fd 3 (not environ), port peercred to
macOS, verify broker listener ancestry, unattributed fallback for
terminal resume, and refuse RPC method shadowing of spawn payloads.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sync-consume the runner tag pipe on module load and at bootstrap entry
so same-UID siblings cannot race /proc/<pid>/fd during ApiClient awaits.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop putting create-time tag on resumed CLI fds/env (pidfd_getfd).
Hub arms a 30s single-use mint when spawning resume; CLI connect
consumes it. Create path still uses in-memory sessionTag.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop first-connector /cli mint during the arm window. Hub arms a
spawn-RPC nonce; runner redeems over HTTP and injects capability into
the expected child via peercred/PID-checked unix socket.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ss2i B1)

CLI CI uses Vitest, not bun:test. Align peerCapabilityInject tests with
peerDeliverBroker peers so the suite gates CI. Refresh stale connect-mint comment.

Co-authored-by: Cursor <cursoragent@cursor.com>
Address Codex review on tiann#1473:
- Require create-time machineTag for machine room join and machine-scoped
  rpc-register so namespace token + machineId cannot steal spawn nonce
- Load Linux peercred via libc.so.6 getsockopt only (no Darwin getpeereid)
- Ack + retry rpc-register across reconnect ownership overlap
- Await runner capability inject before resume bootstrap returns

Co-authored-by: Cursor <cursoragent@cursor.com>
Unit mocks (and older clients) lack socket.timeout; fall back to plain emit
and treat missing ack as fire-and-forget success.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fail closed on untagged machine re-bind (upgrade first-claim forgery),
authorize session-scoped rpc-register, keep resume mint nonce idempotent
under concurrent /resume, and auto-rotate machineId on hub 409 re-enroll.
Also widen flaky claudeRemote timeout to match sibling tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
getOrCreateMachine now always receives the optional tag slot; update
the mock assertion so CI matches the call shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
Host-fallback resume routing trusts self-reported metadata.host, so a
same-namespace machine can steal the mint. Arm the nonce only when the
online machine matches the session's recorded machineId; shorten default
broker socket paths to fit portable unix sockaddr limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
Make resume capability inject async with platform/listen failure returning
null so the runner stays up on Windows or bad paths. Persist rotated
machineId into runner local state after legacy re-enroll.

Co-authored-by: Cursor <cursoragent@cursor.com>
Notify lazy session materialization when hub forces machine re-enroll so
the pending snapshot cannot overwrite the new machineId. Swallow broker
connection handler rejections so mid-request disconnects do not crash
the parent session via unhandledRejection.

Co-authored-by: Cursor <cursoragent@cursor.com>
Authorize session RPC only with create-time tag or HMAC capability, and
reconnect after resume inject so auth carries the capability. Require
machineTag on already-tagged machine rows. Make legacy machineId rotation
idempotent across concurrent 409 handlers.

Co-authored-by: Cursor <cursoragent@cursor.com>
Gate peer deliveries before wrapper slash/special-command parsers and
prefix Pi provenance so first-line skills cannot fire. Terminal resume
mints sessionCapability via machine-tag ownership of the session's
recorded machineId so session-scoped RPC can register without the
create-time session tag.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pecials

Remove the sibling-forgery resume mint that trusted shared machineTag.
Terminal resume now requires runner one-shot inject. Peer /clear|/compact
no longer enter Codex's second parser.

Co-authored-by: Cursor <cursoragent@cursor.com>
machineTag in shared settings can no longer own spawn-happy-session after
the runner drops. Require a process-local runnerProof lease (sticky on
disconnect). Restore direct hapi resume without inject as unattributed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ases

Hub stores sha256(runnerProof) on machine rows (schema v24); websocket auth
only verifies that generation. Reject meta.sentFrom=peer on generic CLI
message ingress. Keep previousMachineIds so local resume survives legacy
machine rotation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Do not persist runnerProof in runner.state. Existing machines with a null
proof hash (or mismatched proof) force machine-id re-enroll. Bump migration
tests to schema v24.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pass memory-only runnerProof through authorized version-handoff env (drained
immediately). Await PeerDeliverBroker listen before exporting broker env so
agents cannot snapshot a missing path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop putting runnerProof in replacement-runner environ; deliver via the
existing PID-checked inject socket (path only in env). Terminal bootstrap
no longer POSTs /cli/machines (runner INSERT binds the proof hash). Direct
hapi resume without inject fails before launch so session RPC is not a
zombie surface.

Co-authored-by: Cursor <cursoragent@cursor.com>
Unwrap message envelopes before rejecting forged peer meta. Require the
bound runnerProof on every machine registration. Recover proof from the
OS keyring across ordinary restarts and migrate session machineIds on
re-enroll. Direct hapi resume acquires runner-proven inject before any
remote handoff stop; Windows gets a named-pipe inject transport.

Co-authored-by: Cursor <cursoragent@cursor.com>
techotaku39 and others added 30 commits August 16, 2026 22:39
* fix(web): preserve session sidebar scroll during navigation

* fix(web): preserve nested session pane scroll

* fix(web): avoid repeated file scroll restoration

* fix(web): preserve scroll when leaving chat file previews

* fix(web): preserve scroll after session resolution
…equest (tiann#1534)

* fix(codex): cache codex model list to avoid spawning app-server per request

listCodexModels() spawned a fresh `codex app-server` subprocess on every
request: exec a version probe, boot the app-server, validate the ChatGPT
session (token refresh over the network when needed), list models, then
kill the process. Fleet measurement showed 0.5-4.4s per call on healthy
machines and 33s (initialize timeout) on a machine with a slow OpenAI
network path, and the web refetches on every session open / dialog mount
(staleTime 30s).

Mirror the opencode model cache: cache successful non-empty lists for 5
minutes and coalesce concurrent requests into a single app-server spawn.
Failures and empty results are never cached, so a broken machine retries
on the next request.

Fixes tiann#1533

* test(codex): cover TTL expiration of the model list cache
tiann#1563)

* fix(pi): settle autonomous agent lifecycles instead of swallowing them

When Pi starts an agent lifecycle on its own — a subagent completion
wake-up, scheduled work — no HAPI prompt is in flight, so the previous
prompt lifecycle has already delivered its settlement and
deliveredSettlement is still true. agent_start unconditionally set
thinking=true, but every settlement path (agent_settled delivery, the
legacy agent_end grace, the prompt-lifecycle fallback) was gated shut by
that stale flag, so the autonomous turn's completion was swallowed:
thinking stayed true forever, the FIFO pump stayed blocked
(piIsStreaming), new messages queued without ever being sent, and abort
waited on a settlement that could never arrive. The only escape was
killing the session.

Open a fresh settlement cycle when agent_start/turn_start arrives with
deliveredSettlement still true: reset deliveredSettlement,
agentEndObserved and activeAgentSettledSeen so the existing settlement
paths (direct agent_settled, legacy agent_end grace) apply to the
autonomous lifecycle. Prompt-driven lifecycles are unaffected because
beginPromptLifecycle has already reset the flag before their agent_start
arrives; mid-cycle retries are unaffected because their cycle has not
settled yet.

Evidence: hapi log 2026-08-13-18-39-10-pid-85758.log — 19:24:07
agent_start (no prompt accepted) → 19:25:40 agent_end + agent_settled
both swallowed → session stuck thinking=true for 27+ minutes until
killed.

* fix(pi): generation-scope the settled callback against autonomous lifecycle races

Review follow-up (HAPI Bot, Major): deliverSettlement() notifies
onAgentSettled only after an async conversationHistory.syncEntries().
An autonomous lifecycle can begin in that window; the stale finally
callback would then mark the new lifecycle's abort boundary as settled
before it emits agent_settled.

Capture lifecycleGeneration at settlement time and skip the
notification when it no longer matches, and advance the generation when
agent_start reopens a settlement cycle for an autonomous lifecycle so
the in-flight callback turns stale.

New regression test holds syncEntries open across the autonomous
agent_start and asserts the stale callback does not settle the new
boundary (fails without the fix).
* fix(web): prevent replaying historical assistant output

* fix(web): handle history pagination during typing handoff

* fix(web): preserve streaming handoff across history windows

* fix(web): distinguish hydrated active runs from new output

* fix(web): preserve handoff across tail hydration

* fix(web): preserve first output in user-only turns
)

* feat(search): add wildcard matching to search fields

* fix(search): harden wildcard matching and file globs

* fix(search): align file matching with shared wildcard semantics

* fix(search): bound file wildcard search in runner

* fix(search): normalize outline queries through shared matcher

* fix(web): remove duplicate markdown test context field
* fix(cursor): retry transient ACP connection errors

* fix(cursor): keep retry classification conservative

* fix(cursor): avoid retrying completed tool effects

* fix(cursor): require terminal retry failures

* fix(cursor): track retry activity across extension events

* fix(cursor): honor permission abort before retry
…sion (part of tiann#1438) (tiann#1475)

* feat(web): composer model/effort value buttons and settings order

Wide composers now show [model] and [effort] value buttons for non-Pi
flavors (labels from the current session values), opening the settings
sheet on click. Narrow viewports collapse to the settings button only
via a new useNarrowViewport hook. The settings sheet reorders to
Model -> Effort -> Permission -> other settings (Fast mode,
collaboration, Copilot agent mode) so permission is first-class.

Toolbar customization gains 'model'/'effort' items with settings labels.
Pi keeps its dedicated model/thinking panels unchanged (unified
descriptor-driven sheet is a follow-up).

* fix(web): satisfy strict types in composer value-button test harness

* fix(web): address review findings on composer model/effort value buttons

- Normalize null/'auto'/'default' model wire values onto the value:null
  option so default-model sessions keep a localized label button (Major)
- Exempt model/effort value buttons from the settings outside-click
  dismissal so a second click closes the sheet instead of reopening it
- Read matchMedia synchronously in useNarrowViewport so narrow first
  paints never flash the wide toolbar
- Add regression tests: model=null/'auto' labels, toggle-close behavior,
  and initial narrow-viewport render

* feat(web): fold Pi into the generic composer model/effort value buttons

Pi sessions previously exposed model/effort twice: dedicated 'Pi model' /
'Pi thinking level' toolbar buttons (PiModelPanel/PiThinkingLevelPanel)
AND the settings sheet's generic Model/Effort sections. Consolidate so Pi
looks exactly like every other flavor:

- Pi now uses the generic model/effort value buttons; labels resolve from
  the provider-qualified piModels catalog (name -> modelId -> session id).
- The settings sheet's Model section already renders provider-grouped Pi
  rows and Effort renders Pi thinking levels, so the dedicated panels and
  their toolbar slots are deleted.
- Keep Pi's mid-turn control affordance (tiann#1442): Pi turns hold
  thread.isDisabled for minutes, so Pi model/effort controls stay enabled
  while a turn is running (configurationControlsDisabled instead of
  controlsDisabled), including the sheet rows.
- Drop 'piModel'/'piThinking' toolbar layout items; persisted layouts
  normalize them away automatically.

Tests: pi value-button label/sheet tests, mid-turn model selection via the
unified sheet, toolbar layout defaults.

* fix(web): keep a session-settings trigger on narrow viewports and mid-turn Pi

Address the HAPI review bot's two Minor findings on the Pi consolidation:

- Narrow viewports collapse the model/effort value buttons into the
  settings sheet, so a persisted toolbar layout hiding the gear left no
  session-settings trigger at all. ComposerButtons now forces the gear
  back into the rendered layout on narrow viewports (wide layouts keep
  honoring the user's hidden choice).
- The Pi mid-turn live-control rule only reached the value buttons and
  sheet rows; with those buttons gone on narrow, the gear was still
  disabled by controlsDisabled for the whole (minutes-long) Pi turn.
  HappyComposer now passes settingsDisabled={modelEffortControlsDisabled}
  so the gear stays clickable mid-turn for Pi exactly like the buttons.

Tests: narrow + hidden-gear layout keeps Settings; narrow Pi mid-turn
gear stays enabled and opens the provider-grouped sheet.

* fix(web): address HAPI Bot Pi settings-sheet findings

Three Minor findings from the review bot on the unified Pi sheet:

- Provider-qualified selection: rows compared only modelId, so duplicate
  model IDs across providers all looked selected. Compare against
  piSelectedModel's provider+modelId when available.
- Thinking-level reset: the removed Pi panel toggled the current level
  back to null; the unified effort rows only submitted concrete values.
  Re-clicking the selected effort row now clears it for Pi.
- Memo staleness: the settings-sheet memo did not depend on
  modelEffortControlsDisabled, so a Pi disabled-state transition while
  the sheet was open left rows enabled from the prior render.

Tests: colliding model IDs highlight only the matching provider row,
re-clicking the selected effort row sends null, and a rerender with
active=false disables the open sheet's rows.

* fix(web): include piSelectedModel in settings-sheet memo deps

The overlays memo reads piSelectedModel for provider-qualified row
highlighting but only declared the derived selectedPiModel. When
piSelectedModel hydrates from absent to a qualifier that resolves to
the same catalog object, the memo is reused and duplicate model IDs
stay highlighted across providers. Add the raw prop to the dep array.

* fix(web): gate Pi model rows on catalog; reset drill-down via value button

Address the HAPI Bot review on the unified composer settings sheet:

- Pi no longer falls back to the generic synthesized modelOptions rows
  when its provider catalog is empty/loading. Selecting one of those
  would post a bare model id that runPi cannot resolve to a provider
  (first cached match or 409). The Model section now only renders for
  Pi when piModelGroups exists, and renders grouped rows exclusively.
- Closing the sheet through the model/effort value button now goes
  through handleSettingsToggle, so a Cursor variant drill-down resets
  to the base model list on reopen (previously only the gear and
  outside-click paths cleared it).

Tests: empty Pi catalog hides Model section; value-button close resets
Cursor drill-down.

* fix(web): hide Pi effort controls until the selected model resolves

Address the HAPI Bot review: with the catalog still loading/failed there
is no selectedPiModel to derive a capability map from, but the unified
effort control stayed enabled (mid-turn Pi controls are intentionally
live). Selecting a level would send set_thinking_level for a model that
may not support reasoning, and the RPC can be rejected after the sheet
closed. The old dedicated panel guarded this state.

showEffortSettings now requires a resolved, reasoning-capable Pi model;
the effort value button hides the same way. With an empty catalog Pi
exposes no settings trigger at all, matching the old control states.

Tests: unresolved Pi catalog mid-turn exposes no effort action.

* fix(web): hide the Pi model trigger until the catalog resolves

Address the HAPI Bot Minor finding: with an empty/loading Pi catalog the
model value button fell back to the bare session model id and rendered
an enabled trigger that opens no Model section. Show the button only
once the provider-qualified catalog entry resolves.
…dation) (tiann#1469)

* feat(config): add agent config descriptor protocol and advertise via runner capability

Introduce shared agent configuration descriptors covering model, effort,
permission, and secondary settings per agent flavor, plus the canonical
HAPI YOLO -> native permission mode mapping. Runners advertise the
builtin descriptors through the runner-state capability so hubs and web
can render configuration without hardcoded flavor branches.

Migrate the OpenCode create-session model picker from a bespoke radio
list to the shared SelectControl combobox.

* feat(web): render create-session permission from agent config descriptor

Replace the flavor-branched Grok/Codex-family/YOLO permission block with
a descriptor-driven PermissionField. Pi now reports permission as managed
instead of silently ignoring the YOLO toggle, and YOLO-only flavors show
the native permission mode the preference maps to.

Removes the superseded GrokPermissionModeSelector and
CodexFamilyPermissionModeSelector components.

* ci: retry flaky claudeRemote 5s-timeout failure

* fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model

The parent initialization effect treated every null selected model as
'uninitialized' and auto-picked a concrete advertised model, clobbering
the user's explicit Default choice (and a restored Default preference).
null now means explicit Default and is preserved; only undefined (no
choice made yet) triggers probe-based initialization. Add parent-level
regression tests for Default persistence and remembered-model restore.

* fix(web): accept undefined selected model in OpencodeModelSelector props

* feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support

Pi's agent config descriptor now advertises model (machine) and effort
(static thinking levels) for create AND session availability:
- cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight
  deduped) and parses the provider/model table; startup model match accepts
  provider-qualified ids
- hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough
- web: NewSession renders Pi models grouped by provider through the generic
  ModelSelector and a new descriptor-driven EffortField (replaces the
  per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload
  forwards Pi model + thinking-level effort (runner already supported --model/
  --effort for pi)

* fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning

- ModelSelector now renders grouped options as <optgroup> (Pi models are
  provider-grouped; identical modelIds from different providers stay distinct)
- PermissionField only receives autoPermissionModeSupported for Grok — a
  cached Grok probe result no longer leaks the Grok warning onto other agents

Addresses HAPI Bot Minor findings on tiann#1469.

* fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog

- ModelSelector buckets options with a reduce instead of Object.groupBy
  (Safari < 17.4 has no polyfill — New Session would throw on those clients)
- Pi restored model/effort are cleared when the value is absent from the live
  machine catalog, and Create waits for the catalog while a non-default Pi
  choice is being validated (mirrors Codex/Grok/Copilot handling)

Addresses HAPI Bot findings on tiann#1469.

* fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import

- PiSession gains startupModelSettled; the startup set_thinking_level waits for
  the requested model's set_model attempt to settle first, so a level the
  default model rejects is not lost before the requested model is confirmed
  (set_model and set_thinking_level were already serialized by the runtime
  mutation lock; this pins the model-first ordering)
- Create Session hides Pi model/effort controls while a Pi history import is
  selected — the import reopens the native session as-is and would silently
  ignore launch-only model/effort values

Addresses HAPI Bot findings on tiann#1469.

* fix(pi): settle startup-model gate when model discovery fails or returns no models

A failed or empty get_available_models response would leave the
startupModelSettled gate unresolved, stranding a requested startup effort
indefinitely. Resolve the gate on the error path and the empty-models path;
adds regression tests for both.
…ransport (tiann#1591)

Replace the Antigravity (agy) integration — a PTY wrapping the TUI with
output-marker scraping ('? for shortcuts', 'Generating', trust dialogs,
/model picker navigation, quota-screen regex) — with a headless print-mode
transport: every user turn spawns `agy -p <msg> --conversation <uuid>
--output-format stream-json`, and NDJSON events (init/step_update/result)
map onto the existing transcript-entry channel (sendAgySessionMessage), so
hub/web rendering is unchanged. ~8.3k LOC (incl. tests) removed.

Fixes tiann#1588. Design: docs/design/agy-headless-transport.md.

CLI:
- new cli/src/agy/headless/: agyNdjsonParser (pure functions, malformed-line
  tolerance, step conversation-id adoption), AgyPlannerAccumulator (per-step
  delta accumulation with settling retries), AgyHeadlessDriver (per-turn
  spawn/kill loop, NDJSON chunk buffering, authoritative delivery ack via
  user_input/result, interrupt + retry + shutdown lifecycle with consume/
  restore, process-tree termination, SSH agent preserved, prompt log
  redaction, per-turn model snapshot with conversation-DB fallback)
- runAgy/loop/session rewired; agy is remote-only (no PTY, no local mode,
  no local-switch action); queued batches snapshot model/effort/mode
- deleted agyPty, agyPtyLauncher, agyHookCarrier(+scope cache), agyModelKeys,
  agyQuestionKeys, agyAskQuestion, agySessionScanner, agyPermissionHandler
  (+tests); buildAgyHooksJson removed; startHookServer agy-pre-invocation
  route → 200 no-op
- runner: agy reopen/resume via generic --existing-session-id; commands/
  agy.ts defaults remote; resume rejects ACTIVE agy sessions (remote-only,
  in-flight turns cannot hand off)
- MCP stays user-managed (agy reads ~/.gemini/config/mcp_config.json and
  workspace .agents/mcp_config.json natively in headless — verified)

Hub/web:
- machines.ts drops agy→pty forcing and rejects non-remote startingMode
- NewSession drops agy startingMode='pty'; terminal toggle disappears
  automatically; RemoteModeDisplay hides the local-switch hint when absent
- docs/guide/agents.md updated: headless print mode, no PTY/hooks, MCP via
  user's own mcp_config.json

Tests: 47 parser+driver tests (fake-binary e2e, chunk-split NDJSON, delivery
ack semantics, interrupt/retry/shutdown races, model attribution, EOF
framing, malformed envelopes); full suite green (cli ~2340, hub 1093,
web 2474, shared 262). Real-binary smoke on agy 1.1.13: single turn exit 0,
--conversation resume keeps the same conversation_id.
* feat(hub): add background-only ServerChan fallback

* fix(hub): validate ServerChan background setting type

* fix(web): preserve pending visibility transitions

* fix(web): guard visibility reports across subscriptions
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	cli/src/agy/runAgy.ts
#	cli/src/commands/resume.test.ts
#	cli/src/commands/resume.ts
#	web/src/lib/assistant-runtime.ts
Unverified peer previews and session/machine labels are stored untrusted text. Neutralize C0/C1 before printing so OSC/ESC cannot hijack the operator TTY.

Co-authored-by: Cursor <cursoragent@cursor.com>
…line

Sanitize remaining doctor interpolations (ids, flavor, lifecycle). Put session names on a Name: line so parentheses cannot split the From: header.

Co-authored-by: Cursor <cursoragent@cursor.com>
tryReceiveOnce only finished on newline or socket error, so accept-then-clean-close hung the handoff retry loop. Bound each attempt with end/close handlers and a 1s silence timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
Client update-metadata could rewrite session machineId and redirect local-resume-capability mint to an attacker-proven machine. Preserve machineId like other hub-owned keys. Pre-tag (NULL) source machines may migrate onto a proven destination.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid null-narrowed expect() so hub tsc --noEmit passes in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Allowing NULL-tag sources let any proven destination absorb sessions. Require create-time tag continuity on this namespace endpoint; legacy recovery needs an operator path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hub correctly rejects untagged migrate-sessions (takeover hole). Do not
abort runner startup after the new machine id is already persisted —
warn and leave legacy sessions on the old id for operator remap.

Co-authored-by: Cursor <cursoragent@cursor.com>
parseSpecialCommand was already skipped for sentFrom=peer, but
expandSkillReference still rewrote leading \$skill into /skill and
mutated peer delivery into receiver-native control syntax.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cold review Majors on tiann#1473 tip:
- Null peercred no longer authorizes on win32 (enumerable pipe path is not auth).
- Delete unused hub reenrollGrant helpers; keep 410 endpoints + orphan tables noted.
- Re-read session machineId immediately before arming resume peer mint.

Co-authored-by: Cursor <cursoragent@cursor.com>
…client

Codex on tip e7275bb: never log resumePeerMintNonce/token before redeem.
Client tryReceiveOnce re-checks peercred before accepting injected secrets
so a connect-time null cred cannot bypass server auth.

Co-authored-by: Cursor <cursoragent@cursor.com>
Active terminal resume without a capability grant must not stop the owner.
Namespace-only session-alive must not refresh active and veto archive.

Co-authored-by: Cursor <cursoragent@cursor.com>
… resumes

Machine tag in settings.json is same-UID readable, so forged proof must not
replace runnerProofHash. Inactive resume without capability cannot register
session RPC after the session-alive gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
…me spawn

Copied machineTag must not take over sessions without the source runnerProof.
Host-fallback spawn without a mint leaves an uncontrollable child after RPC auth gates.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Unrelated shells ask hub/runner to spawn with inject instead of failing.
Cold restart may rebind runnerProofHash only when the machine is offline,
keeping machineId so sessions are not stranded.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove tag-only offline proof rebind (Codex Blocker). Persist runnerProof
under ~/.hapi/runner.proof so cold restart keeps machineId and terminal
hapi resume can mint + open the local TTY. Await redeem/deliver on spawn.

Co-authored-by: Cursor <cursoragent@cursor.com>
Revert durable ~/.hapi/runner.proof (same-UID disk bearer Blocker). Cold
restart strand + unrelated-shell resume remain accepted residuals until
trusted remap. Keep awaited resume inject on the spawn path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cold-review-clean Fork-side bot review is satisfactory; safe to promote to upstream PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants