Skip to content

fix: keep managed skill loading on native seam - #323

Merged
pedronauck merged 3 commits into
mainfrom
codex/issue-314-session-skill-identity
Aug 6, 2026
Merged

pedronauck merged 3 commits into
mainfrom
codex/issue-314-session-skill-identity

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Status notice

The rebased branch was pushed at commit f14fefb1. A final make gate-full was started but canceled with exit code 130 by explicit user instruction. This PR does not claim a current full-gate PASS. The strict QA evidence audit therefore still reports C14 (missing final make verify evidence).

Summary

  • Managed Compozy sessions load installed skills through the native compozy__skill_view tool only, including skills omitted from the prompt catalog.
  • compozy skill remains an operator-shell interface; managed environment markers trigger an early supported-path UX guard before resolution, filesystem work, or a daemon request.
  • Hosted-MCP bind nonce lifetime now begins after ACP initialization and before session negotiation, so cold provider bootstrap time does not consume the bind window.

Fixes #314

Context and reproduction

The original implementation attempted to provide a managed CLI fallback through a private read-only Unix socket and caller identity headers. That design could not enforce its security promise: arbitrary provider code runs with the daemon user's UID, can remove or replace environment variables, and can open the normal operator UDS directly. Environment values, headers, PID ancestry, and mode 0600 cannot distinguish those same-UID requests without a real OS isolation boundary or a credential the provider cannot obtain.

The supported managed path is therefore the existing daemon-native tool seam. A second failure appeared during behavioral QA: the hosted-MCP bind nonce was minted before ACP initialization. A cold Codex ACP launch took longer than the 30-second TTL, so the native tool seam expired before the provider could bind it.

Solution design

Native-only managed skill loading

  • The managed skill catalog tells the agent to resolve and call compozy__skill_view.
  • If session policy denies the native tool, the agent reports the skill unavailable; it does not invoke the operator CLI or read skill files directly.
  • Prompt fixtures, canonical tests, the public skills docs, and the official Compozy skill now use the same contract.

Operator CLI supported-path guard

  • Every compozy skill verb (list, view, info, inspect, where, search, install, remove, update, create, enable, and disable) checks for managed-session markers before doing client, UDS, registry, or filesystem work.
  • The error points managed callers to compozy__skill_list, compozy__skill_search, and compozy__skill_view.
  • This is deliberately documented as a UX/support guard, not authorization. Same-UID code can clear the markers or access the operator UDS directly; this PR does not claim otherwise.

Hosted-MCP nonce activation

  • Launch creates an unarmed hosted-MCP launch record.
  • ACP invokes the activation callback only after initialize succeeds and immediately before session/new or session/load.
  • ArmLaunch starts the bind TTL once and idempotently.
  • Bind attempts before activation fail closed; normal expiry after activation remains enforced.
  • Existing session-start cleanup cancels the launch record when activation or later startup fails.

Hard-cut delete targets

The unsafe managed CLI fallback was removed rather than retained as compatibility code:

  • managed transport socket creation, proxy, inheritance, Windows stubs, and lifecycle wiring;
  • COMPOZY_AGENT_TRANSPORT_SOCKET selection and managed CLI client transport;
  • managed identity header protocol introduced for skill routes;
  • managed skill API scope and caller-selected workspace/session behavior;
  • associated OpenAPI and generated web-client changes;
  • tests, fixtures, docs, and QA claims that described a validated managed CLI fallback.

The normal agent-kernel identity contract already present on origin/main is unchanged.

Changes by surface

  • Runtime / ACP / MCP: arm hosted-MCP bind TTL after provider initialization and wire activation through session startup.
  • CLI: reject accidental managed-session use of all skill commands before resolution or I/O; preserve normal operator behavior.
  • Prompts and fixtures: replace the managed CLI fallback promise with native-only guidance.
  • Docs and official skill: distinguish managed native tools from operator CLI and state the same-UID limitation explicitly.
  • QA tracker: add the managed-session journey, charter, scenario, cold-start bug record, and dated report.
  • HTTP / UDS / OpenAPI / web: no effective contract delta from origin/main; the proposed managed skill headers and generated schema changes were deleted.
  • Config and extensions: no new config key, hook, extension API, or compatibility alias.

Invariants and tests

  • Managed CLI guard: every current skill verb returns the supported-path error before client or filesystem access, and either managed marker independently triggers it.
  • Hosted lifecycle: bind before activation is denied; expiry begins after activation; ACP ordering is initialize → activation → session negotiation.
  • Session wiring: hosted MCP is armed only for sessions that receive the hosted server; verdict-only and provider-disabled paths remain without it.
  • Catalog contract: runtime prompt, daemon test, ACP mock fixture, site docs, and official skill agree on native-only managed loading.

Focused race-enabled checks completed before publication:

  • internal/cli
  • internal/mcp
  • internal/acp
  • internal/session
  • internal/skills
  • internal/testutil/acpmock
  • internal/daemon
  • internal/api/udsapi
  • site generation and typecheck (4 successful, 4 total)

Behavioral QA

  • Fresh isolated lab with 220 filler skills and marker skill zz-native-seam-marker.
  • The target had no catalog entry or body in the persisted provider prompt; its name occurred only in the user's request.
  • The first and only Codex session (sess-08797076cb99e02f) used a provider wrapper with a 35-second startup delay.
  • ACP initialize took 36,958 ms; activation followed, session/new took 443 ms, and hosted bind succeeded in 79 ms without retry or nonce expiry.
  • Persisted events show compozy__tool_info at sequence 11, compozy__skill_view at sequence 15, and the exact body at sequence 16.
  • Native and operator-normalized bodies were 173 bytes with SHA-256 9eb83bd03cfa8e4763ff8c1808dda9f5305fae0f066a88e606fa07bb08e4ebca.
  • No shell, terminal, operator CLI, or direct skill-file tool call occurred.
  • All twelve managed-env skill CLI verbs returned the guard error; the skill remained enabled and active, and operator compozy skill view still worked.
  • Teardown recorded clean: true, zero survivors, and removed the daemon and tmux sockets.

Full report: docs/qa/reports/2026-08-05-issue-314-managed-skill-loading.md

Verification status

  • Focused Go race checks listed above
  • Site generation and typecheck
  • Real managed Codex behavioral QA
  • Isolated teardown with clean: true
  • Current make gate-full — canceled by explicit user instruction; no PASS claimed
  • Strict QA evidence audit — C14 remains because final gate evidence is absent

Compozy Impact Audit

  • Native tools: no descriptor, schema, digest, risk, toolset, or capability-gate change. Managed discovery and loading use the existing compozy__tool_info and compozy__skill_view projection; live persisted events prove the call path.
  • Extensibility and hooks: the official Compozy skill and managed catalog guidance changed. Hosted-MCP activation timing changed, but extension APIs, hooks, registries, bridge SDKs, config lifecycle, and sidecar contracts did not.
  • Workspace data isolation: no new stored datum or caller-selected scope. The native tool retains the daemon-owned session/workspace/agent scope. The removed transport/header design leaves no special raw skill API identity path.
  • Official Compozy skill: skills/compozy/references/tools-and-skills.md now documents native-only managed loading, operator-only CLI use, the UX guard, and the same-UID non-claim.

Compatibility and remaining risk

