Skip to content

Commit b6c4277

Browse files
committed
Fixes after merge
1 parent 8c6b501 commit b6c4277

9 files changed

Lines changed: 104 additions & 14 deletions

File tree

config/services.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ parameters:
2020
docker.host_base_path_default: "/var/www"
2121
docker.host_base_path: "%env(default:docker.host_base_path_default:HOST_PROJECT_PATH)%"
2222

23+
# Docker image that contains the Cursor Agent CLI.
24+
# Set via CURSOR_AGENT_IMAGE env var (docker-compose sets this to the app image).
25+
cursor_agent.image_default: "node:22-slim"
26+
cursor_agent.image: "%env(default:cursor_agent.image_default:CURSOR_AGENT_IMAGE)%"
27+
2328
services:
2429
# default configuration for services in *this* file
2530
_defaults:
@@ -135,7 +140,9 @@ services:
135140

136141
App\LlmContentEditor\Infrastructure\LlmContentEditorAdapter: ~
137142

138-
App\CursorAgentContentEditor\Infrastructure\CursorAgentContentEditorAdapter: ~
143+
App\CursorAgentContentEditor\Infrastructure\CursorAgentContentEditorAdapter:
144+
arguments:
145+
$cursorAgentImage: "%cursor_agent.image%"
139146

140147
App\AgenticContentEditor\Facade\AgenticContentEditorFacadeInterface:
141148
class: App\AgenticContentEditor\Facade\AgenticContentEditorFacade

docker/app/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ RUN mkdir -p /opt/mise/data /opt/mise/cache /opt/mise/state && \
7777
chmod -R 777 /opt/mise && \
7878
mise install node@24
7979

80+
# Create a custom entrypoint that adds the mise node bin directory to PATH.
81+
# This makes node/npm/npx available in the default PATH for all invocations:
82+
# docker-compose services, docker run commands, and non-interactive sh -c calls.
83+
# The resolved path is baked at build time so there is no runtime lookup overhead.
84+
RUN NODE_BIN="$(mise where node)/bin" && \
85+
printf '#!/bin/sh\nset -e\nexport PATH="%s:$PATH"\nexec docker-php-entrypoint "$@"\n' "$NODE_BIN" \
86+
> /usr/local/bin/app-entrypoint && \
87+
chmod +x /usr/local/bin/app-entrypoint
88+
89+
ENTRYPOINT ["app-entrypoint"]
90+
8091
# Shell activation for interactive use (debugging)
8192
RUN echo 'eval "$(/usr/bin/env mise activate bash)"' >> ~/.bashrc
8293
RUN echo 'eval "$(/usr/bin/env mise activate zsh)"' >> ~/.zshrc

src/CursorAgentContentEditor/Domain/Agent/ContentEditorAgent.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,9 @@ private function buildCommand(string $prompt, string $apiKey, ?string $resumeSes
5151
$sessionArg = '--resume ' . escapeshellarg($resumeSessionId);
5252
}
5353

54-
// Create a script that sets up PATH with mise node installation, then set BASH_ENV to source it.
55-
// The Cursor CLI spawns fresh bash processes for shellToolCall that don't inherit environment
56-
// variables from the parent. BASH_ENV tells non-interactive bash to source this file first.
54+
// Node/npm are symlinked into /usr/local/bin in the Dockerfile, so they are
55+
// available in the default PATH for all shells (including Cursor CLI subprocesses).
5756
return sprintf(
58-
'echo \'export PATH="/opt/mise/data/installs/node/24.13.0/bin:$PATH"\' > /etc/profile.d/mise-path.sh && ' .
59-
'export BASH_ENV=/etc/profile.d/mise-path.sh && ' .
6057
'AGENT_BIN=%s; ' .
6158
'if [ ! -x "$AGENT_BIN" ]; then echo "agent not found" >&2; exit 127; fi; ' .
6259
'"$AGENT_BIN" --output-format stream-json --stream-partial-output --force %s --api-key %s -p %s',

src/CursorAgentContentEditor/Infrastructure/CursorAgentContentEditorAdapter.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class CursorAgentContentEditorAdapter implements AgenticContentEditorAdapt
3030
public function __construct(
3131
private readonly WorkspaceToolingServiceInterface $workspaceTooling,
3232
private readonly AgentExecutionContextInterface $executionContext,
33+
private readonly string $cursorAgentImage,
3334
) {
3435
}
3536

@@ -65,6 +66,10 @@ public function streamEdit(
6566

6667
yield new EditStreamChunkDto(EditStreamChunkType::Event, null, new AgentEventDto('inference_start'));
6768

69+
// The Cursor CLI is installed in the app image, not in the project's agent image.
70+
// Override the agent image so the IsolatedShellExecutor uses the correct image.
71+
$this->executionContext->overrideAgentImage($this->cursorAgentImage);
72+
6873
$agent = new ContentEditorAgent($this->workspaceTooling);
6974
$process = $agent->startAsync('/workspace', $prompt, $apiKey, $backendSessionState);
7075

@@ -85,6 +90,9 @@ public function streamEdit(
8590
yield $chunk;
8691
}
8792

93+
// Restore the project's agent image for the build step
94+
$this->executionContext->restoreAgentImage();
95+
8896
$lastSessionId = $collector->getLastSessionId();
8997

9098
// Always run the build after the agent completes. The Cursor CLI cannot run shell
@@ -114,6 +122,7 @@ public function streamEdit(
114122
yield new EditStreamChunkDto(EditStreamChunkType::Event, null, new AgentEventDto('inference_stop'));
115123
yield new EditStreamChunkDto(EditStreamChunkType::Done, null, null, false, $e->getMessage());
116124
} finally {
125+
$this->executionContext->restoreAgentImage();
117126
$this->executionContext->setOutputCallback(null);
118127
}
119128
}

src/WorkspaceTooling/Facade/AgentExecutionContextInterface.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,20 @@ public function getSuggestedCommitMessage(): ?string;
8686
* @return string|null The agent image, or null if not set
8787
*/
8888
public function getAgentImage(): ?string;
89+
90+
/**
91+
* Temporarily override the agent image.
92+
*
93+
* Use this when a specific adapter needs a different Docker image than the
94+
* project-level default (e.g., the Cursor agent needs the app image with
95+
* the CLI installed, not the project's node image).
96+
*
97+
* The original image is preserved and can be restored via restoreAgentImage().
98+
*/
99+
public function overrideAgentImage(string $image): void;
100+
101+
/**
102+
* Restore the agent image to the value before the last overrideAgentImage() call.
103+
*/
104+
public function restoreAgentImage(): void;
89105
}

