Skip to content

feat(agent-server): add seatbelt option for macOS sandbox-exec sandbo…#3186

Draft
rbren wants to merge 1 commit intomainfrom
feat/seatbelt-conversation-option
Draft

feat(agent-server): add seatbelt option for macOS sandbox-exec sandbo…#3186
rbren wants to merge 1 commit intomainfrom
feat/seatbelt-conversation-option

Conversation

@rbren
Copy link
Copy Markdown
Contributor

@rbren rbren commented May 10, 2026

…xing

Adds a seatbelt boolean option to the conversation creation API. When true, the agent-server validates that the host is macOS with sandbox-exec available (returning HTTP 400 otherwise) and threads the flag through LocalConversation -> ConversationState -> TerminalTool, so shell processes spawned by the conversation are wrapped with sandbox-exec -p <profile>.

The default profile permits arbitrary reads, network, and fork/exec, but restricts writes to the workspace and standard temp directories.

  • New openhands.sdk.utils.seatbelt helper (availability check, default profile, argv wrapper)
  • Plumbed through StartConversationRequest, StartACPConversationRequest, Conversation/LocalConversation/RemoteConversation, ConversationState, EventService, TerminalTool, TerminalExecutor, and both terminal backends (subprocess + tmux, including the tmux pane pool)
  • Tests for the helper, terminal wiring, and the new router validation
  • A human has tested these changes.

Why

Summary

Issue Number

How to Test

Video/Screenshots

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:2a84be5-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-2a84be5-python \
  ghcr.io/openhands/agent-server:2a84be5-python

All tags pushed for this build

ghcr.io/openhands/agent-server:2a84be5-golang-amd64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-golang-amd64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-golang-amd64
ghcr.io/openhands/agent-server:2a84be5-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:2a84be5-golang-arm64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-golang-arm64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-golang-arm64
ghcr.io/openhands/agent-server:2a84be5-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:2a84be5-java-amd64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-java-amd64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-java-amd64
ghcr.io/openhands/agent-server:2a84be5-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:2a84be5-java-arm64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-java-arm64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-java-arm64
ghcr.io/openhands/agent-server:2a84be5-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:2a84be5-python-amd64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-python-amd64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-python-amd64
ghcr.io/openhands/agent-server:2a84be5-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:2a84be5-python-arm64
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-python-arm64
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-python-arm64
ghcr.io/openhands/agent-server:2a84be5-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:2a84be5-golang
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-golang
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-golang
ghcr.io/openhands/agent-server:2a84be5-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:2a84be5-java
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-java
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-java
ghcr.io/openhands/agent-server:2a84be5-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:2a84be5-python
ghcr.io/openhands/agent-server:2a84be5d189e2e9329cf945c006236e4ad9f534e-python
ghcr.io/openhands/agent-server:feat-seatbelt-conversation-option-python
ghcr.io/openhands/agent-server:2a84be5-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 2a84be5-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 2a84be5-python-amd64) are also available if needed

…xing

Adds a `seatbelt` boolean option to the conversation creation API. When
true, the agent-server validates that the host is macOS with
`sandbox-exec` available (returning HTTP 400 otherwise) and threads the
flag through LocalConversation -> ConversationState -> TerminalTool, so
shell processes spawned by the conversation are wrapped with
`sandbox-exec -p <profile>`.

The default profile permits arbitrary reads, network, and fork/exec, but
restricts writes to the workspace and standard temp directories.

- New `openhands.sdk.utils.seatbelt` helper (availability check, default
  profile, argv wrapper)
- Plumbed through StartConversationRequest, StartACPConversationRequest,
  Conversation/LocalConversation/RemoteConversation, ConversationState,
  EventService, TerminalTool, TerminalExecutor, and both terminal
  backends (subprocess + tmux, including the tmux pane pool)
- Tests for the helper, terminal wiring, and the new router validation

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_router.py1571093%326, 417–420, 432–435, 469
   conversation_router_acp.py39197%108
   event_service.py4379678%75–76, 101, 105, 108–109, 113–114, 121, 131–135, 138–141, 161, 265, 282, 323, 333, 357–358, 362, 370, 373, 414, 430, 432, 436–438, 442, 451–452, 454, 458, 464, 466, 496–501, 527, 530, 581, 585, 732, 734–735, 739, 753–755, 757, 761–764, 768–771, 779–782, 830–831, 833–840, 842–843, 852–853, 855–856, 863–864, 866–867, 887, 893, 899, 908–909
