Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,11 @@ Mocked unit tests are not evidence of a successful real sandbox run.

Licensed under the Apache License, Version 2.0. See `LICENSE` for the full terms.
Read `SECURITY.md` before enabling the workflow.

### Optional system prompt

Use `system-prompt` to pass a trusted absolute path to a UTF-8 file (up to
128 KiB). Its contents are appended to the default Claude Code system prompt
for planning and execution, not video editing. Omit it to retain the default.
Keep repository-specific instructions in your own repository; the action does
not interpret them or configure your environment. Do not include credentials.
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ inputs:
pool:
description: Cua pool accessible to the supplied credentials.
required: true
system-prompt:
description: Optional trusted absolute path to UTF-8 text appended to planning and execution system prompts (128 KiB maximum).
default: ''
planning-prompt:
description: Optional trusted absolute path overriding the planning prompt.
default: ''
Expand Down Expand Up @@ -107,6 +110,7 @@ runs:
CUA_BASE_URL: ${{ inputs.cua-base-url }}
CUA_TOKEN_URL: ${{ inputs.cua-token-url }}
E2E_POOL: ${{ inputs.pool }}
E2E_SYSTEM_PROMPT: ${{ inputs.system-prompt }}
E2E_PLANNING_PROMPT: ${{ inputs.planning-prompt }}
E2E_EXECUTION_PROMPT: ${{ inputs.execution-prompt }}
E2E_VIDEO_EDITING_PROMPT: ${{ inputs.video-editing-prompt }}
Expand Down
14 changes: 13 additions & 1 deletion agent/e2e_test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ async def guard(input_data, _tool_use_id, _context):
return guard


def agent_system_prompt() -> dict[str, str]:
prompt = {"type": "preset", "preset": "claude_code"}
extra = env("E2E_SYSTEM_PROMPT")
if extra:
with Path(extra).open("rb") as source:
content = source.read(128 * 1024 + 1)
if len(content) > 128 * 1024:
raise ValueError("System prompt exceeds the 128 KiB limit")
prompt["append"] = content.decode("utf-8")
return prompt


async def run_agent(
*,
prompt: str,
Expand Down Expand Up @@ -233,7 +245,7 @@ async def run_agent(
sandbox=sandbox,
setting_sources=[],
strict_mcp_config=bool(mcp_servers),
system_prompt={"type": "preset", "preset": "claude_code"},
system_prompt=agent_system_prompt(),
tools=tools or ["Read", "Glob", "Grep", "Write"],
)
result: ResultMessage | None = None
Expand Down
2 changes: 2 additions & 0 deletions scripts/run_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ def command_env(self, phase):
'MCP_PROXY_PORT': '3333',
})
environment.update(self.metadata)
if phase in ('plan', 'execute') and self.environment.get('E2E_SYSTEM_PROMPT'):
environment['E2E_SYSTEM_PROMPT'] = self.environment['E2E_SYSTEM_PROMPT']
if phase in ('plan', 'execute', 'edit-video'):
names = ('ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL',
'E2E_PLANNING_PROMPT', 'E2E_EXECUTION_PROMPT', 'E2E_VIDEO_EDITING_PROMPT',
Expand Down
9 changes: 9 additions & 0 deletions tests/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def setUp(self):
def runner(self):
return run_action.Runner(run_action.Config(self.env), self.env)

def test_system_prompt_is_only_forwarded_to_planning_and_execution(self):
self.env['E2E_SYSTEM_PROMPT'] = '/trusted/custom-system.md'
runner = self.runner()
for phase in ('plan', 'execute'):
self.assertEqual(runner.command_env(phase)['E2E_SYSTEM_PROMPT'],
'/trusted/custom-system.md')
for phase in ('pr-context', 'claim-create', 'edit-video'):
self.assertNotIn('E2E_SYSTEM_PROMPT', runner.command_env(phase))

def test_required_configuration_fails_before_running(self):
for key in ('E2E_REPOSITORY', 'PR_NUMBER', 'HEAD_SHA', 'E2E_POOL',
'GH_TOKEN', 'CUA_CLIENT_ID', 'CUA_CLIENT_SECRET', 'ANTHROPIC_API_KEY',
Expand Down
22 changes: 22 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ class E2ETestAgentTests(unittest.TestCase):
def setUp(self) -> None:
self.agent = load_agent_module()

def test_custom_text_is_appended_to_system_prompt(self):
with tempfile.TemporaryDirectory() as directory:
context = Path(directory) / 'custom-system.md'
context.write_text('Three nspawn nodes and local Keycloak.')
with mock.patch.dict('os.environ', {'E2E_SYSTEM_PROMPT': str(context)}):
prompt = self.agent.agent_system_prompt()
self.assertEqual(prompt['preset'], 'claude_code')
self.assertIn('Three nspawn nodes and local Keycloak.', prompt['append'])

def test_default_system_prompt_is_unchanged(self):
with mock.patch.dict('os.environ', {}, clear=True):
self.assertEqual(self.agent.agent_system_prompt(),
{'type': 'preset', 'preset': 'claude_code'})

def test_system_prompt_rejects_oversized_file(self):
with tempfile.TemporaryDirectory() as directory:
context = Path(directory) / 'large.md'
context.write_bytes(b'x' * (128 * 1024 + 1))
with mock.patch.dict('os.environ', {'E2E_SYSTEM_PROMPT': str(context)}):
with self.assertRaises(ValueError):
self.agent.agent_system_prompt()

def test_renders_all_prompt_tokens(self) -> None:
with tempfile.TemporaryDirectory() as directory:
prompt = Path(directory) / "prompt.md"
Expand Down
Loading