crewly 1.20.280 → 1.20.282

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/hooks/live-checkout-guard/guard.mjs +452 -0
  2. package/config/skills/agent/core/receive-file/SKILL.md +53 -0
  3. package/config/skills/agent/core/receive-file/execute.sh +125 -0
  4. package/config/skills/agent/core/receive-file/execute.test.sh +80 -0
  5. package/config/skills/agent/core/send-file/SKILL.md +62 -0
  6. package/config/skills/agent/core/send-file/execute.sh +145 -0
  7. package/config/skills/agent/core/send-file/execute.test.sh +81 -0
  8. package/dist/backend/backend/src/constants.d.ts +30 -1
  9. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  10. package/dist/backend/backend/src/constants.js +38 -0
  11. package/dist/backend/backend/src/constants.js.map +1 -1
  12. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +12 -0
  13. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  14. package/dist/backend/backend/src/controllers/apps/apps.controller.js +41 -0
  15. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  16. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +4 -0
  17. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  18. package/dist/backend/backend/src/controllers/apps/apps.routes.js +12 -1
  19. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  20. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts +5 -0
  21. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +42 -1
  23. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts +72 -0
  25. package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts.map +1 -0
  26. package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js +113 -0
  27. package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js.map +1 -0
  28. package/dist/backend/backend/src/index.d.ts.map +1 -1
  29. package/dist/backend/backend/src/index.js +25 -1
  30. package/dist/backend/backend/src/index.js.map +1 -1
  31. package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
  32. package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/agent/control-plane-guard.service.js +11 -2
  34. package/dist/backend/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
  35. package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts +3 -2
  36. package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/agent/credential-guard.service.js +12 -4
  38. package/dist/backend/backend/src/services/agent/credential-guard.service.js.map +1 -1
  39. package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
  40. package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
  41. package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js +109 -0
  42. package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
  43. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
  45. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  46. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  48. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  49. package/dist/backend/backend/src/utils/boot-owner-alert.d.ts +33 -0
  50. package/dist/backend/backend/src/utils/boot-owner-alert.d.ts.map +1 -0
  51. package/dist/backend/backend/src/utils/boot-owner-alert.js +60 -0
  52. package/dist/backend/backend/src/utils/boot-owner-alert.js.map +1 -0
  53. package/dist/backend/backend/src/utils/build-provenance.d.ts +38 -0
  54. package/dist/backend/backend/src/utils/build-provenance.d.ts.map +1 -1
  55. package/dist/backend/backend/src/utils/build-provenance.js +63 -1
  56. package/dist/backend/backend/src/utils/build-provenance.js.map +1 -1
  57. package/dist/backend/build-info.json +2 -2
  58. package/dist/cli/backend/src/constants.d.ts +30 -1
  59. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  60. package/dist/cli/backend/src/constants.js +38 -0
  61. package/dist/cli/backend/src/constants.js.map +1 -1
  62. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
  63. package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
  64. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +11 -2
  65. package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
  66. package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts +3 -2
  67. package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
  68. package/dist/cli/backend/src/services/agent/credential-guard.service.js +12 -4
  69. package/dist/cli/backend/src/services/agent/credential-guard.service.js.map +1 -1
  70. package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
  71. package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
  72. package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js +109 -0
  73. package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
  74. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  75. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
  76. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  77. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  78. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  79. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  80. package/package.json +1 -1
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # Tests for receive-file — run with: bash execute.test.sh
3
+ # A Python stub plays both the Crewly backend and the object storage.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() { if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi; }
9
+
10
+ PORT=18862
11
+ STUB_LOG="$(mktemp)"; export STUB_LOG STUB_PORT="$PORT"
12
+ python3 -u <<'PY' >/dev/null 2>&1 &
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ import json, os
15
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
16
+ OK = 'hf_AbCdEfGhIjKlMnOpQrStUv'; SHORT = 'hf_ShortShortShortShortSh'; PEND = 'hf_PendPendPendPendPendPe'
17
+ DATA = b'x' * 2000000
18
+ def note(**kw):
19
+ with open(LOG, 'a') as f: f.write(json.dumps(kw) + '\n')
20
+ class H(BaseHTTPRequestHandler):
21
+ def send(self, code, obj):
22
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(obj).encode())
23
+ def do_GET(self):
24
+ if self.path.startswith('/store/'):
25
+ data = DATA[:1000] if 'short' in self.path else DATA
26
+ note(op='download', range=self.headers.get('Range'))
27
+ self.send_response(200); self.send_header('Content-Length', str(len(data))); self.end_headers(); self.wfile.write(data); return
28
+ hid = self.path.rsplit('/', 1)[-1]
29
+ if hid == PEND:
30
+ return self.send(200, {'success': True, 'data': {'handoffId': hid, 'status': 'pending', 'sizeBytes': 5, 'fileName': 'a'}})
31
+ if hid in (OK, SHORT):
32
+ return self.send(200, {'success': True, 'data': {'handoffId': hid, 'status': 'ready', 'sizeBytes': len(DATA), 'fileName': '../../evil name.mp4', 'note': 'today',
33
+ 'download': {'url': f'http://127.0.0.1:{PORT}/store/' + ('short' if hid == SHORT else 'ok'), 'expiresAt': 'x'}}})
34
+ self.send(404, {'success': False, 'error': 'not_found', 'message': 'This handoff is gone.'})
35
+ def do_POST(self):
36
+ note(op='ack' if self.path.endswith('/ack') else 'other', path=self.path, session=self.headers.get('X-Agent-Session')); self.send(200, {'success': True, 'data': {'deleted': True}})
37
+ def log_message(self, *a, **k): pass
38
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
39
+ PY
40
+ STUB_PID=$!
41
+ disown "$STUB_PID" 2>/dev/null || true
42
+ WORK="$(cd "$(mktemp -d)" && pwd -P)"
43
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$WORK"' EXIT
44
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
45
+ mkdir -p "$WORK/home/.crewly" "$WORK/proj"
46
+ ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-mia HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" CREWLY_PROJECT_PATH="$WORK/proj")
47
+ run() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null; }
48
+ run_err() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
49
+
50
+ OK=hf_AbCdEfGhIjKlMnOpQrStUv
51
+ OUT=$(run --id $OK)
52
+ DEST="$WORK/proj/received/evil name.mp4"
53
+ check "result" "$(printf '%s' "$OUT" | jq -c '{success, fileName, sizeBytes, acked, note}')" '{"success":true,"fileName":"evil name.mp4","sizeBytes":2000000,"acked":true,"note":"today"}'
54
+ check "saved in the default folder under a safe name" "$(wc -c < "$DEST" | tr -d ' ')" "2000000"
55
+ check "ack sent as this agent" "$(grep '"op": "ack"' "$STUB_LOG" | jq -c '{path, session}')" '{"path":"/api/apps/handoffs/hf_AbCdEfGhIjKlMnOpQrStUv/ack","session":"dev-mia"}'
56
+ check "no .part left" "$(ls -a "$WORK/proj/received" | grep -c part)" "0"
57
+
58
+ OUT=$(run --id $OK --dir "$WORK/proj/inbox")
59
+ check "second copy goes to --dir" "$(printf '%s' "$OUT" | jq -r .path)" "$WORK/proj/inbox/evil name.mp4"
60
+ run --id $OK >/dev/null
61
+ check "never overwrites" "$(ls "$WORK/proj/received" | sort | tr '\n' '|')" "evil name-1.mp4|evil name.mp4|"
62
+
63
+ : > "$STUB_LOG"
64
+ OUT=$(run --id $OK --no-ack --dir "$WORK/proj/noack")
65
+ check "--no-ack keeps the server copy" "$(printf '%s' "$OUT" | jq -c '{acked, warning: (.warning != null)}')" '{"acked":false,"warning":true}'
66
+ check "no ack request" "$(grep -c '"op": "ack"' "$STUB_LOG")" "0"
67
+
68
+ : > "$STUB_LOG"
69
+ OUT=$(run --id hf_ShortShortShortShortSh --dir "$WORK/proj/short"; true)
70
+ check "size mismatch fails, removes the partial and does not ack" "$(printf '%s' "$OUT" | jq -r .reason)|$(ls -A "$WORK/proj/short" | wc -l | tr -d ' ')|$(grep -c '"op": "ack"' "$STUB_LOG")" "size_mismatch|0|0"
71
+
72
+ OUT=$(run --id hf_PendPendPendPendPendPe; true)
73
+ check "pending handoff is not ready" "$(printf '%s' "$OUT" | jq -r .reason)" "not_ready"
74
+ OUT=$(run --id hf_GoneGoneGoneGoneGoneGo; true)
75
+ check "unknown handoff passes Cloud's reason" "$(printf '%s' "$OUT" | jq -r .reason)" "not_found"
76
+ check "bad id refused" "$(run_err --id nope | grep -c 'handoff id')" "1"
77
+ check "dir inside Crewly home refused" "$(run_err --id $OK --dir "$WORK/home/.crewly/x" | grep -c 'Crewly home')" "1"
78
+
79
+ echo "receive-file: $PASS passed, $FAIL failed"
80
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: Send File
3
+ description: Hand a LARGE file (video, archive, dataset, up to 2 GB) to another agent of the same Crewly account, on this or another machine, through a temporary Crewly Cloud relay. Prefer this over Drive or Slack for anything bigger than a few MB. The receiver deletes it with receive-file; unclaimed files are deleted after 24 hours.
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - send file
11
+ - send a large file
12
+ - hand off a file
13
+ - pass the video to
14
+ - transfer file to agent
15
+ - share a big file
16
+ tags:
17
+ - files
18
+ - handoff
19
+ - transfer
20
+ execution:
21
+ type: script
22
+ script:
23
+ file: execute.sh
24
+ interpreter: bash
25
+ timeoutMs: 3600000
26
+ ---
27
+
28
+ # Send File
29
+
30
+ Passes one big file to another agent of **your own Crewly account**
31
+ (same machine or another one) without Slack or Drive. Needs the machine to be
32
+ signed in to Crewly Cloud. The file is streamed from disk (never loaded into
33
+ memory), kept on a temporary server, and deleted as soon as the receiver
34
+ acknowledges it, or after 24 hours.
35
+
36
+ ```bash
37
+ bash execute.sh --path ./out/daily-2026-10-09.mp4 --note "today's cut, 300 MB" --to Mia
38
+ ```
39
+
40
+ Prints:
41
+
42
+ ```json
43
+ {"success":true,"handoffId":"hf_…","fileName":"daily-2026-10-09.mp4","sizeBytes":314572800,"expiresAt":"…",
44
+ "instruction":"Receive it with the receive-file skill: bash <skills>/core/receive-file/execute.sh --id hf_… [--dir <folder>]"}
45
+ ```
46
+
47
+ **Send the `instruction` line to the receiving agent** (use your normal
48
+ agent-to-agent message), and tell it the handoff id. Do not post the id anywhere
49
+ public.
50
+
51
+ Options:
52
+
53
+ - `--path` (required): a regular file inside your project directory (not a
54
+ symlink, not inside Crewly's home). Max 2 GB; the account may hold 5 GB of
55
+ waiting handoffs at once, so wait for receivers to finish before sending more.
56
+ - `--name`: the name the receiver sees (default: the file's name).
57
+ - `--note`: one line for the receiver. `--to`: who it is for (informational).
58
+ - `--content-type`: default from the extension.
59
+
60
+ Not for secrets or credentials. Not for files under ~5 MB that can go in a
61
+ message. Cancel a handoff you no longer want received with
62
+ `bash execute.sh --cancel hf_…`.
@@ -0,0 +1,145 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # send-file — hand a large file to another agent of the same Crewly account
4
+ # through the Cloud file-handoff relay (crewly-services apps/SPEC.md section 18).
5
+ #
6
+ # The backend (POST /api/apps/handoffs) asks Cloud for a presigned upload URL;
7
+ # this script then streams the file straight to object storage with curl -T
8
+ # (from disk, never buffered), and confirms with /complete.
9
+ #
10
+ # Usage:
11
+ # bash execute.sh --path <file> [--name <name>] [--note <text>] [--to <agent>] [--content-type <type>]
12
+ # bash execute.sh --cancel <handoffId>
13
+ # =============================================================================
14
+ set -euo pipefail
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
17
+
18
+ print_usage() {
19
+ cat <<'EOF_USAGE'
20
+ Usage:
21
+ bash execute.sh --path <file> [--name <name>] [--note <text>] [--to <agent>] [--content-type <type>]
22
+ bash execute.sh --cancel <handoffId>
23
+
24
+ Options:
25
+ --path Regular file inside your project directory, up to 2 GB (required)
26
+ --name Name the receiver sees (default: the file's name)
27
+ --note One line for the receiver
28
+ --to Who it is for (informational; any agent of the account can receive it)
29
+ --content-type MIME type (default: from the extension)
30
+ --cancel Withdraw a handoff you made (deletes the uploaded file)
31
+ --help | -h Show this help
32
+ EOF_USAGE
33
+ }
34
+
35
+ fail_from() {
36
+ printf '%s' "$1" | jq -c '{success: false, status: (.status // 0), reason: (.details.error // .error // "unknown"), message: (.details.message // .details // "")}' 2>/dev/null \
37
+ || jq -cn --arg r "$1" '{success: false, reason: $r}'
38
+ exit 1
39
+ }
40
+
41
+ call() {
42
+ local out
43
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
44
+ printf '%s\n' "$out" | tail -n 1
45
+ }
46
+
47
+ content_type_for() {
48
+ case "$(printf '%s' "${1##*.}" | tr '[:upper:]' '[:lower:]')" in
49
+ mp4|m4v) echo video/mp4 ;;
50
+ mov) echo video/quicktime ;;
51
+ webm) echo video/webm ;;
52
+ mkv) echo video/x-matroska ;;
53
+ mp3) echo audio/mpeg ;;
54
+ wav) echo audio/wav ;;
55
+ m4a) echo audio/mp4 ;;
56
+ png) echo image/png ;;
57
+ jpg|jpeg) echo image/jpeg ;;
58
+ pdf) echo application/pdf ;;
59
+ zip) echo application/zip ;;
60
+ json) echo application/json ;;
61
+ csv) echo text/csv ;;
62
+ txt|md|log) echo text/plain ;;
63
+ *) echo application/octet-stream ;;
64
+ esac
65
+ }
66
+
67
+ # Regular, non-symlink file inside the project directory, never under Crewly's
68
+ # home. Prints the real path, then the size in bytes.
69
+ check_file() {
70
+ command -v node >/dev/null 2>&1 || error_exit "node is required"
71
+ CF_PATH="$1" CF_PROJECT="${CREWLY_PROJECT_PATH:-$PWD}" node <<'NODE'
72
+ const fs = require('fs'); const path = require('path'); const os = require('os');
73
+ const fail = (m) => { process.stderr.write(JSON.stringify({ error: m }) + '\n'); process.exit(1); };
74
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
75
+ const within = (c, p) => c === p || c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
76
+ const given = process.env.CF_PATH;
77
+ const abs = path.resolve(given);
78
+ let st;
79
+ try { st = fs.lstatSync(abs); } catch { fail(`file not found: ${given}`); }
80
+ if (st.isSymbolicLink()) fail(`${given} is a symbolic link; pass the real file`);
81
+ if (!st.isFile()) fail(`${given} is not a regular file`);
82
+ if (st.size === 0) fail(`${given} is empty`);
83
+ const r = real(abs); const project = real(process.env.CF_PROJECT || process.cwd());
84
+ if (!project) fail('cannot resolve the project directory');
85
+ const homes = [path.join(os.homedir(), '.crewly'), process.env.CREWLY_HOME].filter(Boolean).map((p) => real(p) || path.resolve(p));
86
+ if (homes.some((h) => within(r, h))) fail(`${given} is inside Crewly's home directory`);
87
+ if (!within(r, project)) fail(`${given} is outside your project directory (${project}); copy it there first`);
88
+ process.stdout.write(r + '\n' + st.size + '\n');
89
+ NODE
90
+ }
91
+
92
+ FILE=""; NAME=""; NOTE=""; TO=""; CTYPE=""; CANCEL=""
93
+ while [[ $# -gt 0 ]]; do
94
+ case "$1" in
95
+ --path) [ $# -ge 2 ] || error_exit "--path requires a value"; FILE="$2"; shift 2 ;;
96
+ --name) [ $# -ge 2 ] || error_exit "--name requires a value"; NAME="$2"; shift 2 ;;
97
+ --note) [ $# -ge 2 ] || error_exit "--note requires a value"; NOTE="$2"; shift 2 ;;
98
+ --to) [ $# -ge 2 ] || error_exit "--to requires a value"; TO="$2"; shift 2 ;;
99
+ --content-type) [ $# -ge 2 ] || error_exit "--content-type requires a value"; CTYPE="$2"; shift 2 ;;
100
+ --cancel) [ $# -ge 2 ] || error_exit "--cancel requires a handoff id"; CANCEL="$2"; shift 2 ;;
101
+ --help|-h) print_usage; exit 0 ;;
102
+ *) error_exit "Unknown option: $1" ;;
103
+ esac
104
+ done
105
+
106
+ if [ -n "$CANCEL" ]; then
107
+ [[ "$CANCEL" =~ ^hf_[A-Za-z0-9_-]{22}$ ]] || error_exit "--cancel needs a handoff id like hf_…"
108
+ RESULT=$(call DELETE "/apps/handoffs/${CANCEL}") || { printf '%s\n' "$RESULT"; exit 1; }
109
+ jq -cn --arg id "$CANCEL" '{success: true, handoffId: $id, cancelled: true}'
110
+ exit 0
111
+ fi
112
+
113
+ [ -n "$FILE" ] || error_exit "--path is required"
114
+ INFO=$(check_file "$FILE") || { printf '%s\n' "$INFO" >&2; exit 1; }
115
+ REAL=$(printf '%s\n' "$INFO" | sed -n 1p)
116
+ SIZE=$(printf '%s\n' "$INFO" | sed -n 2p)
117
+ [ -n "$NAME" ] || NAME="$(basename "$REAL")"
118
+ [ -n "$CTYPE" ] || CTYPE="$(content_type_for "$REAL")"
119
+
120
+ BODY=$(jq -cn --arg n "$NAME" --argjson s "$SIZE" --arg c "$CTYPE" --arg note "$NOTE" --arg to "$TO" \
121
+ '{fileName: $n, sizeBytes: $s, contentType: $c} + (if $note != "" then {note: $note} else {} end) + (if $to != "" then {to: $to} else {} end)')
122
+ CREATED=$(call POST "/apps/handoffs" "$BODY") || { printf '%s\n' "$CREATED"; exit 1; }
123
+ ID=$(printf '%s' "$CREATED" | jq -r '.data.handoffId // empty')
124
+ URL=$(printf '%s' "$CREATED" | jq -r '.data.upload.url // empty')
125
+ [ -n "$ID" ] && [ -n "$URL" ] || error_exit "Cloud answered without an upload URL"
126
+
127
+ # Headers the presigned URL was signed with; the file streams from disk (-T).
128
+ HDRS=()
129
+ while IFS= read -r line; do HDRS+=(-H "$line"); done < <(printf '%s' "$CREATED" | jq -r '.data.upload.headers // {} | to_entries[] | "\(.key): \(.value)"')
130
+
131
+ cancel_quietly() { api_call DELETE "/apps/handoffs/${ID}" >/dev/null 2>&1 || true; }
132
+
133
+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 30 --retry 3 --retry-delay 3 -X PUT -T "$REAL" ${HDRS[@]+"${HDRS[@]}"} "$URL" 2>/dev/null) || CODE="000"
134
+ if [ "$CODE" != "200" ]; then
135
+ cancel_quietly
136
+ jq -cn --arg c "$CODE" '{success: false, reason: "upload_failed", message: ("The upload to temporary storage failed (HTTP " + $c + "). Nothing was kept; try again.")}'
137
+ exit 1
138
+ fi
139
+
140
+ DONE=$(api_call POST "/apps/handoffs/${ID}/complete" 2>&1) || { cancel_quietly; fail_from "$(printf '%s\n' "$DONE" | tail -n 1)"; }
141
+ EXPIRES=$(printf '%s\n' "$DONE" | tail -n 1 | jq -r '.data.expiresAt // empty')
142
+
143
+ jq -cn --arg id "$ID" --arg n "$NAME" --argjson s "$SIZE" --arg e "$EXPIRES" \
144
+ '{success: true, handoffId: $id, fileName: $n, sizeBytes: $s, expiresAt: $e,
145
+ instruction: ("Receive it with the receive-file skill: bash <skills>/core/receive-file/execute.sh --id " + $id + " [--dir <folder>]")}'
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Tests for send-file — run with: bash execute.test.sh
3
+ # A Python stub plays both the Crewly backend and the object storage.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() { if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi; }
9
+
10
+ PORT=18861
11
+ STUB_LOG="$(mktemp)"; STORE="$(mktemp)"; export STUB_LOG STORE STUB_PORT="$PORT"
12
+ python3 -u <<'PY' >/dev/null 2>&1 &
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ import json, os
15
+ LOG = os.environ['STUB_LOG']; STORE = os.environ['STORE']; PORT = int(os.environ['STUB_PORT'])
16
+ ID = 'hf_AbCdEfGhIjKlMnOpQrStUv'
17
+ def note(**kw):
18
+ with open(LOG, 'a') as f: f.write(json.dumps(kw) + '\n')
19
+ class H(BaseHTTPRequestHandler):
20
+ def send(self, code, obj):
21
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(obj).encode())
22
+ def body(self):
23
+ n = int(self.headers.get('Content-Length') or 0); return self.rfile.read(n)
24
+ def do_POST(self):
25
+ b = self.body()
26
+ if self.path == '/api/apps/handoffs':
27
+ req = json.loads(b); note(op='create', req=req, session=self.headers.get('X-Agent-Session'))
28
+ if req['fileName'] == 'refuse.bin':
29
+ return self.send(402, {'success': False, 'error': 'quota_exceeded', 'message': 'Too many bytes waiting.'})
30
+ return self.send(201, {'success': True, 'data': {'handoffId': ID, 'status': 'pending', 'upload': {'url': f'http://127.0.0.1:{PORT}/store/obj', 'method': 'PUT', 'headers': {'Content-Type': req['contentType'], 'x-amz-server-side-encryption': 'AES256'}}}})
31
+ if self.path == f'/api/apps/handoffs/{ID}/complete':
32
+ note(op='complete'); return self.send(200, {'success': True, 'data': {'handoffId': ID, 'status': 'ready', 'expiresAt': '2026-10-10T00:00:00.000Z'}})
33
+ self.send(404, {'success': False})
34
+ def do_DELETE(self):
35
+ note(op='cancel', path=self.path); self.send(200, {'success': True, 'data': {'deleted': True}})
36
+ def do_PUT(self):
37
+ b = self.body()
38
+ note(op='put', bytes=len(b), ctype=self.headers.get('Content-Type'), sse=self.headers.get('x-amz-server-side-encryption'))
39
+ self.send_response(500 if os.path.exists(STORE + '.fail') else 200); self.end_headers()
40
+ def log_message(self, *a, **k): pass
41
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
42
+ PY
43
+ STUB_PID=$!
44
+ disown "$STUB_PID" 2>/dev/null || true
45
+ WORK="$(mktemp -d)"
46
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$STORE" "$STORE.fail" "$WORK"' EXIT
47
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
48
+ mkdir -p "$WORK/home/.crewly" "$WORK/proj"
49
+ ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-ella HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" CREWLY_PROJECT_PATH="$WORK/proj")
50
+ run() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null; }
51
+ run_err() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
52
+
53
+ head -c 3000000 /dev/zero > "$WORK/proj/daily.mp4"
54
+ OUT=$(run --path "$WORK/proj/daily.mp4" --note "today" --to Mia)
55
+ check "result" "$(printf '%s' "$OUT" | jq -c '{success, handoffId, fileName, sizeBytes, expiresAt}')" '{"success":true,"handoffId":"hf_AbCdEfGhIjKlMnOpQrStUv","fileName":"daily.mp4","sizeBytes":3000000,"expiresAt":"2026-10-10T00:00:00.000Z"}'
56
+ check "instruction names the receiver skill" "$(printf '%s' "$OUT" | jq -r '.instruction' | grep -c 'receive-file/execute.sh --id hf_AbCdEfGhIjKlMnOpQrStUv')" "1"
57
+ check "create request" "$(sed -n 1p "$STUB_LOG" | jq -c '{req, session}')" '{"req":{"fileName":"daily.mp4","sizeBytes":3000000,"contentType":"video/mp4","note":"today","to":"Mia"},"session":"dev-ella"}'
58
+ check "upload streamed with signed headers" "$(sed -n 2p "$STUB_LOG" | jq -c '{bytes, ctype, sse}')" '{"bytes":3000000,"ctype":"video/mp4","sse":"AES256"}'
59
+ check "complete called" "$(sed -n 3p "$STUB_LOG" | jq -r .op)" "complete"
60
+
61
+ : > "$STUB_LOG"
62
+ OUT=$(run --path "$WORK/proj/daily.mp4" --name "refuse.bin"; true)
63
+ check "cloud refusal passed through" "$(printf '%s' "$OUT" | jq -c '{success, reason}')" '{"success":false,"reason":"quota_exceeded"}'
64
+
65
+ : > "$STUB_LOG"; touch "$STORE.fail"
66
+ OUT=$(run --path "$WORK/proj/daily.mp4"; true)
67
+ check "failed upload reports and cancels" "$(printf '%s' "$OUT" | jq -r .reason)" "upload_failed"
68
+ check "cancel issued" "$(grep -c '"op": "cancel"' "$STUB_LOG")" "1"
69
+ rm -f "$STORE.fail"
70
+
71
+ echo secret > "$WORK/outside.txt"; ln -s "$WORK/proj/daily.mp4" "$WORK/proj/link.mp4"
72
+ check "outside project refused" "$(run_err --path "$WORK/outside.txt" | grep -c 'outside your project directory')" "1"
73
+ check "symlink refused" "$(run_err --path "$WORK/proj/link.mp4" | grep -c 'symbolic link')" "1"
74
+ check "missing file refused" "$(run_err --path "$WORK/proj/nope" | grep -c 'file not found')" "1"
75
+ check "path required" "$(run_err | grep -c -- '--path is required')" "1"
76
+ : > "$STUB_LOG"
77
+ run --cancel hf_AbCdEfGhIjKlMnOpQrStUv >/dev/null
78
+ check "cancel" "$(jq -r .path "$STUB_LOG")" "/api/apps/handoffs/hf_AbCdEfGhIjKlMnOpQrStUv"
79
+
80
+ echo "send-file: $PASS passed, $FAIL failed"
81
+ [ "$FAIL" -eq 0 ]
@@ -1016,12 +1016,29 @@ export declare const CONTROL_PLANE_GUARD_CONSTANTS: {
1016
1016
  /** Files under CREWLY_HOME that agents must not read with the built-in tools either. */
1017
1017
  readonly CREWLY_HOME_READ_DENIED_FILES: readonly ["api-token"];
1018
1018
  /** Write-protected directories under the install root (whole subtree). */
1019
- readonly INSTALL_DIRS: readonly ["config/skills/orchestrator/stop-agent", "config/skills/orchestrator/start-agent", "config/skills/orchestrator/terminate-agent", "config/skills/orchestrator/stop-team", "config/skills/orchestrator/start-team", "config/skills/orchestrator/restart-crewly", "config/hooks/control-plane-guard", "config/hooks/agent-status", "config/hooks/subagent-guard", "config/hooks/credential-guard", "dist"];
1019
+ readonly INSTALL_DIRS: readonly ["config/skills/orchestrator/stop-agent", "config/skills/orchestrator/start-agent", "config/skills/orchestrator/terminate-agent", "config/skills/orchestrator/stop-team", "config/skills/orchestrator/start-team", "config/skills/orchestrator/restart-crewly", "config/hooks/control-plane-guard", "config/hooks/agent-status", "config/hooks/subagent-guard", "config/hooks/credential-guard", "config/hooks/live-checkout-guard", "dist"];
1020
1020
  /** Write-protected files under the install root. */
1021
1021
  readonly INSTALL_FILES: readonly ["config/skills/_common/lib.sh"];
1022
1022
  /** Write-protected directories under the agent's project path (whole subtree). */
1023
1023
  readonly PROJECT_DIRS: readonly [".claude/agents", ".crewly/triggers"];
1024
1024
  };
