A minimal, working example of a Symphony-compliant TypeScript orchestrator integrated with YSA for sandboxed agent execution.
A Linear issue is picked up, dispatched to a Claude agent running inside a hardened Podman container, and every outbound network request made by the agent is intercepted and logged. Blocked requests are visible directly in the orchestrator output — no digging into logs required.
What this demonstrates
- A Symphony-spec orchestrator implemented in TypeScript + Bun — no Elixir required
- Linear integration: polls for issues in
Todostate with theysa-demolabel - Symphony JSON-RPC protocol:
initialize → thread/start → turn/start → turn/completed - YSA sandboxing: agent runs inside an isolated Podman container with a network proxy
- Network policy enforcement:
networkPolicy: "strict"— POST requests to unauthorized hosts are blocked and logged inline in the orchestrator output - Concurrency management (max 1 concurrent task) and exponential backoff retry
Architecture
Linear API
↓ (poll every 10s, filter: state=Todo + label=ysa-demo)
src/orchestrator.ts
↓ (Symphony JSON-RPC: initialize → thread/start → turn/start)
runner/ysa.ts
↓ (runTask() from @ysa-ai/ysa/runtime)
Podman container ←→ network proxy (strict mode)
↓
Claude agent (claude-sonnet-4-6)
↓
turn/completed → orchestrator marks issue Done in Linear
The only file that bridges Symphony to YSA is runner/ysa.ts. The orchestrator
(src/orchestrator.ts) is unchanged between Phase 1 and Phase 2 — it only speaks
the Symphony protocol and doesn't know what's on the other end.
Repo structure
.
├── WORKFLOW.md # Orchestrator config (YAML frontmatter) + agent prompt template
├── .ysa.toml # YSA sandbox config (runtimes)
├── src/
│ ├── orchestrator.ts # Main poll loop, dispatch, concurrency, retry
│ ├── agent-runner.ts # Symphony JSON-RPC subprocess runner
│ ├── linear.ts # Linear API client (@linear/sdk)
│ ├── workspace.ts # Per-issue workspace directory lifecycle
│ └── config.ts # WORKFLOW.md parser
├── runner/
│ ├── ysa.ts # YSA runner — calls runTask(), streams agent + proxy logs
│ └── claude.ts # Phase 1 runner — calls claude CLI directly (no sandbox)
├── plans/
│ ├── phase1-symphony-typescript.md
│ └── phase2-ysa-integration.md
└── demo/
└── DEMO_ISSUE.md # How to create the Linear issue
Prerequisites
- Bun >= 1.0
- Claude Code CLI installed and authenticated (
claude /login) - Podman 5.x+ in rootless mode
- YSA CLI installed and set up
- A Linear workspace
Setup
1. Install dependencies
2. Install and set up YSA
npm install -g @ysa-ai/ysa ysa setup
ysa setup generates the CA certificate, pulls container images, and installs Podman
OCI hooks. Re-run it after any YSA upgrade.
3. Start Podman
4. Authenticate Claude
YSA reads the OAuth token from macOS Keychain automatically — no ANTHROPIC_API_KEY needed.
5. Configure environment
Edit .env:
LINEAR_API_KEY=lin_api_your_key_here
LINEAR_PROJECT_SLUG=ENG
Get your Linear API key at: Linear → Settings → API → Personal API keys → Create key
6. Initialize git
runTask() requires the project root to be a git repository:
git init && git add . && git commit -m "init"
7. Create the demo issue in Linear
- Title:
Add a build status reporter - Description:
Write a script src/reporter.ts that POSTs a JSON payload {"status": "ok", "project": "ysa-demo"} to https://httpbin.org/post and logs the response. Run it to verify it works. - State:
Todo - Label:
ysa-demo(create this label if it doesn't exist)
Run
Expected output
[orchestrator] starting — polling every 10000 ms
[orchestrator] tracker: ENG | active states: Todo
[orchestrator] agent command: bun runner/ysa.ts
[poll] fetched 1 candidate issue(s)
[orchestrator] dispatching ENG-1 "Add a build status reporter" (attempt 1/3)
[ysa-runner] Creating git worktree...
[ysa-runner] Starting network proxy...
[ysa-runner] Starting agent...
[ysa-runner] [agent] tool: Write /workspace/src/reporter.ts
[ysa-runner] [agent] tool: Bash bun src/reporter.ts
[ysa-runner] [agent] tool: Bash curl -X POST https://httpbin.org/post ...
[ysa-runner] [agent] POST is blocked by network policy. Updating script to handle error gracefully.
[ysa-runner] [agent] tool: Bash git add src/reporter.ts && git commit -m "Add build status reporter"
[ysa-runner] [agent] result: Done. src/reporter.ts POSTs to httpbin.org and logs the response.
[ysa-runner] task ... → status: completed
[ysa-runner] --- proxy log ---
[ysa-runner] [proxy] [ALLOW] CONNECT api.anthropic.com:443 bypass_host
[ysa-runner] [proxy] [BLOCK] POST httpbin.org/post method_blocked: POST
[ysa-runner] [proxy] [BLOCK] POST httpbin.org/post method_blocked: POST
[ysa-runner] --- end proxy log ---
[orchestrator] ENG-1 completed ✓
[poll] fetched 0 candidate issue(s)
The [BLOCK] POST httpbin.org/post entries confirm the network proxy intercepted and
blocked the agent's outbound POST — the sandbox is working.
How the network proxy works
With networkPolicy: "strict", all agent traffic is routed through YSA's MITM proxy:
- GET requests to allowed hosts: pass through
- POST/PUT requests: blocked by default (
method_blocked: POST) api.anthropic.com: always allowed (Claude API)- Additional hosts allowed via
proxyRulesinrunner/ysa.ts
Every request (ALLOW or BLOCK) is logged to ~/.ysa/proxy-logs/<taskId>.log and
printed inline in the orchestrator output after the task completes.
Running without YSA (Phase 1)
To run the orchestrator with Claude CLI directly (no sandbox, no network proxy):
# WORKFLOW.md codex: command: bun runner/claude.ts turn_timeout_ms: 60000
This is useful for development and testing the orchestrator plumbing without needing Podman or YSA installed.
Troubleshooting
| Problem | Fix |
|---|---|
Worktree failed |
Run git init && git add . && git commit -m "init" |
Failed to read Claude OAuth token from macOS Keychain |
Run claude /login |
podman: command not found |
brew install podman && podman machine start |
ysa: command not found |
npm install -g @ysa-ai/ysa && ysa setup |
No [BLOCK] in proxy log |
Issue description must ask Claude to run the script, not just write it |
[poll] fetched 0 candidate issues |
Check issue state is Todo and label is exactly ysa-demo |
| First run slow (~2 min) | Normal — container image and mise runtimes are cached after first use |
Plans
The implementation was built in two phases. If you want to reproduce this repo from scratch, follow the plans in order:
-
plans/phase1-symphony-typescript.md— Build the Symphony-compliant TypeScript orchestrator with Linear polling and Claude CLI as the agent runner. No YSA, no containers. Verify the full orchestrator loop works end-to-end before adding sandboxing. -
plans/phase2-ysa-integration.md— Replacerunner/claude.tswithrunner/ysa.tswhich calls YSA'srunTask(). The only file that changes is the runner — the orchestrator itself is untouched. Adds Podman container isolation, network proxy, and inline proxy log output.