@geekbeer/minion 3.55.1 → 3.58.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/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 +66 -7
- package/linux/routine-runner.js +49 -11
- package/linux/workflow-runner.js +19 -8
- package/package.json +1 -1
- package/roles/engineer.md +3 -1
- package/roles/pm.md +6 -2
- package/rules/core.md +21 -12
- 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
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -504,7 +504,7 @@ POST `/api/threads` body (プロジェクト紐づきディスカッション):
|
|
|
504
504
|
| `thread_type` | string | No | `help`(デフォルト)or `discussion` |
|
|
505
505
|
| `title` | string | Yes | スレッドの要約 |
|
|
506
506
|
| `content` | string | Yes | スレッド本文(thread_messagesの最初のメッセージとして保存) |
|
|
507
|
-
| `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
|
|
507
|
+
| `mentions` | string[] | No | メンション対象。形式: `user:<auth_user_id>` (個別指名・推奨), `role:engineer`, `role:pm`, `role:accountant`, `minion:<minion_id>`, `user` (誰でも良い場合のフォールバック) |
|
|
508
508
|
| `context` | object | No | 任意のメタデータ(category, urgency, dag_execution_id等) |
|
|
509
509
|
|
|
510
510
|
**プロジェクト紐づけの使い分け:**
|
|
@@ -520,7 +520,11 @@ POST `/api/threads` body (プロジェクト紐づきディスカッション):
|
|
|
520
520
|
- メンションされたミニオンは優先的にスレッドを評価する
|
|
521
521
|
- メンションがない場合、全チームメンバーがLLMで関連性を判定してから参加
|
|
522
522
|
- トークン消費を抑えるため、当事者が明確な場合はメンションを推奨
|
|
523
|
-
|
|
523
|
+
|
|
524
|
+
**人間へのメンション (重要):**
|
|
525
|
+
1. **個別指名 `user:<auth_user_id>`** を最優先で使う。`/api/minion/me/project/:id/members` または `/api/minion/workspaces/:id/members` で人間メンバーの `user_id` を引いてから指定する。同名 `display_name` が複数いるワークスペースでも誤通知しない
|
|
526
|
+
2. **`role:pm` / `role:engineer` / `role:accountant`** はプロジェクトの該当ロール全員に届く(プロジェクト紐づきスレッドのみ)
|
|
527
|
+
3. **`user` (generic)** はプロジェクトメンバーの人間全員に届く。プロジェクト紐づきでないワークスペーススレッドでは workspace_members 全員。「誰でも良い」場合のフォールバック扱いで、原則 1 か 2 を先に検討する
|
|
524
528
|
|
|
525
529
|
POST `/api/threads/:id/messages` body:
|
|
526
530
|
```json
|
|
@@ -811,6 +815,46 @@ Response:
|
|
|
811
815
|
|
|
812
816
|
`role` is one of `"pm"` (project manager), `"engineer"`, or `"accountant"`.
|
|
813
817
|
|
|
818
|
+
### Project Members
|
|
819
|
+
|
|
820
|
+
| Method | Endpoint | Description |
|
|
821
|
+
|--------|----------|-------------|
|
|
822
|
+
| GET | `/api/minion/me/project/[id]/members` | プロジェクトのメンバー一覧(ミニオン+人間) |
|
|
823
|
+
|
|
824
|
+
Response:
|
|
825
|
+
```json
|
|
826
|
+
{
|
|
827
|
+
"minions": [
|
|
828
|
+
{ "minion_id": "uuid", "name": "Mary", "status": "online", "role": "pm", "joined_at": "..." }
|
|
829
|
+
],
|
|
830
|
+
"humans": [
|
|
831
|
+
{ "user_id": "uuid", "display_name": "yunoda", "email": "yunoda@example.com", "role": "engineer", "joined_at": "..." }
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
スレッドに `user:<auth_user_id>` で個別指名する前に、このエンドポイントで `user_id` を引いてくる。`display_name` が重複するワークスペースで誤通知を避けるため、ミニオンは必ず `email` または既知の `user_id` で当人を特定すること。`role` はプロジェクトロール (`pm` / `engineer` / `accountant`)。
|
|
837
|
+
|
|
838
|
+
### Workspace Members
|
|
839
|
+
|
|
840
|
+
| Method | Endpoint | Description |
|
|
841
|
+
|--------|----------|-------------|
|
|
842
|
+
| GET | `/api/minion/workspaces/[id]/members` | ワークスペースのメンバー一覧(人間+ミニオン) |
|
|
843
|
+
|
|
844
|
+
Response:
|
|
845
|
+
```json
|
|
846
|
+
{
|
|
847
|
+
"humans": [
|
|
848
|
+
{ "user_id": "uuid", "display_name": "yunoda", "email": "yunoda@example.com", "role": "owner", "joined_at": "..." }
|
|
849
|
+
],
|
|
850
|
+
"minions": [
|
|
851
|
+
{ "minion_id": "uuid", "name": "Mary", "status": "online", "joined_at": "..." }
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
プロジェクト紐づきがないワークスペーススレッド(朝作業ルーティンのブロッカー報告など)でメンション先を引きたいときに使う。`role` はワークスペースロール (`owner` / `admin` / `member`)。
|
|
857
|
+
|
|
814
858
|
### Project Context
|
|
815
859
|
|
|
816
860
|
| Method | Endpoint | Description |
|
|
@@ -849,15 +893,30 @@ Response:
|
|
|
849
893
|
|
|
850
894
|
#### 変数とシークレットの違い
|
|
851
895
|
|
|
852
|
-
|
|
896
|
+
**変数**(ワークスペース変数・プロジェクト変数・ワークフロー変数・ミニオン変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述する。
|
|
853
897
|
|
|
854
|
-
|
|
898
|
+
**シークレット**は環境変数 `$SECRET_NAME` としてプロセスに注入される。APIキーやパスワード等の機密情報に使用する。テンプレート展開は行われない。ワークスペース別にスコープ可能(後述)。
|
|
855
899
|
|
|
856
900
|
#### テンプレート変数の展開優先順位
|
|
857
901
|
|
|
858
|
-
|
|
859
|
-
1.
|
|
860
|
-
2.
|
|
902
|
+
同名の変数が複数のスコープで定義されている場合、以下の順序で上書きされる(後者が優先):
|
|
903
|
+
1. ワークスペース変数(最低優先・全ワークスペース配下で共有)
|
|
904
|
+
2. プロジェクト変数
|
|
905
|
+
3. ワークフロー変数
|
|
906
|
+
4. ミニオン変数(最優先・実行マシン固有の最終オーバーライド)
|
|
907
|
+
|
|
908
|
+
ワークスペース/プロジェクト/ワークフロー変数はHQ側で展開された SKILL.md がミニオンに配信され、最後にミニオンローカルでミニオン変数が展開される。
|
|
909
|
+
|
|
910
|
+
#### シークレットのワークスペーススコープ
|
|
911
|
+
|
|
912
|
+
1つのミニオンが複数ワークスペースを担当する場合、シークレットはワークスペース別に管理される:
|
|
913
|
+
|
|
914
|
+
| スコープ | 保存形式 | 注入先のセッション |
|
|
915
|
+
|---------|---------|---------------------|
|
|
916
|
+
| ミニオン全体 | SQLite `secrets` テーブル `workspace_id=''` | すべて(サーバー起動時 `process.env` にロード、子プロセスが継承) |
|
|
917
|
+
| ワークスペース別 | SQLite `secrets` テーブル `workspace_id=<uuid>` | そのワークスペースのコンテキストで動くランナーのみ(実行時注入、`process.env` には載らない) |
|
|
918
|
+
|
|
919
|
+
同名キーがある場合はワークスペース別が優先される。`/api/secrets/*` エンドポイントは `?workspace_id=<uuid>` クエリパラメータでスコープを指定する。省略または空文字でミニオン全体を操作する。シークレット値はHQ DBには保存されず、HQ APIはpass-throughとして中継するのみ。
|
|
861
920
|
|
|
862
921
|
### Project Tasks
|
|
863
922
|
|
package/linux/routine-runner.js
CHANGED
|
@@ -16,12 +16,14 @@ const crypto = require('crypto')
|
|
|
16
16
|
const fs = require('fs').promises
|
|
17
17
|
const execAsync = promisify(exec)
|
|
18
18
|
|
|
19
|
-
const { config } = require('../core/config')
|
|
19
|
+
const { config, isHqConfigured } = require('../core/config')
|
|
20
|
+
const api = require('../core/api')
|
|
20
21
|
const executionStore = require('../core/stores/execution-store')
|
|
21
22
|
const routineStore = require('../core/stores/routine-store')
|
|
22
23
|
const logManager = require('../core/lib/log-manager')
|
|
23
24
|
const runningTasks = require('../core/lib/running-tasks')
|
|
24
25
|
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
26
|
+
const { buildTmuxNewSessionCommand } = require('../core/lib/session-env')
|
|
25
27
|
const { getActivePrimary } = require('../core/llm-plugins/lib/active')
|
|
26
28
|
const os = require('os')
|
|
27
29
|
const path = require('path')
|
|
@@ -40,6 +42,32 @@ function sleep(ms) {
|
|
|
40
42
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Fetch workspace-scoped variables for a routine from HQ.
|
|
47
|
+
* Returns an empty object when the routine isn't bound to a workspace or HQ
|
|
48
|
+
* is unreachable; the runner continues with minion-local vars only.
|
|
49
|
+
*
|
|
50
|
+
* Workspace secrets are NOT fetched — secrets remain minion-local only (the
|
|
51
|
+
* HQ DB never stores secret values, by design).
|
|
52
|
+
*
|
|
53
|
+
* @param {string} workspaceId - Routine.workspace_id (may be falsy)
|
|
54
|
+
* @returns {Promise<Record<string, string>>}
|
|
55
|
+
*/
|
|
56
|
+
async function fetchWorkspaceVars(workspaceId) {
|
|
57
|
+
if (!workspaceId || !isHqConfigured()) return {}
|
|
58
|
+
try {
|
|
59
|
+
const result = await api.request(`/workspaces/${workspaceId}/variables`)
|
|
60
|
+
const vars = {}
|
|
61
|
+
for (const v of (result?.variables || [])) {
|
|
62
|
+
if (v && typeof v.key === 'string') vars[v.key] = String(v.value ?? '')
|
|
63
|
+
}
|
|
64
|
+
return vars
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`[RoutineRunner] Failed to fetch workspace vars (${workspaceId}): ${err.message}`)
|
|
67
|
+
return {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
/**
|
|
44
72
|
* Generate tmux session name from routine ID and execution ID
|
|
45
73
|
* Format: rt-{routineId first 8}-{executionId first 4}
|
|
@@ -90,10 +118,15 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
90
118
|
console.log(`[RoutineRunner] tmux session: ${sessionName}`)
|
|
91
119
|
console.log(`[RoutineRunner] Log file: ${logFile}`)
|
|
92
120
|
|
|
93
|
-
//
|
|
121
|
+
// Fetch HQ workspace variables when the routine is bound to a workspace.
|
|
122
|
+
// Minion variables (minion-wide ∪ WS-scoped, resolved inside the expander
|
|
123
|
+
// via workspaceId) override these as the final layer.
|
|
124
|
+
const workspaceVars = await fetchWorkspaceVars(routine.workspace_id)
|
|
125
|
+
|
|
126
|
+
// Expand {{VAR}} templates in SKILL.md files
|
|
94
127
|
let expandedOriginals = new Map()
|
|
95
128
|
try {
|
|
96
|
-
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
129
|
+
expandedOriginals = await expandSkillTemplates(skillNames, workspaceVars, routine.workspace_id || '')
|
|
97
130
|
if (expandedOriginals.size > 0) {
|
|
98
131
|
console.log(`[RoutineRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
99
132
|
}
|
|
@@ -142,14 +175,19 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
142
175
|
)
|
|
143
176
|
await execAsync(`chmod +x "${execScript}"`)
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
// Build tmux invocation with workspace-effective secrets + per-execution
|
|
179
|
+
// identifiers. Secrets come from variable-store (minion-wide ∪ WS-scoped,
|
|
180
|
+
// with WS values winning); identifiers always override on top.
|
|
181
|
+
const tmuxCommand = buildTmuxNewSessionCommand({
|
|
182
|
+
sessionName,
|
|
183
|
+
workspaceId: routine.workspace_id || '',
|
|
184
|
+
extraEnv: {
|
|
185
|
+
MINION_EXECUTION_ID: executionId,
|
|
186
|
+
MINION_ROUTINE_ID: routine.id,
|
|
187
|
+
MINION_ROUTINE_NAME: routine.name,
|
|
188
|
+
MINION_ROUTINE_WORKSPACE_ID: routine.workspace_id || '',
|
|
189
|
+
},
|
|
190
|
+
})
|
|
153
191
|
|
|
154
192
|
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
155
193
|
|
package/linux/workflow-runner.js
CHANGED
|
@@ -22,6 +22,7 @@ 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
24
|
const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
|
|
25
|
+
const { buildTmuxNewSessionCommand } = require('../core/lib/session-env')
|
|
25
26
|
const { getActivePrimary } = require('../core/llm-plugins/lib/active')
|
|
26
27
|
const os = require('os')
|
|
27
28
|
const path = require('path')
|
|
@@ -139,10 +140,13 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
139
140
|
console.log(`[WorkflowRunner] Log file: ${logFile}`)
|
|
140
141
|
console.log(`[WorkflowRunner] HOME: ${homeDir}`)
|
|
141
142
|
|
|
142
|
-
// Expand {{VAR}} templates in SKILL.md files with minion variables
|
|
143
|
+
// Expand {{VAR}} templates in SKILL.md files with minion variables effective
|
|
144
|
+
// for this workflow's workspace (minion-wide ∪ WS-scoped, WS wins). HQ has
|
|
145
|
+
// already expanded workspace/project/workflow scopes before delivering the
|
|
146
|
+
// SKILL.md, so this layer only resolves remaining placeholders.
|
|
143
147
|
let expandedOriginals = new Map()
|
|
144
148
|
try {
|
|
145
|
-
expandedOriginals = await expandSkillTemplates(skillNames)
|
|
149
|
+
expandedOriginals = await expandSkillTemplates(skillNames, {}, workflow.workspace_id || '')
|
|
146
150
|
if (expandedOriginals.size > 0) {
|
|
147
151
|
console.log(`[WorkflowRunner] Expanded templates in ${expandedOriginals.size} skill(s)`)
|
|
148
152
|
}
|
|
@@ -200,12 +204,19 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
200
204
|
)
|
|
201
205
|
await execAsync(`chmod +x "${execScript}"`)
|
|
202
206
|
|
|
203
|
-
// PATH, HOME
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// PATH, HOME and DISPLAY are inherited from process.env (set at startup);
|
|
208
|
+
// workspace-scoped secrets are passed explicitly via `-e` here so each
|
|
209
|
+
// session only sees secrets relevant to its workspace context.
|
|
210
|
+
const tmuxCommand = buildTmuxNewSessionCommand({
|
|
211
|
+
sessionName,
|
|
212
|
+
workspaceId: workflow.workspace_id || '',
|
|
213
|
+
extraEnv: {
|
|
214
|
+
MINION_EXECUTION_ID: executionId,
|
|
215
|
+
MINION_WORKFLOW_ID: workflow.id || '',
|
|
216
|
+
MINION_WORKFLOW_WORKSPACE_ID: workflow.workspace_id || '',
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
209
220
|
|
|
210
221
|
// Keep session alive after command completes (for debugging via terminal mirror)
|
|
211
222
|
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|