crewly 1.20.40 → 1.20.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/config/skills/_common/desktop-guards.sh +485 -0
  2. package/config/skills/_common/desktop-guards.test.sh +242 -0
  3. package/config/skills/_common/desktop-perceive.swift +530 -0
  4. package/config/skills/_common/desktop-presence.swift +343 -0
  5. package/config/skills/agent/_common/desktop-guards.sh +4 -0
  6. package/config/skills/agent/computer-use/SKILL.md +88 -0
  7. package/config/skills/agent/computer-use/execute.sh +249 -3
  8. package/config/skills/agent/desktop-app-control/SKILL.md +19 -0
  9. package/config/skills/agent/remote-browser/SKILL.md +19 -0
  10. package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts +105 -0
  11. package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts.map +1 -0
  12. package/dist/backend/backend/src/controllers/desktop/desktop.controller.js +278 -0
  13. package/dist/backend/backend/src/controllers/desktop/desktop.controller.js.map +1 -0
  14. package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts +21 -0
  15. package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts.map +1 -0
  16. package/dist/backend/backend/src/controllers/desktop/desktop.routes.js +31 -0
  17. package/dist/backend/backend/src/controllers/desktop/desktop.routes.js.map +1 -0
  18. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  19. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  20. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  21. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  22. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +9 -0
  23. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  24. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +18 -0
  25. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  26. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +33 -6
  27. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  28. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
  29. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +60 -1
  31. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/slack/slack.service.d.ts +18 -1
  33. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  34. package/dist/backend/backend/src/services/slack/slack.service.js +38 -2
  35. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  36. package/dist/backend/backend/src/types/slack.types.d.ts +10 -0
  37. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  38. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  39. package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
  40. package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
  41. package/dist/backend/backend/src/utils/incomplete-turn.utils.js +4 -0
  42. package/dist/backend/backend/src/utils/incomplete-turn.utils.js.map +1 -1
  43. package/dist/backend/build-info.json +2 -2
  44. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +18 -0
  45. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  46. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +33 -6
  47. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  48. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
  49. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  50. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +60 -1
  51. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  52. package/dist/cli/backend/src/services/slack/slack.service.d.ts +18 -1
  53. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  54. package/dist/cli/backend/src/services/slack/slack.service.js +38 -2
  55. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  56. package/dist/cli/backend/src/types/slack.types.d.ts +10 -0
  57. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  58. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  59. package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
  60. package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
  61. package/dist/cli/backend/src/utils/incomplete-turn.utils.js +4 -0
  62. package/dist/cli/backend/src/utils/incomplete-turn.utils.js.map +1 -1
  63. package/package.json +1 -1
  64. package/packages/crewly-agent/src/eval/desktop/desktop-tasks.test.ts +96 -0
  65. package/packages/crewly-agent/src/eval/desktop/desktop-tasks.ts +226 -0
  66. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +20 -1
  67. package/packages/crewly-agent/src/runtime/computer.tool.test.ts +219 -0
  68. package/packages/crewly-agent/src/runtime/computer.tool.ts +405 -0
  69. package/packages/crewly-agent/src/runtime/desktop-checkpoint.test.ts +136 -0
  70. package/packages/crewly-agent/src/runtime/desktop-checkpoint.ts +231 -0
  71. package/packages/crewly-agent/src/runtime/desktop-recovery.test.ts +100 -0
  72. package/packages/crewly-agent/src/runtime/desktop-recovery.ts +195 -0
  73. package/packages/crewly-agent/src/runtime/desktop-task-runtime.test.ts +251 -0
  74. package/packages/crewly-agent/src/runtime/desktop-task-runtime.ts +423 -0
  75. package/packages/crewly-agent/src/runtime/desktop-task.tool.test.ts +218 -0
  76. package/packages/crewly-agent/src/runtime/desktop-task.tool.ts +343 -0
  77. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +17 -0
  78. package/packages/crewly-agent/src/runtime/tool-registry.ts +54 -0
  79. package/packages/crewly-agent/src/runtime/types.ts +10 -1
  80. package/config/skills/agent/vnc-browser/SKILL.md +0 -140