src/WorkspaceTooling/Infrastructure/Execution/AgentExecutionContext.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ final class AgentExecutionContext implements AgentExecutionContextInterface
3333
private ?string $conversationId = null;
3434
private ?string $projectName = null;
3535
private ?string $agentImage = null;
36+
private ?string $originalAgentImage = null;
3637
private ?string $suggestedCommitMessage = null;
3738
private ?Closure $outputCallback = null;
3839

@@ -72,6 +73,7 @@ public function clearContext(): void
7273
$this->conversationId = null;
7374
$this->projectName = null;
7475
$this->agentImage = null;
76+
$this->originalAgentImage = null;
7577
$this->suggestedCommitMessage = null;
7678
$this->remoteContentAssetsManifestUrls = null;
7779
$this->outputCallback = null;
@@ -129,6 +131,20 @@ public function getAgentImage(): ?string
129131
return $this->agentImage;
130132
}
131133

134+
public function overrideAgentImage(string $image): void
135+
{
136+
$this->originalAgentImage = $this->agentImage;
137+
$this->agentImage = $image;
138+
}
139+
140+
public function restoreAgentImage(): void
141+
{
142+
if ($this->originalAgentImage !== null) {
143+
$this->agentImage = $this->originalAgentImage;
144+
$this->originalAgentImage = null;
145+
}
146+
}
147+
132148
/**
133149
* Set a suggested commit message from the agent.
134150
*

src/WorkspaceTooling/Infrastructure/Execution/DockerExecutor.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,6 @@ private function buildDockerCommand(
250250
$dockerCmd[] = '-e';
251251
$dockerCmd[] = 'NODE_OPTIONS=--max-old-space-size=1536';
252252

253-
// Set BASH_ENV to source a script that sets up PATH with mise node installation.
254-
// The Cursor CLI spawns fresh bash processes for shellToolCall that don't inherit
255-
// environment variables from the parent. BASH_ENV tells non-interactive bash to
256-
// source a file before running commands.
257-
$dockerCmd[] = '-e';
258-
$dockerCmd[] = 'BASH_ENV=/etc/profile.d/mise-path.sh';
259-
260253
// Add the image
261254
$dockerCmd[] = $image;
262255

tests/Unit/CursorAgentContentEditor/CursorAgentContentEditorAdapterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected function setUp(): void
2222

2323
$executionContext = $this->createMock(AgentExecutionContextInterface::class);
2424

25-
$this->adapter = new CursorAgentContentEditorAdapter($workspaceTooling, $executionContext);
25+
$this->adapter = new CursorAgentContentEditorAdapter($workspaceTooling, $executionContext, 'test-cursor-image');
2626
}
2727

2828
public function testBuildAgentContextDumpIncludesSystemContextOnFirstMessage(): void

tests/Unit/WorkspaceTooling/AgentExecutionContextTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,45 @@ public function testClearContextResetsRemoteContentAssetsManifestUrls(): void
8282

8383
self::assertSame([], $context->getRemoteContentAssetsManifestUrls());
8484
}
85+
86+
public function testOverrideAgentImageReplacesCurrentImage(): void
87+
{
88+
$context = new AgentExecutionContext();
89+
$context->setContext('ws-id', '/path', null, 'project', 'node:22-slim');
90+
91+
$context->overrideAgentImage('app-image-with-cursor-cli');
92+
93+
self::assertSame('app-image-with-cursor-cli', $context->getAgentImage());
94+
}
95+
96+
public function testRestoreAgentImageBringsBackOriginal(): void
97+
{
98+
$context = new AgentExecutionContext();
99+
$context->setContext('ws-id', '/path', null, 'project', 'node:22-slim');
100+
101+
$context->overrideAgentImage('app-image-with-cursor-cli');
102+
$context->restoreAgentImage();
103+
104+
self::assertSame('node:22-slim', $context->getAgentImage());
105+
}
106+
107+
public function testRestoreAgentImageIsNoOpWithoutOverride(): void
108+
{
109+
$context = new AgentExecutionContext();
110+
$context->setContext('ws-id', '/path', null, 'project', 'node:22-slim');
111+
112+
$context->restoreAgentImage();
113+
114+
self::assertSame('node:22-slim', $context->getAgentImage());
115+
}
116+
117+
public function testClearContextResetsOverriddenAgentImage(): void
118+
{
119+
$context = new AgentExecutionContext();
120+
$context->setContext('ws-id', '/path', null, 'project', 'node:22-slim');
121+
$context->overrideAgentImage('app-image');
122+
$context->clearContext();
123+
124+
self::assertNull($context->getAgentImage());
125+
}
85126
}

0 commit comments

Comments
 (0)