Skip to content

Read the hosted-agent environment through one type - #838

Merged
alexeyzimarev merged 3 commits into
mainfrom
pavel/gh-836-hosted-agent-env
Sep 10, 2026
Merged

alexeyzimarev merged 3 commits into
mainfrom
pavel/gh-836-hosted-agent-env

Conversation

@Inok

@Inok Inok commented Sep 9, 2026

Copy link
Copy Markdown
Member

Closes #836 — AI-2620

What & why

The daemon exports KCAP_AGENT_ID, KCAP_RENDERED_AGENT and KCAP_DAEMON_URL into every agent it hosts, and the hook side reads them back to learn whether it is hosted and where its bridge is. Both sides reached for the raw strings, each site inventing its own presence test. HostedAgent in Core now owns the three names — a contract between two executables belongs with its reader — and both rules: a blank variable names nothing, and a bridge must be http loopback. Commands take it from the composition root.

DaemonBridge keeps three cases rather than collapsing to string?, because callers answer a refused address differently: the Codex hook fails closed, Claude's falls back to the server route, the input-wait relay gives up silently.

Where to look

Two presence bugs the raw reads hid. is { } and is not null accept an empty string, so a blank id was forwarded as agent_host_id; and CodexHookCommand branched on daemonUrl is null, so a blank bridge took the bridge path, failed validation inside it, and denied — every permission request for the whole session, rather than falling back to Codex's own prompt.

The daemon's five writers are deliberately not one helper: the ACP factories stamp the id with no bridge, and LocalIpc gates the set on --private.

Verification

Mutating DaemonBridge.Parse to read a blank value as an address fails PermissionRequest_with_a_blank_daemon_url_yields_to_codex_rather_than_denying and nothing else. Three further mutations — the refused-bridge report, the relay's POST, the blank-id rule — each fail only their own tests.

Suite Result
Cli.Core 3067 / 0
Cli 3997 / 0
Cli integration 285 / 0
Cli.Daemon 3039 / 1

The daemon failure is Installed_codex_schema_matches_the_vendored_pin, comparing the installed codex 0.153.4 against the 0.147.0 pin; it fails identically on main. dotnet publish -c Release is IL2026/IL3050-clean for kcap and kcap-daemon.

The daemon writes these three variables and every hook reads them back, so their
names are a contract between two executables that share only Core, and they live
with the reader. A blank value is not a value: read as one, an empty bridge
address made every Codex permission request fail closed instead of falling back
to Codex's own prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Centralize hosted-agent environment handling

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Centralizes hosted-agent environment names and parsing in Core.
• Injects typed hosted context into hooks and daemon bridge relays.
• Treats blank IDs and bridge URLs as absent, preserving prompt fallbacks.
Diagram

graph TD
    D["Daemon Runtime"] --> E["Hosted Environment"] --> H["HostedAgent Context"] --> C["Agent Hooks"]
    H --> B{"Bridge State"}
    B -->|Loopback| L["Daemon Bridge"]
    B -->|Not named| F["Server or Prompt"]
    B -->|Refused| R["Report or Deny"]
Loading
High-Level Assessment

The PR's shared Core-owned contract is the best approach because the daemon and CLI are separate executables that must agree on variable names and semantics. Keeping raw environment reads in each hook or reducing bridge state to a nullable string would reintroduce inconsistent blank-value handling and erase the caller-specific distinction between an absent and an unsafe bridge.

Files changed (55) +506 / -389

Enhancement (2) +35 / -0
HostedAgent.csCentralize the hosted-agent environment contract +34/-0

Centralize the hosted-agent environment contract

• Defines the three shared environment names and reads them into a typed hosted-agent record. Empty agent IDs and blank bridge URLs are normalized as absent.

src/Capacitor.Cli.Core/HostedAgent.cs

CommandServices.csRegister hosted-agent state at composition time +1/-0

Register hosted-agent state at composition time

• Creates a singleton HostedAgent from the process environment so commands receive one normalized snapshot.

src/Capacitor.Cli/Commands/CommandServices.cs

Bug fix (10) +98 / -54
DaemonBridge.csModel daemon bridge outcomes as explicit states +42/-0

Model daemon bridge outcomes as explicit states

• Adds parsing for absent, refused, and valid loopback bridge addresses. Only literal HTTP 127.0.0.1 endpoints are accepted, with trailing slashes normalized.

src/Capacitor.Cli.Core/DaemonBridge.cs

AntigravityHookCommand.csInject hosted attribution into Antigravity hooks +2/-2

Inject hosted attribution into Antigravity hooks