@@ -35,6 +35,14 @@ INPUT=$(read_json_input "${1:-}")
35
35
  ACTION=$(echo "$INPUT" | jq -r '.action // empty')
36
36
  [ -z "$ACTION" ] && error_exit "Missing required parameter: action"
37
37
 
38
+ # ---------------------------------------------------------------------------
39
+ # Safety rails (permissions, stop switch, desktop lock, destructive-key and
40
+ # secure-field refusals, audit) — shared with the marketplace fork.
41
+ # ---------------------------------------------------------------------------
42
+ source "${SCRIPT_DIR}/../../_common/desktop-guards.sh"
43
+ cu_apply_guards
44
+
45
+
38
46
  # ---------------------------------------------------------------------------
39
47
  # Helper: get screen dimensions and scale factor
40
48
  # ---------------------------------------------------------------------------
@@ -56,9 +64,15 @@ get_screen_info() {
56
64
  # optionally overlays a grid, optionally crops.
57
65
  # ---------------------------------------------------------------------------
58
66
  do_screenshot() {
59
- local output="${TMPDIR_CU}/screen_$(date +%s%N).png"
67
+ local output=$(echo "$INPUT" | jq -r '.output // empty')
68
+ [ -z "$output" ] && output="${TMPDIR_CU}/screen_$(date +%s%N).png"
60
69
  local grid=$(echo "$INPUT" | jq -r '.grid // empty')
61
70
  local crop_json=$(echo "$INPUT" | jq -r '.crop // empty')
71
+ # Cap the width the caller wants to reason in. A model's pointing accuracy
72
+ # falls off on a wide image because it is downsampled before the model sees
73
+ # it, and one reasoning in the original coordinate space then points at the
74
+ # wrong place — so the caller asks for the width it will think in.
75
+ local max_width=$(echo "$INPUT" | jq -r '.maxWidth // empty')
62
76
 
63
77
  # Capture full screen (silent)
64
78
  screencapture -x "$output"
@@ -82,6 +96,15 @@ do_screenshot() {
82
96
  local img_h
83
97
  img_h=$(sips -g pixelHeight "$output" | tail -1 | awk '{print $2}')
84
98
 
99
+ # Then, if asked, shrink further to the caller's reasoning width. Never
100
+ # enlarge: a small screen is already easier to point at, and scaling up
101
+ # would invent precision the model does not have.
102
+ if [ -n "$max_width" ] && [ "$max_width" -gt 0 ] 2>/dev/null && [ "$img_w" -gt "$max_width" ]; then
103
+ sips --resampleWidth "$max_width" "$output" --out "$output" >/dev/null 2>&1
104
+ img_w=$(sips -g pixelWidth "$output" | tail -1 | awk '{print $2}')
105
+ img_h=$(sips -g pixelHeight "$output" | tail -1 | awk '{print $2}')
106
+ fi
107
+
85
108
  # Crop FIRST, then grid — so grid labels show absolute screen coordinates
86
109
  local origin_x=0 origin_y=0
87
110
  if [ -n "$crop_json" ]; then
@@ -289,7 +312,9 @@ do_key() {
289
312
  if [ ${#parts[@]} -eq 1 ]; then
290
313
  key_name="${parts[0]}"
291
314
  else
292
- key_name="${parts[-1]}"
315
+ # macOS ships bash 3.2, which has no negative array indices — using one
316
+ # here made every modifier combo fail with "bad array subscript".
317
+ key_name="${parts[$(( ${#parts[@]} - 1 ))]}"
293
318
  for ((i=0; i<${#parts[@]}-1; i++)); do
294
319
  case "${parts[$i]}" in
295
320
  command|cmd) modifiers="${modifiers}command down, " ;;
@@ -844,9 +869,221 @@ do_click_at() {
844
869
  " >/dev/null 2>&1
845
870
  }
846
871
 
872
+ # =============================================================================
873
+ # Element-level perception and action (Phase 2)
874
+ #
875
+ # Everything above works in screen coordinates: the agent takes a screenshot,
876
+ # guesses where a button is, and clicks a point. That misses, costs an image
877
+ # per step, and breaks on a different theme or window position.
878
+ #
879
+ # These work in elements. `snapshot` returns refs (@e1, @e2 …) with roles,
880
+ # names and frames; `click`/`type` accept a ref and go through the
881
+ # accessibility action, which does not care where the window sits or whether
882
+ # something overlaps it.
883
+ # =============================================================================
884
+
885
+ # ---------------------------------------------------------------------------
886
+ # snapshot — the elements of an app, as refs the agent can act on.
887
+ # ---------------------------------------------------------------------------
888
+ do_snapshot() {
889
+ local app max flags=()
890
+ app=$(printf '%s' "$INPUT" | jq -r '.app // empty')
891
+ max=$(printf '%s' "$INPUT" | jq -r '.max // empty')
892
+ [ -n "$app" ] && flags+=(--app "$app")
893
+ [ -n "$max" ] && flags+=(--max "$max")
894
+ [ "$(printf '%s' "$INPUT" | jq -r '.allWindows // false')" = "true" ] && flags+=(--all-windows)
895
+ [ "$(printf '%s' "$INPUT" | jq -r '.menus // false')" = "true" ] && flags+=(--menus)
896
+ cu_perceive snapshot ${flags[@]+"${flags[@]}"}
897
+ }
898
+
899
+ # ---------------------------------------------------------------------------
900
+ # click-ref — press an element by ref.
901
+ #
902
+ # AXPress first: it reaches a button that is scrolled out of view or covered
903
+ # by another window, which a coordinate click cannot. Falls back to clicking
904
+ # the element's centre for the many controls that expose no press action.
905
+ # ---------------------------------------------------------------------------
906
+ do_click_ref() {
907
+ local ref result ok cx cy
908
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
909
+ require_param "ref" "$ref"
910
+
911
+ result=$(cu_perceive press --ref "$ref" 2>&1) || { printf '%s\n' "$result"; exit 1; }
912
+ ok=$(printf '%s' "$result" | jq -r '.success // false')
913
+ if [ "$ok" = "true" ]; then
914
+ printf '%s' "$result" | jq -c '{success:true, action:"click-ref", ref:.ref, method:"AXPress"}'
915
+ return 0
916
+ fi
917
+
918
+ # No press action (or it refused) — click where the element is.
919
+ cx=$(printf '%s' "$result" | jq -r '.center[0] // empty')
920
+ cy=$(printf '%s' "$result" | jq -r '.center[1] // empty')
921
+ if [ -z "$cx" ] || [ -z "$cy" ]; then
922
+ printf '%s\n' "$result"
923
+ exit 1
924
+ fi
925
+ do_click_at "$cx" "$cy"
926
+ jq -n --arg r "$ref" --argjson x "$cx" --argjson y "$cy" \
927
+ '{success:true, action:"click-ref", ref:$r, method:"coordinate-fallback", x:$x, y:$y}'
928
+ }
929
+
930
+ # ---------------------------------------------------------------------------
931
+ # fill-ref — set a field's value directly.
932
+ #
933
+ # More reliable than typing: no dependence on focus, on the keyboard layout,
934
+ # or on an input method being in the right mode.
935
+ # ---------------------------------------------------------------------------
936
+ do_fill_ref() {
937
+ local ref text
938
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
939
+ text=$(printf '%s' "$INPUT" | jq -r '.text // empty')
940
+ require_param "ref" "$ref"
941
+ require_param "text" "$text"
942
+ cu_perceive set-value --ref "$ref" --text "$text"
943
+ }
944
+
945
+ # ---------------------------------------------------------------------------
946
+ # resolve — what is at this ref now.
947
+ #
948
+ # Worth calling after something may have changed the window: it reports
949
+ # whether the element still matches what the snapshot recorded.
950
+ # ---------------------------------------------------------------------------
951
+ do_resolve() {
952
+ local ref
953
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
954
+ require_param "ref" "$ref"
955
+ cu_perceive resolve --ref "$ref"
956
+ }
957
+
958
+ # ---------------------------------------------------------------------------
959
+ # ocr — the text on screen and where it is.
960
+ #
961
+ # Covers what accessibility does not expose: a canvas, a PDF page, an app
962
+ # that simply does not implement AX. Local and free (macOS Vision).
963
+ # ---------------------------------------------------------------------------
964
+ do_ocr() {
965
+ local region flags=()
966
+ region=$(printf '%s' "$INPUT" | jq -r 'if .region then "\(.region.x),\(.region.y),\(.region.w),\(.region.h)" else empty end')
967
+ [ -n "$region" ] && flags+=(--region "$region")
968
+ local image; image=$(printf '%s' "$INPUT" | jq -r '.image // empty')
969
+ [ -n "$image" ] && flags+=(--image "$image")
970
+ cu_perceive ocr ${flags[@]+"${flags[@]}"}
971
+ }
972
+
973
+ # ---------------------------------------------------------------------------
974
+ # displays — every screen and its frame, so coordinates are unambiguous.
975
+ # ---------------------------------------------------------------------------
976
+ do_displays() {
977
+ cu_perceive displays
978
+ }
979
+
980
+ # ---------------------------------------------------------------------------
981
+ # wait-for — block until the screen is in the expected state.
982
+ #
983
+ # The alternative is sleeping and hoping: an agent that clicks during an
984
+ # animation hits nothing, and one that sleeps long enough to be safe is slow
985
+ # on every step instead.
986
+ #
987
+ # {"action":"wait-for","app":"Numbers"} that app is frontmost
988
+ # {"action":"wait-for","ref":"@e12"} that element resolves
989
+ # {"action":"wait-for","text":"Export"} that text is on screen
990
+ # {"action":"wait-for","idle":true} the screen stopped changing
991
+ # ---------------------------------------------------------------------------
992
+ do_wait_for() {
993
+ local app ref text idle timeout deadline
994
+ app=$(printf '%s' "$INPUT" | jq -r '.app // empty')
995
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
996
+ text=$(printf '%s' "$INPUT" | jq -r '.text // empty')
997
+ idle=$(printf '%s' "$INPUT" | jq -r '.idle // false')
998
+ timeout=$(printf '%s' "$INPUT" | jq -r '.timeoutMs // 10000')
999
+ deadline=$(( $(date +%s) + timeout / 1000 ))
1000
+
1001
+ if [ -z "$app" ] && [ -z "$ref" ] && [ -z "$text" ] && [ "$idle" != "true" ]; then
1002
+ error_exit "wait-for needs one of: app, ref, text, idle"
1003
+ fi
1004
+
1005
+ local previous="" current
1006
+ while [ "$(date +%s)" -le "$deadline" ]; do
1007
+ if [ -n "$app" ]; then
1008
+ local front
1009
+ front=$(osascript -e 'tell application "System Events" to get name of first process whose frontmost is true' 2>/dev/null)
1010
+ if [ "$front" = "$app" ]; then
1011
+ jq -n --arg a "$app" '{success:true, action:"wait-for", matched:"app", app:$a}'; return 0
1012
+ fi
1013
+ fi
1014
+ if [ -n "$ref" ]; then
1015
+ if cu_perceive resolve --ref "$ref" 2>/dev/null | jq -e '.success and .matches' >/dev/null 2>&1; then
1016
+ jq -n --arg r "$ref" '{success:true, action:"wait-for", matched:"ref", ref:$r}'; return 0
1017
+ fi
1018
+ fi
1019
+ if [ -n "$text" ]; then
1020
+ if cu_perceive ocr 2>/dev/null | jq -e --arg t "$text" '[.items[]?.text | select(contains($t))] | length > 0' >/dev/null 2>&1; then
1021
+ jq -n --arg t "$text" '{success:true, action:"wait-for", matched:"text", text:$t}'; return 0
1022
+ fi
1023
+ fi
1024
+ if [ "$idle" = "true" ]; then
1025
+ # Two consecutive identical frames mean nothing is animating.
1026
+ local shot="${TMPDIR_CU}/idle-$$.png"
1027
+ screencapture -x -t jpg "$shot" 2>/dev/null
1028
+ current=$(shasum -a 1 "$shot" 2>/dev/null | cut -d' ' -f1)
1029
+ rm -f "$shot"
1030
+ if [ -n "$previous" ] && [ "$current" = "$previous" ]; then
1031
+ jq -n '{success:true, action:"wait-for", matched:"idle"}'; return 0
1032
+ fi
1033
+ previous="$current"
1034
+ fi
1035
+ sleep 0.4
1036
+ done
1037
+
1038
+ cu_fail "wait_timeout" \
1039
+ "Nothing matched within ${timeout}ms. Take a snapshot to see what is actually on screen." \
1040
+ "$(jq -n --argjson t "$timeout" '{timeoutMs:$t}')"
1041
+ }
1042
+
1043
+ # ---------------------------------------------------------------------------
1044
+ # request-human — hand the machine back for something an agent must not do.
1045
+ #
1046
+ # The deleted `vnc-browser` skill described this and shipped no script. The
1047
+ # honest version is smaller than a VNC tunnel and covers the cases that
1048
+ # actually arise: a CAPTCHA, a two-factor prompt, a login, a decision the
1049
+ # owner has to make. The agent pauses itself, says what it needs and why, and
1050
+ # waits — rather than trying to be clever, which for a login means typing a
1051
+ # password it must never type.
1052
+ #
1053
+ # Pausing (not stopping) is the point: the owner deals with it and the task
1054
+ # picks up, using the same banner button they already have.
1055
+ # ---------------------------------------------------------------------------
1056
+ do_request_human() {
1057
+ local reason detail shot
1058
+ reason=$(printf '%s' "$INPUT" | jq -r '.reason // empty')
1059
+ detail=$(printf '%s' "$INPUT" | jq -r '.detail // empty')
1060
+ require_param "reason" "$reason"
1061
+
1062
+ # A picture of what the agent is stuck on is worth more than its
1063
+ # description of it, and the owner may be reading this on a phone.
1064
+ shot=$(capture_thumb handover 2>/dev/null || true)
1065
+
1066
+ # Pause rather than stop: the task is not cancelled, it is waiting.
1067
+ printf 'awaiting-human\n' > "$DESKTOP_PAUSE" 2>/dev/null || true
1068
+ cu_presence begin --agent "$HOLDER" --goal "needs you: $reason"
1069
+
1070
+ jq -n --arg r "$reason" --arg d "$detail" --arg s "$HOLDER" --arg shot "$shot" \
1071
+ '{success:true, action:"request-human", waiting:true, reason:$r,
1072
+ detail:$d, agent:$s,
1073
+ note:"Desktop control is paused until the owner resumes it (banner → Resume, or remove ~/.crewly/desktop.pause). Do not try to work around this."}
1074
+ + (if $shot == "" then {} else {screenshot:$shot} end)'
1075
+ }
1076
+
847
1077
  # ---------------------------------------------------------------------------
848
1078
  # Action dispatch
1079
+ #
1080
+ # The "after" audit shot is taken on the way out, so the log holds a pair
1081
+ # showing what the action actually changed — the point of keeping evidence at
1082
+ # all. It runs on every exit path, including a failure, because a failed
1083
+ # action that moved something is exactly what one wants to look at later.
849
1084
  # ---------------------------------------------------------------------------
1085
+ trap 'cu_log_after 2>/dev/null || true' EXIT
1086
+
850
1087
  case "$ACTION" in
851
1088
  screenshot) do_screenshot ;;
852
1089
  click) do_click ;;
@@ -860,5 +1097,14 @@ case "$ACTION" in
860
1097
  list-apps) do_list_apps ;;
861
1098
  find) do_find ;;
862
1099
  click-text) do_click_text ;;
863
- *) error_exit "Unknown action: $ACTION. Valid: screenshot, click, move, type, key, scroll, drag, focus, open-url, list-apps, find, click-text" ;;
1100
+ check-permissions) do_check_permissions ;;
1101
+ snapshot) do_snapshot ;;
1102
+ click-ref) do_click_ref ;;
1103
+ fill-ref) do_fill_ref ;;
1104
+ resolve) do_resolve ;;
1105
+ ocr) do_ocr ;;
1106
+ displays) do_displays ;;
1107
+ wait-for) do_wait_for ;;
1108
+ request-human) do_request_human ;;
1109
+ *) error_exit "Unknown action: $ACTION. Valid: screenshot, click, move, type, key, scroll, drag, focus, open-url, list-apps, find, click-text, check-permissions, snapshot, click-ref, fill-ref, resolve, ocr, displays, wait-for, request-human" ;;
864
1110
  esac