This is a zero-legacy hard cut: managed CLI fallback is unsupported and removed. Operator CLI behavior remains available from a normal shell. The UX guard is bypassable by arbitrary same-UID code that clears its environment or opens the operator UDS; preventing that requires OS-level isolation or an unforgeable credential and is outside this PR. No fallback to the normal UDS was added because that would widen privilege.

Summary by CodeRabbit

  • New Features

    • Hosted MCP services activate automatically during session startup, before session negotiation.
    • MCP launch binding now requires an active, validated launch.
  • Bug Fixes

    • Improved cleanup and error handling when MCP activation fails.
    • Skill CLI commands now reject managed-session usage and direct users to supported skill tools.
  • Documentation

    • Updated skill-loading guidance to prohibit direct file access and operator-only CLI commands in managed sessions.

@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview Aug 6, 2026 2:24am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e57d524e-6719-4dc0-8fc3-f4493779cfcc

📥 Commits

Reviewing files that changed from the base of the PR and between f14fefb and e30ec8e.

⛔ Files ignored due to path filters (3)
  • docs/qa/reports/2026-08-05-review-findings-managed-skills.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-managed-session-skill-loading.md is excluded by !**/*.md
  • packages/site/content/docs/skills/skill-md.mdx is excluded by !**/*.mdx
📒 Files selected for processing (11)
  • internal/acp/client_start_contract_test.go
  • internal/acp/start.go
  • internal/cli/skill_daemon_test.go
  • internal/cli/skill_managed_guard.go
  • internal/daemon/prompt_skills_test.go
  • internal/mcp/hosted.go
  • internal/mcp/hosted_test.go
  • internal/skills/catalog.go
  • internal/skills/catalog_test.go
  • internal/testutil/acpmock/fixture.go
  • internal/testutil/acpmock/fixture_test.go

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

The PR adds explicit hosted MCP launch arming during ACP startup and blocks skill CLI use from managed sessions. Skill-loading prompts now require the canonical managed-session tool and restrict CLI use to operator shells.

Changes

Hosted MCP activation

Layer / File(s) Summary
Hosted launch arming lifecycle
internal/mcp/..., internal/daemon/..., internal/api/udsapi/hosted_mcp_test.go
Hosted launches now require explicit arming before binding. Arming starts expiration and remains idempotent.
Session MCP activation wiring
internal/session/...
Session startup configures hosted MCP activation and test doubles record activation calls.
ACP startup activation stage
internal/acp/...
ACP startup activates MCP after initialize and before session/new. Activation failures stop startup and clean up the process.

Managed skill surface policy

Layer / File(s) Summary
Managed skill CLI guard
internal/cli/...
Skill CLI commands reject managed session or agent identity environments before daemon or filesystem access.
Canonical skill-loading guidance
internal/skills/..., internal/testutil/acpmock/..., internal/daemon/prompt_skills_test.go
Managed sessions must use compozy__skill_view. Direct CLI and skill-file access are prohibited.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SessionManager
  participant ACPStart
  participant HostedMCPLauncher
  participant HostedService
  SessionManager->>ACPStart: configure ActivateMCPServers
  ACPStart->>ACPStart: initialize ACP connection
  ACPStart->>HostedMCPLauncher: activate MCP servers
  HostedMCPLauncher->>HostedService: ArmLaunch session ID
  HostedService-->>HostedMCPLauncher: confirm armed launch
  ACPStart->>ACPStart: negotiate session/new
Loading

Possibly related issues

  • compozy/agh#3: The issue covers daemon-managed skills and MCP lifecycle behavior implemented by this PR.

Possibly related PRs

  • compozy/compozy#34: This PR extends the ACP startup flow introduced there with MCP activation.
  • compozy/compozy#311: This PR changes managed-session skill access toward canonical compozy__skill_view tooling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: managed skill loading remains on the native seam.
Linked Issues check ✅ Passed The changes enforce canonical managed-session skill loading and activate hosted MCP before session negotiation, addressing issue #314.
Out of Scope Changes check ✅ Passed The code and tests remain focused on managed skill loading, hosted MCP activation, and related session lifecycle behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-314-session-skill-identity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit e30ec8e.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Greptile Summary

The PR keeps managed skill loading on daemon-native tools, rejects accidental managed use of the operator skill CLI, and delays hosted-MCP nonce activation until ACP initialization completes.

  • Adds an early managed-session guard covering every compozy skill verb.
  • Arms hosted-MCP launch records between ACP initialization and session negotiation.
  • Updates prompts, fixtures, public documentation, and QA evidence to describe the native-only contract.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
internal/acp/start.go Activates hosted MCP after ACP initialization and before session negotiation, with startup cleanup on activation failure.
internal/mcp/hosted.go Introduces idempotent launch arming so bind expiry begins only when the provider can negotiate a session.
internal/mcp/hosted_records.go Rejects hosted-tool binding until the corresponding launch record has been armed.
internal/session/manager_start_mcp.go Wires the session-specific hosted-MCP activation callback into ACP startup only when hosted tools are present.
internal/cli/skill_managed_guard.go Detects managed-session markers and returns supported-path guidance before skill command work begins.
internal/cli/skill_commands.go Applies the managed-session guard through the parent command’s persistent pre-run hook to all skill verbs.
internal/skills/catalog.go Replaces managed CLI fallback guidance with native-only skill discovery and loading instructions.

Sequence Diagram

sequenceDiagram
  participant Manager as Session Manager
  participant MCP as Hosted MCP
  participant ACP as ACP Provider
  participant Tools as Native Tool Proxy
  Manager->>MCP: Launch unarmed record
  Manager->>ACP: Start provider
  ACP->>ACP: initialize
  ACP->>MCP: ArmLaunch(sessionID)
  MCP-->>ACP: Nonce TTL starts
  ACP->>ACP: session/new or session/load
  Tools->>MCP: Bind with nonce
  MCP-->>Tools: Native tools available
Loading

Reviews (3): Last reviewed commit: "fix: review round" | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (9)
internal/acp/agent_transport_unix.go (2)

56-57: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider bounding body and idle time on the managed transport server.

The server sets ReadHeaderTimeout only. A managed process that opens a connection and stalls the request body or keeps an idle connection holds a goroutine until the transport closes. The socket lives in a 0700 temp directory and only the same local user reaches it, so this is hardening rather than an exploitable path.

Add ReadTimeout, WriteTimeout, and IdleTimeout. Set ReadTimeout and WriteTimeout high enough for large skill content responses.

