Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
cf7c3e4
fix(build): bump springdoc to 2.8.6 for Spring Framework 6.2 compatib…
mp-orkes Aug 14, 2026
6c62d12
docs: require issue discussion before PR for external contributors (#…
nthmost-orkes Aug 14, 2026
9e97d21
Add end to end integration tests and bump coverage to 90% requirement…
ajlai-orkes Aug 18, 2026
bdf6930
Fix incorrect conductor version when app is loading (#1511)
ajlai-orkes Aug 18, 2026
e7958b7
Clear out workflowDefinition in subworkflow param if it was touched t…
ajlai-orkes Aug 18, 2026
cb6e3cb
fix: propagate CANCELED instead of FAILED from JOIN when a forked bra…
nikhiln64 Aug 18, 2026
c61e9b4
Base changes to resolving the path after auth (#1526)
ajlai-orkes Aug 18, 2026
bc7f0b2
Fix inconsistent ES8 workflow state refresh handling (#1012)
rajeshwar-nu Aug 18, 2026
bbd4d0c
Fix monaco editor context menu paste by forcing loader to use monaco …
ajlai-orkes Aug 18, 2026
58b461b
Agent docs (#1406)
v1r3n Aug 20, 2026
98a0a73
Fix references to design docs removed from the repo (#1555)
v1r3n Aug 20, 2026
9b6e4c0
Fix docs deploy: make main.py importable in the mkdocs container
v1r3n Aug 20, 2026
6ba9a5b
Add a prompt preview tab for LLM tasks in the agent execution view (#…
NicholasDCole Aug 21, 2026
b54f0d4
Docs refresh work (#1554)
nickorkes Aug 21, 2026
724fd33
ci: set org-level e2e server version on release (#1552)
kowser-orkes Aug 24, 2026
1444b62
https://orkes.atlassian.net/browse/CCOR-13360 (#1572)
ajlai-orkes Aug 26, 2026
0ff3884
https://orkes.atlassian.net/browse/CCOR-13363 (#1575)
ajlai-orkes Aug 27, 2026
a203b3e
fix(ui): escape non-ASCII chars in workflow save body to prevent WAF …
shaileshpadave Aug 28, 2026
14488d9
Add Human task manager role and Event handler manager role (#1580)
ajlai-orkes Aug 28, 2026
17260e0
Fix sync-execute param docs: remove nonexistent EVENTUAL value, docum…
nickorkes Aug 28, 2026
af8c6fa
Fix #1569 Postgres metadata status codes (#1571)
sergio-u-simspace Aug 28, 2026
1bda1d6
Make ES8 backwards compatible for introduction of classifier field (#…
rajeshwar-nu Aug 28, 2026
ce46eac
https://orkes.atlassian.net/browse/CCOR-13361 (#1581)
ajlai-orkes Aug 28, 2026
c4086f3
[CCOR-12995] Dedicated scheduler management role (#1588)
ajlai-orkes Aug 31, 2026
7c91005
[CCOR-13219] Add color for Queue Monitor role (#1593)
ajlai-orkes Sep 1, 2026
85e50c6
Docs: match Search executions to ui-next and document reasonForIncomp…
nickorkes Sep 2, 2026
467f72b
Fix flaky test (#1597)
ajlai-orkes Sep 2, 2026
75770d3
Add schema registry to OSS Conductor (#1592)
ambiorix2099 Sep 3, 2026
8d4c376
fix(deps): bump netty to 4.1.136.Final and micrometer to 1.15.12 (#15…
nthmost-orkes Sep 3, 2026
aaef68a
feat(redis-sentinel): add conductor.redis.sentinelPassword, no AUTH t…
jerrynieRe Sep 3, 2026
3627a16
Add size-based review-policy gate (extra approval for large PRs) (#1595)
nthmost-orkes Sep 4, 2026
ab305c8
review-policy: make label/comment writes best-effort so the gate neve…
nthmost-orkes Sep 4, 2026
30a103f
docs(ai): replace deprecated dall-e-3 with gpt-image-1 (#1512)
braxtonzhangio Sep 4, 2026
1327221
Delete .github/workflows/playwright-e2e.yml
v1r3n Sep 4, 2026
98cfed7
[CCOR-13358] Save silently fails with no error shown when backend rej…
ajlai-orkes Sep 4, 2026
09786aa
[CCOR-13460]fix: open sub-workflow definition when the version is bla…
najeebkp Sep 7, 2026
24cc345
[CCOR-13474]fix: copy token via sidebar user block extension point (#…
najeebkp Sep 8, 2026
08d0ae7
enh:add new integration types (#1223)
najeebkp Sep 8, 2026
7d05c6c
convert everything that is put as chat message into string (#1608)
c4lm Sep 9, 2026
56e1eb8
Show tasks that run inside a workflow but outside its DAG (#1604)
v1r3n Sep 9, 2026
42663e5
fix(deps): bump amqp-client to 5.35.0 and httpcore5 to 5.4.3 (#1528) …
nthmost-orkes Sep 9, 2026
80b8c71
fix(status-notifier): encode webhook body as UTF-8 to prevent Content…
shaileshpadave Sep 10, 2026
1291bf6
enh: add boolean filter control and rename Substring to Contains (#1607)
najeebkp Sep 10, 2026
fc160d0
[CCOR-13383]fix:task definition form missing fields (#1586)
najeebkp Sep 10, 2026
325a402
Fix tool-call selection and naming in the agent event listener (#1610)
ambiorix2099 Sep 10, 2026
fa2bf61
enh: add content and user-block slots to BaseLayout (#1620)
najeebkp Sep 11, 2026
24ac3cb
feat(cassandra): support username/password authentication (#1623)
nthmost-orkes Sep 12, 2026
96dfed1
fix(ai): honor OLLAMA_BASE_URL for remote Ollama server (#1624)
nthmost-orkes Sep 14, 2026
dcf9703
[CCOR-13375]fix:agent guide scroll stuck
najeebkp Sep 15, 2026
3437d1c
[CCOR-13473]enh: support required on ConductorMultiSelect (#1627)
najeebkp Sep 15, 2026
247ddd8
[CCOR-13357] Execute button gives no option to view/edit input parame…
ajlai-orkes Sep 15, 2026
191f3aa
[CCOR-13451]enh: add OLLAMA to IntegrationType (#1629)
najeebkp Sep 15, 2026
740ea38
fix: drop scroll-lock override, neutralise only the stranded modal root
najeebkp Sep 16, 2026
0ccdbac
Merge branch 'main' into CCOR-13375-agent-guide-scroll-stuck
najeebkp Sep 16, 2026
4e0e4b8
[CCOR-13376]fix:update example links of java agent create page (#1634)
najeebkp Sep 16, 2026
a0c0d44
Convert failedWorkflow input to a Map so nested references resolve (#…
nikhiln64 Sep 16, 2026
93440bf
Merge branch 'main' of ssh://ssh.github.com:443/conductor-oss/conduct…
najeebkp Sep 17, 2026
e35a93b
fix: navigate after the framework menu has closed
najeebkp Sep 17, 2026
2ccd3f8
[CCOR-13434]chore: add schema validation docs link url
najeebkp Sep 17, 2026
efa127b
Scheduler Definitions list "Last Run Time" column never populates des…
ajlai-orkes Sep 17, 2026
b23ed59
https://orkes.atlassian.net/browse/CCOR-13371 (#1635)
ajlai-orkes Sep 17, 2026
ecdbe34
Return SQL batch polls as soon as one task is available (#142) (#1636)
nthmost-orkes Sep 17, 2026
749bf62
Merge branch 'main' into CCOR-13434-schemas-docs-link
najeebkp Sep 18, 2026
f11cdc5
Merge pull request #1637 from conductor-oss/CCOR-13434-schemas-docs-link
najeebkp Sep 18, 2026
7d538fd
Merge branch 'main' into CCOR-13375-agent-guide-scroll-stuck
najeebkp Sep 18, 2026
a365d05
Merge pull request #1630 from conductor-oss/CCOR-13375-agent-guide-sc…
najeebkp Sep 18, 2026
8ac94cf
ci: add least-privilege permissions blocks to GitHub Actions workflow…
nthmost-orkes Sep 19, 2026
9ba8af9
fix(webhook): accept any 2XX status code in webhook response (#966) (…
mtalhasid Sep 21, 2026
d9bcda2
https://orkes.atlassian.net/browse/CCOR-13384 (#1650)
ajlai-orkes Sep 21, 2026
4845c0a
fix(security): hoist Pattern and prevent ReDoS in IndexQueryBuilder (…
jhaabhijeet864 Sep 21, 2026
25db03d
[CCOR-13507] LLM Chat complete task that uses AI Prompt in a saved wo…
ajlai-orkes Sep 22, 2026
daf7573
Add LLM Recording/Playback Functionality (#1614)
NicholasDCole Sep 23, 2026
96cb899
fix(core): retry a stranded SUB_WORKFLOW instead of timing it out aft…
manan164 Sep 24, 2026
7a73540
fix(deps): align micrometer OTLP registry with Spring Boot 3.5 (#1534…
nthmost-orkes Sep 24, 2026
15a8730
[CCOR-13431]enh(clone): allow clone dialog to offer a source version …
najeebkp Sep 24, 2026
e171f9d
fix(security): replace backtracking regex in ParametersUtils with a l…
Naman-Gururani Sep 24, 2026
b3b9ad3
fix(security): remove no-op identity replacement in errorInspector pa…
nthmost-orkes Sep 25, 2026
346a39e
docs(ai): note litellm requirement for LLMGuardrail in Python SDK (#1…
jhaabhijeet864 Sep 25, 2026
8899261
Fix/agent execution diagram fixes (#1515)
meghana21-arch Sep 25, 2026
2250c67
fix(ui): resolve ws to CVE-2024-37890 patched versions (#1672)
nthmost-orkes Sep 25, 2026
b05fb6a
[CCOR-13476]enh(grpc): add compression codec field to GRPC task form …
najeebkp Sep 25, 2026
da32513
fix(core): prevent path traversal in DummyPayloadStorage (#1651)
Denimworld12 Sep 25, 2026
6dcf095
[CCOR-13377]fix(agent): show why the agent failed to start
najeebkp Sep 25, 2026
97d4a49
fix(ui): show the cron expression column by default in the schedule list
liketed Sep 25, 2026
b96cbae
[CCOR-13510]fix(task-def): drop the dead input template docs link
najeebkp Sep 25, 2026
cd8e88f
[CCOR-13514] Task features are visible in enterprise but backend is o…
ajlai-orkes Sep 28, 2026
f421832
resume workflow call - lock and idempotent (#1666)
adonthi-fws Sep 28, 2026
9696683
test(ui): cover the schedule list's default visible columns
liketed Sep 29, 2026
54cd16a
DateRange picker styling is misaligned and dates are not visible on h…
ajlai-orkes Sep 29, 2026
4196b99
[CCOR-13544] Restore task execution search functionality in the new U…
ajlai-orkes Sep 29, 2026
0a095d3
Merge branch 'main' into fix/ui-schedule-list-cron-column
liketed Sep 29, 2026
14eec8b
Merge pull request #1686 from conductor-oss/fix/ui-schedule-list-cron…
liketed Sep 29, 2026
305884a
Merge branch 'main' into CCOR-13377-run-agent-panel-error-detail
najeebkp Sep 30, 2026
940dbe4
Merge branch 'main' into CCOR-13510-task-definition-remove-learn-more…
najeebkp Sep 30, 2026
fb425d5
Merge pull request #1684 from conductor-oss/CCOR-13377-run-agent-pane…
najeebkp Sep 30, 2026
904ce9b
Merge branch 'main' into CCOR-13510-task-definition-remove-learn-more…
najeebkp Sep 30, 2026
a9a0b2d
Merge pull request #1687 from conductor-oss/CCOR-13510-task-definitio…
najeebkp Sep 30, 2026
602bf6e
fix(security): cap nesting depth of ${...} expressions in ParametersU…
jhaabhijeet864 Oct 1, 2026
ab2de6d
[CCOR-13556] Query Processor task form shows an unsupported "Conducto…
ajlai-orkes Oct 2, 2026
eb525e8
Merge upstream conductor-oss/conductor main (2026-10-02) into the Ope…
matteius Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 20 additions & 0 deletions .github/actions/check-playback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Check LLM playback

Validate SDK examples against a dedicated playback server:

```yaml
- uses: conductor-oss/conductor/.github/actions/check-playback@main
with:
server-url: http://localhost:8080/api
```

Completed workflows pass. Failed workflows pass only when persisted tasks show
that a completed guardrail decision rejected the response and directly caused
the failed termination. Failed LLM tasks, unrelated failures, and unfinished
workflows fail. No SDK exception lists or example names are used.

Requires `curl` and `jq`. Run locally with:

```sh
sh .github/actions/check-playback/check-playback.sh http://localhost:8080/api
```
12 changes: 12 additions & 0 deletions .github/actions/check-playback/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Check LLM playback
description: Validate completed workflows and guardrail rejections from persisted server state.
inputs:
server-url:
description: Conductor API URL reachable from this runner, including /api.
required: true
runs:
using: composite
steps:
- name: Check playback
shell: sh
run: sh "$GITHUB_ACTION_PATH/check-playback.sh" "${{ inputs.server-url }}"
49 changes: 49 additions & 0 deletions .github/actions/check-playback/check-playback.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/sh
# Validate playback outcomes from persisted workflow state, for every SDK.
set -eu
if [ "$#" -ne 1 ]; then
printf '%s\n' "Usage: $0 <server-api-url>" >&2
exit 2
fi
server_url=${1%/}
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
start=0
failures=0
while :; do
result=$(curl --silent --show-error --fail-with-body --get \
--data-urlencode 'query=status IN (COMPLETED,RUNNING,PAUSED,FAILED,TERMINATED,TIMED_OUT)' \
--data-urlencode 'size=100' --data-urlencode "start=$start" \
"$server_url/workflow/search")
total=$(printf '%s' "$result" | jq -er '.totalHits')
rows=$(printf '%s' "$result" | jq '.results | length')
if [ "$rows" -eq 0 ]; then
if [ "$start" -lt "$total" ]; then
printf '%s\n' 'FAIL: workflow search returned an incomplete page'
exit 1
fi
break
fi
for id in $(printf '%s' "$result" | jq -r '.results[].workflowId | @uri'); do
workflow=$(curl --silent --show-error --fail-with-body "$server_url/workflow/$id?includeTasks=true")
if printf '%s' "$workflow" | jq -e '.status == "COMPLETED"' > /dev/null; then
continue
fi
if printf '%s' "$workflow" | jq -e -f "$script_dir/guardrail-rejection.jq" > /dev/null; then
printf 'PASS: %s rejected by its guardrail\n' "$id"
else
printf '%s' "$workflow" | jq -r '"\(.workflowType) \(.workflowId): \(.status) \(.reasonForIncompletion // "")"'
failures=$((failures + 1))
fi
done
start=$((start + rows))
[ "$start" -lt "$total" ] || break
done
if [ "$start" -eq 0 ]; then
printf '%s\n' 'FAIL: no workflows found; the SDK examples never ran'
exit 1
fi
if [ "$failures" -ne 0 ]; then
printf 'FAIL: %s unexpected workflow outcomes\n' "$failures"
exit 1
fi
printf '%s\n' 'PASS: every workflow completed or was rejected by its guardrail'
26 changes: 26 additions & 0 deletions .github/actions/check-playback/guardrail-rejection.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GuardrailCompiler routes a rejected decision to a TERMINATE task whose reason
# references that decision's message. Require that causal link, not just a
# rejection somewhere in an otherwise broken workflow.
. as $workflow
| .status == "FAILED"
and any(.tasks[];
. as $guard
| (.outputData.result // .outputData) as $decision
| .status == "COMPLETED"
and ($decision | type) == "object"
and $decision.passed == false
and $decision.on_fail == "raise"
and ($decision.guardrail_name | type) == "string"
and ($decision.guardrail_name | length) > 0
and ($decision.message | type) == "string"
and $workflow.reasonForIncompletion == $decision.message
and any($workflow.tasks[];
.taskType == "TERMINATE"
and .status == "COMPLETED"
and .inputData.terminationStatus == "FAILED"
and .inputData.terminationReason == $decision.message
and (.workflowTask.inputParameters.terminationReason as $reason
| ($guard.workflowTask.taskReferenceName // $guard.referenceTaskName) as $ref
| $reason == ("${" + $ref + ".output.result.message}")
or $reason == ("${" + $ref + ".output.message}"))))
and all(.tasks[] | select(.taskType == "LLM_CHAT_COMPLETE"); .status == "COMPLETED")
136 changes: 136 additions & 0 deletions .github/actions/check-playback/test_check_playback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Exercise the shared verifier through its HTTP interface."""
import copy
import json
import os
from pathlib import Path
import subprocess
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

SCRIPT = Path(__file__).with_name('check-playback.sh')


def rejection():
return {
'workflowId': 'arbitrary-id', 'workflowType': 'any-agent', 'status': 'FAILED',
'reasonForIncompletion': 'policy rejected the response',
'tasks': [
{'taskType': 'LLM_CHAT_COMPLETE', 'status': 'COMPLETED'},
{'referenceTaskName': 'decision__1', 'status': 'COMPLETED',
'workflowTask': {'taskReferenceName': 'decision'},
'outputData': {'result': {'guardrail_name': 'any-policy', 'passed': False,
'on_fail': 'raise', 'message': 'policy rejected the response'}}},
{'taskType': 'TERMINATE', 'status': 'COMPLETED',
'inputData': {'terminationStatus': 'FAILED', 'terminationReason': 'policy rejected the response'},
'workflowTask': {'inputParameters': {'terminationReason': '${decision.output.result.message}'}}},
],
}


class PlaybackCheckTest(unittest.TestCase):
def verify(self, workflows, total=None):
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
url = urlparse(self.path)
if url.path == '/api/workflow/search':
start = int(parse_qs(url.query)['start'][0])
response = {'results': workflows[start:start + 100],
'totalHits': len(workflows) if total is None else total}
else:
workflow_id = url.path.rsplit('/', 1)[-1]
response = next(w for w in workflows if w['workflowId'] == workflow_id)
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(response).encode())

def log_message(self, *_args):
pass

with HTTPServer(('127.0.0.1', 0), Handler) as server:
thread = threading.Thread(target=server.serve_forever)
thread.start()
try:
return subprocess.run(
['sh', str(SCRIPT), f'http://127.0.0.1:{server.server_port}/api'],
env=os.environ, capture_output=True, text=True, timeout=30,
)
finally:
server.shutdown()
thread.join()

def assert_failed(self, workflow):
self.assertNotEqual(self.verify([workflow]).returncode, 0)

def test_completed_workflows_pass(self):
self.assertEqual(self.verify([]).returncode, 0)

def test_guardrail_rejection_passes_without_sdk_metadata(self):
result = self.verify([rejection()])
self.assertEqual(result.returncode, 0, result.stderr)

def test_worker_guardrail_rejection_passes(self):
workflow = rejection()
guard = workflow['tasks'][1]
guard['outputData'] = guard['outputData']['result']
workflow['tasks'][2]['workflowTask']['inputParameters']['terminationReason'] = '${decision.output.message}'
self.assertEqual(self.verify([workflow]).returncode, 0)

def test_unrelated_failure_is_not_a_guardrail_rejection(self):
self.assert_failed({'workflowId': 'broken', 'status': 'FAILED', 'tasks': []})

def test_failed_llm_task_fails_even_with_guardrail_output(self):
workflow = rejection()
workflow['tasks'][0]['status'] = 'FAILED'
self.assert_failed(workflow)

def test_rejection_followed_by_an_unrelated_failure_fails(self):
workflow = rejection()
workflow['reasonForIncompletion'] = 'worker crashed'
self.assert_failed(workflow)

def test_termination_must_reference_the_rejecting_guardrail(self):
workflow = rejection()
workflow['tasks'][2]['workflowTask']['inputParameters']['terminationReason'] = '${another_task.output.message}'
self.assert_failed(workflow)

def test_missing_or_incomplete_termination_fails(self):
workflow = rejection()
workflow['tasks'].pop()
self.assert_failed(workflow)
workflow = rejection()
workflow['tasks'][2]['status'] = 'IN_PROGRESS'
self.assert_failed(workflow)

def test_non_rejecting_decisions_fail(self):
for field, value in [('passed', True), ('on_fail', 'retry'), ('guardrail_name', '')]:
with self.subTest(field=field):
workflow = rejection()
workflow['tasks'][1]['outputData']['result'][field] = value
self.assert_failed(workflow)

def test_unfinished_and_other_terminal_states_fail(self):
for status in ['RUNNING', 'PAUSED', 'TERMINATED', 'TIMED_OUT']:
with self.subTest(status=status):
workflow = rejection()
workflow['status'] = status
self.assert_failed(workflow)

def test_unrelated_failure_after_first_page_fails(self):
workflows = []
for index in range(100):
workflow = copy.deepcopy(rejection())
workflow['workflowId'] = str(index)
workflows.append(workflow)
workflows.append({'workflowId': 'broken', 'status': 'FAILED', 'tasks': []})
result = self.verify(workflows)
self.assertNotEqual(result.returncode, 0)
self.assertIn('broken', result.stdout)

def test_incomplete_search_page_fails(self):
self.assertNotEqual(self.verify([], total=1).returncode, 0)


if __name__ == '__main__':
unittest.main()
20 changes: 20 additions & 0 deletions .github/actions/start-playback-services/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Start playback services
description: Start and check the authenticated HTTP and MCP fixtures used by SDK playback.
inputs:
work-directory:
description: Directory for fixture logs and PID files.
default: tmp/agent-playback
runs:
using: composite
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install MCP test service
shell: bash
run: pip install mcp-testkit==1.0.4
- name: Start and check HTTP and MCP services
shell: bash
env:
CONDUCTOR_PLAYBACK_WORK_DIR: ${{ inputs.work-directory }}
run: bash "$GITHUB_ACTION_PATH/start-services.sh"
45 changes: 45 additions & 0 deletions .github/actions/start-playback-services/http_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""HTTP dependency for example 16e; uses the shared recording's response bytes.

The agent, HTTP task, credential substitution, and model playback still run on
Conductor. This fixture only replaces the external GitHub endpoint.
"""
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path


def github_response(recordings):
for path in (recordings / '16e_credentials_http_tool').glob('*.json'):
for message in json.loads(path.read_text())['request']['messages']:
for result in message['toolResults']:
if result['name'] == 'list_github_repos':
return result['value']['response']
raise RuntimeError('Shared GitHub HTTP response is missing')


class Handler(BaseHTTPRequestHandler):
response = None

def do_GET(self):
if self.path != '/users/Conductor/repos?per_page=5&sort=updated':
self.send_error(404)
return
if self.headers.get('Authorization') != 'Bearer playback-test-key':
self.send_error(401)
return
body = json.dumps(self.response['body'], separators=(',', ':')).encode()
self.send_response_only(self.response['statusCode'], self.response['reasonPhrase'])
for name, values in self.response['headers'].items():
if name.lower() == 'content-length':
continue
for value in values:
self.send_header(name, value)
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)


if __name__ == '__main__':
Handler.response = github_response(Path(os.environ['CONDUCTOR_RECORDINGS_DIR']))
HTTPServer(('127.0.0.1', 3002), Handler).serve_forever()
69 changes: 69 additions & 0 deletions .github/actions/start-playback-services/start-services.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Start the authenticated HTTP/MCP fixtures; --check only checks existing services.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
conductor_dir=$(cd "$script_dir/../../.." && pwd)
export CONDUCTOR_RECORDINGS_DIR="$conductor_dir/llm-recordings"
playback_dir=${CONDUCTOR_PLAYBACK_WORK_DIR:-"$PWD/tmp/agent-playback"}

check_services() {
curl --fail --silent --show-error --max-time 3 \
-H 'Authorization: Bearer playback-test-key' -H 'Content-Type: application/json' \
--data '{"text":"hello world"}' http://localhost:3001/api/string/reverse > /dev/null &&
curl --fail --silent --show-error --max-time 3 \
-H 'Authorization: Bearer playback-test-key' -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sdk-playback","version":"1"}}}' \
http://localhost:3001/mcp > /dev/null &&
curl --fail --silent --show-error --max-time 3 \
-H 'Authorization: Bearer playback-test-key' \
'http://localhost:3002/users/Conductor/repos?per_page=5&sort=updated' > /dev/null
}

if [[ "${1:-}" == --check ]]; then
check_services
exit
fi
if [[ -n "${1:-}" ]]; then
echo "Unknown option: $1" >&2
exit 2
fi
command -v mcp-testkit > /dev/null
# Never replace or stop an existing service.
python3 - <<'PY'
import socket
for port in (3001, 3002):
with socket.socket() as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', port))
PY
mkdir -p "$playback_dir"
pids=()
cleanup_on_error() {
result=$?
if ((result != 0)); then
for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null || true; done
fi
}
trap cleanup_on_error EXIT
nohup python3 "$script_dir/http_fixture.py" < /dev/null > "$playback_dir/http.log" 2>&1 &
pids+=("$!")
echo "$!" > "$playback_dir/http.pid"
nohup mcp-testkit --transport http --host 127.0.0.1 --port 3001 --auth playback-test-key < /dev/null > "$playback_dir/mcp.log" 2>&1 &
pids+=("$!")
echo "$!" > "$playback_dir/mcp.pid"
for attempt in $(seq 1 30); do
for pid in "${pids[@]}"; do
if ! kill -0 "$pid" 2>/dev/null; then
cat "$playback_dir/http.log" "$playback_dir/mcp.log"
exit 1
fi
done
if check_services > "$playback_dir/services-check.log" 2>&1; then
echo 'Authenticated HTTP and MCP fixtures are ready on ports 3001 and 3002.'
exit 0
fi
sleep 1
done
cat "$playback_dir/services-check.log"
exit 1
Loading
Loading