@@ -142,3 +142,22 @@ bash execute.sh snapshot --session vscode --interactive
142
142
  - `agent-browser` (npm install -g agent-browser && agent-browser install)
143
143
  - macOS / Linux / Windows
144
144
  - Target apps must be relaunched with `--remote-debugging-port` flag
145
+
146
+ ## Which control surface to use
147
+
148
+ Crewly has three ways to act on a screen. Pick the **lowest** one that can do
149
+ the job — each step down costs more tokens, breaks more easily, and disturbs
150
+ the user more.
151
+
152
+ | Need | Use | Why |
153
+ |---|---|---|
154
+ | Anything a Crewly skill or connector already does (mail, Drive, Slack, calendar, git, files) | that skill | No screen at all. Fastest and cannot misclick. |
155
+ | Content of a web page, or acting as the signed-in user in Chrome | `remote-browser` | Real Chrome, real session, per-agent bound tab, and the user sees a takeover banner. |
156
+ | An Electron app (VS Code, Slack, Notion, Figma) | `desktop-app-control` | Accessibility snapshot with element refs — no coordinates. Needs the app started with a debug port. |
157
+ | Native macOS apps, system dialogs, anything the above cannot reach | `computer-use` | Last resort: screen coordinates and pixels. Slowest and most fragile. |
158
+
159
+ `computer-use` refuses destructive key combos, typing into password fields and
160
+ driving credential apps, holds a machine-wide lock while it works, and logs
161
+ every action to `~/.crewly/desktop-actions.jsonl`. Run
162
+ `{"action":"check-permissions"}` first — without Screen Recording and
163
+ Accessibility every action fails, and the refusal tells you what to grant.
@@ -176,3 +176,22 @@ bash execute.sh -a unbind-tab
176
176
  they block the extension until dismissed by hand.