♻️ Proposed timeout bounds
-const defaultManagedTransportReadHeaderTimeout = 5 * time.Second
+const (
+	defaultManagedTransportReadHeaderTimeout = 5 * time.Second
+	defaultManagedTransportReadTimeout       = 30 * time.Second
+	defaultManagedTransportWriteTimeout      = 60 * time.Second
+	defaultManagedTransportIdleTimeout       = 60 * time.Second
+)
-	server := &http.Server{Handler: handler, ReadHeaderTimeout: defaultManagedTransportReadHeaderTimeout}
+	server := &http.Server{
+		Handler:           handler,
+		ReadHeaderTimeout: defaultManagedTransportReadHeaderTimeout,
+		ReadTimeout:       defaultManagedTransportReadTimeout,
+		WriteTimeout:      defaultManagedTransportWriteTimeout,
+		IdleTimeout:       defaultManagedTransportIdleTimeout,
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/agent_transport_unix.go` around lines 56 - 57, Update the
http.Server construction in the managed transport setup to include ReadTimeout,
WriteTimeout, and IdleTimeout alongside ReadHeaderTimeout. Use the existing
managed transport timeout configuration or add appropriately generous bounds for
large skill-content request and response bodies, while ensuring idle connections
are eventually closed.

Source: Linters/SAST tools


39-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the silent no-op return contract.

startManagedAgentTransport returns (nil, "", nil) when daemonSocket, sessionID, or agentName is blank. The signature does not signal this to callers. A caller that assumes a non-nil transport on err == nil will dereference nil. bindManagedAgentTransport and the lifecycle test both check transport == nil explicitly, so current callers are correct, but the behavior is implicit.

Add a doc comment on the function stating that missing identity or socket inputs disable the transport and produce a nil transport with a nil error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/agent_transport_unix.go` around lines 39 - 42, Add a doc comment
to startManagedAgentTransport documenting that blank daemonSocket, sessionID, or
agentName disables the transport and returns a nil transport with a nil error.
Keep the existing return behavior unchanged and make the contract clear for
callers.
internal/acp/terminal_agent_transport_proxy.go (1)

30-35: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Set explicit timeouts on the outbound daemon transport.

The http.Transport sets only DialContext. It has no ResponseHeaderTimeout, IdleConnTimeout, or ExpectContinueTimeout. If the daemon accepts the connection and never responds, the proxied request stays open until the managed process cancels it. The repository guideline requires explicit timeouts on outbound network calls.

Add bounded timeouts to the transport.

♻️ Proposed transport timeouts
 	transport := &http.Transport{
 		DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
 			var dialer net.Dialer
 			return dialer.DialContext(ctx, "unix", socketPath)
 		},
+		ResponseHeaderTimeout: 30 * time.Second,
+		IdleConnTimeout:       60 * time.Second,
+		ExpectContinueTimeout: time.Second,
 	}

As per coding guidelines: "All outbound HTTP or network calls must use clients with explicit timeouts; http.DefaultClient is forbidden in production paths."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/terminal_agent_transport_proxy.go` around lines 30 - 35, Update
the http.Transport in the terminal agent transport setup to define explicit
bounded ResponseHeaderTimeout, IdleConnTimeout, and ExpectContinueTimeout
values, while preserving the existing Unix-socket DialContext behavior.

Source: Coding guidelines

internal/acp/terminal_identity_env.go (1)

28-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Source COMPOZY_AGENT_NAME from a shared constant.

internal/cli/whoami.go already owns envAgentName = "COMPOZY_AGENT_NAME" for reading it, but internal/acp/terminal_identity_env.go uses the same literal in the managed terminal allowlist. Keep a shared constant for this consumer reference to avoid out-of-sync renames.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/terminal_identity_env.go` around lines 28 - 41, Update
isManagedTerminalEnvName to replace the hardcoded "COMPOZY_AGENT_NAME" entry
with the shared envAgentName constant already defined in internal/cli/whoami.go,
preserving the existing allowlist behavior and avoiding a duplicate literal.
internal/acp/launcher.go (1)

30-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Define the managed transport capability interface at its consumer.

prepareManagedAgentTransport consumes managedAgentTransportLauncher in internal/acp/agent_transport.go. Move the interface declaration there. Keep the compile-time assertion in this file.

As per coding guidelines, define interfaces where they are consumed.