openhands-sdk/openhands/sdk/conversation
   conversation.py34876%143, 156–157, 163–166, 170
   request.py47197%60
   state.py1922487%268, 355, 387, 435–437, 453–454, 460, 466–469, 473, 479–482, 512, 522, 540, 549, 564, 570
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py44719755%215, 217–218, 264, 300, 305, 315, 346, 353–355, 361, 366–370, 373, 388–389, 395, 398, 401–402, 408–409, 411, 413, 418, 436, 449–450, 452, 454, 461–462, 465–466, 472–474, 477–478, 481–482, 484, 495, 501, 506, 514–515, 519, 528–531, 535, 600, 649–657, 673–679, 784, 796–799, 806–807, 810, 819–820, 823, 830, 851, 857, 861–862, 866–868, 875, 905, 907, 909, 913, 915–917, 919, 921, 927–928, 941–942, 944, 946, 950–953, 983, 991, 993, 997–998, 1009, 1011–1013, 1033, 1036–1038, 1041, 1043, 1047, 1052, 1057, 1062–1065, 1071, 1074, 1078, 1081, 1083–1085, 1087, 1108–1109, 1123, 1127, 1135, 1151–1152, 1156, 1158, 1160, 1199, 1202–1207, 1209, 1211–1214, 1216, 1218–1219, 1222–1225, 1232–1233, 1237, 1240–1242, 1245, 1247, 1249, 1256–1258, 1262, 1264–1265, 1295, 1298–1301, 1306–1308, 1314–1315
   remote_conversation.py65722166%67, 74–75, 77–78, 104–106, 137, 144, 168, 171, 178–179, 184, 186–189, 199, 221–222, 227–230, 273, 287, 314, 324–326, 332, 355–362, 365, 369, 372, 378–379, 390, 406, 426–427, 468, 494, 514, 522, 534, 542–545, 548, 553–556, 558, 563–564, 575–579, 584–588, 593–596, 599, 611–615, 619–620, 624, 628, 631, 724–725, 731–732, 734, 777–778, 782–783, 786, 797, 823–824, 829, 831–832, 843, 854–855, 875–878, 880–881, 894–895, 905–907, 910–914, 916–917, 921, 923–931, 933, 952, 970, 1016, 1018, 1021, 1067, 1099–1101, 1130, 1146, 1148–1149, 1154, 1162–1166, 1173–1174, 1178, 1183–1187, 1191–1199, 1202–1203, 1212, 1217, 1226, 1237, 1244, 1246–1248, 1252, 1255–1256, 1258, 1260–1261, 1284, 1286, 1292–1293, 1309, 1311–1312, 1329, 1367–1368, 1373–1379, 1381, 1387–1388, 1390–1391, 1397, 1399, 1423, 1446–1447, 1465–1466
openhands-sdk/openhands/sdk/utils
   seatbelt.py191142%27–29, 33, 35, 49–50, 54, 91–93
openhands-tools/openhands/tools/terminal
   definition.py1115352%70, 73, 76–77, 79, 82–84, 86–88, 90–92, 94, 122, 130, 157, 159–161, 164, 166, 168–170, 172, 176–177, 180–182, 184–185, 188–191, 195–197, 202, 206–211, 213–214, 216, 231, 265
   impl.py2319359%116, 124–125, 154–158, 161–163, 165–169, 176, 195–196, 201–202, 231–234, 236, 254–263, 265, 270, 276–277, 300, 303, 306, 310, 328, 331, 339–340, 349, 382–383, 385, 393, 397–400, 402–403, 411, 413, 417, 441–442, 444–446, 451–452, 454–455, 457, 466, 468–469, 471, 491–494, 499–500, 522, 532–535, 539–540, 548, 553, 561–562
openhands-tools/openhands/tools/terminal/terminal
   factory.py735721%27–28, 33–35, 37, 39–43, 50–54, 59, 68, 70–72, 74–75, 105–109, 111–113, 115–117, 124–126, 131, 137, 141–142, 145, 147, 149–151, 158–159, 161–163, 165, 169, 174–177
   subprocess_terminal.py28123217%15, 62–63, 89–92, 95–100, 106–107, 113–114, 116–118, 125–126, 130–131, 137–138, 141–144, 146, 151–152, 154–155, 158, 160–162, 176–179, 181, 184–185, 188, 191–192, 195, 199–200, 202, 204, 208–209, 211–212, 214–220, 222–228, 231–236, 238–239, 241–242, 247, 249–257, 261–263, 265–266, 268–269, 272–274, 276–279, 281–282, 284–285, 287–292, 297–299, 301, 304, 307–308, 311–312, 318–320, 322–329, 331–334, 338–346, 381–382, 384–385, 388–389, 391, 393, 395–397, 400–403, 405–407, 409–410, 412–413, 422–425, 434–436, 439–440, 443, 445, 448–449, 451, 458–459, 463, 465–470, 474–475, 477–480, 482–488, 490–491, 493–497, 501–502, 504–510, 514–515, 518–519, 521–522, 524–526
   tmux_pane_pool.py1443079%51–53, 111, 117, 147, 162–163, 171, 225, 229–230, 250–251, 267, 270–271, 273–276, 279–280, 284–288, 290–291
   tmux_terminal.py1086044%56–57, 85–86, 88, 90–92, 94, 96, 100–102, 109–110, 114–115, 118–119, 124–128, 132, 135, 137–139, 143–148, 152–153, 170, 177–178, 183–184, 199, 221, 233–240, 248–249, 251–252, 254, 256–258
TOTAL268611183955% 

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.

2 participants