177
177
  - Always `unbind-tab` when a job is done, or abandoned tabs accumulate in the
178
178
  user's window.
179
+
180
+ ## Which control surface to use
181
+
182
+ Crewly has three ways to act on a screen. Pick the **lowest** one that can do
183
+ the job — each step down costs more tokens, breaks more easily, and disturbs
184
+ the user more.
185
+
186
+ | Need | Use | Why |
187
+ |---|---|---|
188
+ | Anything a Crewly skill or connector already does (mail, Drive, Slack, calendar, git, files) | that skill | No screen at all. Fastest and cannot misclick. |
189
+ | Content of a web page, or acting as the signed-in user in Chrome | `remote-browser` | Real Chrome, real session, per-agent bound tab, and the user sees a takeover banner. |
190
+ | An Electron app (VS Code, Slack, Notion, Figma) | `desktop-app-control` | Accessibility snapshot with element refs — no coordinates. Needs the app started with a debug port. |
191
+ | Native macOS apps, system dialogs, anything the above cannot reach | `computer-use` | Last resort: screen coordinates and pixels. Slowest and most fragile. |
192
+
193
+ `computer-use` refuses destructive key combos, typing into password fields and
194
+ driving credential apps, holds a machine-wide lock while it works, and logs
195
+ every action to `~/.crewly/desktop-actions.jsonl`. Run
196
+ `{"action":"check-permissions"}` first — without Screen Recording and
197
+ Accessibility every action fails, and the refusal tells you what to grant.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Desktop control over HTTP.
3
+ *
4
+ * Phase 6 of docs/research/computer-use-capability-assessment.md. Until now
5
+ * desktop control was reachable only by a process on the machine itself: an
6
+ * agent in a shell, running the skill. Nothing at the portal, on the phone,
7
+ * or on another Mac could see or touch it, so "check what that machine is
8
+ * doing" meant walking over to it.
9
+ *
10
+ * This is the same shape `/api/browser/*` already has, deliberately. It does
11
+ * not invent a transport: the REST-over-relay path the mobile app uses
12
+ * carries these routes once they are allowlisted, so the portal and the phone
13
+ * reach a machine's desktop through machinery that already exists and is
14
+ * already authenticated.
15
+ *
16
+ * Every route shells out to the computer-use skill. That keeps the safety
17
+ * rails — permissions, stop switch, pause, desktop lock, destructive-key and
18
+ * password-field refusals, the banner, the audit log — in one place. A second
19
+ * implementation behind an HTTP route would be a second thing to keep in
20
+ * step, and it would be the one reachable from the internet.
21
+ *
22
+ * @module controllers/desktop/desktop.controller
23
+ */
24
+ import type { Request, Response } from 'express';
25
+ /**
26
+ * Actions this route will run.
27
+ *
28
+ * An allowlist rather than a pass-through: these routes are reachable from
29
+ * the relay, so the set of things the internet can ask a Mac to do should be
30
+ * written down in one visible place rather than inferred from whatever the
31
+ * skill happens to support today.
32
+ *
33
+ * `read` actions are safe to expose broadly. `act` actions move the mouse and
34
+ * keyboard, so they are gated separately below.
35
+ */
36
+ export declare const DESKTOP_READ_ACTIONS: Set<string>;
37
+ export declare const DESKTOP_ACT_ACTIONS: Set<string>;
38
+ /**
39
+ * Run the skill and return whatever JSON it printed.
40
+ *
41
+ * The skill's own refusals are passed through untouched: they already name
42
+ * the reason and what to do about it, and rewording them at this layer would
43
+ * blur the one thing a caller needs.
44
+ *
45
+ * @param input - Skill payload
46
+ * @param agentSession - Who is asking, for the audit log and the banner
47
+ * @returns The parsed reply and the HTTP status to send it with
48
+ */
49
+ export declare function runDesktopAction(input: Record<string, unknown>, agentSession?: string): Promise<{
50
+ status: number;
51
+ body: Record<string, unknown>;
52
+ }>;
53
+ /**
54
+ * Parse the skill's output, which is one JSON object possibly preceded by a
55
+ * shell warning.
56
+ *
57
+ * @param raw - Captured stdout/stderr
58
+ * @returns The object, or a described failure
59
+ */
60
+ export declare function parseSkillJson(raw: string): Record<string, unknown>;
61
+ /**
62
+ * Decide whether an action may run, and say why not.
63
+ *
64
+ * @param action - Requested action
65
+ * @param allowActing - Whether mouse/keyboard actions are permitted here
66
+ * @returns null when allowed, otherwise the refusal body
67
+ */
68
+ export declare function checkAction(action: string, allowActing: boolean): Record<string, unknown> | null;
69
+ /**
70
+ * POST /api/desktop/act — run one desktop action.
71
+ *
72
+ * @param req - Body is the skill payload, `{ action, … }`
73
+ * @param res - The skill's own JSON
74
+ */
75
+ export declare function desktopAct(req: Request, res: Response): Promise<void>;
76
+ /**
77
+ * POST /api/desktop/look — the reading half, for callers that may watch but
78
+ * not touch (the portal showing what a machine is doing, say).
79
+ *
80
+ * @param req - Body is the skill payload
81
+ * @param res - The skill's own JSON
82
+ */
83
+ export declare function desktopLook(req: Request, res: Response): Promise<void>;
84
+ /**
85
+ * GET /api/desktop/status — is desktop control usable, and is anyone using it.
86
+ *
87
+ * The one call the portal needs to show a machine's state, and the one that
88
+ * must work even when everything else is refused.
89
+ *
90
+ * @param _req - Unused
91
+ * @param res - Permissions, presence, and whether it is paused or stopped
92
+ */
93
+ export declare function desktopStatus(_req: Request, res: Response): Promise<void>;
94
+ /**
95
+ * POST /api/desktop/stop — halt everything, from anywhere.
96
+ *
97
+ * The remote twin of the ⌃⌥⌘. hotkey, so an owner who is not at the machine
98
+ * can still take the mouse back. Deliberately not gated behind the acting
99
+ * allowlist: stopping is always allowed.
100
+ *
101
+ * @param req - Body `{ resume: true }` to lift it instead
102
+ * @param res - The new state
103
+ */
104
+ export declare function desktopStop(req: Request, res: Response): Promise<void>;
105
+ //# sourceMappingURL=desktop.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop.controller.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/controllers/desktop/desktop.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAYjD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,aAE/B,CAAC;AAEH,eAAO,MAAM,mBAAmB,aAE9B,CAAC;AAQH;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAgC5D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BnE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAoBhG;AAUD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3E;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB5E"}