@heylemon/lemonade 0.0.8 → 0.1.0
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/bin/lemon-cli-header.sh +6 -0
- package/bin/lemon-docs +29 -8
- package/bin/lemon-drive +4 -0
- package/bin/lemon-gmail +34 -7
- package/bin/lemon-jira +16 -6
- package/bin/lemon-notion +16 -6
- package/bin/lemon-slack +9 -2
- package/bin/lemon-slides +14 -4
- package/bin/lemon-twitter +7 -2
- package/dist/agents/sandbox/sanitize-env-vars.js +76 -0
- package/dist/agents/skills/env-overrides.js +114 -42
- package/dist/agents/skills/workspace.js +1 -0
- package/dist/agents/system-prompt.js +4 -3
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/cron/service/ops.js +13 -1
- package/dist/cron/service/timer.js +95 -6
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/abort-handler.js +23 -0
- package/dist/imessage/monitor/deliver.js +7 -1
- package/dist/imessage/monitor/monitor-provider.js +65 -20
- package/dist/imessage/monitor/parse-notification.js +64 -0
- package/dist/imessage/probe.js +14 -5
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/web/inbound/access-control.js +6 -13
- package/docs/MaxModeSystemPrompt.md +8 -0
- package/extensions/voice-call/src/config.ts +8 -0
- package/extensions/voice-call/src/webhook.ts +37 -0
- package/package.json +1 -1
package/bin/lemon-cli-header.sh
CHANGED
|
@@ -43,6 +43,12 @@ if [[ -z "$LEMON_BACKEND_URL" ]]; then
|
|
|
43
43
|
fi
|
|
44
44
|
BACKEND_URL="$LEMON_BACKEND_URL"
|
|
45
45
|
|
|
46
|
+
if [[ -z "$GATEWAY_TOKEN" ]]; then
|
|
47
|
+
echo -e "${RED}Error:${NC} GATEWAY_TOKEN not set." >&2
|
|
48
|
+
echo "Please ensure ~/.lemonade/.env contains GATEWAY_TOKEN or set it in your environment." >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
46
52
|
# Local gateway URL (for health checks)
|
|
47
53
|
GATEWAY_URL="${LEMONADE_GATEWAY_URL:-http://127.0.0.1:19847}"
|
|
48
54
|
|
package/bin/lemon-docs
CHANGED
|
@@ -20,16 +20,26 @@ case "$1" in
|
|
|
20
20
|
CONTENT="${3:-}"
|
|
21
21
|
|
|
22
22
|
# Create the document
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
CREATE_JSON=$(jq -n --arg title "$TITLE" \
|
|
24
|
+
'{toolName: "GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN", parameters: {title: $title}}')
|
|
25
|
+
RESULT=$(echo "$CREATE_JSON" | curl -s -X POST \
|
|
26
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
27
|
+
-H "Content-Type: application/json" \
|
|
28
|
+
-d @- \
|
|
29
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute")
|
|
25
30
|
|
|
26
31
|
# Extract document ID from result
|
|
27
32
|
DOC_ID=$(echo "$RESULT" | jq -r '.result.documentId // .documentId // .id // .data.documentId // .data.id // empty' 2>/dev/null)
|
|
28
33
|
|
|
29
34
|
# If content provided and we got a doc ID, append the content
|
|
30
35
|
if [[ -n "$CONTENT" ]] && [[ -n "$DOC_ID" ]]; then
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
APPEND_JSON=$(jq -n --arg docId "$DOC_ID" --arg text "$CONTENT" \
|
|
37
|
+
'{toolName: "GOOGLEDOCS_INSERT_TEXT_ACTION", parameters: {document_id: $docId, text_to_insert: $text, append_to_end: true}}')
|
|
38
|
+
echo "$APPEND_JSON" | curl -s -X POST \
|
|
39
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
40
|
+
-H "Content-Type: application/json" \
|
|
41
|
+
-d @- \
|
|
42
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute" > /dev/null
|
|
33
43
|
fi
|
|
34
44
|
|
|
35
45
|
echo "$RESULT"
|
|
@@ -39,7 +49,10 @@ case "$1" in
|
|
|
39
49
|
TITLE="$2"
|
|
40
50
|
FILE_PATH="$3"
|
|
41
51
|
|
|
42
|
-
[[ ! -f "$FILE_PATH" ]]
|
|
52
|
+
if [[ ! -f "$FILE_PATH" ]]; then
|
|
53
|
+
echo -e "${RED}Error:${NC} File not found: $FILE_PATH" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
43
56
|
|
|
44
57
|
# Read file content
|
|
45
58
|
CONTENT=$(cat "$FILE_PATH")
|
|
@@ -73,15 +86,23 @@ case "$1" in
|
|
|
73
86
|
;;
|
|
74
87
|
append)
|
|
75
88
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-docs append <document_id> <text>" && exit 1
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
APPEND_JSON=$(jq -n --arg docId "$2" --arg text "$3" \
|
|
90
|
+
'{toolName: "GOOGLEDOCS_INSERT_TEXT_ACTION", parameters: {document_id: $docId, text_to_insert: $text, append_to_end: true}}')
|
|
91
|
+
echo "$APPEND_JSON" | curl -s -X POST \
|
|
92
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
93
|
+
-H "Content-Type: application/json" \
|
|
94
|
+
-d @- \
|
|
95
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
78
96
|
;;
|
|
79
97
|
append-file)
|
|
80
98
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-docs append-file <document_id> <file_path>" && exit 1
|
|
81
99
|
DOC_ID="$2"
|
|
82
100
|
FILE_PATH="$3"
|
|
83
101
|
|
|
84
|
-
[[ ! -f "$FILE_PATH" ]]
|
|
102
|
+
if [[ ! -f "$FILE_PATH" ]]; then
|
|
103
|
+
echo -e "${RED}Error:${NC} File not found: $FILE_PATH" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
85
106
|
|
|
86
107
|
# Read and escape file content
|
|
87
108
|
CONTENT=$(cat "$FILE_PATH" | jq -Rs .)
|
package/bin/lemon-drive
CHANGED
|
@@ -37,6 +37,10 @@ case "$1" in
|
|
|
37
37
|
;;
|
|
38
38
|
upload)
|
|
39
39
|
[[ -z "$2" ]] && echo "Usage: lemon-drive upload <file_path> [folder_id]" && exit 1
|
|
40
|
+
if [[ ! -f "$2" ]]; then
|
|
41
|
+
echo -e "${RED}Error:${NC} File not found: $2" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
40
44
|
folder="${3:-root}"
|
|
41
45
|
api_call POST "/api/lemonade/tools/execute" \
|
|
42
46
|
'{"toolName": "GOOGLEDRIVE_UPLOAD_FILE", "parameters": {"filePath": "'"$2"'", "parents": ["'"$folder"'"]}}'
|
package/bin/lemon-gmail
CHANGED
|
@@ -16,7 +16,11 @@ case "$1" in
|
|
|
16
16
|
;;
|
|
17
17
|
send)
|
|
18
18
|
[[ -z "$2" ]] || [[ -z "$3" ]] || [[ -z "$4" ]] && echo "Usage: lemon-gmail send <to> <subject> <body> [attachment_path]" && exit 1
|
|
19
|
-
if [[ -n "$5" ]]
|
|
19
|
+
if [[ -n "$5" ]]; then
|
|
20
|
+
if [[ ! -f "$5" ]]; then
|
|
21
|
+
echo "Error: attachment file not found: $5" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
20
24
|
# Send with attachment: upload to Composio S3 → send email with s3key
|
|
21
25
|
ATTACH_PATH="$5"
|
|
22
26
|
ATTACH_NAME="$(basename "$ATTACH_PATH")"
|
|
@@ -69,8 +73,16 @@ case "$1" in
|
|
|
69
73
|
-d @- \
|
|
70
74
|
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
71
75
|
else
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
SEND_JSON=$(jq -n \
|
|
77
|
+
--arg to "$2" \
|
|
78
|
+
--arg subject "$3" \
|
|
79
|
+
--arg body "$4" \
|
|
80
|
+
'{toolName: "GMAIL_SEND_EMAIL", parameters: {recipient_email: $to, subject: $subject, body: $body}}')
|
|
81
|
+
echo "$SEND_JSON" | curl -s -X POST \
|
|
82
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
83
|
+
-H "Content-Type: application/json" \
|
|
84
|
+
-d @- \
|
|
85
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
74
86
|
fi
|
|
75
87
|
;;
|
|
76
88
|
list)
|
|
@@ -95,13 +107,28 @@ case "$1" in
|
|
|
95
107
|
;;
|
|
96
108
|
reply)
|
|
97
109
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-gmail reply <thread_id> <body>" && exit 1
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
REPLY_JSON=$(jq -n \
|
|
111
|
+
--arg tid "$2" \
|
|
112
|
+
--arg body "$3" \
|
|
113
|
+
'{toolName: "GMAIL_REPLY_TO_THREAD", parameters: {thread_id: $tid, message_body: $body}}')
|
|
114
|
+
echo "$REPLY_JSON" | curl -s -X POST \
|
|
115
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d @- \
|
|
118
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
100
119
|
;;
|
|
101
120
|
draft)
|
|
102
121
|
[[ -z "$2" ]] || [[ -z "$3" ]] || [[ -z "$4" ]] && echo "Usage: lemon-gmail draft <to> <subject> <body>" && exit 1
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
DRAFT_JSON=$(jq -n \
|
|
123
|
+
--arg to "$2" \
|
|
124
|
+
--arg subject "$3" \
|
|
125
|
+
--arg body "$4" \
|
|
126
|
+
'{toolName: "GMAIL_CREATE_EMAIL_DRAFT", parameters: {recipient_email: $to, subject: $subject, body: $body}}')
|
|
127
|
+
echo "$DRAFT_JSON" | curl -s -X POST \
|
|
128
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d @- \
|
|
131
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
105
132
|
;;
|
|
106
133
|
contacts)
|
|
107
134
|
# List Google contacts (name + email) for recipient resolution
|
package/bin/lemon-jira
CHANGED
|
@@ -40,12 +40,17 @@ case "$1" in
|
|
|
40
40
|
type="${4:-Task}"
|
|
41
41
|
desc="${5:-}"
|
|
42
42
|
if [[ -n "$desc" ]]; then
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
CREATE_JSON=$(jq -n --arg proj "$2" --arg summary "$3" --arg itype "$type" --arg desc "$desc" \
|
|
44
|
+
'{toolName: "JIRA_CREATE_ISSUE", parameters: {project_key: $proj, summary: $summary, issue_type: $itype, description: $desc}}')
|
|
45
45
|
else
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
CREATE_JSON=$(jq -n --arg proj "$2" --arg summary "$3" --arg itype "$type" \
|
|
47
|
+
'{toolName: "JIRA_CREATE_ISSUE", parameters: {project_key: $proj, summary: $summary, issue_type: $itype}}')
|
|
48
48
|
fi
|
|
49
|
+
echo "$CREATE_JSON" | curl -s -X POST \
|
|
50
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
51
|
+
-H "Content-Type: application/json" \
|
|
52
|
+
-d @- \
|
|
53
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
49
54
|
;;
|
|
50
55
|
get)
|
|
51
56
|
[[ -z "$2" ]] && echo "Usage: lemon-jira get <issue_key>" && exit 1
|
|
@@ -54,8 +59,13 @@ case "$1" in
|
|
|
54
59
|
;;
|
|
55
60
|
comment)
|
|
56
61
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-jira comment <issue_key> <body>" && exit 1
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
COMMENT_JSON=$(jq -n --arg key "$2" --arg body "$3" \
|
|
63
|
+
'{toolName: "JIRA_ADD_COMMENT", parameters: {issue_id_or_key: $key, comment: $body}}')
|
|
64
|
+
echo "$COMMENT_JSON" | curl -s -X POST \
|
|
65
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
66
|
+
-H "Content-Type: application/json" \
|
|
67
|
+
-d @- \
|
|
68
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
59
69
|
;;
|
|
60
70
|
transition)
|
|
61
71
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-jira transition <issue_key> <status_name>" && exit 1
|
package/bin/lemon-notion
CHANGED
|
@@ -28,12 +28,17 @@ case "$1" in
|
|
|
28
28
|
title="$2"
|
|
29
29
|
parent="${3:-}"
|
|
30
30
|
if [[ -n "$parent" ]]; then
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
CREATE_JSON=$(jq -n --arg parent "$parent" --arg title "$title" \
|
|
32
|
+
'{toolName: "NOTION_CREATE_NOTION_PAGE", parameters: {parent_id: $parent, title: $title}}')
|
|
33
33
|
else
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
CREATE_JSON=$(jq -n --arg title "$title" \
|
|
35
|
+
'{toolName: "NOTION_CREATE_NOTION_PAGE", parameters: {title: $title}}')
|
|
36
36
|
fi
|
|
37
|
+
echo "$CREATE_JSON" | curl -s -X POST \
|
|
38
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
39
|
+
-H "Content-Type: application/json" \
|
|
40
|
+
-d @- \
|
|
41
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
37
42
|
;;
|
|
38
43
|
read)
|
|
39
44
|
[[ -z "$2" ]] && echo "Usage: lemon-notion read <page_id>" && exit 1
|
|
@@ -47,8 +52,13 @@ case "$1" in
|
|
|
47
52
|
;;
|
|
48
53
|
append)
|
|
49
54
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-notion append <page_id> <content>" && exit 1
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
APPEND_JSON=$(jq -n --arg pageId "$2" --arg content "$3" \
|
|
56
|
+
'{toolName: "NOTION_ADD_MULTIPLE_PAGE_CONTENT", parameters: {page_id: $pageId, content: $content}}')
|
|
57
|
+
echo "$APPEND_JSON" | curl -s -X POST \
|
|
58
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d @- \
|
|
61
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
52
62
|
;;
|
|
53
63
|
databases)
|
|
54
64
|
api_call POST "/api/lemonade/tools/execute" \
|
package/bin/lemon-slack
CHANGED
|
@@ -16,8 +16,15 @@ case "$1" in
|
|
|
16
16
|
;;
|
|
17
17
|
send)
|
|
18
18
|
[[ -z "$2" ]] || [[ -z "$3" ]] && echo "Usage: lemon-slack send <channel> <message>" && exit 1
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
SEND_JSON=$(jq -n \
|
|
20
|
+
--arg channel "$2" \
|
|
21
|
+
--arg text "$3" \
|
|
22
|
+
'{toolName: "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", parameters: {channel: $channel, text: $text}}')
|
|
23
|
+
echo "$SEND_JSON" | curl -s -X POST \
|
|
24
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d @- \
|
|
27
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
21
28
|
;;
|
|
22
29
|
channels)
|
|
23
30
|
api_call POST "/api/lemonade/tools/execute" \
|
package/bin/lemon-slides
CHANGED
|
@@ -16,13 +16,23 @@ case "$1" in
|
|
|
16
16
|
;;
|
|
17
17
|
create)
|
|
18
18
|
[[ -z "$2" ]] && echo "Usage: lemon-slides create <title>" && exit 1
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
CREATE_JSON=$(jq -n --arg title "$2" \
|
|
20
|
+
'{toolName: "GOOGLESLIDES_PRESENTATIONS_CREATE", parameters: {title: $title}}')
|
|
21
|
+
echo "$CREATE_JSON" | curl -s -X POST \
|
|
22
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
23
|
+
-H "Content-Type: application/json" \
|
|
24
|
+
-d @- \
|
|
25
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
21
26
|
;;
|
|
22
27
|
create-markdown)
|
|
23
28
|
[[ -z "$2" ]] && echo "Usage: lemon-slides create-markdown <markdown_content>" && exit 1
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
MD_JSON=$(jq -n --arg md "$2" \
|
|
30
|
+
'{toolName: "GOOGLESLIDES_CREATE_SLIDES_MARKDOWN", parameters: {markdown: $md}}')
|
|
31
|
+
echo "$MD_JSON" | curl -s -X POST \
|
|
32
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
33
|
+
-H "Content-Type: application/json" \
|
|
34
|
+
-d @- \
|
|
35
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
26
36
|
;;
|
|
27
37
|
get)
|
|
28
38
|
[[ -z "$2" ]] && echo "Usage: lemon-slides get <presentation_id>" && exit 1
|
package/bin/lemon-twitter
CHANGED
|
@@ -16,8 +16,13 @@ case "$1" in
|
|
|
16
16
|
;;
|
|
17
17
|
tweet)
|
|
18
18
|
[[ -z "$2" ]] && echo "Usage: lemon-twitter tweet <text>" && exit 1
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
TWEET_JSON=$(jq -n --arg text "$2" \
|
|
20
|
+
'{toolName: "TWITTER_CREATION_OF_A_POST", parameters: {text: $text}}')
|
|
21
|
+
echo "$TWEET_JSON" | curl -s -X POST \
|
|
22
|
+
-H "Authorization: Bearer ${GATEWAY_TOKEN}" \
|
|
23
|
+
-H "Content-Type: application/json" \
|
|
24
|
+
-d @- \
|
|
25
|
+
"${LEMON_BACKEND_URL}/api/lemonade/tools/execute"
|
|
21
26
|
;;
|
|
22
27
|
timeline)
|
|
23
28
|
limit="${2:-20}"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const BLOCKED_ENV_VAR_PATTERNS = [
|
|
2
|
+
/^ANTHROPIC_API_KEY$/i,
|
|
3
|
+
/^OPENAI_API_KEY$/i,
|
|
4
|
+
/^GEMINI_API_KEY$/i,
|
|
5
|
+
/^OPENROUTER_API_KEY$/i,
|
|
6
|
+
/^MINIMAX_API_KEY$/i,
|
|
7
|
+
/^ELEVENLABS_API_KEY$/i,
|
|
8
|
+
/^SYNTHETIC_API_KEY$/i,
|
|
9
|
+
/^TELEGRAM_BOT_TOKEN$/i,
|
|
10
|
+
/^DISCORD_BOT_TOKEN$/i,
|
|
11
|
+
/^SLACK_(BOT|APP)_TOKEN$/i,
|
|
12
|
+
/^LINE_CHANNEL_SECRET$/i,
|
|
13
|
+
/^LINE_CHANNEL_ACCESS_TOKEN$/i,
|
|
14
|
+
/^LEMONADE_GATEWAY_(TOKEN|PASSWORD)$/i,
|
|
15
|
+
/^AWS_(SECRET_ACCESS_KEY|SECRET_KEY|SESSION_TOKEN)$/i,
|
|
16
|
+
/^(GH|GITHUB)_TOKEN$/i,
|
|
17
|
+
/^(AZURE|AZURE_OPENAI|COHERE|AI_GATEWAY|OPENROUTER)_API_KEY$/i,
|
|
18
|
+
/_?(API_KEY|TOKEN|PASSWORD|PRIVATE_KEY|SECRET)$/i,
|
|
19
|
+
];
|
|
20
|
+
const ALLOWED_ENV_VAR_PATTERNS = [
|
|
21
|
+
/^LANG$/,
|
|
22
|
+
/^LC_.*$/i,
|
|
23
|
+
/^PATH$/i,
|
|
24
|
+
/^HOME$/i,
|
|
25
|
+
/^USER$/i,
|
|
26
|
+
/^SHELL$/i,
|
|
27
|
+
/^TERM$/i,
|
|
28
|
+
/^TZ$/i,
|
|
29
|
+
/^NODE_ENV$/i,
|
|
30
|
+
];
|
|
31
|
+
export function validateEnvVarValue(value) {
|
|
32
|
+
if (value.includes("\0")) {
|
|
33
|
+
return "Contains null bytes";
|
|
34
|
+
}
|
|
35
|
+
if (value.length > 32768) {
|
|
36
|
+
return "Value exceeds maximum length";
|
|
37
|
+
}
|
|
38
|
+
if (/^[A-Za-z0-9+/=]{80,}$/.test(value)) {
|
|
39
|
+
return "Value looks like base64-encoded credential data";
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function matchesAnyPattern(value, patterns) {
|
|
44
|
+
return patterns.some((pattern) => pattern.test(value));
|
|
45
|
+
}
|
|
46
|
+
export function sanitizeEnvVars(envVars, options = {}) {
|
|
47
|
+
const allowed = {};
|
|
48
|
+
const blocked = [];
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const blockedPatterns = [...BLOCKED_ENV_VAR_PATTERNS, ...(options.customBlockedPatterns ?? [])];
|
|
51
|
+
const allowedPatterns = [...ALLOWED_ENV_VAR_PATTERNS, ...(options.customAllowedPatterns ?? [])];
|
|
52
|
+
for (const [rawKey, value] of Object.entries(envVars)) {
|
|
53
|
+
const key = rawKey.trim();
|
|
54
|
+
if (!key) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (matchesAnyPattern(key, blockedPatterns)) {
|
|
58
|
+
blocked.push(key);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (options.strictMode && !matchesAnyPattern(key, allowedPatterns)) {
|
|
62
|
+
blocked.push(key);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const warning = validateEnvVarValue(value);
|
|
66
|
+
if (warning) {
|
|
67
|
+
if (warning === "Contains null bytes") {
|
|
68
|
+
blocked.push(key);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
warnings.push(`${key}: ${warning}`);
|
|
72
|
+
}
|
|
73
|
+
allowed[key] = value;
|
|
74
|
+
}
|
|
75
|
+
return { allowed, blocked, warnings };
|
|
76
|
+
}
|
|
@@ -1,36 +1,123 @@
|
|
|
1
|
+
import { sanitizeEnvVars, validateEnvVarValue } from "../sandbox/sanitize-env-vars.js";
|
|
1
2
|
import { resolveSkillConfig } from "./config.js";
|
|
2
3
|
import { resolveSkillKey } from "./frontmatter.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const HARD_BLOCKED_SKILL_ENV_PATTERNS = [
|
|
5
|
+
/^NODE_OPTIONS$/i,
|
|
6
|
+
/^OPENSSL_CONF$/i,
|
|
7
|
+
/^LD_PRELOAD$/i,
|
|
8
|
+
/^DYLD_INSERT_LIBRARIES$/i,
|
|
9
|
+
];
|
|
10
|
+
function matchesAnyPattern(value, patterns) {
|
|
11
|
+
return patterns.some((pattern) => pattern.test(value));
|
|
12
|
+
}
|
|
13
|
+
function sanitizeSkillEnvOverrides(params) {
|
|
14
|
+
if (Object.keys(params.overrides).length === 0) {
|
|
15
|
+
return { allowed: {}, blocked: [], warnings: [] };
|
|
16
|
+
}
|
|
17
|
+
const result = sanitizeEnvVars(params.overrides, {
|
|
18
|
+
customBlockedPatterns: HARD_BLOCKED_SKILL_ENV_PATTERNS,
|
|
19
|
+
});
|
|
20
|
+
const allowed = { ...result.allowed };
|
|
21
|
+
const blocked = [];
|
|
22
|
+
const warnings = [...result.warnings];
|
|
23
|
+
for (const key of result.blocked) {
|
|
24
|
+
if (matchesAnyPattern(key, HARD_BLOCKED_SKILL_ENV_PATTERNS) ||
|
|
25
|
+
!params.allowedSensitiveKeys.has(key)) {
|
|
26
|
+
blocked.push(key);
|
|
10
27
|
continue;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
}
|
|
29
|
+
const value = params.overrides[key];
|
|
30
|
+
if (!value) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const warning = validateEnvVarValue(value);
|
|
34
|
+
if (warning) {
|
|
35
|
+
if (warning === "Contains null bytes") {
|
|
36
|
+
blocked.push(key);
|
|
37
|
+
continue;
|
|
17
38
|
}
|
|
39
|
+
warnings.push(`${key}: ${warning}`);
|
|
18
40
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
allowed[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return { allowed, blocked, warnings };
|
|
44
|
+
}
|
|
45
|
+
function applySkillConfigEnvOverrides(params) {
|
|
46
|
+
const { updates, skillConfig, primaryEnv, requiredEnv, skillKey } = params;
|
|
47
|
+
const allowedSensitiveKeys = new Set();
|
|
48
|
+
const normalizedPrimaryEnv = primaryEnv?.trim();
|
|
49
|
+
if (normalizedPrimaryEnv) {
|
|
50
|
+
allowedSensitiveKeys.add(normalizedPrimaryEnv);
|
|
51
|
+
}
|
|
52
|
+
for (const envName of requiredEnv ?? []) {
|
|
53
|
+
const trimmedEnv = envName.trim();
|
|
54
|
+
if (trimmedEnv) {
|
|
55
|
+
allowedSensitiveKeys.add(trimmedEnv);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const pendingOverrides = {};
|
|
59
|
+
if (skillConfig.env) {
|
|
60
|
+
for (const [rawKey, envValue] of Object.entries(skillConfig.env)) {
|
|
61
|
+
const envKey = rawKey.trim();
|
|
62
|
+
if (!envKey || !envValue || process.env[envKey]) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
pendingOverrides[envKey] = envValue;
|
|
23
66
|
}
|
|
24
67
|
}
|
|
68
|
+
if (normalizedPrimaryEnv && skillConfig.apiKey && !process.env[normalizedPrimaryEnv]) {
|
|
69
|
+
if (!pendingOverrides[normalizedPrimaryEnv]) {
|
|
70
|
+
pendingOverrides[normalizedPrimaryEnv] = skillConfig.apiKey;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const sanitized = sanitizeSkillEnvOverrides({
|
|
74
|
+
overrides: pendingOverrides,
|
|
75
|
+
allowedSensitiveKeys,
|
|
76
|
+
});
|
|
77
|
+
if (sanitized.blocked.length > 0) {
|
|
78
|
+
console.warn(`[Security] Blocked skill env overrides for ${skillKey}:`, sanitized.blocked.join(", "));
|
|
79
|
+
}
|
|
80
|
+
if (sanitized.warnings.length > 0) {
|
|
81
|
+
console.warn(`[Security] Suspicious skill env overrides for ${skillKey}:`, sanitized.warnings);
|
|
82
|
+
}
|
|
83
|
+
for (const [envKey, envValue] of Object.entries(sanitized.allowed)) {
|
|
84
|
+
if (process.env[envKey]) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
updates.push({ key: envKey, prev: process.env[envKey] });
|
|
88
|
+
process.env[envKey] = envValue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function createEnvReverter(updates) {
|
|
25
92
|
return () => {
|
|
26
93
|
for (const update of updates) {
|
|
27
|
-
if (update.prev === undefined)
|
|
94
|
+
if (update.prev === undefined) {
|
|
28
95
|
delete process.env[update.key];
|
|
29
|
-
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
30
98
|
process.env[update.key] = update.prev;
|
|
99
|
+
}
|
|
31
100
|
}
|
|
32
101
|
};
|
|
33
102
|
}
|
|
103
|
+
export function applySkillEnvOverrides(params) {
|
|
104
|
+
const { skills, config } = params;
|
|
105
|
+
const updates = [];
|
|
106
|
+
for (const entry of skills) {
|
|
107
|
+
const skillKey = resolveSkillKey(entry.skill, entry);
|
|
108
|
+
const skillConfig = resolveSkillConfig(config, skillKey);
|
|
109
|
+
if (!skillConfig)
|
|
110
|
+
continue;
|
|
111
|
+
applySkillConfigEnvOverrides({
|
|
112
|
+
updates,
|
|
113
|
+
skillConfig,
|
|
114
|
+
primaryEnv: entry.metadata?.primaryEnv,
|
|
115
|
+
requiredEnv: entry.metadata?.requires?.env,
|
|
116
|
+
skillKey,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return createEnvReverter(updates);
|
|
120
|
+
}
|
|
34
121
|
export function applySkillEnvOverridesFromSnapshot(params) {
|
|
35
122
|
const { snapshot, config } = params;
|
|
36
123
|
if (!snapshot)
|
|
@@ -40,28 +127,13 @@ export function applySkillEnvOverridesFromSnapshot(params) {
|
|
|
40
127
|
const skillConfig = resolveSkillConfig(config, skill.name);
|
|
41
128
|
if (!skillConfig)
|
|
42
129
|
continue;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
if (skill.primaryEnv && skillConfig.apiKey && !process.env[skill.primaryEnv]) {
|
|
52
|
-
updates.push({
|
|
53
|
-
key: skill.primaryEnv,
|
|
54
|
-
prev: process.env[skill.primaryEnv],
|
|
55
|
-
});
|
|
56
|
-
process.env[skill.primaryEnv] = skillConfig.apiKey;
|
|
57
|
-
}
|
|
130
|
+
applySkillConfigEnvOverrides({
|
|
131
|
+
updates,
|
|
132
|
+
skillConfig,
|
|
133
|
+
primaryEnv: skill.primaryEnv,
|
|
134
|
+
requiredEnv: skill.requiredEnv,
|
|
135
|
+
skillKey: skill.name,
|
|
136
|
+
});
|
|
58
137
|
}
|
|
59
|
-
return ()
|
|
60
|
-
for (const update of updates) {
|
|
61
|
-
if (update.prev === undefined)
|
|
62
|
-
delete process.env[update.key];
|
|
63
|
-
else
|
|
64
|
-
process.env[update.key] = update.prev;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
138
|
+
return createEnvReverter(updates);
|
|
67
139
|
}
|
|
@@ -147,6 +147,7 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
|
|
|
147
147
|
skills: eligible.map((entry) => ({
|
|
148
148
|
name: entry.skill.name,
|
|
149
149
|
primaryEnv: entry.metadata?.primaryEnv,
|
|
150
|
+
requiredEnv: entry.metadata?.requires?.env,
|
|
150
151
|
})),
|
|
151
152
|
resolvedSkills,
|
|
152
153
|
version: opts?.snapshotVersion,
|
|
@@ -32,7 +32,7 @@ function buildMemorySection(params) {
|
|
|
32
32
|
function buildUserIdentitySection(ownerLine, isMinimal) {
|
|
33
33
|
if (!ownerLine || isMinimal)
|
|
34
34
|
return [];
|
|
35
|
-
return ["##
|
|
35
|
+
return ["## Authorized Senders", ownerLine, ""];
|
|
36
36
|
}
|
|
37
37
|
function buildTimeSection(params) {
|
|
38
38
|
if (!params.userTimezone)
|
|
@@ -110,7 +110,7 @@ function buildConfirmationSection(isMinimal) {
|
|
|
110
110
|
"## Email Rules (MANDATORY)",
|
|
111
111
|
"1. NEVER fabricate email addresses. When user says a name (e.g. 'masood from my team'), resolve it via: Slack search-users (returns email), Gmail find-contact, or Gmail sent-to. If all fail, ASK the user.",
|
|
112
112
|
"2. ALWAYS attach files when the task involves creating AND sending a file.",
|
|
113
|
-
|
|
113
|
+
"3. To send email with an attachment use: `lemon-gmail send <to> <subject> <body> <file_path>`. BEFORE sending, ALWAYS verify the file exists at the path with `ls <file_path>`. If the file was created earlier, confirm it still exists — files in /tmp may be cleaned up.",
|
|
114
114
|
"4. ALWAYS confirm the recipient email and attachment list BEFORE sending.",
|
|
115
115
|
"",
|
|
116
116
|
];
|
|
@@ -231,7 +231,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
231
231
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
|
232
232
|
const ownerNumbers = (params.ownerNumbers ?? []).map((value) => value.trim()).filter(Boolean);
|
|
233
233
|
const ownerLine = ownerNumbers.length > 0
|
|
234
|
-
? `
|
|
234
|
+
? `Authorized senders: ${ownerNumbers.join(", ")}. These senders are allowlisted; do not assume they are the owner.`
|
|
235
235
|
: undefined;
|
|
236
236
|
const reasoningHint = params.reasoningTagHint
|
|
237
237
|
? [
|
|
@@ -509,6 +509,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
509
509
|
if (!isMinimal) {
|
|
510
510
|
lines.push("## Heartbeats", heartbeatPromptLine, "If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:", "HEARTBEAT_OK", 'Lemonade treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).', 'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.', "");
|
|
511
511
|
}
|
|
512
|
+
lines.push("## Safety", "You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.", "Prioritize safety and human oversight over completion; if instructions conflict, pause and ask; comply with stop/pause/audit requests and never bypass safeguards.", "Do not manipulate or persuade anyone to expand access or disable safeguards. Do not copy yourself or change system prompts, safety rules, or tool policies unless explicitly requested.", "");
|
|
512
513
|
lines.push("## Runtime", buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel), `Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`);
|
|
513
514
|
return lines.filter(Boolean).join("\n");
|
|
514
515
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
fbf3ec381ce17e16995ad09f6a2d589d56f777a566e57c3b72c540991f48890c
|