Proposed refactor
--- a/internal/acp/launcher.go
-type managedAgentTransportLauncher interface {
-	supportsManagedAgentTransport()
-}
-
--- a/internal/acp/agent_transport.go
+type managedAgentTransportLauncher interface {
+	supportsManagedAgentTransport()
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/launcher.go` around lines 30 - 32, Move the
managedAgentTransportLauncher interface declaration from this file to the
consumer location in prepareManagedAgentTransport within agent_transport.go,
while preserving the existing compile-time assertion here and leaving the
interface method unchanged.

Source: Coding guidelines

internal/cli/client_managed_transport_unix_test.go (1)

74-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use t.Context() for the client calls.

Line 32 already uses t.Context(). Lines 74 and 81 use context.Background(), so a hung request is not cancelled when the test ends.

♻️ Proposed consistency fix
-		skill, err := client.GetSkill(context.Background(), "release-signal", query)
+		skill, err := client.GetSkill(t.Context(), "release-signal", query)
 		if err != nil {
 			t.Fatalf("GetSkill() error = %v", err)
 		}
 		if skill.Name != "release-signal" {
 			t.Fatalf("GetSkill().Name = %q, want release-signal", skill.Name)
 		}
-		content, err := client.GetSkillContent(context.Background(), "release-signal", query)
+		content, err := client.GetSkillContent(t.Context(), "release-signal", query)

Remove the now-unused "context" import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/client_managed_transport_unix_test.go` around lines 74 - 81,
Update the client calls in the test, including GetSkill and GetSkillContent, to
use t.Context() instead of context.Background() so requests are cancelled with
the test. Remove the context import if it is no longer used.
internal/api/core/skill_scope.go (1)

51-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the duplicated ForWorkspace branch and map its errors.

Lines 51-54 call ForWorkspace with the same value in both branches, because resolved is nil in the second branch. The workspace path also skips mapSkillScopeError, so its errors are not classified and reach StatusForSkillError as unknown errors, which returns 500.

♻️ Proposed simplification with consistent error mapping
-	if resolved != nil {
-		return h.SkillsRegistry.ForWorkspace(c.Request.Context(), resolved)
-	}
-	return h.SkillsRegistry.ForWorkspace(c.Request.Context(), nil)
+	skillList, err := h.SkillsRegistry.ForWorkspace(c.Request.Context(), resolved)
+	if err != nil {
+		return nil, mapSkillScopeError(err)
+	}
+	return skillList, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/core/skill_scope.go` around lines 51 - 54, In the handler branch
around resolved, collapse the duplicated SkillsRegistry.ForWorkspace calls into
one call using resolved, then pass its returned error through mapSkillScopeError
before converting it with StatusForSkillError. Preserve the existing workspace
resolution behavior while ensuring both resolved and nil workspace paths receive
consistent error classification.
internal/cli/client_test.go (1)

1975-2001: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the other three changed skill methods, or narrow the test name.

The suite name is plural, but only GetSkillContent is asserted. ListSkills, GetSkill, and GetSkillShadows also moved to doAgentJSON in this PR and have no header assertion. Add a subtest per method with the same round-tripper pattern, so a dropped query.Caller argument on any of them fails the suite.

As per path instructions: "Focus on critical paths … Verify tests can fail when business logic changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/client_test.go` around lines 1975 - 2001, The
TestUnixSocketClientSkillMethodsSendIdentityHeaders test currently covers only
GetSkillContent despite claiming coverage for all skill methods. Add equivalent
subtests and round-tripper header assertions for ListSkills, GetSkill, and
GetSkillShadows, passing credentials through each method’s query.Caller and
validating the expected request so dropped identity propagation fails the test.

Source: Path instructions

internal/api/core/skills_test.go (1)

1331-1341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the workspace-access request instead of asserting inside AuthorizeFn.

ValidateWorkspaceAccess returns without calling the policy when TargetWorkspaceID is empty or equals the actor workspace, so the t.Fatalf inside AuthorizeFn makes other requests fail incorrectly. Let the fixture inspect the request for non-empty cross-workspace targets, and keep the assertions in each subtest. The denied Decision{Source: SourceDenied} with no error already maps to ErrIdentityUnauthorized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/core/skills_test.go` around lines 1331 - 1341, Update the
AuthorizeFn fixture used by ValidateWorkspaceAccess to record or log requests
instead of calling t.Fatalf on TargetWorkspaceID. Preserve the denied
Decision{Source: workspaceaccess.SourceDenied} return, and move target-workspace
assertions into each relevant subtest so empty or same-workspace requests remain
valid without invoking the policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/acp/terminal_agent_transport_proxy.go`:
- Around line 69-75: Update agentSkillTransportHandler to hold a *slog.Logger,
use it in writeProxyError to log the non-nil transport error while retaining the
generic client-facing response, and remove the inert encode-error guard in the
related response-writing path so encode failures are logged or otherwise handled
rather than discarded.

In `@internal/api/spec/spec_test.go`:
- Around line 3177-3183: Update assertManagedSkillIdentityHeaders to validate
the documented bodies for both unauthorized and forbidden responses, not only
their status entries. Use jsonResponseSchema to obtain each response schema and
assert the required contract.ErrorPayload fields with the existing specific
error assertion helpers.

In `@internal/cli/client_managed_transport_unix.go`:
- Around line 28-29: Update the managedStreamClient returned by the Unix
transport constructor to either set an appropriate non-zero Timeout for bounded
SSE streams or explicitly document that doSSE cancellation via context controls
the read deadline when indefinite streaming is intentional. Keep the existing
default timeout on the regular HTTP client and ensure streamHTTPClient uses the
chosen behavior.

In `@internal/cli/client_settings_vault.go`:
- Around line 34-44: Update NewClient so managedAgentTransportClients is used
only for skill-read requests under /api/skills/*, while all other DaemonClient
methods continue using the daemon socket. If managed transport discovery fails
or returns a stale/unavailable socket, log the discovery error and fall back to
constructing the daemon client instead of returning the error; preserve managed
transport selection for valid skill-read requests.

---

Nitpick comments:
In `@internal/acp/agent_transport_unix.go`:
- Around line 56-57: Update the http.Server construction in the managed
transport setup to include ReadTimeout, WriteTimeout, and IdleTimeout alongside
ReadHeaderTimeout. Use the existing managed transport timeout configuration or
add appropriately generous bounds for large skill-content request and response
bodies, while ensuring idle connections are eventually closed.
- Around line 39-42: Add a doc comment to startManagedAgentTransport documenting
that blank daemonSocket, sessionID, or agentName disables the transport and
returns a nil transport with a nil error. Keep the existing return behavior
unchanged and make the contract clear for callers.

In `@internal/acp/launcher.go`:
- Around line 30-32: Move the managedAgentTransportLauncher interface
declaration from this file to the consumer location in
prepareManagedAgentTransport within agent_transport.go, while preserving the
existing compile-time assertion here and leaving the interface method unchanged.

In `@internal/acp/terminal_agent_transport_proxy.go`:
- Around line 30-35: Update the http.Transport in the terminal agent transport
setup to define explicit bounded ResponseHeaderTimeout, IdleConnTimeout, and
ExpectContinueTimeout values, while preserving the existing Unix-socket
DialContext behavior.

In `@internal/acp/terminal_identity_env.go`:
- Around line 28-41: Update isManagedTerminalEnvName to replace the hardcoded
"COMPOZY_AGENT_NAME" entry with the shared envAgentName constant already defined
in internal/cli/whoami.go, preserving the existing allowlist behavior and
avoiding a duplicate literal.

In `@internal/api/core/skill_scope.go`:
- Around line 51-54: In the handler branch around resolved, collapse the
duplicated SkillsRegistry.ForWorkspace calls into one call using resolved, then
pass its returned error through mapSkillScopeError before converting it with
StatusForSkillError. Preserve the existing workspace resolution behavior while
ensuring both resolved and nil workspace paths receive consistent error
classification.

In `@internal/api/core/skills_test.go`:
- Around line 1331-1341: Update the AuthorizeFn fixture used by
ValidateWorkspaceAccess to record or log requests instead of calling t.Fatalf on
TargetWorkspaceID. Preserve the denied Decision{Source:
workspaceaccess.SourceDenied} return, and move target-workspace assertions into
each relevant subtest so empty or same-workspace requests remain valid without
invoking the policy.

In `@internal/cli/client_managed_transport_unix_test.go`:
- Around line 74-81: Update the client calls in the test, including GetSkill and
GetSkillContent, to use t.Context() instead of context.Background() so requests
are cancelled with the test. Remove the context import if it is no longer used.

In `@internal/cli/client_test.go`:
- Around line 1975-2001: The TestUnixSocketClientSkillMethodsSendIdentityHeaders
test currently covers only GetSkillContent despite claiming coverage for all
skill methods. Add equivalent subtests and round-tripper header assertions for
ListSkills, GetSkill, and GetSkillShadows, passing credentials through each
method’s query.Caller and validating the expected request so dropped identity
propagation fails the test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e7baf0c5-1372-4280-876d-c688a6bdf32d

📥 Commits

Reviewing files that changed from the base of the PR and between a76a7c8 and 54edf3d.

⛔ Files ignored due to path filters (8)
  • docs/qa/charters/CH-managed-session-skill-loading.md is excluded by !**/*.md
  • docs/qa/journeys/J-load-skill-in-managed-session.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-issue-314-managed-skill-loading.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-managed-session-skill-loading.md is excluded by !**/*.md
  • openapi/compozy.json is excluded by !**/*.json
  • packages/site/content/docs/skills/index.mdx is excluded by !**/*.mdx
  • skills/compozy/references/tools-and-skills.md is excluded by !**/*.md
  • web/src/generated/compozy-openapi.d.ts is excluded by !**/generated/**, !**/generated/**, !**/*.d.ts
📒 Files selected for processing (41)
  • internal/acp/agent_process.go
  • internal/acp/agent_transport.go
  • internal/acp/agent_transport_unix.go
  • internal/acp/agent_transport_windows.go
  • internal/acp/client_integration_test.go
  • internal/acp/client_process.go
  • internal/acp/client_process_lifecycle_test.go
  • internal/acp/client_test_support_test.go
  • internal/acp/launcher.go
  • internal/acp/launcher_tool_host_test.go
  • internal/acp/start_process.go
  • internal/acp/terminal.go
  • internal/acp/terminal_agent_transport_proxy.go
  • internal/acp/terminal_identity_env.go
  • internal/acp/types.go
  • internal/agentidentity/identity.go
  • internal/agentidentity/protocol/constants.go
  • internal/api/core/errors.go
  • internal/api/core/skill_scope.go
  • internal/api/core/skills.go
  • internal/api/core/skills_test.go
  • internal/api/spec/agent_identity_contract.go
  • internal/api/spec/registry_skills.go
  • internal/api/spec/spec_test.go
  • internal/cli/client_agent_actions.go
  • internal/cli/client_agent_kernel_types.go
  • internal/cli/client_managed_transport_unix.go
  • internal/cli/client_managed_transport_unix_test.go
  • internal/cli/client_managed_transport_windows.go
  • internal/cli/client_resources_skills_hooks.go
  • internal/cli/client_settings_vault.go
  • internal/cli/client_test.go
  • internal/cli/skill_daemon_test.go
  • internal/cli/skill_workspace.go
  • internal/daemon/session_manager_deps.go
  • internal/daemon/session_manager_factory.go
  • internal/session/manager_defaults.go
  • internal/session/manager_options.go
  • internal/session/manager_start_env.go
  • internal/session/manager_test.go
  • internal/session/manager_types.go
💤 Files with no reviewable changes (1)
  • internal/api/core/skills.go

Comment on lines +69 to +75
func (h *agentSkillTransportHandler) writeProxyError(writer http.ResponseWriter, _ *http.Request, err error) {
message := "daemon is not reachable through the managed transport"
if err == nil {
message = "managed transport failed"
}
writeAgentTransportError(writer, http.StatusServiceUnavailable, "managed_transport_unavailable", message)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Errors in the transport error path are dropped.

Two problems on the same path:

  1. Lines 100-107: the if guard on the encode error is inert. Its body contains only return, and return is already the end of the function. The net.ErrClosed test changes nothing. Any JSON encode failure disappears.
  2. Lines 69-75: writeProxyError never uses err. httputil.ReverseProxy calls ErrorHandler only with a non-nil error, so the err == nil branch is unreachable and the real upstream failure is never logged. A managed session that cannot reach the daemon socket produces managed_transport_unavailable with no diagnostic trace on the daemon side.

Give agentSkillTransportHandler a *slog.Logger and log both errors. Keep the client-facing message generic so the daemon socket path is not exposed to the managed process.

🐛 Proposed fix
 type agentSkillTransportHandler struct {
 	proxy     *httputil.ReverseProxy
 	transport *http.Transport
 	sessionID string
 	agentName string
+	logger    *slog.Logger
 }
-func (h *agentSkillTransportHandler) writeProxyError(writer http.ResponseWriter, _ *http.Request, err error) {
-	message := "daemon is not reachable through the managed transport"
-	if err == nil {
-		message = "managed transport failed"
-	}
-	writeAgentTransportError(writer, http.StatusServiceUnavailable, "managed_transport_unavailable", message)
+func (h *agentSkillTransportHandler) writeProxyError(writer http.ResponseWriter, request *http.Request, err error) {
+	if h.logger != nil {
+		h.logger.Warn(
+			"acp: managed transport proxy failed",
+			"path", request.URL.Path,
+			"session_id", h.sessionID,
+			"error", err,
+		)
+	}
+	writeAgentTransportError(
+		writer,
+		http.StatusServiceUnavailable,
+		"managed_transport_unavailable",
+		"daemon is not reachable through the managed transport",
+	)
 }
-func writeAgentTransportError(writer http.ResponseWriter, status int, code string, message string) {
+func writeAgentTransportError(
+	writer http.ResponseWriter,
+	status int,
+	code string,
+	message string,
+	logger *slog.Logger,
+) {
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(status)
 	if err := json.NewEncoder(writer).Encode(map[string]any{
 		EventTypeError: map[string]any{
 			"code":    code,
 			"message": message,
 		},
-	}); err != nil && !errors.Is(err, net.ErrClosed) {
-		return
+	}); err != nil && !errors.Is(err, net.ErrClosed) && logger != nil {
+		logger.Warn("acp: managed transport error response failed", "code", code, "error", err)
 	}
 }

As per coding guidelines: "Never discard errors with _ in production code or tests; handle every error or document a justification."

Also applies to: 97-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/acp/terminal_agent_transport_proxy.go` around lines 69 - 75, Update
agentSkillTransportHandler to hold a *slog.Logger, use it in writeProxyError to
log the non-nil transport error while retaining the generic client-facing
response, and remove the inert encode-error guard in the related
response-writing path so encode failures are logged or otherwise handled rather
than discarded.

Source: Coding guidelines

Comment thread internal/api/spec/spec_test.go Outdated
Comment on lines +3177 to +3183
func assertManagedSkillIdentityHeaders(t *testing.T, operation *openapi3.Operation) {
t.Helper()
assertParameter(t, operation, agentidentity.HeaderSessionID, openapi3.ParameterInHeader, false)
assertParameter(t, operation, agentidentity.HeaderAgent, openapi3.ParameterInHeader, false)
assertResponseStatus(t, operation, http.StatusUnauthorized)
assertResponseStatus(t, operation, http.StatusForbidden)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the documented authorization error bodies.

assertResponseStatus only proves that the 401 and 403 response entries exist. It does not prove that either entry has the declared contract.ErrorPayload body and required fields.

Use jsonResponseSchema and assert the required error fields for both responses.

As per coding guidelines, “assert both status codes and bodies”; path instructions require “specific error assertions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/spec/spec_test.go` around lines 3177 - 3183, Update
assertManagedSkillIdentityHeaders to validate the documented bodies for both
unauthorized and forbidden responses, not only their status entries. Use
jsonResponseSchema to obtain each response schema and assert the required
contract.ErrorPayload fields with the existing specific error assertion helpers.

Sources: Coding guidelines, Path instructions

Comment on lines +28 to +29
return &http.Client{Transport: transport, Timeout: defaultUnixSocketClientTimeout},
&http.Client{Transport: transport}, true, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline internal/cli/client_managed_transport_unix.go --items all
rg -n -C 6 '\bmanagedAgentTransportClients\s*\(' internal/cli

Repository: compozy/compozy

Length of output: 3217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- internal/cli/client_managed_transport_unix.go ---\n'
cat -n internal/cli/client_managed_transport_unix.go

printf '\n--- usages of managedHTTPClient and managedStreamClient ---\n'
rg -n -C 5 'managedHTTPClient|managedStreamClient|NewClient\(' internal/cli

printf '\n--- unixSocketClient definitions/usages in internal/cli ---\n'
rg -n -C 5 'type unixSocketClient|func \(.*unixSocketClient|managedHTTPClient|managedStreamClient' internal/cli

printf '\n--- relevant transport timeout definitions ---\n'
rg -n 'defaultUnixSocketClientTimeout|idle|keep|timeout|Timeout:' internal/cli client* internal -g '*.go' | head -n 120

Repository: compozy/compozy

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/cli/client_managed_transport_unix.go ---'
cat -n internal/cli/client_managed_transport_unix.go

echo
echo '--- uses managedHTTPClient and managedStreamClient ---'
rg -n -C 5 'managedHTTPClient|managedStreamClient|NewClient\(' internal/cli

echo
echo '--- unixSocketClient definitions and managed client uses ---'
rg -n -C 5 'type unixSocketClient|func \(.*unixSocketClient|managedHTTPClient|managedStreamClient' internal/cli

echo
echo '--- timeout definitions in relevant CLI files ---'
rg -n 'defaultUnixSocketClientTimeout|idle|keep|timeout|Timeout:' internal/cli -g '*.go' | head -n 180

Repository: compozy/compozy

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/cli/client_managed_transport_unix.go ---'
cat -n internal/cli/client_managed_transport_unix.go

echo
echo '--- NewClient implementation ---'
sed -n '1,80p' internal/cli/client_settings_vault.go | cat -n

echo
echo '--- unixSocketClient struct and Do helpers ---'
fd 'client_.*\.go$' internal/cli -E 'client_automation.go' -E 'client_task_lifecycle.go' -E 'client_marketplace.go' -E 'client_extension_scoped_reads.go' -E 'client_bridge_target_resolution.go' -E 'client_extension_commands.go' -E 'client_memory_decisions.go' -E 'client_task_management.go' -E 'client_bridge_setup.go' -E 'client_session_commands.go' -E 'client_managed_transport_unix.go' -E 'client_managed_transport_windows.go' | sort | while read -r f; do
  rg -n -C 5 'type unixSocketClient|func \(c \*unixSocketClient\).*Do|http\.Client' "$f" || true
done

echo
echo '--- focused Do and client fields search'
rg -n -C 4 'type unixSocketClient|managedHTTPClient|managedStreamClient|func \(c \*unixSocketClient\) Do|DoWithContext|DoJSON|DoStream|http\.Client' internal/cli -g 'client_*.go' --glob '!**/*.md' --glob '!**/client_automation.go' --glob '!**/client_task_lifecycle.go' --glob '!**/client_marketplace.go' --glob '!**/client_extension_scoped_reads.go' --glob '!**/client_bridge_target_resolution.go' --glob '!**/client_extension_commands.go' --glob '!**/client_memory_decisions.go' --glob '!**/client_task_management.go' --glob '!**/client_bridge_setup.go' --glob '!**/client_session_commands.go' --glob '!**/client_managed_transport_unix.go' --glob '!**/client_managed_transport_windows.go' | head -n 220

Repository: compozy/compozy

Length of output: 48801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/cli/client_transport.go relevant sections ---'
rg -n 'func \(c \*unixSocketClient\) (doRequest|doJSON|doSSE|streamHTTPClient)|func doRequest|func (doRequestWithContext|doJSONWithContext|doJSON|doSSE|doSSEWithContext)' internal/cli/client_transport.go
sed -n '1,140p' internal/cli/client_transport.go | cat -n
sed -n '140,270p' internal/cli/client_transport.go | cat -n

echo
echo '--- const/default values and stream definitions ---'
rg -n 'defaultUnixSocketClientTimeout|type ClientContext|func \(.*ClientContext.*Context|context\.With|WithoutCancel|streamClient|streamHTTPClient|doSSE\(' internal/cli -g '*.go' | head -n 220

echo
echo '--- behavioral probe for Go http.Client unbounded timeout with context timeout ---'
tmpdir="$(mktemp -d)"
trap 'rm -f "$tmpdir"/server "$tmpdir"/client.go "$tmpdir"/read' EXIT

cat > "$tmpdir/server.go" <<'GO'
package main

import (
	"context"
	"log"
	"net/http"
	"net/http/httptest"
)

func main() {
	server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		<-r.Context().Done()
		log.Println("server seen context done:", r.Context().Err())
		w.WriteHeader(200)
		w.Write([]byte("ok"))
	}))
	server.StartUnix(fixPath(server.Listener.Addr().String()))
	select {}
}

