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.
- package/config/hooks/live-checkout-guard/guard.mjs +452 -0
- package/config/skills/agent/core/receive-file/SKILL.md +53 -0
- package/config/skills/agent/core/receive-file/execute.sh +125 -0
- package/config/skills/agent/core/receive-file/execute.test.sh +80 -0
- package/config/skills/agent/core/send-file/SKILL.md +62 -0
- package/config/skills/agent/core/send-file/execute.sh +145 -0
- package/config/skills/agent/core/send-file/execute.test.sh +81 -0
- package/dist/backend/backend/src/constants.d.ts +30 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +38 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +12 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.js +41 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +4 -0
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js +12 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts +5 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +42 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts +72 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js +113 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-tool-policy.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +25 -1
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js +11 -2
- package/dist/backend/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts +3 -2
- package/dist/backend/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/credential-guard.service.js +12 -4
- package/dist/backend/backend/src/services/agent/credential-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js +109 -0
- package/dist/backend/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/utils/boot-owner-alert.d.ts +33 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.js +60 -0
- package/dist/backend/backend/src/utils/boot-owner-alert.js.map +1 -0
- package/dist/backend/backend/src/utils/build-provenance.d.ts +38 -0
- package/dist/backend/backend/src/utils/build-provenance.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/build-provenance.js +63 -1
- package/dist/backend/backend/src/utils/build-provenance.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +30 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +38 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts +4 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js +11 -2
- package/dist/cli/backend/src/services/agent/control-plane-guard.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts +3 -2
- package/dist/cli/backend/src/services/agent/credential-guard.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/credential-guard.service.js +12 -4
- package/dist/cli/backend/src/services/agent/credential-guard.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts +45 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js +109 -0
- package/dist/cli/backend/src/services/agent/live-checkout-guard.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +13 -7
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- 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.
|