@geekbeer/minion 3.53.1 → 3.57.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/core/db/migrations/20260510100000_secrets_workspace.js +36 -0
- package/core/db/migrations/20260511100000_variables_workspace.js +37 -0
- package/core/lib/dag-step-poller.js +245 -1
- package/core/lib/session-env.js +96 -0
- package/core/lib/step-poller.js +8 -3
- package/core/lib/template-expander.js +20 -6
- package/core/routes/variables.js +67 -17
- package/core/stores/variable-store.js +278 -76
- package/docs/api-reference.md +55 -297
- package/docs/task-guides.md +76 -75
- package/linux/routes/chat.js +1 -16
- package/linux/routine-runner.js +49 -11
- package/linux/workflow-runner.js +19 -8
- package/package.json +1 -1
- package/rules/core.md +8 -10
- package/win/routes/chat.js +1 -14
- package/win/routine-runner.js +35 -8
- package/win/workflow-runner.js +12 -5
|
@@ -1,34 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Variable Store
|
|
3
3
|
*
|
|
4
|
-
* Manages minion-local
|
|
5
|
-
* - Secrets: ~/.minion/.env.secrets (or DATA_DIR/.env.secrets)
|
|
6
|
-
* - Variables: ~/.minion/.env.variables (or DATA_DIR/.env.variables)
|
|
4
|
+
* Manages minion-local variables and secrets — both workspace-scoped.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Storage: SQLite tables `variables(workspace_id, key, value, updated_at)`
|
|
7
|
+
* and `secrets(workspace_id, key, value, updated_at)`. `workspace_id=''` is
|
|
8
|
+
* the minion-wide bucket; any other value is a workspace-specific bucket.
|
|
9
|
+
*
|
|
10
|
+
* At skill execution time the runner / template-expander merges
|
|
11
|
+
* minion-wide + current-workspace entries for the relevant scope, with the
|
|
12
|
+
* WS-scoped value winning on conflict.
|
|
13
|
+
*
|
|
14
|
+
* Migration: on first read after the SQLite tables are created, any legacy
|
|
15
|
+
* `.env.variables` / `.env.secrets` flat files are imported into the
|
|
16
|
+
* minion-wide bucket and renamed to `.env.variables.migrated` /
|
|
17
|
+
* `.env.secrets.migrated` (kept as one-time backups).
|
|
18
|
+
*
|
|
19
|
+
* Secrets never leave the minion and are never persisted in HQ DB. Variables
|
|
20
|
+
* are non-sensitive and may also be defined at HQ scopes (workspace /
|
|
21
|
+
* project / workflow); see packages/docs-internal/src/content/docs/design/
|
|
22
|
+
* variables-and-secrets.md for the full resolution model.
|
|
10
23
|
*/
|
|
11
24
|
|
|
12
25
|
const fs = require('fs')
|
|
13
26
|
const path = require('path')
|
|
14
27
|
const { DATA_DIR } = require('../lib/platform')
|
|
15
28
|
const { config } = require('../config')
|
|
29
|
+
const { getDb } = require('../db')
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
|
-
* Resolve
|
|
19
|
-
* @param {'secrets' | 'variables'} type
|
|
32
|
+
* Resolve the legacy `.env.variables` path (for one-time migration).
|
|
20
33
|
* @returns {string}
|
|
21
34
|
*/
|
|
22
|
-
function
|
|
23
|
-
const filename = type === 'secrets' ? '.env.secrets' : '.env.variables'
|
|
35
|
+
function getLegacyVariablesFilePath() {
|
|
24
36
|
try {
|
|
25
37
|
fs.accessSync(DATA_DIR, fs.constants.W_OK)
|
|
26
|
-
return path.join(DATA_DIR,
|
|
38
|
+
return path.join(DATA_DIR, '.env.variables')
|
|
27
39
|
} catch {
|
|
28
|
-
return path.join(config.HOME_DIR, '.minion',
|
|
40
|
+
return path.join(config.HOME_DIR, '.minion', '.env.variables')
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the legacy `.env.secrets` path (for one-time migration).
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function getLegacySecretsFilePath() {
|
|
49
|
+
try {
|
|
50
|
+
fs.accessSync(DATA_DIR, fs.constants.W_OK)
|
|
51
|
+
return path.join(DATA_DIR, '.env.secrets')
|
|
52
|
+
} catch {
|
|
53
|
+
return path.join(config.HOME_DIR, '.minion', '.env.secrets')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize a workspace identifier to its canonical string form.
|
|
59
|
+
* '' (empty string) represents the minion-wide bucket.
|
|
60
|
+
* @param {string|null|undefined} workspaceId
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function normalizeWorkspaceId(workspaceId) {
|
|
64
|
+
return workspaceId == null ? '' : String(workspaceId)
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
/**
|
|
33
68
|
* Parse a .env file into a key-value object.
|
|
34
69
|
* @param {string} filePath
|
|
@@ -54,105 +89,272 @@ function parseEnvFile(filePath) {
|
|
|
54
89
|
return result
|
|
55
90
|
}
|
|
56
91
|
|
|
92
|
+
// ─── Legacy flat-file → SQLite migration ────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
let _legacyVariablesMigrationDone = false
|
|
95
|
+
let _legacySecretsMigrationDone = false
|
|
96
|
+
|
|
97
|
+
function migrateLegacyFile(legacyPath, table, doneFlagRef) {
|
|
98
|
+
if (doneFlagRef.done) return
|
|
99
|
+
doneFlagRef.done = true
|
|
100
|
+
|
|
101
|
+
let entries
|
|
102
|
+
try {
|
|
103
|
+
entries = parseEnvFile(legacyPath)
|
|
104
|
+
} catch {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const keys = Object.keys(entries)
|
|
108
|
+
if (keys.length === 0) return
|
|
109
|
+
|
|
110
|
+
const db = getDb()
|
|
111
|
+
const now = Date.now()
|
|
112
|
+
const insert = db.prepare(
|
|
113
|
+
`INSERT OR IGNORE INTO ${table} (workspace_id, key, value, updated_at) VALUES (?, ?, ?, ?)`
|
|
114
|
+
)
|
|
115
|
+
const tx = db.transaction(() => {
|
|
116
|
+
for (const k of keys) {
|
|
117
|
+
insert.run('', k, String(entries[k] ?? ''), now)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
tx()
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
fs.renameSync(legacyPath, `${legacyPath}.migrated`)
|
|
124
|
+
console.log(`[VariableStore] Migrated ${keys.length} legacy ${table} entry/ies from ${legacyPath} into SQLite (minion-wide scope)`)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error(`[VariableStore] Migrated ${table} to SQLite but failed to rename ${legacyPath}: ${err.message}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const _variablesFlag = { done: false }
|
|
131
|
+
const _secretsFlag = { done: false }
|
|
132
|
+
|
|
133
|
+
function migrateLegacyVariablesFile() {
|
|
134
|
+
migrateLegacyFile(getLegacyVariablesFilePath(), 'variables', _variablesFlag)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function migrateLegacySecretsFile() {
|
|
138
|
+
migrateLegacyFile(getLegacySecretsFilePath(), 'secrets', _secretsFlag)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Generic table operations (variables and secrets share the shape) ───────
|
|
142
|
+
|
|
143
|
+
function getRowsForScope(table, workspaceId) {
|
|
144
|
+
const wsId = normalizeWorkspaceId(workspaceId)
|
|
145
|
+
const db = getDb()
|
|
146
|
+
return db.prepare(`SELECT key, value FROM ${table} WHERE workspace_id = ? ORDER BY key`).all(wsId)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getScopeAsObject(table, workspaceId) {
|
|
150
|
+
const rows = getRowsForScope(table, workspaceId)
|
|
151
|
+
const out = {}
|
|
152
|
+
for (const r of rows) out[r.key] = r.value
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getEffective(table, workspaceId) {
|
|
157
|
+
const wide = getScopeAsObject(table, '')
|
|
158
|
+
if (!workspaceId) return wide
|
|
159
|
+
const scoped = getScopeAsObject(table, workspaceId)
|
|
160
|
+
return { ...wide, ...scoped }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function upsertEntry(table, workspaceId, key, value) {
|
|
164
|
+
const wsId = normalizeWorkspaceId(workspaceId)
|
|
165
|
+
const db = getDb()
|
|
166
|
+
db.prepare(`
|
|
167
|
+
INSERT INTO ${table} (workspace_id, key, value, updated_at) VALUES (?, ?, ?, ?)
|
|
168
|
+
ON CONFLICT(workspace_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
169
|
+
`).run(wsId, key, String(value ?? ''), Date.now())
|
|
170
|
+
return wsId
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function deleteEntry(table, workspaceId, key) {
|
|
174
|
+
const wsId = normalizeWorkspaceId(workspaceId)
|
|
175
|
+
const db = getDb()
|
|
176
|
+
const info = db.prepare(`DELETE FROM ${table} WHERE workspace_id = ? AND key = ?`).run(wsId, key)
|
|
177
|
+
return { removed: info.changes > 0, wsId }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function listScopes(table) {
|
|
181
|
+
const db = getDb()
|
|
182
|
+
const rows = db.prepare(`SELECT DISTINCT workspace_id FROM ${table}`).all()
|
|
183
|
+
return rows.map(r => r.workspace_id)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Variables (workspace-scoped) ────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function getVariablesForScope(workspaceId) {
|
|
189
|
+
migrateLegacyVariablesFile()
|
|
190
|
+
return getScopeAsObject('variables', workspaceId)
|
|
191
|
+
}
|
|
192
|
+
|
|
57
193
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @param {Record<string, string>} data
|
|
194
|
+
* Merge minion-wide and workspace-scoped variables. Workspace-scoped values
|
|
195
|
+
* override minion-wide values when the key is the same.
|
|
61
196
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
197
|
+
function getEffectiveVariables(workspaceId) {
|
|
198
|
+
migrateLegacyVariablesFile()
|
|
199
|
+
return getEffective('variables', workspaceId)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getVariable(workspaceId, key) {
|
|
203
|
+
const all = getVariablesForScope(workspaceId)
|
|
204
|
+
return all[key] ?? null
|
|
205
|
+
}
|
|
65
206
|
|
|
66
|
-
|
|
67
|
-
|
|
207
|
+
function setVariable(workspaceId, key, value) {
|
|
208
|
+
migrateLegacyVariablesFile()
|
|
209
|
+
const wsId = upsertEntry('variables', workspaceId, key, value)
|
|
210
|
+
console.log(`[VariableStore] Set variable (scope=${wsId || 'minion-wide'}): ${key}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function removeVariable(workspaceId, key) {
|
|
214
|
+
migrateLegacyVariablesFile()
|
|
215
|
+
const { removed, wsId } = deleteEntry('variables', workspaceId, key)
|
|
216
|
+
if (removed) {
|
|
217
|
+
console.log(`[VariableStore] Removed variable (scope=${wsId || 'minion-wide'}): ${key}`)
|
|
218
|
+
}
|
|
219
|
+
return removed
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function listVariableKeys(workspaceId) {
|
|
223
|
+
migrateLegacyVariablesFile()
|
|
224
|
+
const rows = getRowsForScope('variables', workspaceId)
|
|
225
|
+
return rows.map(r => r.key)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function listVariableScopes() {
|
|
229
|
+
migrateLegacyVariablesFile()
|
|
230
|
+
return listScopes('variables')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Secrets (workspace-scoped, sensitive) ───────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function getSecretsForScope(workspaceId) {
|
|
236
|
+
migrateLegacySecretsFile()
|
|
237
|
+
return getScopeAsObject('secrets', workspaceId)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getEffectiveSecrets(workspaceId) {
|
|
241
|
+
migrateLegacySecretsFile()
|
|
242
|
+
return getEffective('secrets', workspaceId)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function listSecretKeys(workspaceId) {
|
|
246
|
+
migrateLegacySecretsFile()
|
|
247
|
+
const rows = getRowsForScope('secrets', workspaceId)
|
|
248
|
+
return rows.map(r => r.key)
|
|
68
249
|
}
|
|
69
250
|
|
|
70
251
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
252
|
+
* Set (insert or update) a secret value in the given scope.
|
|
253
|
+
*
|
|
254
|
+
* For minion-wide secrets (workspace_id=''), the value is also synced into
|
|
255
|
+
* `process.env` so in-process consumers (chat plugin sessions, ad-hoc Bash
|
|
256
|
+
* commands) see the change immediately without restarting the server.
|
|
257
|
+
*
|
|
258
|
+
* WS-scoped secrets are NEVER written to `process.env` to prevent cross-WS
|
|
259
|
+
* leakage; runners must call `getEffectiveSecrets(workspaceId)` and inject
|
|
260
|
+
* them explicitly into the session env.
|
|
74
261
|
*/
|
|
262
|
+
function setSecret(workspaceId, key, value) {
|
|
263
|
+
migrateLegacySecretsFile()
|
|
264
|
+
const wsId = upsertEntry('secrets', workspaceId, key, value)
|
|
265
|
+
if (wsId === '') {
|
|
266
|
+
process.env[key] = String(value ?? '')
|
|
267
|
+
}
|
|
268
|
+
console.log(`[VariableStore] Set secret (scope=${wsId || 'minion-wide'}): ${key}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeSecret(workspaceId, key) {
|
|
272
|
+
migrateLegacySecretsFile()
|
|
273
|
+
const { removed, wsId } = deleteEntry('secrets', workspaceId, key)
|
|
274
|
+
if (removed) {
|
|
275
|
+
if (wsId === '') {
|
|
276
|
+
delete process.env[key]
|
|
277
|
+
}
|
|
278
|
+
console.log(`[VariableStore] Removed secret (scope=${wsId || 'minion-wide'}): ${key}`)
|
|
279
|
+
}
|
|
280
|
+
return removed
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function listSecretScopes() {
|
|
284
|
+
migrateLegacySecretsFile()
|
|
285
|
+
return listScopes('secrets')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── Generic / legacy dispatchers (back-compat) ─────────────────────────────
|
|
289
|
+
//
|
|
290
|
+
// The original API used `getAll('variables')` / `getAll('secrets')` etc. with
|
|
291
|
+
// no workspace awareness. We retain the same names but route them to the
|
|
292
|
+
// minion-wide bucket so any straggler caller keeps working.
|
|
293
|
+
|
|
75
294
|
function getAll(type) {
|
|
76
|
-
|
|
295
|
+
if (type === 'secrets') return getSecretsForScope('')
|
|
296
|
+
return getVariablesForScope('')
|
|
77
297
|
}
|
|
78
298
|
|
|
79
|
-
/**
|
|
80
|
-
* Get a single value by key.
|
|
81
|
-
* @param {'secrets' | 'variables'} type
|
|
82
|
-
* @param {string} key
|
|
83
|
-
* @returns {string | null}
|
|
84
|
-
*/
|
|
85
299
|
function get(type, key) {
|
|
86
|
-
|
|
87
|
-
|
|
300
|
+
if (type === 'secrets') {
|
|
301
|
+
const all = getSecretsForScope('')
|
|
302
|
+
return all[key] ?? null
|
|
303
|
+
}
|
|
304
|
+
return getVariable('', key)
|
|
88
305
|
}
|
|
89
306
|
|
|
90
|
-
/**
|
|
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.
|
|
94
|
-
* @param {'secrets' | 'variables'} type
|
|
95
|
-
* @param {string} key
|
|
96
|
-
* @param {string} value
|
|
97
|
-
*/
|
|
98
307
|
function set(type, key, value) {
|
|
99
|
-
const filePath = getFilePath(type)
|
|
100
|
-
const data = parseEnvFile(filePath)
|
|
101
|
-
data[key] = value
|
|
102
|
-
writeEnvFile(filePath, data)
|
|
103
|
-
// Only sync secrets to process.env; variables use template expansion instead
|
|
104
308
|
if (type === 'secrets') {
|
|
105
|
-
|
|
309
|
+
setSecret('', key, value)
|
|
310
|
+
return
|
|
106
311
|
}
|
|
107
|
-
|
|
312
|
+
setVariable('', key, value)
|
|
108
313
|
}
|
|
109
314
|
|
|
110
|
-
/**
|
|
111
|
-
* Remove a key.
|
|
112
|
-
* @param {'secrets' | 'variables'} type
|
|
113
|
-
* @param {string} key
|
|
114
|
-
* @returns {boolean} true if key existed
|
|
115
|
-
*/
|
|
116
315
|
function remove(type, key) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!(key in data)) return false
|
|
120
|
-
delete data[key]
|
|
121
|
-
writeEnvFile(filePath, data)
|
|
122
|
-
// Only sync secrets to process.env; variables use template expansion instead
|
|
123
|
-
if (type === 'secrets') {
|
|
124
|
-
delete process.env[key]
|
|
125
|
-
}
|
|
126
|
-
console.log(`[VariableStore] Removed ${type} key: ${key}`)
|
|
127
|
-
return true
|
|
316
|
+
if (type === 'secrets') return removeSecret('', key)
|
|
317
|
+
return removeVariable('', key)
|
|
128
318
|
}
|
|
129
319
|
|
|
130
|
-
/**
|
|
131
|
-
* List all keys for a store type.
|
|
132
|
-
* @param {'secrets' | 'variables'} type
|
|
133
|
-
* @returns {string[]}
|
|
134
|
-
*/
|
|
135
320
|
function listKeys(type) {
|
|
136
|
-
|
|
321
|
+
if (type === 'secrets') return listSecretKeys('')
|
|
322
|
+
return listVariableKeys('')
|
|
137
323
|
}
|
|
138
324
|
|
|
139
325
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* expansion in skill content (same mechanism as project/workflow variables).
|
|
143
|
-
*
|
|
144
|
-
* @returns {Record<string, string>} Secret key-value pairs for process.env
|
|
326
|
+
* @deprecated Use `getEffectiveSecrets(workspaceId)` instead. Retained so any
|
|
327
|
+
* stragglers continue compiling; returns minion-wide secrets only.
|
|
145
328
|
*/
|
|
146
329
|
function buildEnv() {
|
|
147
|
-
return
|
|
330
|
+
return getSecretsForScope('')
|
|
148
331
|
}
|
|
149
332
|
|
|
150
333
|
module.exports = {
|
|
334
|
+
// Generic (back-compat)
|
|
151
335
|
getAll,
|
|
152
336
|
get,
|
|
153
337
|
set,
|
|
154
338
|
remove,
|
|
155
339
|
listKeys,
|
|
156
340
|
buildEnv,
|
|
157
|
-
|
|
341
|
+
|
|
342
|
+
// Variables (workspace-scoped)
|
|
343
|
+
getVariablesForScope,
|
|
344
|
+
getEffectiveVariables,
|
|
345
|
+
getVariable,
|
|
346
|
+
setVariable,
|
|
347
|
+
removeVariable,
|
|
348
|
+
listVariableKeys,
|
|
349
|
+
listVariableScopes,
|
|
350
|
+
migrateLegacyVariablesFile,
|
|
351
|
+
|
|
352
|
+
// Secrets (workspace-scoped)
|
|
353
|
+
getSecretsForScope,
|
|
354
|
+
getEffectiveSecrets,
|
|
355
|
+
listSecretKeys,
|
|
356
|
+
setSecret,
|
|
357
|
+
removeSecret,
|
|
358
|
+
listSecretScopes,
|
|
359
|
+
migrateLegacySecretsFile,
|
|
158
360
|
}
|