• Uses the hosted-agent context to add agent host attribution without accepting blank environment IDs.

src/Capacitor.Cli/Commands/Harness/AntigravityHookCommand.cs

CodexHookCommand.csCorrect Codex bridge fallback and refusal handling +19/-17

Correct Codex bridge fallback and refusal handling

• Routes permission requests by explicit bridge state: valid bridges are used, unsafe named bridges deny, and absent or blank bridges yield to Codex's prompt. Hosted attribution and input-wait relays also use the injected context.

src/Capacitor.Cli/Commands/Harness/CodexHookCommand.cs

CopilotHookCommand.csInject hosted attribution into Copilot hooks +3/-3

Inject hosted attribution into Copilot hooks

• Uses normalized hosted-agent state for session start and end attribution.

src/Capacitor.Cli/Commands/Harness/CopilotHookCommand.cs

CursorHookCommand.csInject hosted attribution into Cursor hooks +2/-2

Inject hosted attribution into Cursor hooks

• Reads the normalized agent host ID from the injected hosted-agent context.

src/Capacitor.Cli/Commands/Harness/CursorHookCommand.cs

GeminiHookCommand.csInject hosted attribution into Gemini hooks +3/-3

Inject hosted attribution into Gemini hooks

• Uses normalized hosted-agent state for session start and end attribution.

src/Capacitor.Cli/Commands/Harness/GeminiHookCommand.cs

KiroHookCommand.csInject hosted attribution into Kiro hooks +2/-2

Inject hosted attribution into Kiro hooks

• Uses the hosted-agent context instead of directly reading the agent ID environment variable.

src/Capacitor.Cli/Commands/Harness/KiroHookCommand.cs

OpenCodeHookCommand.csInject hosted attribution into OpenCode hooks +2/-2

Inject hosted attribution into OpenCode hooks

• Sources agent host attribution from the normalized hosted-agent context.

src/Capacitor.Cli/Commands/Harness/OpenCodeHookCommand.cs

PiHookCommand.csInject hosted attribution into Pi hooks +3/-3

Inject hosted attribution into Pi hooks

• Uses normalized hosted-agent state for both session start and session end attribution.

src/Capacitor.Cli/Commands/Harness/PiHookCommand.cs

PermissionRequestCommand.csRoute Claude permissions from typed bridge state +20/-20

Route Claude permissions from typed bridge state

• Uses injected rendered, agent ID, and bridge state. Valid bridges remain local, absent bridges fall back to the server, and refused addresses are reported before fallback.

src/Capacitor.Cli/Commands/PermissionRequestCommand.cs

Refactor (10) +37 / -30
AntigravityHostedAgentRuntimeFactory.csUse the shared hosted-agent ID variable +1/-1

Use the shared hosted-agent ID variable

• Replaces the Antigravity runtime's raw agent ID environment key with the Core-owned constant.

src/Capacitor.Cli.Daemon/Harness/Antigravity/AntigravityHostedAgentRuntimeFactory.cs

CodexHostedAgentRuntimeFactory.csUse shared hosted-agent environment constants +4/-3

Use shared hosted-agent environment constants

• Builds Codex's rendered, agent ID, and bridge environment entries from the shared Core contract.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs

PiRpcHostedAgentRuntimeFactory.csUse the shared agent ID environment key +2/-1

Use the shared agent ID environment key

• Replaces Pi RPC's raw hosted-agent ID key with the Core-owned constant.

src/Capacitor.Cli.Daemon/Harness/Pi/PiRpcHostedAgentRuntimeFactory.cs

PtyEnvScrub.csScrub hosted variables through the shared contract +4/-3

Scrub hosted variables through the shared contract

• Builds the PTY hosted-variable scrub list from the centralized environment constants.

src/Capacitor.Cli.Daemon/Pty/PtyEnvScrub.cs

AgentOrchestrator.LocalIpc.csStamp local agents with shared environment names +3/-3

Stamp local agents with shared environment names

• Uses the hosted-agent contract for rendered status, agent ID, and optional bridge URL while preserving the private-spawn gate.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs

OrphanReaper.csRead orphan markers through the shared agent key +2/-2

Read orphan markers through the shared agent key

• Uses the centralized agent ID environment name for both orphan discovery and live marker verification.

src/Capacitor.Cli.Daemon/Services/OrphanReaper.cs

ProcessReaper.csVerify process identity with the shared agent key +1/-1

Verify process identity with the shared agent key

• Replaces the raw hosted-agent ID key used by Linux process identity checks.

src/Capacitor.Cli.Daemon/Services/ProcessReaper.cs