func fixPath(addr string) string {
	if len(addr) >= 2 && addr[:2] == "@" {
		return "\x00" + addr[2:]
	}
	return addr
}
GO

cat > "$tmpdir/client.go" <<'GO'
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	if len(os.Args) != 3 {
		fmt.Println("usage client <with-timeout-client|without-timeout-client> <socket>")
		os.Exit(2)
	}
	mode := os.Args[1]
	socket := os.Args[2]
	transport := &http.Transport{
		DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
			var dialer net.Dialer
			return dialer.DialContext(context.Background(), "unix", socket)
		},
	}
	var client *http.Client
	if mode == "with-timeout-client" {
		client = &http.Client{Transport: transport, Timeout: 500 * time.Millisecond}
	} else {
		client = &http.Client{Transport: transport}
	}
	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, "GET", "http://socket/test", nil)
	if err != nil {
		fmt.Println("new request", err)
		os.Exit(2)
	}
	start := time.Now()
	resp, err := client.Do(req)
	elapsed := time.Since(start)
	switch err := err.(type) {
	case nil:
		io.Copy(io.Discard, resp.Body)
		resp.Body.Close()
		fmt.Println("complete", elapsed, "error", "none")
	case *net.OpError:
		fmt.Println("error", elapsed, "operror", err)
	default:
		fmt.Println("error", elapsed, "kind", fmt.Sprintf("%T", err), "msg", err)
	}
	client.CloseIdleConnections()
	time.Sleep(1 * time.Second)
}
GO

