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.
- package/config/skills/_common/desktop-guards.sh +485 -0
- package/config/skills/_common/desktop-guards.test.sh +242 -0
- package/config/skills/_common/desktop-perceive.swift +530 -0
- package/config/skills/_common/desktop-presence.swift +343 -0
- package/config/skills/agent/_common/desktop-guards.sh +4 -0
- package/config/skills/agent/computer-use/SKILL.md +88 -0
- package/config/skills/agent/computer-use/execute.sh +249 -3
- package/config/skills/agent/desktop-app-control/SKILL.md +19 -0
- package/config/skills/agent/remote-browser/SKILL.md +19 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts +105 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.controller.js +278 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts +21 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.routes.js +31 -0
- package/dist/backend/backend/src/controllers/desktop/desktop.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +9 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +18 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +33 -6
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +60 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +18 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +38 -2
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +10 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
- package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/incomplete-turn.utils.js +4 -0
- package/dist/backend/backend/src/utils/incomplete-turn.utils.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +18 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +33 -6
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +60 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +18 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +38 -2
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +10 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
- package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
- package/dist/cli/backend/src/utils/incomplete-turn.utils.js +4 -0
- package/dist/cli/backend/src/utils/incomplete-turn.utils.js.map +1 -1
- package/package.json +1 -1
- package/packages/crewly-agent/src/eval/desktop/desktop-tasks.test.ts +96 -0
- package/packages/crewly-agent/src/eval/desktop/desktop-tasks.ts +226 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +20 -1
- package/packages/crewly-agent/src/runtime/computer.tool.test.ts +219 -0
- package/packages/crewly-agent/src/runtime/computer.tool.ts +405 -0
- package/packages/crewly-agent/src/runtime/desktop-checkpoint.test.ts +136 -0
- package/packages/crewly-agent/src/runtime/desktop-checkpoint.ts +231 -0
- package/packages/crewly-agent/src/runtime/desktop-recovery.test.ts +100 -0
- package/packages/crewly-agent/src/runtime/desktop-recovery.ts +195 -0
- package/packages/crewly-agent/src/runtime/desktop-task-runtime.test.ts +251 -0
- package/packages/crewly-agent/src/runtime/desktop-task-runtime.ts +423 -0
- package/packages/crewly-agent/src/runtime/desktop-task.tool.test.ts +218 -0
- package/packages/crewly-agent/src/runtime/desktop-task.tool.ts +343 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +17 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +54 -0
- package/packages/crewly-agent/src/runtime/types.ts +10 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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"}
|