PtyHostedAgentRuntimeFactory.csExport PTY hosted state through shared constants +4/-3

Export PTY hosted state through shared constants

• Uses the centralized rendered, agent ID, and bridge names when constructing PTY runtime environments.

src/Capacitor.Cli.Daemon/Services/PtyHostedAgentRuntimeFactory.cs

DaemonInputWaitRelay.csRelay input-wait events from typed hosted state +11/-8

Relay input-wait events from typed hosted state

• Accepts an injected HostedAgent instead of rereading raw environment variables. Relays only when both an agent ID and validated loopback bridge are present.

src/Capacitor.Cli/Commands/DaemonInputWaitRelay.cs

ClaudeHookCommand.csDrive Claude hook behavior from hosted context +5/-5

Drive Claude hook behavior from hosted context

• Uses injected state for input-wait relays, rendered-session policy routing, agent attribution, and permission requests.

src/Capacitor.Cli/Commands/Harness/ClaudeHookCommand.cs

Tests (33) +336 / -305
DaemonBridgeTests.csCover daemon bridge classification +34/-0

Cover daemon bridge classification

• Tests accepted loopback URLs, slash normalization, blank values, malformed URLs, HTTPS, DNS localhost, IPv6, and remote hosts.

test/Capacitor.Cli.Core.Tests.Unit/DaemonBridgeTests.cs

HostedAgentTests.csCover hosted-agent environment parsing +48/-0

Cover hosted-agent environment parsing

• Verifies blank ID normalization, verbatim IDs, independent bridge absence, and bridge parsing alongside hosted identity.

test/Capacitor.Cli.Core.Tests.Unit/HostedAgentTests.cs

AntigravitySessionStartTests.csProvide terminal context to Antigravity integration tests +3/-2

Provide terminal context to Antigravity integration tests

• Updates direct hook construction to supply an explicit non-hosted context.

test/Capacitor.Cli.Tests.Integration/AntigravitySessionStartTests.cs

ClaudeHookStdoutTests.csProvide terminal context to Claude stdout tests +2/-1

Provide terminal context to Claude stdout tests

• Updates the Claude hook fixture for hosted-agent constructor injection.

test/Capacitor.Cli.Tests.Integration/ClaudeHookStdoutTests.cs

CodexSessionStartHandshakeOnPostFailureTests.csProvide terminal context to Codex handshake tests +2/-1

Provide terminal context to Codex handshake tests

• Keeps the post-failure handshake scenario explicitly outside daemon hosting.

test/Capacitor.Cli.Tests.Integration/CodexSessionStartHandshakeOnPostFailureTests.cs

CodexSessionStartVisibilityTests.csProvide terminal context to Codex visibility tests +3/-2

Provide terminal context to Codex visibility tests

• Updates Codex hook construction while preserving session visibility coverage.

test/Capacitor.Cli.Tests.Integration/CodexSessionStartVisibilityTests.cs

CursorSessionStartVisibilityTests.csProvide terminal context to Cursor visibility tests +1/-1

Provide terminal context to Cursor visibility tests

• Supplies explicit non-hosted state to the Cursor session-start integration path.

test/Capacitor.Cli.Tests.Integration/CursorSessionStartVisibilityTests.cs

CursorTailingWatcherTests.csProvide terminal context to Cursor watcher tests +2/-2

Provide terminal context to Cursor watcher tests

• Updates watcher reactivation scenarios for hosted-agent constructor injection.

test/Capacitor.Cli.Tests.Integration/CursorTailingWatcherTests.cs

GeminiSessionStartHandshakeOnPostFailureTests.csProvide terminal context to Gemini handshake tests +2/-1

Provide terminal context to Gemini handshake tests

• Keeps Gemini's failed-post handshake scenario explicitly non-hosted.

test/Capacitor.Cli.Tests.Integration/GeminiSessionStartHandshakeOnPostFailureTests.cs

GeminiStderrShadowedOnPostFailureTests.csProvide terminal context to Gemini output tests +2/-1

Provide terminal context to Gemini output tests

• Updates direct Gemini hook construction without changing output assertions.

test/Capacitor.Cli.Tests.Integration/GeminiStderrShadowedOnPostFailureTests.cs

PermissionRequestRecoveryTests.csModel rendered permission recovery explicitly +4/-3

Model rendered permission recovery explicitly

• Replaces process environment setup with an injected rendered HostedAgent that has no local bridge.

test/Capacitor.Cli.Tests.Integration/PermissionRequestRecoveryTests.cs