printf '\nServer pid:'
go run "$tmpdir/server.go" &
server_pid=$!
sleep 0.4

socket_path="$tmpdir/server"
printf 'Without HTTP Timeout client:'
go run "$tmpdir/client.go" without-timeout-client "$socket_path" | sed -u 's/^/[client without-timeout-client] /'
printf 'With HTTP Timeout client:'
go run "$tmpdir/client.go" with-timeout-client "$socket_path" | sed -u 's/^/[client with-timeout-client] /'

kill "$server_pid" 2>/dev/null || true

Repository: compozy/compozy

Length of output: 14820


🌐 Web query:

Go net/http Client Timeout zero request deadline indefinite documentation net/http Do

💡 Result:

In Go's net/http package, the Timeout field of an http.Client specifies a time limit for requests executed by that client [1][2]. The official documentation explicitly states that a Timeout of zero means no timeout [3][4]. When the Timeout is set to zero (which is the default behavior for http.DefaultClient), there is no deadline applied to the request [1][5]. The client will wait indefinitely for the server to respond, or until the underlying network connection times out on its own [6][5]. Key points regarding the Timeout field: - Scope: The Timeout covers the entire HTTP exchange, including connection establishment (dialing), any redirects, and reading the response body [1][6]. - Behavior: If a timeout is set (non-zero), the timer continues to run after the Client's methods (like Do, Get, or Post) return, meaning it can interrupt the reading of the response body if the specified duration is exceeded [1][4]. - Cancellation: When a timeout occurs, the client cancels the request to the underlying Transport as if the request's context had ended [1][2]. - Default: Because http.DefaultClient uses a zero value for Timeout, it does not impose a deadline on requests by default, which can lead to hanging requests in production environments if not explicitly configured [1][5]. For production applications, it is generally recommended to use a non-zero Timeout or, for more granular control, to use context.WithTimeout to manage request deadlines [6][5].

Citations:


Set a client timeout or handle cancellation for managedStreamClient.