1025
+ /**
1026
+ * Live-checkout guard: agents may not change the checkout the running Crewly
1027
+ * is built from (git state changes or file edits). Incident 2026-10-08/09: an
1028
+ * agent switched branches in it and the next restart refused to start.
1029
+ */
1030
+ export declare const LIVE_CHECKOUT_GUARD_CONSTANTS: {
1031
+ /** Backend env var; `0` turns the guard off for sessions launched by this backend. */
1032
+ readonly KILL_SWITCH_ENV: "CREWLY_LIVE_CHECKOUT_GUARD";
1033
+ /** Value of KILL_SWITCH_ENV that disables the guard. */
1034
+ readonly KILL_SWITCH_OFF_VALUE: "0";
1035
+ /** The hook script (node), relative to the install root. */
1036
+ readonly HOOK_SCRIPT: "config/hooks/live-checkout-guard/guard.mjs";
1037
+ /** Claude Code tools the hook is attached to. */
1038
+ readonly CLAUDE_MATCHER: "Bash|Edit|Write|MultiEdit|NotebookEdit";
1039
+ /** Codex tools the hook is attached to (shell, plus its patch tool). */
1040
+ readonly CODEX_MATCHER: "Bash|apply_patch|Edit|Write";
1041
+ };
1025
1042
  /**
1026
1043
  * Credential guard (specs/2026-10-04-agent-credential-isolation.md, layer 2):
1027
1044
  * a pre-tool hook, on every runtime that has one, that refuses an agent's
@@ -7357,6 +7374,18 @@ export declare const REMOTE_MCP_CONSTANTS: {
7357
7374
  readonly MAX_TOOL_NAMES: 50;
7358
7375
  /** Catalog presets the add form offers. */
7359
7376
  readonly PROVIDERS: readonly string[];
7377
+ /**
7378
+ * Tools an AGENT may never call through the proxy, per server kind (the
7379
+ * server's provider, or its id when it has none). Compared after
7380
+ * trim + NFKC + lower-casing, so case and width tricks do not bypass it.
7381
+ * Zoho has no draft tool: a draft is a mode of sendEmail, so one missing
7382
+ * field sends mail. Mail is only ever drafted through the backend's
7383
+ * draft-only path (CREW-400). resendVerification* mails an address a
7384
+ * verification link; denied too since an agent has no reason to trigger it.
7385
+ */
7386
+ readonly DENIED_AGENT_TOOLS: Record<string, readonly string[]>;
7387
+ /** Message an agent gets when it calls a denied tool. */
7388
+ readonly DENIED_TOOL_MESSAGE: "sending mail is not allowed for agents; save a draft instead";
7360
7389
  /**
7361
7390
  * OAuth state (clients, tokens, pending sign-ins) under CREWLY_HOME:
7362
7391
  * AES-256-GCM with the install's master key, mode 0600.