PermissionRequestWatcherSelfHealTests.csProvide terminal context to watcher self-heal tests +3/-3

Provide terminal context to watcher self-heal tests

• Updates permission command construction while retaining watcher creation and suppression coverage.

test/Capacitor.Cli.Tests.Integration/PermissionRequestWatcherSelfHealTests.cs

SessionStartCoordinationNoticesTests.csProvide terminal context to coordination notice tests +4/-3

Provide terminal context to coordination notice tests

• Updates Claude hook construction for the new hosted-agent dependency.

test/Capacitor.Cli.Tests.Integration/SessionStartCoordinationNoticesTests.cs

SessionStartMemoryRedirectTests.csProvide terminal context to memory redirect tests +2/-1

Provide terminal context to memory redirect tests

• Supplies explicit terminal state to the Gemini memory-index redirect scenario.

test/Capacitor.Cli.Tests.Integration/SessionStartMemoryRedirectTests.cs

SessionStartVisibilityTests.csProvide terminal context to Claude visibility tests +6/-5

Provide terminal context to Claude visibility tests

• Updates Claude session-start scenarios to use explicit non-hosted state.

test/Capacitor.Cli.Tests.Integration/SessionStartVisibilityTests.cs

SpoolOutageRecoveryTests.csProvide terminal context to spool recovery tests +1/-1

Provide terminal context to spool recovery tests

• Updates the Claude hook fixture while preserving outage and replay behavior.

test/Capacitor.Cli.Tests.Integration/SpoolOutageRecoveryTests.cs

AntigravityHookCommandTests.csUpdate Antigravity tests for hosted-state injection +7/-6

Update Antigravity tests for hosted-state injection

• Supplies terminal hosted-agent state to malformed, missing, and ignored event scenarios.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/AntigravityHookCommandTests.cs

ClaudeHookCommandTests.csUpdate Claude hook fixtures for hosted state +24/-24

Update Claude hook fixtures for hosted state

• Passes explicit terminal context throughout policy, lifecycle, timeout, spool, memory, and authentication tests.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/ClaudeHookCommandTests.cs

ClaudeHookInputWaitRelayTests.csTest Claude relays with typed hosted state +14/-17

Test Claude relays with typed hosted state

• Replaces process environment mutation with explicit hosted contexts for relay, unhosted, subagent, and exhausted-budget cases.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/ClaudeHookInputWaitRelayTests.cs

CodexHookCommandTests.csCover Codex's explicit bridge-state behavior +92/-108

Cover Codex's explicit bridge-state behavior

• Reworks hook fixtures to inject hosted state and adds regression coverage proving a blank bridge yields to Codex rather than denying. Existing bridge success, failure, refusal, and lane-selection cases now use typed state.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/CodexHookCommandTests.cs

CodexHookInputWaitRelayTests.csTest Codex relays with typed hosted state +7/-7

Test Codex relays with typed hosted state

• Injects an agent ID and validated bridge directly instead of mutating process-wide environment variables.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/CodexHookInputWaitRelayTests.cs

CursorHookCommandTests.csUpdate Cursor fixtures for hosted state +14/-16

Update Cursor fixtures for hosted state

• Adds configurable HostedAgent state to the fixture and directly verifies hosted attribution without global environment mutation.

test/Capacitor.Cli.Tests.Unit/Commands/Harness/CursorHookCommandTests.cs

PermissionRequestCommandTests.csTest permission routing by bridge state +26/-67

Test permission routing by bridge state

• Replaces raw environment parsing tests with explicit loopback, absent, and refused bridge cases. Verifies silent absence, refusal reporting, and loopback HTTP lane selection.

test/Capacitor.Cli.Tests.Unit/Commands/PermissionRequestCommandTests.cs

PermissionRequestPolicySeamTests.csModel rendered policy tests with hosted context +5/-10

Model rendered policy tests with hosted context

• Removes process-wide environment setup and injects rendered or terminal hosted state per scenario.

test/Capacitor.Cli.Tests.Unit/Commands/PermissionRequestPolicySeamTests.cs

UnusableUrlGuardTests.csUpdate unusable URL tests for hosted injection +5/-5

Update unusable URL tests for hosted injection

• Supplies terminal state to Claude and Cursor guard scenarios.

test/Capacitor.Cli.Tests.Unit/Commands/UnusableUrlGuardTests.cs

AntigravitySessionStartMemoryTests.csUpdate Antigravity memory fixtures +5/-4

Update Antigravity memory fixtures

• Passes explicit terminal hosted-agent state through memory and malformed-event scenarios.

