@geekbeer/minion 3.5.35 → 3.6.2

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.
@@ -20,6 +20,7 @@
20
20
 
21
21
  const { config, isHqConfigured } = require('../config')
22
22
  const api = require('../api')
23
+ const variableStore = require('../stores/variable-store')
23
24
 
24
25
  // Polling interval: 30 seconds (matches heartbeat frequency)
25
26
  const POLL_INTERVAL_MS = 30_000
@@ -129,11 +130,14 @@ async function executeStep(step) {
129
130
  }
130
131
 
131
132
  // 2. Fetch the skill from HQ to ensure it's deployed locally
132
- // Pass template_vars as ?vars= so HQ expands {{VAR_NAME}} in SKILL.md
133
+ // Merge minion variables into template_vars so HQ expands {{VAR_NAME}} in SKILL.md.
134
+ // Merge order: minion vars < project vars < workflow vars (template_vars already merged by HQ)
133
135
  if (skill_name) {
134
136
  try {
135
- const varsParam = template_vars
136
- ? `?vars=${Buffer.from(JSON.stringify(template_vars)).toString('base64')}`
137
+ const minionVars = variableStore.getAll('variables')
138
+ const mergedVars = { ...minionVars, ...(template_vars || {}) }
139
+ const varsParam = Object.keys(mergedVars).length > 0
140
+ ? `?vars=${Buffer.from(JSON.stringify(mergedVars)).toString('base64')}`
137
141
  : ''
138
142
  const fetchUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/fetch/${encodeURIComponent(skill_name)}${varsParam}`
139
143
  const fetchResp = await fetch(fetchUrl, {
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Template Expander
3
+ *
4
+ * Expands {{VAR_NAME}} placeholders in SKILL.md files using minion variables.
5
+ * Same syntax as project/workflow variable expansion done on HQ side.
6
+ *
7
+ * Design:
8
+ * - Variables (non-sensitive config) use {{VAR}} template expansion in skill content
9
+ * - Secrets (sensitive credentials) remain as environment variables ($ENV_VAR)
10
+ * - Expand at execution time (not write time) so variable changes take effect immediately
11
+ * - Save/restore original SKILL.md to avoid persisting expanded content
12
+ */
13
+
14
+ const fs = require('fs').promises
15
+ const path = require('path')
16
+ const { config } = require('../config')
17
+ const variableStore = require('../stores/variable-store')
18
+
19
+ /**
20
+ * Expand {{VAR_NAME}} template placeholders in content string.
21
+ * Unmatched placeholders are left as-is (same behavior as HQ-side expansion).
22
+ *
23
+ * @param {string} content - Skill content with {{VAR}} placeholders
24
+ * @param {Record<string, string>} vars - Key-value pairs for substitution
25
+ * @returns {string} Content with matched placeholders replaced
26
+ */
27
+ function expandTemplateVars(content, vars) {
28
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) =>
29
+ vars[key] !== undefined ? vars[key] : match
30
+ )
31
+ }
32
+
33
+ /**
34
+ * Expand templates in SKILL.md files for the given skill names.
35
+ * Reads each SKILL.md, expands {{VAR}} with minion variables, writes back.
36
+ * Returns the original contents for restoration after execution.
37
+ *
38
+ * @param {string[]} skillNames - Skill slugs to expand
39
+ * @returns {Promise<Map<string, string>>} Map of skillName -> original content (for restoration)
40
+ */
41
+ async function expandSkillTemplates(skillNames) {
42
+ const minionVars = variableStore.getAll('variables')
43
+
44
+ // If no variables defined, skip
45
+ if (Object.keys(minionVars).length === 0) return new Map()
46
+
47
+ const originals = new Map()
48
+ const skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
49
+
50
+ for (const name of skillNames) {
51
+ const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
52
+ try {
53
+ const original = await fs.readFile(skillMdPath, 'utf-8')
54
+ const expanded = expandTemplateVars(original, minionVars)
55
+ if (expanded !== original) {
56
+ originals.set(name, original)
57
+ await fs.writeFile(skillMdPath, expanded, 'utf-8')
58
+ console.log(`[TemplateExpander] Expanded {{VAR}} in skill: ${name}`)
59
+ }
60
+ } catch (err) {
61
+ console.error(`[TemplateExpander] Failed to expand ${name}: ${err.message}`)
62
+ }
63
+ }
64
+
65
+ return originals
66
+ }
67
+
68
+ /**
69
+ * Restore original SKILL.md contents after execution.
70
+ *
71
+ * @param {Map<string, string>} originals - Map from expandSkillTemplates
72
+ */
73
+ async function restoreSkillTemplates(originals) {
74
+ const skillsDir = path.join(config.HOME_DIR, '.claude', 'skills')
75
+ for (const [name, content] of originals) {
76
+ const skillMdPath = path.join(skillsDir, name, 'SKILL.md')
77
+ try {
78
+ await fs.writeFile(skillMdPath, content, 'utf-8')
79
+ } catch (err) {
80
+ console.error(`[TemplateExpander] Failed to restore ${name}: ${err.message}`)
81
+ }
82
+ }
83
+ }
84
+
85
+ module.exports = { expandTemplateVars, expandSkillTemplates, restoreSkillTemplates }
@@ -89,6 +89,8 @@ function get(type, key) {
89
89
 
90
90
  /**
91
91
  * Set a key-value pair (creates or updates).
92
+ * Only secrets are synced to process.env (for child process inheritance).
93
+ * Variables use {{VAR}} template expansion in skill content instead.
92
94
  * @param {'secrets' | 'variables'} type
93
95
  * @param {string} key
94
96
  * @param {string} value
@@ -98,8 +100,10 @@ function set(type, key, value) {
98
100
  const data = parseEnvFile(filePath)
99
101
  data[key] = value
100
102
  writeEnvFile(filePath, data)
101
- // Sync to process.env so running child processes inherit the updated value
102
- process.env[key] = value
103
+ // Only sync secrets to process.env; variables use template expansion instead
104
+ if (type === 'secrets') {
105
+ process.env[key] = value
106
+ }
103
107
  console.log(`[VariableStore] Set ${type} key: ${key}`)
104
108
  }
105
109
 
@@ -115,8 +119,10 @@ function remove(type, key) {
115
119
  if (!(key in data)) return false
116
120
  delete data[key]
117
121
  writeEnvFile(filePath, data)
118
- // Sync to process.env so running child processes no longer inherit the removed value
119
- delete process.env[key]
122
+ // Only sync secrets to process.env; variables use template expansion instead
123
+ if (type === 'secrets') {
124
+ delete process.env[key]
125
+ }
120
126
  console.log(`[VariableStore] Removed ${type} key: ${key}`)
121
127
  return true
122
128
  }
@@ -131,18 +137,14 @@ function listKeys(type) {
131
137
  }
132
138
 
133
139
  /**
134
- * Build a merged environment object from minion variables and secrets.
135
- * Used by workflow/routine runners to inject into execution environment.
136
- * Secrets override variables when keys collide.
140
+ * Build environment object from minion secrets only.
141
+ * Variables are no longer injected as env vars; they use {{VAR}} template
142
+ * expansion in skill content (same mechanism as project/workflow variables).
137
143
  *
138
- * @param {Record<string, string>} [extraVars] - Additional variables (e.g. project/workflow vars from HQ)
139
- * @returns {Record<string, string>} Merged key-value pairs
144
+ * @returns {Record<string, string>} Secret key-value pairs for process.env
140
145
  */
141
- function buildEnv(extraVars = {}) {
142
- const variables = getAll('variables')
143
- const secrets = getAll('secrets')
144
- // Merge order: variables < secrets < extraVars (later wins)
145
- return { ...variables, ...secrets, ...extraVars }
146
+ function buildEnv() {
147
+ return getAll('secrets')
146
148
  }
147
149
 
148
150
  module.exports = {
@@ -544,11 +544,20 @@ Response:
544
544
  }
545
545
  ```
546
546
 
547
- ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)。実行時のマージ順序:
548
- 1. ミニオンローカル変数
549
- 2. ミニオンシークレット
550
- 3. プロジェクト変数
551
- 4. ワークフロー変数(最優先)
547
+ ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)。
548
+
549
+ #### 変数とシークレットの違い
550
+
551
+ **変数**(ミニオン変数・プロジェクト変数・ワークフロー変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述する。
552
+
553
+ **シークレット**(ミニオンシークレット)は環境変数 `$SECRET_NAME` としてプロセスに注入される。APIキーやパスワード等の機密情報に使用する。テンプレート展開は行われない。
554
+
555
+ #### テンプレート変数の展開優先順位
556
+
557
+ 同名の変数が複数のスコープで定義されている場合、以下の順序で上書きされる:
558
+ 1. ミニオン変数(最低優先)
559
+ 2. プロジェクト変数
560
+ 3. ワークフロー変数(最優先)
552
561
 
553
562
  ### Workflows (project-scoped, versioned)
554
563
 
@@ -49,7 +49,7 @@ Use {{PROJECT_VAR}} to reference project/workflow variables.
49
49
 
50
50
  ### テンプレート変数
51
51
 
52
- スキル本文中で `{{VAR_NAME}}` と記述すると、ワークフロー実行時にHQ上のプロジェクト変数・ワークフロー変数の値で自動的に置換される。スキルを再利用しつつ、プロジェクトごとに異なるパラメータを渡したい場合に使う。
52
+ スキル本文中で `{{VAR_NAME}}` と記述すると、実行時に変数の値で自動的に置換される。スキルを再利用しつつ、スコープごとに異なるパラメータを渡したい場合に使う。
53
53
 
54
54
  ```markdown
55
55
  ---
@@ -62,8 +62,19 @@ description: サイトをデプロイする
62
62
 
63
63
  - 変数名は英数字とアンダースコアのみ(`\w+`)
64
64
  - 未定義の変数は `{{VAR_NAME}}` のまま残る(エラーにはならない)
65
- - ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)
66
- - ミニオン変数・シークレットは `process.env` 経由で利用可能(テンプレートではなく環境変数として)
65
+ - 展開優先順位: ミニオン変数 < プロジェクト変数 < ワークフロー変数(後者が上書き)
66
+ - ルーティン実行時もミニオン変数による `{{VAR}}` 展開が行われる
67
+
68
+ ### 変数とシークレットの使い分け
69
+
70
+ | 種別 | 構文 | 用途 | 例 |
71
+ |------|------|------|-----|
72
+ | 変数 | `{{VAR_NAME}}` | 設定・パラメータ(非機密) | デプロイ先、サイトURL、プロジェクト名 |
73
+ | シークレット | `$SECRET_NAME`(環境変数) | 機密情報 | APIキー、パスワード、トークン |
74
+
75
+ - **変数**はスキル本文のテンプレートとして展開される。全スコープ(ミニオン・プロジェクト・ワークフロー)で同じ `{{VAR}}` 構文を使用する
76
+ - **シークレット**は環境変数としてプロセスに注入される。テンプレート展開は行われない
77
+ - デイリーログやメモリーから変数・シークレットの値を推測して使用しないこと
67
78
 
68
79
  ### 2. HQ に反映する
69
80
 
@@ -22,6 +22,7 @@ const executionStore = require('../core/stores/execution-store')
22
22
  const routineStore = require('../core/stores/routine-store')
23
23
  const logManager = require('../core/lib/log-manager')
24
24
  const runningTasks = require('../core/lib/running-tasks')
25
+ const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
25
26
 
26
27
  // Active cron jobs keyed by routine ID
27
28
  const activeJobs = new Map()
@@ -83,6 +84,17 @@ async function executeRoutineSession(routine, executionId, skillNames) {
83
84
  console.log(`[RoutineRunner] tmux session: ${sessionName}`)
84
85
  console.log(`[RoutineRunner] Log file: ${logFile}`)
85
86
 
87
+ // Expand {{VAR}} templates in SKILL.md files with minion variables
88
+ let expandedOriginals = new Map()
89
+ try {
90
+ expandedOriginals = await expandSkillTemplates(skillNames)
91
+ if (expandedOriginals.size > 0) {
92
+ console.log(`[RoutineRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
93
+ }
94
+ } catch (err) {
95
+ console.error(`[RoutineRunner] Template expansion failed: ${err.message}`)
96
+ }
97
+
86
98
  try {
87
99
  // Ensure log directory exists and prune old logs
88
100
  await logManager.ensureLogDir()
@@ -102,7 +114,7 @@ async function executeRoutineSession(routine, executionId, skillNames) {
102
114
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
103
115
 
104
116
  // Create tmux session with the LLM command.
105
- // PATH, HOME, DISPLAY, and minion variables/secrets are already set in
117
+ // PATH, HOME, DISPLAY, and minion secrets are already set in
106
118
  // process.env at server startup, so child processes inherit them automatically.
107
119
  // Per-execution identifiers are passed via -e flags for the session environment.
108
120
  const tmuxCommand = [
@@ -173,6 +185,11 @@ async function executeRoutineSession(routine, executionId, skillNames) {
173
185
  } catch (error) {
174
186
  console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
175
187
  return { success: false, error: error.message, sessionName }
188
+ } finally {
189
+ // Restore original SKILL.md files after execution
190
+ if (expandedOriginals.size > 0) {
191
+ await restoreSkillTemplates(expandedOriginals)
192
+ }
176
193
  }
177
194
  }
178
195
 
package/linux/server.js CHANGED
@@ -302,13 +302,14 @@ async function start() {
302
302
  process.env.HOME = config.HOME_DIR
303
303
  process.env.DISPLAY = process.env.DISPLAY || ':99'
304
304
 
305
- // Load minion variables/secrets into process.env for child process inheritance
305
+ // Load minion secrets into process.env for child process inheritance.
306
+ // Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
306
307
  const variableStore = require('../core/stores/variable-store')
307
308
  const minionEnv = variableStore.buildEnv()
308
309
  for (const [key, value] of Object.entries(minionEnv)) {
309
310
  if (!(key in process.env)) process.env[key] = value
310
311
  }
311
- console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion variables/secrets into process.env`)
312
+ console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
312
313
 
313
314
  // Sync bundled assets
314
315
  syncBundledRules()
@@ -21,6 +21,7 @@ const executionStore = require('../core/stores/execution-store')
21
21
  const workflowStore = require('../core/stores/workflow-store')
22
22
  const logManager = require('../core/lib/log-manager')
23
23
  const runningTasks = require('../core/lib/running-tasks')
24
+ const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
24
25
 
25
26
  // Active cron jobs keyed by workflow ID
26
27
  const activeJobs = new Map()
@@ -87,6 +88,17 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
87
88
  console.log(`[WorkflowRunner] Log file: ${logFile}`)
88
89
  console.log(`[WorkflowRunner] HOME: ${homeDir}`)
89
90
 
91
+ // Expand {{VAR}} templates in SKILL.md files with minion variables
92
+ let expandedOriginals = new Map()
93
+ try {
94
+ expandedOriginals = await expandSkillTemplates(skillNames)
95
+ if (expandedOriginals.size > 0) {
96
+ console.log(`[WorkflowRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
97
+ }
98
+ } catch (err) {
99
+ console.error(`[WorkflowRunner] Template expansion failed: ${err.message}`)
100
+ }
101
+
90
102
  try {
91
103
  // Ensure log directory exists and prune old logs
92
104
  await logManager.ensureLogDir()
@@ -106,7 +118,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
106
118
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
107
119
 
108
120
  // Create tmux session with the LLM command.
109
- // PATH, HOME, DISPLAY, and minion variables/secrets are already set in
121
+ // PATH, HOME, DISPLAY, and minion secrets are already set in
110
122
  // process.env at server startup, so child processes inherit them automatically.
111
123
  const tmuxCommand = [
112
124
  'tmux new-session -d',
@@ -173,6 +185,11 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
173
185
  } catch (error) {
174
186
  console.error(`[WorkflowRunner] Workflow ${workflow.name} failed: ${error.message}`)
175
187
  return { success: false, error: error.message, sessionName }
188
+ } finally {
189
+ // Restore original SKILL.md files after execution
190
+ if (expandedOriginals.size > 0) {
191
+ await restoreSkillTemplates(expandedOriginals)
192
+ }
176
193
  }
177
194
  }
178
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.5.35",
3
+ "version": "3.6.2",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
package/rules/core.md CHANGED
@@ -133,9 +133,11 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
133
133
  - `MINION_ROUTINE_ID` — ルーティンUUID
134
134
  - `MINION_ROUTINE_NAME` — ルーティン名
135
135
 
136
- ミニオン変数・シークレット(HQ UIまたはAPI経由で設定)はサーバー起動時に `process.env` にロードされ、全子プロセスで利用可能。
136
+ **変数**(ミニオン変数・プロジェクト変数・ワークフロー変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。展開優先順位: ミニオン変数 < プロジェクト変数 < ワークフロー変数(後者が優先)。
137
137
 
138
- プロジェクト変数・ワークフロー変数はスキル本文の `{{VAR_NAME}}` テンプレートとして展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。
138
+ **シークレット**(ミニオンシークレット)はサーバー起動時に `process.env` にロードされ、全子プロセスで環境変数 `$SECRET_NAME` として利用可能。APIキーやパスワード等の機密情報に使用する。シークレットは `{{VAR}}` テンプレートでは展開されない。
139
+
140
+ デイリーログやメモリーから変数・シークレットの値を推測して使用してはならない。変数は `{{VAR_NAME}}` テンプレートとして定義し、シークレットは環境変数として参照すること。
139
141
 
140
142
  ## Skills Directory
141
143
 
@@ -451,7 +451,7 @@ function Restart-MinionService {
451
451
  # ============================================================
452
452
 
453
453
  function Invoke-Setup {
454
- $totalSteps = 11
454
+ $totalSteps = 12
455
455
 
456
456
  # Minionization warning
457
457
  Write-Host ""
@@ -877,8 +877,52 @@ function Invoke-Setup {
877
877
  Write-Warn "websockify not available, VNC WebSocket proxy will not be registered"
878
878
  }
879
879
 
880
+ # Step 10: Download and register cloudflared
881
+ Write-Step 10 $totalSteps "Setting up Cloudflare Tunnel (cloudflared)..."
882
+ $cfExe = $null
883
+ if (Get-Command cloudflared -ErrorAction SilentlyContinue) {
884
+ $cfExe = (Get-Command cloudflared).Source
885
+ Write-Detail "cloudflared already installed: $cfExe"
886
+ } else {
887
+ Write-Host " Downloading cloudflared..."
888
+ try {
889
+ $cfUrl = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
890
+ $cfPath = Join-Path $DataDir 'cloudflared.exe'
891
+ Invoke-WebRequest -Uri $cfUrl -OutFile $cfPath -UseBasicParsing
892
+ $cfExe = $cfPath
893
+ Write-Detail "cloudflared downloaded to $cfExe"
894
+ }
895
+ catch {
896
+ Write-Warn "Failed to download cloudflared: $_"
897
+ Write-Host " You can install manually or re-run setup later." -ForegroundColor Gray
898
+ }
899
+ }
900
+ if ($cfExe) {
901
+ # Register cloudflared as NSSM service (config will be set by 'configure --setup-tunnel')
902
+ Invoke-Nssm stop minion-cloudflared
903
+ Invoke-Nssm remove minion-cloudflared confirm
904
+ $cfConfigDir = Join-Path $TargetUserProfile '.cloudflared'
905
+ $cfConfigPath = Join-Path $cfConfigDir 'config.yml'
906
+ Invoke-Nssm install minion-cloudflared $cfExe "tunnel run --config `"$cfConfigPath`""
907
+ Invoke-Nssm set minion-cloudflared Start SERVICE_DEMAND_START
908
+ Invoke-Nssm set minion-cloudflared DisplayName "Minion Cloudflared"
909
+ Invoke-Nssm set minion-cloudflared Description "Cloudflare Tunnel for Minion"
910
+ Invoke-Nssm set minion-cloudflared AppRestartDelay 5000
911
+ Invoke-Nssm set minion-cloudflared AppStdout (Join-Path $LogDir 'cloudflared-stdout.log')
912
+ Invoke-Nssm set minion-cloudflared AppStderr (Join-Path $LogDir 'cloudflared-stderr.log')
913
+ Invoke-Nssm set minion-cloudflared AppStdoutCreationDisposition 4
914
+ Invoke-Nssm set minion-cloudflared AppStderrCreationDisposition 4
915
+ Invoke-Nssm set minion-cloudflared AppRotateFiles 1
916
+ Invoke-Nssm set minion-cloudflared AppRotateBytes 10485760
917
+ Grant-ServiceControlToUser 'minion-cloudflared' $setupUserSid
918
+ if ($targetUserSid -and $targetUserSid -ne $setupUserSid) {
919
+ Grant-ServiceControlToUser 'minion-cloudflared' $targetUserSid
920
+ }
921
+ Write-Detail "minion-cloudflared service registered (starts via 'configure --setup-tunnel')"
922
+ }
923
+
880
924
  # Step 11: Disable screensaver, lock screen, and sleep
881
- Write-Step 10 $totalSteps "Disabling screensaver, lock screen, and sleep..."
925
+ Write-Step 11 $totalSteps "Disabling screensaver, lock screen, and sleep..."
882
926
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
883
927
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
884
928
  Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value ''
@@ -892,7 +936,7 @@ function Invoke-Setup {
892
936
  Write-Detail "Sleep and monitor timeout disabled"
893
937
 
894
938
  # Configure firewall rules
895
- Write-Step 11 $totalSteps "Configuring firewall rules..."
939
+ Write-Step 12 $totalSteps "Configuring firewall rules..."
896
940
  $fwRules = @(
897
941
  @{ Name = 'Minion Agent'; Port = 8080 },
898
942
  @{ Name = 'Minion Terminal'; Port = 7681 },
@@ -985,6 +1029,7 @@ function Invoke-Setup {
985
1029
  Write-Host "Services registered (not yet started):"
986
1030
  Write-Host " minion-agent - AI Agent (port 8080)"
987
1031
  Write-Host " minion-websockify - WebSocket proxy (port 6080)"
1032
+ Write-Host " minion-cloudflared - Cloudflare Tunnel (starts via configure --setup-tunnel)"
988
1033
  Write-Host " MinionVNC (task) - TightVNC (port 5900, starts at logon)"
989
1034
  Write-Host ""
990
1035
  Write-Host "Next step: Connect to HQ (run as regular user):" -ForegroundColor Yellow
@@ -1303,21 +1348,44 @@ function Invoke-Configure {
1303
1348
  [System.IO.File]::WriteAllText((Join-Path $cfConfigDir 'config.yml'), $tunnelData.config_yml, [System.Text.UTF8Encoding]::new($false))
1304
1349
  Write-Detail "Tunnel config saved"
1305
1350
 
1306
- # Register cloudflared as NSSM service (requires admin — will be skipped if non-admin)
1307
- $cfExe = $null
1308
- if (Get-Command cloudflared -ErrorAction SilentlyContinue) {
1309
- $cfExe = (Get-Command cloudflared).Source
1310
- } elseif (Test-Path (Join-Path $DataDir 'cloudflared.exe')) {
1311
- $cfExe = Join-Path $DataDir 'cloudflared.exe'
1312
- }
1313
- if ($cfExe) {
1314
- # Check if cloudflared service already registered
1315
- $cfState = Get-ServiceState 'minion-cloudflared'
1316
- if (-not $cfState) {
1317
- Write-Warn "minion-cloudflared service not registered. Run 'setup' as admin to register tunnel service."
1318
- } else {
1319
- sc.exe start minion-cloudflared 2>&1 | Out-Null
1320
- Write-Detail "minion-cloudflared started"
1351
+ # Start cloudflared service (registered during setup)
1352
+ $cfState = Get-ServiceState 'minion-cloudflared'
1353
+ if ($cfState) {
1354
+ # Enable auto-start now that tunnel config is in place
1355
+ Invoke-Nssm set minion-cloudflared Start SERVICE_AUTO_START 2>&1 | Out-Null
1356
+ sc.exe start minion-cloudflared 2>&1 | Out-Null
1357
+ Write-Detail "minion-cloudflared started"
1358
+ } else {
1359
+ # Fallback: service not registered (e.g. setup was run before v3.6.2)
1360
+ $cfExe = $null
1361
+ if (Get-Command cloudflared -ErrorAction SilentlyContinue) {
1362
+ $cfExe = (Get-Command cloudflared).Source
1363
+ } elseif (Test-Path (Join-Path $DataDir 'cloudflared.exe')) {
1364
+ $cfExe = Join-Path $DataDir 'cloudflared.exe'
1365
+ }
1366
+ if ($cfExe) {
1367
+ $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
1368
+ if ($isAdmin) {
1369
+ $cfConfigPath = Join-Path $cfConfigDir 'config.yml'
1370
+ Invoke-Nssm install minion-cloudflared $cfExe "tunnel run --config `"$cfConfigPath`""
1371
+ Invoke-Nssm set minion-cloudflared Start SERVICE_AUTO_START
1372
+ Invoke-Nssm set minion-cloudflared DisplayName "Minion Cloudflared"
1373
+ Invoke-Nssm set minion-cloudflared Description "Cloudflare Tunnel for Minion"
1374
+ Invoke-Nssm set minion-cloudflared AppRestartDelay 5000
1375
+ Invoke-Nssm set minion-cloudflared AppStdout (Join-Path $LogDir 'cloudflared-stdout.log')
1376
+ Invoke-Nssm set minion-cloudflared AppStderr (Join-Path $LogDir 'cloudflared-stderr.log')
1377
+ Invoke-Nssm set minion-cloudflared AppStdoutCreationDisposition 4
1378
+ Invoke-Nssm set minion-cloudflared AppStderrCreationDisposition 4
1379
+ Invoke-Nssm set minion-cloudflared AppRotateFiles 1
1380
+ Invoke-Nssm set minion-cloudflared AppRotateBytes 10485760
1381
+ $setupUserSid = Get-SetupUserSid
1382
+ Grant-ServiceControlToUser 'minion-cloudflared' $setupUserSid
1383
+ Write-Detail "minion-cloudflared service registered (fallback)"
1384
+ sc.exe start minion-cloudflared 2>&1 | Out-Null
1385
+ Write-Detail "minion-cloudflared started"
1386
+ } else {
1387
+ Write-Warn "minion-cloudflared service not registered. Run 'setup' as admin to register, or run 'configure --setup-tunnel' as admin."
1388
+ }
1321
1389
  }
1322
1390
  }
1323
1391
  }
@@ -17,6 +17,7 @@ const executionStore = require('../core/stores/execution-store')
17
17
  const routineStore = require('../core/stores/routine-store')
18
18
  const logManager = require('../core/lib/log-manager')
19
19
  const { activeSessions } = require('./workflow-runner')
20
+ const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
20
21
 
21
22
  const activeJobs = new Map()
22
23
  const runningExecutions = new Map()
@@ -60,6 +61,17 @@ async function executeRoutineSession(routine, executionId, skillNames) {
60
61
  console.log(`[RoutineRunner] Session: ${sessionName}`)
61
62
  console.log(`[RoutineRunner] Log file: ${logFile}`)
62
63
 
64
+ // Expand {{VAR}} templates in SKILL.md files with minion variables
65
+ let expandedOriginals = new Map()
66
+ try {
67
+ expandedOriginals = await expandSkillTemplates(skillNames)
68
+ if (expandedOriginals.size > 0) {
69
+ console.log(`[RoutineRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
70
+ }
71
+ } catch (err) {
72
+ console.error(`[RoutineRunner] Template expansion failed: ${err.message}`)
73
+ }
74
+
63
75
  try {
64
76
  await logManager.ensureLogDir()
65
77
  await logManager.pruneOldLogs()
@@ -76,7 +88,7 @@ async function executeRoutineSession(routine, executionId, skillNames) {
76
88
  const escapedPrompt = prompt.replace(/'/g, "''")
77
89
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
78
90
 
79
- // PATH, HOME, USERPROFILE, and minion variables/secrets are already set in
91
+ // PATH, HOME, USERPROFILE, and minion secrets are already set in
80
92
  // process.env at server startup. Per-execution identifiers are added here.
81
93
  const env = {
82
94
  ...process.env,
@@ -147,6 +159,11 @@ async function executeRoutineSession(routine, executionId, skillNames) {
147
159
  } catch (error) {
148
160
  console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
149
161
  return { success: false, error: error.message, sessionName }
162
+ } finally {
163
+ // Restore original SKILL.md files after execution
164
+ if (expandedOriginals.size > 0) {
165
+ await restoreSkillTemplates(expandedOriginals)
166
+ }
150
167
  }
151
168
  }
152
169
 
package/win/server.js CHANGED
@@ -249,13 +249,14 @@ async function start() {
249
249
  process.env.HOME = config.HOME_DIR
250
250
  process.env.USERPROFILE = config.HOME_DIR
251
251
 
252
- // Load minion variables/secrets into process.env for child process inheritance
252
+ // Load minion secrets into process.env for child process inheritance.
253
+ // Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
253
254
  const variableStore = require('../core/stores/variable-store')
254
255
  const minionEnv = variableStore.buildEnv()
255
256
  for (const [key, value] of Object.entries(minionEnv)) {
256
257
  if (!(key in process.env)) process.env[key] = value
257
258
  }
258
- console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion variables/secrets into process.env`)
259
+ console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
259
260
 
260
261
  // Sync bundled assets
261
262
  syncBundledRules()
@@ -23,6 +23,7 @@ const { config } = require('../core/config')
23
23
  const executionStore = require('../core/stores/execution-store')
24
24
  const workflowStore = require('../core/stores/workflow-store')
25
25
  const logManager = require('../core/lib/log-manager')
26
+ const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
26
27
 
27
28
  // Active cron jobs keyed by workflow ID
28
29
  const activeJobs = new Map()
@@ -92,6 +93,17 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
92
93
  console.log(`[WorkflowRunner] Log file: ${logFile}`)
93
94
  console.log(`[WorkflowRunner] HOME: ${homeDir}`)
94
95
 
96
+ // Expand {{VAR}} templates in SKILL.md files with minion variables
97
+ let expandedOriginals = new Map()
98
+ try {
99
+ expandedOriginals = await expandSkillTemplates(skillNames)
100
+ if (expandedOriginals.size > 0) {
101
+ console.log(`[WorkflowRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
102
+ }
103
+ } catch (err) {
104
+ console.error(`[WorkflowRunner] Template expansion failed: ${err.message}`)
105
+ }
106
+
95
107
  try {
96
108
  await logManager.ensureLogDir()
97
109
  await logManager.pruneOldLogs()
@@ -110,7 +122,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
110
122
  const escapedPrompt = prompt.replace(/'/g, "''")
111
123
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
112
124
 
113
- // PATH, HOME, USERPROFILE, and minion variables/secrets are already set in
125
+ // PATH, HOME, USERPROFILE, and minion secrets are already set in
114
126
  // process.env at server startup, so child processes inherit them automatically.
115
127
 
116
128
  // Open log file for streaming writes
@@ -192,6 +204,11 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
192
204
  } catch (error) {
193
205
  console.error(`[WorkflowRunner] Workflow ${workflow.name} failed: ${error.message}`)
194
206
  return { success: false, error: error.message, sessionName }
207
+ } finally {
208
+ // Restore original SKILL.md files after execution
209
+ if (expandedOriginals.size > 0) {
210
+ await restoreSkillTemplates(expandedOriginals)
211
+ }
195
212
  }
196
213
  }
197
214