@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.
@@ -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
- RESULT=$(api_call POST "/api/lemonade/tools/execute" \
24
- '{"toolName": "GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN", "parameters": {"title": "'"$TITLE"'"}}')
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
- api_call POST "/api/lemonade/tools/execute" \
32
- '{"toolName": "GOOGLEDOCS_INSERT_TEXT_ACTION", "parameters": {"document_id": "'"$DOC_ID"'", "text_to_insert": "'"$CONTENT"'", "append_to_end": true}}'
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" ]] && echo "Error: File not found: $FILE_PATH" && exit 1
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
- api_call POST "/api/lemonade/tools/execute" \
77
- '{"toolName": "GOOGLEDOCS_INSERT_TEXT_ACTION", "parameters": {"document_id": "'"$2"'", "text_to_insert": "'"$3"'", "append_to_end": true}}'
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" ]] && echo "Error: File not found: $FILE_PATH" && exit 1
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" ]] && [[ -f "$5" ]]; then
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
- api_call POST "/api/lemonade/tools/execute" \
73
- '{"toolName": "GMAIL_SEND_EMAIL", "parameters": {"recipient_email": "'"$2"'", "subject": "'"$3"'", "body": "'"$4"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
99
- '{"toolName": "GMAIL_REPLY_TO_THREAD", "parameters": {"thread_id": "'"$2"'", "message_body": "'"$3"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
104
- '{"toolName": "GMAIL_CREATE_EMAIL_DRAFT", "parameters": {"recipient_email": "'"$2"'", "subject": "'"$3"'", "body": "'"$4"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
44
- '{"toolName": "JIRA_CREATE_ISSUE", "parameters": {"project_key": "'"$2"'", "summary": "'"$3"'", "issue_type": "'"$type"'", "description": "'"$desc"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
47
- '{"toolName": "JIRA_CREATE_ISSUE", "parameters": {"project_key": "'"$2"'", "summary": "'"$3"'", "issue_type": "'"$type"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
58
- '{"toolName": "JIRA_ADD_COMMENT", "parameters": {"issue_id_or_key": "'"$2"'", "comment": "'"$3"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
32
- '{"toolName": "NOTION_CREATE_NOTION_PAGE", "parameters": {"parent_id": "'"$parent"'", "title": "'"$title"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
35
- '{"toolName": "NOTION_CREATE_NOTION_PAGE", "parameters": {"title": "'"$title"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
51
- '{"toolName": "NOTION_ADD_MULTIPLE_PAGE_CONTENT", "parameters": {"page_id": "'"$2"'", "content": "'"$3"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
20
- '{"toolName": "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", "parameters": {"channel": "'"$2"'", "text": "'"$3"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
20
- '{"toolName": "GOOGLESLIDES_PRESENTATIONS_CREATE", "parameters": {"title": "'"$2"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
25
- '{"toolName": "GOOGLESLIDES_CREATE_SLIDES_MARKDOWN", "parameters": {"markdown": "'"$2"'"}}'
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
- api_call POST "/api/lemonade/tools/execute" \
20
- '{"toolName": "TWITTER_CREATION_OF_A_POST", "parameters": {"text": "'"$2"'"}}'
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
- export function applySkillEnvOverrides(params) {
4
- const { skills, config } = params;
5
- const updates = [];
6
- for (const entry of skills) {
7
- const skillKey = resolveSkillKey(entry.skill, entry);
8
- const skillConfig = resolveSkillConfig(config, skillKey);
9
- if (!skillConfig)
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
- if (skillConfig.env) {
12
- for (const [envKey, envValue] of Object.entries(skillConfig.env)) {
13
- if (!envValue || process.env[envKey])
14
- continue;
15
- updates.push({ key: envKey, prev: process.env[envKey] });
16
- process.env[envKey] = envValue;
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
- const primaryEnv = entry.metadata?.primaryEnv;
20
- if (primaryEnv && skillConfig.apiKey && !process.env[primaryEnv]) {
21
- updates.push({ key: primaryEnv, prev: process.env[primaryEnv] });
22
- process.env[primaryEnv] = skillConfig.apiKey;
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
- else
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
- if (skillConfig.env) {
44
- for (const [envKey, envValue] of Object.entries(skillConfig.env)) {
45
- if (!envValue || process.env[envKey])
46
- continue;
47
- updates.push({ key: envKey, prev: process.env[envKey] });
48
- process.env[envKey] = envValue;
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 ["## User Identity", ownerLine, ""];
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
- '3. To attach a file to an email: first read the file as base64 (`base64 -i <path>`), then call GMAIL_SEND_EMAIL with the `attachment` parameter: `{"name": "file.docx", "content": "<base64>", "mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}`.',
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
- ? `Owner numbers: ${ownerNumbers.join(", ")}. Treat messages from these numbers as the user.`
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
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.8",
3
- "commit": "a1fb85deb3c07e445dd89291230ae7c4d9480042",
4
- "builtAt": "2026-02-20T07:05:21.593Z"
2
+ "version": "0.1.0",
3
+ "commit": "c1df1acf2d44bd071f58aa1caa0a16466e07d83f",
4
+ "builtAt": "2026-02-20T22:34:40.921Z"
5
5
  }
@@ -1 +1 @@
1
- 924abd5547396c0e3e1642d40a19a47b8437dc16ed9d2aa7cccdd5f28bba3d73
1
+ fbf3ec381ce17e16995ad09f6a2d589d56f777a566e57c3b72c540991f48890c