test/Capacitor.Cli.Tests.Unit/Harness/Antigravity/AntigravitySessionStartMemoryTests.cs

ClaudeHookExclusionGateTests.csUpdate Claude exclusion fixture +2/-1

Update Claude exclusion fixture

• Adds terminal hosted-agent state to the hook factory.

test/Capacitor.Cli.Tests.Unit/Harness/Claude/ClaudeHookExclusionGateTests.cs

CursorLiveSubagentIntegrationTests.csUpdate Cursor subagent integration fixture +1/-1

Update Cursor subagent integration fixture

• Supplies explicit terminal context to live subagent hook execution.

test/Capacitor.Cli.Tests.Unit/Harness/Cursor/CursorLiveSubagentIntegrationTests.cs

CursorSubagentStaleStateTests.csUpdate Cursor stale-state scenarios +4/-4

Update Cursor stale-state scenarios

• Passes terminal hosted-agent state through stale marker and dual-routing tests.

test/Capacitor.Cli.Tests.Unit/Harness/Cursor/CursorSubagentStaleStateTests.cs

CursorWatcherSpawnTests.csUpdate Cursor watcher-spawn fixtures +5/-5

Update Cursor watcher-spawn fixtures

• Supplies terminal hosted-agent state to direct hook constructions and deferred spool scenarios.

test/Capacitor.Cli.Tests.Unit/Harness/Cursor/CursorWatcherSpawnTests.cs

GeminiHookOutputContractTests.csUpdate Gemini output fixture +2/-1

Update Gemini output fixture

• Adds explicit terminal hosted-agent state to output contract execution.

test/Capacitor.Cli.Tests.Unit/Harness/Gemini/GeminiHookOutputContractTests.cs

GeminiSessionStartMemoryTests.csUpdate Gemini memory fixture +2/-1

Update Gemini memory fixture

• Supplies terminal hosted-agent state to session-start memory tests.

test/Capacitor.Cli.Tests.Unit/Harness/Gemini/GeminiSessionStartMemoryTests.cs

PiSessionStartMemoryTests.csUpdate Pi memory fixture +2/-1

Update Pi memory fixture

• Adds explicit terminal hosted-agent state to the Pi hook factory.

test/Capacitor.Cli.Tests.Unit/Harness/Pi/PiSessionStartMemoryTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Commit subject contains a hyphen ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
The commit subject Read the hosted-agent environment through one type (#836) contains the
hyphenated token hosted-agent, so it does not match the required word-only subject pattern. The
subject is otherwise imperative, single-clause, issue-suffixed, and within the 80-character limit.
Code

src/Capacitor.Cli.Core/HostedAgent.cs[14]

+public sealed record HostedAgent(string? AgentId, bool IsRendered, DaemonBridge Bridge) {
Relevance

●●● Strong

Explicit compliance rule disallows hyphenated subject tokens; changing hosted-agent to hosted agent
is a trivial deterministic fix.

PR-#659
PR-#327

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2897961 requires the complete commit subject to match its word-only pattern; the
supplied subject contains hosted-agent, which includes a disallowed hyphen.

Rule 2897961: Enforce single-clause imperative commit subject with GitHub issue reference and 80-char limit

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Update the commit subject so every clause word matches the required alphanumeric word pattern.

## Issue Context
Use `Read the hosted agent environment through one type (#836)` instead of the current hyphenated subject.

## Fix Focus Areas
- src/Capacitor.Cli.Core/HostedAgent.cs[14-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 61 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: 6f14e3c1)
Review mode: 🧠 Deep: This cross-cutting contract refactor spans daemon writers, multiple agent hooks, permission and relay paths, and 204 edit sites, creating many independent behavior regressions that a redundant review could catch.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/HostedAgent.cs
Inok and others added 2 commits September 9, 2026 18:13
A value made of blanks was set by someone, so reading it as unnamed drops the
line that tells them why their bridge is ignored, and lets a hook that fails
closed on a bad address fall back silently instead. Only an unset or empty
variable names nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing asserted the marker is that exact value, on either side of the contract.
The name is pinned by the scrub tests, and both sides share the constant, so a
rename stays self-consistent — only a changed value could pass unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Inok
Inok requested a review from alexeyzimarev September 9, 2026 17:42
@alexeyzimarev
alexeyzimarev merged commit 453ab52 into main Sep 10, 2026
7 checks passed
@alexeyzimarev
alexeyzimarev deleted the pavel/gh-836-hosted-agent-env branch September 10, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook commands read KCAP_AGENT_ID and KCAP_DAEMON_URL past their own seams

2 participants