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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Tests for the desktop safety rails.
|
|
4
|
+
#
|
|
5
|
+
# These matter more than most skill tests: every rail here stands between an
|
|
6
|
+
# agent and the owner's real keyboard. Before 2026-09-20 none of them existed
|
|
7
|
+
# — a denied TCC prompt produced a black screenshot the agent read as "the
|
|
8
|
+
# click did nothing", ⌘Q quit the user's app, `type` went into password boxes,
|
|
9
|
+
# and two agents fought over one mouse.
|
|
10
|
+
#
|
|
11
|
+
# Run: bash config/skills/_common/desktop-guards.test.sh
|
|
12
|
+
# =============================================================================
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
SKILL="${SCRIPT_DIR}/../agent/computer-use/execute.sh"
|
|
17
|
+
export CREWLY_HOME="$(mktemp -d)"
|
|
18
|
+
export CREWLY_SESSION_NAME="test-agent"
|
|
19
|
+
PASS=0; FAIL=0; SKIP=0
|
|
20
|
+
|
|
21
|
+
# A locked screen refuses everything transient, so the rails that sit behind
|
|
22
|
+
# that check cannot be exercised. Skipping is honest; asserting would either
|
|
23
|
+
# fail on a locked Mac or pass for the wrong reason.
|
|
24
|
+
SCREEN_LOCKED=$(osascript -l JavaScript -e 'ObjC.import("CoreGraphics"); ObjC.bindFunction("CGSessionCopyCurrentDictionary", ["id", []]); var d = $.CGSessionCopyCurrentDictionary(); d && ObjC.unwrap(d.objectForKey("CGSSessionScreenIsLocked")) ? "yes" : "no"' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
# assert_output <description> <json-input> <grep pattern>
|
|
27
|
+
#
|
|
28
|
+
# Captures rather than pipes. `set -o pipefail` above makes `cmd | grep` fail
|
|
29
|
+
# whenever cmd exits non-zero — which a correct refusal does — so piping
|
|
30
|
+
# reported failure even when the pattern matched. That caught me three times
|
|
31
|
+
# before this helper existed.
|
|
32
|
+
assert_output() {
|
|
33
|
+
local desc="$1" input="$2" pattern="$3" out
|
|
34
|
+
out=$(bash "$SKILL" "$input" 2>&1 || true)
|
|
35
|
+
if printf '%s' "$out" | grep -q "$pattern"; then
|
|
36
|
+
PASS=$((PASS+1)); echo " ✓ $desc"
|
|
37
|
+
else
|
|
38
|
+
FAIL=$((FAIL+1)); echo " ✗ $desc"; echo " wanted /$pattern/ in: ${out:0:160}"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# skip_if_locked <description>
|
|
43
|
+
skip_if_locked() {
|
|
44
|
+
[ "$SCREEN_LOCKED" != "yes" ] && return 1
|
|
45
|
+
SKIP=$((SKIP+1)); echo " ⊘ $1 (screen is locked)"
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# assert_reason <description> <json-input> <expected reason>
|
|
50
|
+
assert_reason() {
|
|
51
|
+
local desc="$1" input="$2" want="$3" got
|
|
52
|
+
got=$(bash "$SKILL" "$input" 2>&1 | jq -r '.reason // empty' 2>/dev/null)
|
|
53
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); echo " ✓ $desc"
|
|
54
|
+
else FAIL=$((FAIL+1)); echo " ✗ $desc"; echo " want reason=$want got=${got:-<none>}"; fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# assert_allowed <description> <json-input> — guard must not refuse it.
|
|
58
|
+
# Runs dry so the suite never actually clicks, types or opens an app on the
|
|
59
|
+
# machine it runs on (the first version of this file opened TextEdit).
|
|
60
|
+
assert_allowed() {
|
|
61
|
+
local desc="$1" input="$2" got
|
|
62
|
+
got=$(CREWLY_DESKTOP_DRY_RUN=1 bash "$SKILL" "$input" 2>&1 | jq -r '.reason // "none"' 2>/dev/null)
|
|
63
|
+
case "$got" in
|
|
64
|
+
none|permission_required) PASS=$((PASS+1)); echo " ✓ $desc" ;;
|
|
65
|
+
*) FAIL=$((FAIL+1)); echo " ✗ $desc"; echo " unexpectedly refused: $got" ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
echo "destructive key combos"
|
|
70
|
+
for combo in "command+q" "cmd+w" "Command + Shift + Delete" "COMMAND+BACKSPACE" "command+option+escape"; do
|
|
71
|
+
assert_reason "refuses $combo" "{\"action\":\"key\",\"key\":\"$combo\"}" "destructive_blocked"
|
|
72
|
+
done
|
|
73
|
+
skip_if_locked "allows command+c" || assert_allowed "allows command+c" '{"action":"key","key":"command+c"}'
|
|
74
|
+
skip_if_locked "allows command+shift+s" || assert_allowed "allows command+shift+s" '{"action":"key","key":"command+shift+s"}'
|
|
75
|
+
|
|
76
|
+
echo "credential apps"
|
|
77
|
+
for app in "Keychain Access" "1Password" "System Settings" "bitwarden"; do
|
|
78
|
+
assert_reason "refuses focus on $app" "{\"action\":\"focus\",\"app\":\"$app\"}" "app_not_allowed"
|
|
79
|
+
done
|
|
80
|
+
skip_if_locked "allows focus on TextEdit" || assert_allowed "allows focus on TextEdit" '{"action":"focus","app":"TextEdit"}'
|
|
81
|
+
|
|
82
|
+
echo "stop switch"
|
|
83
|
+
touch "$CREWLY_HOME/desktop.stop"
|
|
84
|
+
assert_reason "halts a click" '{"action":"click","x":5,"y":5}' "stopped_by_user"
|
|
85
|
+
assert_reason "halts a type" '{"action":"type","text":"x"}' "stopped_by_user"
|
|
86
|
+
rm -f "$CREWLY_HOME/desktop.stop"
|
|
87
|
+
|
|
88
|
+
echo "desktop lock"
|
|
89
|
+
printf '{"holder":"other-agent","expiresAt":%s}' "$(( $(date +%s) + 300 ))" > "$CREWLY_HOME/desktop.lock"
|
|
90
|
+
skip_if_locked "refuses while another agent holds it" || assert_reason "refuses while another agent holds it" '{"action":"click","x":5,"y":5}' "desktop_busy"
|
|
91
|
+
printf '{"holder":"dead-agent","expiresAt":%s}' "$(( $(date +%s) - 60 ))" > "$CREWLY_HOME/desktop.lock"
|
|
92
|
+
skip_if_locked "ignores an expired lock" || assert_allowed "ignores an expired lock" '{"action":"list-apps"}'
|
|
93
|
+
rm -f "$CREWLY_HOME/desktop.lock"
|
|
94
|
+
|
|
95
|
+
echo "dry run"
|
|
96
|
+
if skip_if_locked "reports what would run without doing it"; then :
|
|
97
|
+
elif CREWLY_DESKTOP_DRY_RUN=1 bash "$SKILL" '{"action":"click","x":5,"y":5}' 2>&1 | jq -e '.dryRun == true and .wouldRun == true' >/dev/null 2>&1; then
|
|
98
|
+
PASS=$((PASS+1)); echo " ✓ reports what would run without doing it"
|
|
99
|
+
else
|
|
100
|
+
FAIL=$((FAIL+1)); echo " ✗ dry run did not report cleanly"
|
|
101
|
+
fi
|
|
102
|
+
# A refusal must still win over a dry run, or the rails could be inspected away.
|
|
103
|
+
assert_reason "a refusal still wins in dry run" '{"action":"key","key":"command+q"}' "destructive_blocked"
|
|
104
|
+
|
|
105
|
+
echo "perception (Phase 2)"
|
|
106
|
+
# `displays` reads no window and touches nothing, so it must work even when
|
|
107
|
+
# everything else is refused — including on a locked screen.
|
|
108
|
+
if bash "$SKILL" '{"action":"displays"}' 2>&1 | jq -e '.success and (.displays | length > 0)' >/dev/null 2>&1; then
|
|
109
|
+
PASS=$((PASS+1)); echo " ✓ displays works with no permission or lock needed"
|
|
110
|
+
else
|
|
111
|
+
FAIL=$((FAIL+1)); echo " ✗ displays did not report any screen"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Looking must not take the desktop lock: an agent waiting its turn still
|
|
115
|
+
# needs to see, and two readers cannot corrupt each other.
|
|
116
|
+
printf '{"holder":"other-agent","expiresAt":%s}' "$(( $(date +%s) + 300 ))" > "$CREWLY_HOME/desktop.lock"
|
|
117
|
+
for act in snapshot ocr resolve; do
|
|
118
|
+
got=$(bash "$SKILL" "{\"action\":\"$act\",\"ref\":\"@e1\"}" 2>&1 | jq -r '.reason // "none"' 2>/dev/null)
|
|
119
|
+
# Whatever else it answers, it must not have stopped at the other agent's lock.
|
|
120
|
+
if [ "$got" != "desktop_busy" ]; then PASS=$((PASS+1)); echo " ✓ $act does not wait on the desktop lock"
|
|
121
|
+
else FAIL=$((FAIL+1)); echo " ✗ $act was blocked by another agent's lock"; fi
|
|
122
|
+
done
|
|
123
|
+
rm -f "$CREWLY_HOME/desktop.lock"
|
|
124
|
+
|
|
125
|
+
# Changing things must take it.
|
|
126
|
+
printf '{"holder":"other-agent","expiresAt":%s}' "$(( $(date +%s) + 300 ))" > "$CREWLY_HOME/desktop.lock"
|
|
127
|
+
skip_if_locked "click-ref waits on the lock" || assert_reason "click-ref waits on the lock" '{"action":"click-ref","ref":"@e1"}' "desktop_busy"
|
|
128
|
+
skip_if_locked "fill-ref waits on the lock" || assert_reason "fill-ref waits on the lock" '{"action":"fill-ref","ref":"@e1","text":"x"}' "desktop_busy"
|
|
129
|
+
rm -f "$CREWLY_HOME/desktop.lock"
|
|
130
|
+
|
|
131
|
+
# A locked screen makes accessibility answer with placeholder data rather
|
|
132
|
+
# than failing, so reading it would look plausible and be wrong.
|
|
133
|
+
if [ "$(bash "$SKILL" '{"action":"check-permissions"}' 2>&1 | jq -r '.ready')" = "true" ]; then
|
|
134
|
+
locked=$(osascript -l JavaScript -e 'ObjC.import("CoreGraphics"); ObjC.bindFunction("CGSessionCopyCurrentDictionary", ["id", []]); var d = $.CGSessionCopyCurrentDictionary(); d && ObjC.unwrap(d.objectForKey("CGSSessionScreenIsLocked")) ? "yes" : "no"' 2>/dev/null)
|
|
135
|
+
got=$(bash "$SKILL" '{"action":"snapshot"}' 2>&1 | jq -r '.reason // "none"' 2>/dev/null)
|
|
136
|
+
if [ "$locked" = "yes" ]; then
|
|
137
|
+
if [ "$got" = "screen_locked" ]; then PASS=$((PASS+1)); echo " ✓ refuses to read a locked screen"
|
|
138
|
+
else FAIL=$((FAIL+1)); echo " ✗ read a locked screen anyway (got: $got)"; fi
|
|
139
|
+
else
|
|
140
|
+
if [ "$got" = "none" ]; then PASS=$((PASS+1)); echo " ✓ snapshot runs on an unlocked screen"
|
|
141
|
+
else FAIL=$((FAIL+1)); echo " ✗ snapshot failed on an unlocked screen: $got"; fi
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# wait-for must reject a call that says nothing to wait for, rather than
|
|
146
|
+
# blocking for the whole timeout.
|
|
147
|
+
# Captured first, not piped: `set -o pipefail` above makes a pipeline fail
|
|
148
|
+
# when *any* stage does, and this command correctly exits non-zero — so
|
|
149
|
+
# `cmd | grep -q` reported failure even when grep matched.
|
|
150
|
+
skip_if_locked "wait-for demands a condition" || assert_output "wait-for demands a condition" '{"action":"wait-for"}' "one of"
|
|
151
|
+
|
|
152
|
+
echo "takeover (Phase 5)"
|
|
153
|
+
touch "$CREWLY_HOME/desktop.pause"
|
|
154
|
+
assert_reason "pausing holds a click" '{"action":"click","x":5,"y":5}' "paused"
|
|
155
|
+
assert_reason "pausing holds a type" '{"action":"type","text":"x"}' "paused"
|
|
156
|
+
# A pause is the owner using the machine, not a cancellation — the agent has
|
|
157
|
+
# to be told it can come back.
|
|
158
|
+
# Captured, not piped — see the pipefail note above; the skill exits
|
|
159
|
+
# non-zero on a refusal and would fail the pipeline whatever jq said.
|
|
160
|
+
PAUSE_OUT=$(bash "$SKILL" '{"action":"click","x":5,"y":5}' 2>&1 || true)
|
|
161
|
+
if printf '%s' "$PAUSE_OUT" | jq -e '.recoverable == true' >/dev/null 2>&1; then
|
|
162
|
+
PASS=$((PASS+1)); echo " ✓ a pause says the task is not cancelled"
|
|
163
|
+
else
|
|
164
|
+
FAIL=$((FAIL+1)); echo " ✗ a pause did not mark itself recoverable"
|
|
165
|
+
fi
|
|
166
|
+
# Permanent refusals still outrank it, or an agent would wait for a resume
|
|
167
|
+
# that will never let ⌘Q through.
|
|
168
|
+
assert_reason "a refusal still wins over a pause" '{"action":"key","key":"command+q"}' "destructive_blocked"
|
|
169
|
+
rm -f "$CREWLY_HOME/desktop.pause"
|
|
170
|
+
# Looking is not taking the machine over, so it must not be paused either.
|
|
171
|
+
skip_if_locked "reading is allowed while paused" || {
|
|
172
|
+
touch "$CREWLY_HOME/desktop.pause"
|
|
173
|
+
got=$(bash "$SKILL" '{"action":"displays"}' 2>&1 | jq -r '.reason // "none"' 2>/dev/null)
|
|
174
|
+
if [ "$got" = "none" ]; then PASS=$((PASS+1)); echo " ✓ displays still works while paused"
|
|
175
|
+
else FAIL=$((FAIL+1)); echo " ✗ displays was blocked by a pause ($got)"; fi
|
|
176
|
+
rm -f "$CREWLY_HOME/desktop.pause"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# A banner the owner learns to ignore is worse than none, so it must not
|
|
180
|
+
# appear for a dry run (which moves nothing) or for merely looking.
|
|
181
|
+
pkill -f "desktop-presence begin" 2>/dev/null || true
|
|
182
|
+
CREWLY_DESKTOP_DRY_RUN=1 bash "$SKILL" '{"action":"click","x":5,"y":5}' >/dev/null 2>&1 || true
|
|
183
|
+
bash "$SKILL" '{"action":"displays"}' >/dev/null 2>&1 || true
|
|
184
|
+
sleep 0.3
|
|
185
|
+
if pgrep -f "desktop-presence begin" >/dev/null 2>&1; then
|
|
186
|
+
FAIL=$((FAIL+1)); echo " ✗ the banner appeared for a dry run or a read"
|
|
187
|
+
pkill -f "desktop-presence begin" 2>/dev/null || true
|
|
188
|
+
else
|
|
189
|
+
PASS=$((PASS+1)); echo " ✓ no banner for a dry run or for looking"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
echo "reach (Phase 6)"
|
|
193
|
+
# Asking for a person is how an agent gets out of being stuck, so it has to
|
|
194
|
+
# work while paused — otherwise the only move left is retrying the thing it
|
|
195
|
+
# already cannot do.
|
|
196
|
+
touch "$CREWLY_HOME/desktop.pause"
|
|
197
|
+
HUMAN_OUT=$(CREWLY_DESKTOP_NO_BANNER=1 bash "$SKILL" '{"action":"request-human","reason":"needs a 2FA code"}' 2>&1 || true)
|
|
198
|
+
if printf '%s' "$HUMAN_OUT" | jq -e '.waiting == true' >/dev/null 2>&1; then
|
|
199
|
+
PASS=$((PASS+1)); echo " ✓ request-human works even while paused"
|
|
200
|
+
else
|
|
201
|
+
FAIL=$((FAIL+1)); echo " ✗ request-human was refused while paused"
|
|
202
|
+
fi
|
|
203
|
+
# And it leaves the desktop paused, not stopped: the task is waiting, not dead.
|
|
204
|
+
if [ -f "$CREWLY_HOME/desktop.pause" ] && [ ! -f "$CREWLY_HOME/desktop.stop" ]; then
|
|
205
|
+
PASS=$((PASS+1)); echo " ✓ handing over pauses rather than cancels"
|
|
206
|
+
else
|
|
207
|
+
FAIL=$((FAIL+1)); echo " ✗ handing over left the wrong state"
|
|
208
|
+
fi
|
|
209
|
+
rm -f "$CREWLY_HOME/desktop.pause"
|
|
210
|
+
# It must demand a reason — "a person is needed" with no reason is useless
|
|
211
|
+
# to whoever gets the notification.
|
|
212
|
+
assert_output "request-human demands a reason" '{"action":"request-human"}' "reason"
|
|
213
|
+
|
|
214
|
+
echo "permissions report"
|
|
215
|
+
if bash "$SKILL" '{"action":"check-permissions"}' 2>&1 | jq -e 'has("screenRecording") and has("accessibility") and has("askingProcess")' >/dev/null 2>&1; then
|
|
216
|
+
PASS=$((PASS+1)); echo " ✓ check-permissions reports both grants and who to grant them to"
|
|
217
|
+
else
|
|
218
|
+
FAIL=$((FAIL+1)); echo " ✗ check-permissions did not report the expected fields"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
echo "audit trail"
|
|
222
|
+
if [ -s "$CREWLY_HOME/desktop-actions.jsonl" ] \
|
|
223
|
+
&& jq -e 'has("at") and has("session") and has("action")' < <(head -1 "$CREWLY_HOME/desktop-actions.jsonl") >/dev/null 2>&1; then
|
|
224
|
+
PASS=$((PASS+1)); echo " ✓ every action is logged with time, session and action"
|
|
225
|
+
else
|
|
226
|
+
FAIL=$((FAIL+1)); echo " ✗ action log missing or malformed"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
echo "bash 3.2 compatibility"
|
|
230
|
+
# macOS ships bash 3.2, which has no negative array indices. Using one made
|
|
231
|
+
# every modifier combo fail with "bad array subscript".
|
|
232
|
+
if bash -c 'IFS="+" read -ra p <<< "command+shift+s"; echo "${p[$(( ${#p[@]} - 1 ))]}"' 2>/dev/null | grep -q '^s$'; then
|
|
233
|
+
PASS=$((PASS+1)); echo " ✓ key parsing uses a 3.2-safe last-element index"
|
|
234
|
+
else
|
|
235
|
+
FAIL=$((FAIL+1)); echo " ✗ key parsing is not bash 3.2 safe"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
rm -rf "$CREWLY_HOME"
|
|
239
|
+
echo
|
|
240
|
+
echo "=== Results: $PASS passed, $FAIL failed, $SKIP skipped ==="
|
|
241
|
+
[ "$SKIP" -gt 0 ] && echo " (unlock the screen and re-run to cover the skipped rails)"
|
|
242
|
+
[ "$FAIL" -eq 0 ]
|