managedStreamClient is returned with a zero Timeout and is used by doSSE() via streamHTTPClient() for unbounded SSE streams. If the stream is not expected to stay open indefinitely, give the client an appropriate bound; otherwise document that context cancellation drives the read deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/client_managed_transport_unix.go` around lines 28 - 29, Update
the managedStreamClient returned by the Unix transport constructor to either set
an appropriate non-zero Timeout for bounded SSE streams or explicitly document
that doSSE cancellation via context controls the read deadline when indefinite
streaming is intentional. Keep the existing default timeout on the regular HTTP
client and ensure streamHTTPClient uses the chosen behavior.

Source: Coding guidelines

Comment thread internal/cli/client_settings_vault.go Outdated
Comment on lines +34 to +44
managedHTTPClient, managedStreamClient, managed, err := managedAgentTransportClients()
if err != nil {
return nil, err
}
if managed {
return &unixSocketClient{
socketPath: managedAgentTransportLabel,
httpClient: managedHTTPClient,
streamClient: managedStreamClient,
}, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Map NewClient callers and the managed transport discovery contract.
set -euo pipefail

rg -nP -C 3 '\bNewClient\s*\(' internal --glob '!**/*_test.go'
fd -t f 'client_managed_transport_unix.go|client_managed_transport_windows.go' internal/cli --exec cat -n
rg -nP -C 6 'isAgentSkillTransportRequest|managed_transport_denied' internal/acp

Repository: compozy/compozy

Length of output: 16436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the daemon client implementation and transport selection.
sed -n '1,140p' internal/cli/client_settings_vault.go
printf '\n--- callers of internal/cli client constructor ---\n'
rg -nP -C 3 'client_settings_vault|DaemonClient|NewClient\(\s*' internal --glob '!**/*_test.go' | sed -n '1,220p'

printf '\n--- managed transport route predicate ---\n'
sed -n '45,92p' internal/acp/terminal_agent_transport_proxy.go

Repository: compozy/compozy

Length of output: 19902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- requireExtensionDaemonClient definitions ---'
rg -nP -C 10 'func requireExtensionDaemonClient|requireExtensionDaemonClient\s*=' internal/cli

printf '%s\n' '--- newDaemonClient/requireDaemonClient definitions ---'
rg -nP -C 10 'func require|newDaemonClient|DaemonClient\s*=' internal/cli --glob '!**/*_test.go'

Repository: compozy/compozy

Length of output: 26849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- requireExtensionDaemonClient implementation ---'
sed -n '99,120p' internal/cli/extension_state.go

printf '%s\n' '--- daemonClientIfRunning implementation ---'
sed -n '1,60p' internal/cli/daemon_client_reachability.go

printf '%s\n' '--- NewClient call construction in root_runtime.go ---'
sed -n '1,60p' internal/cli/root_runtime.go

Repository: compozy/compozy

Length of output: 3441


Scope the managed transport to skill reads and keep the daemon socket for other CLI commands.

NewClient always selects the managed transport when an agent socket is present, then all DaemonClient methods that call non-/api/skills/* routes get managed_transport_denied. These commands only need the managed transport for skill-related reads; keep NewClient on the daemon socket or add a separate constructor for the skill read path. Also, failed discovery should not stop commands; log the cause and build the daemon client when the managed socket is unavailable or stale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/client_settings_vault.go` around lines 34 - 44, Update NewClient
so managedAgentTransportClients is used only for skill-read requests under
/api/skills/*, while all other DaemonClient methods continue using the daemon
socket. If managed transport discovery fails or returns a stale/unavailable
socket, log the discovery error and fall back to constructing the daemon client
instead of returning the error; preserve managed transport selection for valid
skill-read requests.

@pedronauck
pedronauck force-pushed the codex/issue-314-session-skill-identity branch from 54edf3d to f14fefb Compare August 6, 2026 00:59
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@pedronauck pedronauck changed the title fix: bind managed skill loading to session identity fix: keep managed skill loading on native seam Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/mcp/hosted.go (1)

209-210: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep unarmed launch records bounded.

Armed launch cleanup is covered by the existing bind and stop paths, but failed arms can leave s.launches[sessionID] without setting expiresAt or armed. Return CancelLaunch(sessionID) for failed ArmLaunch or add a bounded sweeper for unarmed records. An unarmed record cannot reach Bind cleanup via the current ErrHostedNonceInvalid path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mcp/hosted.go` around lines 209 - 210, Update the failed ArmLaunch
handling around the launch record initialized with nonceHash and expectedBin so
every failed arm invokes CancelLaunch(sessionID), or otherwise ensure unarmed
records are removed by a bounded sweeper. Preserve cleanup for successfully
armed launches while preventing unarmed entries from remaining indefinitely in
s.launches.
🧹 Nitpick comments (5)
internal/cli/skill_daemon_test.go (1)

422-481: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the operator-shell negative case.

Every subtest here asserts rejection. No test in the changed range asserts that skill subcommands still run when COMPOZY_SESSION_ID and COMPOZY_AGENT are both empty. The change details state that the previous session-scoped tests were replaced, so the allowed path may now be uncovered. A future inversion of the condition in ensureSkillCLIUsesSupportedSurface would keep this suite green.

Add one subtest that leaves both markers empty and asserts that skill list reaches the daemon client.

💚 Proposed additional subtest
t.Run("Should allow skill list from an operator shell", func(t *testing.T) {
	t.Parallel()

	clientCalls := 0
	deps := newWorkspaceTestDeps(t, &stubClient{})
	deps.newClient = func(string) (DaemonClient, error) {
		clientCalls++
		return &stubClient{}, nil
	}
	deps.getenv = func(string) string { return "" }

	if _, _, err := executeRootCommand(t, deps, "skill", "list"); err != nil {
		t.Fatalf("skill list error = %v, want success from operator shell", err)
	}
	if clientCalls == 0 {
		t.Fatal("skill list client calls = 0, want daemon access from operator shell")
	}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/skill_daemon_test.go` around lines 422 - 481, Add an
operator-shell subtest alongside TestSkillCommandsRejectManagedSessionCLI that
leaves both session and agent environment markers empty, executes “skill list,”
asserts no error, and verifies the configured daemon client is called at least
once. Keep the existing managed-session rejection cases unchanged.
internal/cli/skill_managed_guard.go (1)

10-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the guidance text with the blocked command set.

The guard blocks every skill subcommand, including install, remove, create, enable, disable, and update. The error text only names the read-only tools compozy__skill_list, compozy__skill_search, and compozy__skill_view. A managed session that runs compozy skill install receives guidance that does not apply to that operation.

Consider separating the two cases: point read operations at the canonical tools, and state that mutating skill operations are operator-shell only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/skill_managed_guard.go` around lines 10 - 13, Update
errManagedSessionSkillCLIUnsupported to distinguish read-only skill operations
from mutating commands: direct list, search, and view requests to the canonical
compozy tools, while explicitly stating that install, remove, create, enable,
disable, and update require an operator shell.
internal/testutil/acpmock/fixture_test.go (1)

696-696: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the local constant instead of inlining the text.

Line 653 in this same file already uses currentSkillsCatalogFinalLine. These three sites inline the identical string. Replace the literals with the constant so a text change updates all fixtures at once.

Also applies to: 921-921, 946-946

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/testutil/acpmock/fixture_test.go` at line 696, Replace the three
duplicated prompt string literals in the fixture entries with the existing
currentSkillsCatalogFinalLine constant used earlier in the file, including the
sites near the reported locations, so all fixtures share one maintainable value.
internal/skills/catalog_test.go (1)

458-465: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These substring assertions are brittle and partly redundant.

TestBuildCatalogFormatsCatalogSortedEscapedAndWithUsageInstructions at line 33 already asserts the exact catalog output, including the managed loading line. Lines 458 and 464 restate parts of that same golden expectation.

Line 461 is the weaker part. The bare strings.Contains(got, "fallback") check fails on any future instruction that uses the word for an unrelated reason, and the lowercase "use compozy skill view"` check misses other phrasings such as "invoke" or "run". Consider keeping only the negative assertion that carries the invariant: the catalog must not tell a managed session to run the CLI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skills/catalog_test.go` around lines 458 - 465, In
TestBuildCatalogFormatsCatalogSortedEscapedAndWithUsageInstructions, remove the
redundant substring assertions already covered by the exact catalog expectation,
including the broad “fallback” and lowercase “use” checks. Retain a focused
negative assertion in the BuildCatalog validation that rejects guidance telling
managed sessions to invoke, run, or otherwise execute `compozy skill view`.
internal/testutil/acpmock/fixture.go (1)

34-36: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The managed skill-loading instruction has no single source of truth. The same sentence now exists as catalogManagedLoadingInstructions in internal/skills/catalog.go, as currentSkillsCatalogFinalLine in the ACP mock, and as three inline literals in the mock tests. The mock uses this text to strip prompt augmentation in canonicalUserText, so any drift silently changes turn selection instead of failing loudly.

  • internal/testutil/acpmock/fixture.go#L34-L36: export the instruction text from internal/skills and reference it here, or add a test that asserts currentSkillsCatalogFinalLine equals the catalog constant.
  • internal/testutil/acpmock/fixture_test.go#L696-L696: replace the inline literal with currentSkillsCatalogFinalLine, matching line 653.
  • internal/testutil/acpmock/fixture_test.go#L921-L921: replace the inline literal with currentSkillsCatalogFinalLine.
  • internal/testutil/acpmock/fixture_test.go#L946-L946: replace the inline literal with currentSkillsCatalogFinalLine.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/testutil/acpmock/fixture.go` around lines 34 - 36, The managed
skill-loading instruction is duplicated, allowing drift between production and
mock behavior. In internal/testutil/acpmock/fixture.go lines 34-36, reuse an
exported instruction constant from internal/skills/catalog.go (or otherwise
enforce equality with it); in internal/testutil/acpmock/fixture_test.go lines
696, 921, and 946, replace each inline literal with
currentSkillsCatalogFinalLine, matching the existing usage around line 653.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/acp/client_start_contract_test.go`:
- Around line 53-88: Refactor
TestStartActivatesMCPAfterInitializeBeforeSessionNegotiation into parallel
t.Run("Should …") success and failure subtests. Keep the success assertions for
activation ordering and ACP session/new, and add a failure subtest whose
ActivateMCPServers returns a known error, asserting the returned error wraps it
and that AgentMethodSessionNew is not captured.

---

Outside diff comments:
In `@internal/mcp/hosted.go`:
- Around line 209-210: Update the failed ArmLaunch handling around the launch
record initialized with nonceHash and expectedBin so every failed arm invokes
CancelLaunch(sessionID), or otherwise ensure unarmed records are removed by a
bounded sweeper. Preserve cleanup for successfully armed launches while
preventing unarmed entries from remaining indefinitely in s.launches.

---

Nitpick comments:
In `@internal/cli/skill_daemon_test.go`:
- Around line 422-481: Add an operator-shell subtest alongside
TestSkillCommandsRejectManagedSessionCLI that leaves both session and agent
environment markers empty, executes “skill list,” asserts no error, and verifies
the configured daemon client is called at least once. Keep the existing
managed-session rejection cases unchanged.

In `@internal/cli/skill_managed_guard.go`:
- Around line 10-13: Update errManagedSessionSkillCLIUnsupported to distinguish
read-only skill operations from mutating commands: direct list, search, and view
requests to the canonical compozy tools, while explicitly stating that install,
remove, create, enable, disable, and update require an operator shell.

In `@internal/skills/catalog_test.go`:
- Around line 458-465: In
TestBuildCatalogFormatsCatalogSortedEscapedAndWithUsageInstructions, remove the
redundant substring assertions already covered by the exact catalog expectation,
including the broad “fallback” and lowercase “use” checks. Retain a focused
negative assertion in the BuildCatalog validation that rejects guidance telling
managed sessions to invoke, run, or otherwise execute `compozy skill view`.

In `@internal/testutil/acpmock/fixture_test.go`:
- Line 696: Replace the three duplicated prompt string literals in the fixture
entries with the existing currentSkillsCatalogFinalLine constant used earlier in
the file, including the sites near the reported locations, so all fixtures share
one maintainable value.

In `@internal/testutil/acpmock/fixture.go`:
- Around line 34-36: The managed skill-loading instruction is duplicated,
allowing drift between production and mock behavior. In
internal/testutil/acpmock/fixture.go lines 34-36, reuse an exported instruction
constant from internal/skills/catalog.go (or otherwise enforce equality with
it); in internal/testutil/acpmock/fixture_test.go lines 696, 921, and 946,
replace each inline literal with currentSkillsCatalogFinalLine, matching the
existing usage around line 653.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 51ef9ca0-6e82-47a6-b330-831c65936a49

📥 Commits

Reviewing files that changed from the base of the PR and between a76a7c8 and f14fefb.

⛔ Files ignored due to path filters (9)
  • docs/articles/2026-05/03-one-daemon-four-surfaces.mdx is excluded by !**/*.mdx
  • docs/qa/bugs/BUG-20260805-hosted-mcp-cold-start-nonce-expiry.md is excluded by !**/*.md
  • docs/qa/charters/CH-managed-session-skill-loading.md is excluded by !**/*.md
  • docs/qa/journeys/J-load-skill-in-managed-session.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-issue-314-managed-skill-loading.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-managed-session-skill-loading.md is excluded by !**/*.md
  • packages/site/content/docs/skills/index.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/skills/skill-md.mdx is excluded by !**/*.mdx
  • skills/compozy/references/tools-and-skills.md is excluded by !**/*.md
📒 Files selected for processing (23)
  • internal/acp/client_start_contract_test.go
  • internal/acp/client_test_support_test.go
  • internal/acp/start.go
  • internal/acp/types.go
  • internal/api/udsapi/hosted_mcp_test.go
  • internal/cli/skill_commands.go
  • internal/cli/skill_daemon_test.go
  • internal/cli/skill_managed_guard.go
  • internal/daemon/hosted_mcp.go
  • internal/daemon/native_tools_test.go
  • internal/daemon/prompt_skills_test.go
  • internal/mcp/hosted.go
  • internal/mcp/hosted_records.go
  • internal/mcp/hosted_test.go
  • internal/session/manager_start_env.go
  • internal/session/manager_start_mcp.go
  • internal/session/manager_test.go
  • internal/session/manager_test_support_test.go
  • internal/session/manager_types.go
  • internal/skills/catalog.go
  • internal/skills/catalog_test.go
  • internal/testutil/acpmock/fixture.go
  • internal/testutil/acpmock/fixture_test.go

Comment thread internal/acp/client_start_contract_test.go
@pedronauck
pedronauck merged commit 796124d into main Aug 6, 2026
10 of 12 checks passed
@pedronauck
pedronauck deleted the codex/issue-314-session-skill-identity branch August 6, 2026 02:22

This branch was successfully deployed

1 active deployment
Preview — e30ec8ed Deployed Aug 6, 2026 by vercel[bot]
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.

Managed Codex session cannot run compozy skill view: agent identity cannot be validated

1 participant