@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move minion secrets from `.env.secrets` (flat file) into SQLite with
|
|
3
|
+
* workspace scoping.
|
|
4
|
+
*
|
|
5
|
+
* Design:
|
|
6
|
+
* - PRIMARY KEY (workspace_id, key); workspace_id='' is the minion-wide bucket.
|
|
7
|
+
* - Same key can exist at multiple scopes; at execution time the runner merges
|
|
8
|
+
* minion-wide + current-workspace secrets, with the WS-scoped value winning.
|
|
9
|
+
* - The actual data migration from `.env.secrets` is performed by
|
|
10
|
+
* variable-store at server start (it needs filesystem access which sits
|
|
11
|
+
* outside the DB transaction). See `migrateLegacySecretsFile()`.
|
|
12
|
+
*
|
|
13
|
+
* Secrets stay minion-local — they are NEVER persisted in HQ DB, by design.
|
|
14
|
+
* HQ→minion APIs only pass values through to the minion's local store.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
version: 9,
|
|
19
|
+
name: 'secrets_workspace',
|
|
20
|
+
|
|
21
|
+
up(db, { tableExists }) {
|
|
22
|
+
if (tableExists(db, 'secrets')) return
|
|
23
|
+
|
|
24
|
+
db.exec(`
|
|
25
|
+
CREATE TABLE secrets (
|
|
26
|
+
workspace_id TEXT NOT NULL DEFAULT '',
|
|
27
|
+
key TEXT NOT NULL,
|
|
28
|
+
value TEXT NOT NULL,
|
|
29
|
+
updated_at INTEGER NOT NULL,
|
|
30
|
+
PRIMARY KEY (workspace_id, key)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_secrets_workspace ON secrets(workspace_id);
|
|
34
|
+
`)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move minion variables from `.env.variables` (flat file) into SQLite with
|
|
3
|
+
* workspace scoping — mirrors the secrets workspace migration.
|
|
4
|
+
*
|
|
5
|
+
* Design:
|
|
6
|
+
* - PRIMARY KEY (workspace_id, key); workspace_id='' is the minion-wide bucket.
|
|
7
|
+
* - Same key can exist at multiple scopes; at execution time the
|
|
8
|
+
* template-expander merges minion-wide + current-workspace variables, with
|
|
9
|
+
* the WS-scoped value winning.
|
|
10
|
+
* - Variables are non-sensitive — values are returned by the API (unlike
|
|
11
|
+
* secrets) — but they still benefit from scoping so a single minion serving
|
|
12
|
+
* multiple workspaces doesn't leak per-tenant config across runs.
|
|
13
|
+
* - Actual data migration from `.env.variables` is performed by variable-store
|
|
14
|
+
* at first use (filesystem access lives outside this transaction). See
|
|
15
|
+
* `migrateLegacyVariablesFile()`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
version: 10,
|
|
20
|
+
name: 'variables_workspace',
|
|
21
|
+
|
|
22
|
+
up(db, { tableExists }) {
|
|
23
|
+
if (tableExists(db, 'variables')) return
|
|
24
|
+
|
|
25
|
+
db.exec(`
|
|
26
|
+
CREATE TABLE variables (
|
|
27
|
+
workspace_id TEXT NOT NULL DEFAULT '',
|
|
28
|
+
key TEXT NOT NULL,
|
|
29
|
+
value TEXT NOT NULL,
|
|
30
|
+
updated_at INTEGER NOT NULL,
|
|
31
|
+
PRIMARY KEY (workspace_id, key)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_variables_workspace ON variables(workspace_id);
|
|
35
|
+
`)
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session environment builder
|
|
3
|
+
*
|
|
4
|
+
* Builds workspace-scoped environment variable injection for runners.
|
|
5
|
+
* Resolves minion-wide + workspace-scoped secrets, and exposes both
|
|
6
|
+
* `tmux -e` flags (Linux/macOS) and a node-pty env object (Windows).
|
|
7
|
+
*
|
|
8
|
+
* Why this exists:
|
|
9
|
+
* Before this module, all secrets were loaded once into `process.env`
|
|
10
|
+
* at server start and inherited by every child process — fine for a
|
|
11
|
+
* single-workspace minion but a leakage path for multi-workspace
|
|
12
|
+
* setups. Each runner now passes the effective env explicitly for the
|
|
13
|
+
* workspace it is running on; workspace-scoped values override
|
|
14
|
+
* minion-wide ones at this layer.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const variableStore = require('../stores/variable-store')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shell-escape a value for a `KEY=VALUE` argument passed via `-e` to tmux.
|
|
21
|
+
* We use single-quote wrapping; the surrounding shell sees one literal arg.
|
|
22
|
+
* @param {string} s
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function singleQuote(s) {
|
|
26
|
+
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build an array of `-e 'KEY=VALUE'` shell-safe flags for tmux new-session.
|
|
31
|
+
*
|
|
32
|
+
* @param {string|null|undefined} workspaceId - The session's workspace scope
|
|
33
|
+
* @param {Record<string,string>} [extraEnv] - Per-execution identifiers (e.g.
|
|
34
|
+
* MINION_EXECUTION_ID). Layered on top so they always win.
|
|
35
|
+
* @returns {string[]} List of `-e <quoted>` fragments to splice into a shell command
|
|
36
|
+
*/
|
|
37
|
+
function buildTmuxEnvFlags(workspaceId, extraEnv = {}) {
|
|
38
|
+
const merged = { ...variableStore.getEffectiveSecrets(workspaceId), ...extraEnv }
|
|
39
|
+
const flags = []
|
|
40
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
41
|
+
if (v == null) continue
|
|
42
|
+
flags.push(`-e ${singleQuote(`${k}=${v}`)}`)
|
|
43
|
+
}
|
|
44
|
+
return flags
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a `tmux new-session` invocation line including all secret env vars.
|
|
49
|
+
*
|
|
50
|
+
* Caller typically does:
|
|
51
|
+
* `${buildTmuxNewSessionCommand({sessionName, workspaceId, extraEnv, width, height})}`
|
|
52
|
+
*
|
|
53
|
+
* The session is created detached (`-d`) with no command so we can configure
|
|
54
|
+
* `remain-on-exit` and `pipe-pane` before injecting the real workload via
|
|
55
|
+
* `send-keys`.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} opts
|
|
58
|
+
* @param {string} opts.sessionName
|
|
59
|
+
* @param {string|null|undefined} opts.workspaceId
|
|
60
|
+
* @param {Record<string,string>} [opts.extraEnv]
|
|
61
|
+
* @param {number} [opts.width]
|
|
62
|
+
* @param {number} [opts.height]
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function buildTmuxNewSessionCommand({ sessionName, workspaceId, extraEnv = {}, width = 200, height = 50 }) {
|
|
66
|
+
const envFlags = buildTmuxEnvFlags(workspaceId, extraEnv)
|
|
67
|
+
return [
|
|
68
|
+
'tmux new-session -d',
|
|
69
|
+
`-s "${sessionName}"`,
|
|
70
|
+
`-x ${width} -y ${height}`,
|
|
71
|
+
...envFlags,
|
|
72
|
+
].join(' ')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build an env object for node-pty / child_process.spawn that contains the
|
|
77
|
+
* caller's `process.env` plus the workspace-effective secrets and any extra
|
|
78
|
+
* per-execution identifiers (extraEnv wins last).
|
|
79
|
+
*
|
|
80
|
+
* @param {string|null|undefined} workspaceId
|
|
81
|
+
* @param {Record<string,string>} [extraEnv]
|
|
82
|
+
* @returns {Record<string,string>}
|
|
83
|
+
*/
|
|
84
|
+
function buildSpawnEnv(workspaceId, extraEnv = {}) {
|
|
85
|
+
return {
|
|
86
|
+
...process.env,
|
|
87
|
+
...variableStore.getEffectiveSecrets(workspaceId),
|
|
88
|
+
...extraEnv,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
buildTmuxEnvFlags,
|
|
94
|
+
buildTmuxNewSessionCommand,
|
|
95
|
+
buildSpawnEnv,
|
|
96
|
+
}
|
package/core/lib/step-poller.js
CHANGED
|
@@ -104,6 +104,7 @@ async function executeStep(step) {
|
|
|
104
104
|
skill_name,
|
|
105
105
|
revision_feedback,
|
|
106
106
|
template_vars,
|
|
107
|
+
workspace_id,
|
|
107
108
|
} = step
|
|
108
109
|
|
|
109
110
|
console.log(
|
|
@@ -135,11 +136,15 @@ async function executeStep(step) {
|
|
|
135
136
|
|
|
136
137
|
// 2. Fetch the skill from HQ to ensure it's deployed locally
|
|
137
138
|
// Merge minion variables into template_vars so HQ expands {{VAR_NAME}} in SKILL.md.
|
|
138
|
-
//
|
|
139
|
+
// Resolution order: workspace < project < workflow < minion (minion wins).
|
|
140
|
+
// template_vars already merges workspace/project/workflow on HQ side; we
|
|
141
|
+
// layer effective minion vars (minion-wide ∪ WS-scoped) on top so any
|
|
142
|
+
// conflicting key resolves to the minion-local value before HQ rewrites
|
|
143
|
+
// the SKILL.md.
|
|
139
144
|
if (skill_name) {
|
|
140
145
|
try {
|
|
141
|
-
const minionVars = variableStore.
|
|
142
|
-
const mergedVars = { ...
|
|
146
|
+
const minionVars = variableStore.getEffectiveVariables(workspace_id || '')
|
|
147
|
+
const mergedVars = { ...(template_vars || {}), ...minionVars }
|
|
143
148
|
const varsParam = Object.keys(mergedVars).length > 0
|
|
144
149
|
? `?vars=${Buffer.from(JSON.stringify(mergedVars)).toString('base64')}`
|
|
145
150
|
: ''
|
|
@@ -32,17 +32,31 @@ function expandTemplateVars(content, vars) {
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Expand templates in SKILL.md files for the given skill names.
|
|
35
|
-
* Reads each SKILL.md, expands {{VAR}} with
|
|
35
|
+
* Reads each SKILL.md, expands {{VAR}} with the merged variable set, writes back.
|
|
36
36
|
* Returns the original contents for restoration after execution.
|
|
37
37
|
*
|
|
38
|
+
* Resolution order (later entries override earlier ones):
|
|
39
|
+
* extraVars (e.g., HQ workspace vars passed by the caller)
|
|
40
|
+
* < minion-wide variables
|
|
41
|
+
* < workspace-scoped minion variables for the given workspaceId
|
|
42
|
+
*
|
|
43
|
+
* The minion-local layer always wins at this stage, which mirrors the
|
|
44
|
+
* cross-scope rule (workspace < project < workflow < minion). For workflow
|
|
45
|
+
* execution, project/workflow scopes are pre-expanded by HQ before the
|
|
46
|
+
* SKILL.md reaches the minion; for routine execution the caller can pass
|
|
47
|
+
* workspace-scoped HQ vars via `extraVars`.
|
|
48
|
+
*
|
|
38
49
|
* @param {string[]} skillNames - Skill slugs to expand
|
|
50
|
+
* @param {Record<string, string>} [extraVars] - Additional vars (lowest priority of the three layers)
|
|
51
|
+
* @param {string|null|undefined} [workspaceId] - Workspace context for resolving minion variables (defaults to minion-wide only)
|
|
39
52
|
* @returns {Promise<Map<string, string>>} Map of skillName -> original content (for restoration)
|
|
40
53
|
*/
|
|
41
|
-
async function expandSkillTemplates(skillNames) {
|
|
42
|
-
const minionVars = variableStore.
|
|
54
|
+
async function expandSkillTemplates(skillNames, extraVars = {}, workspaceId = '') {
|
|
55
|
+
const minionVars = variableStore.getEffectiveVariables(workspaceId)
|
|
56
|
+
const mergedVars = { ...extraVars, ...minionVars }
|
|
43
57
|
|
|
44
|
-
// If
|
|
45
|
-
if (Object.keys(
|
|
58
|
+
// If nothing to expand, skip
|
|
59
|
+
if (Object.keys(mergedVars).length === 0) return new Map()
|
|
46
60
|
|
|
47
61
|
// originals key: `${root}::${name}` so we restore the right copy in each
|
|
48
62
|
// plugin's skill dir (multi-plugin distribution).
|
|
@@ -55,7 +69,7 @@ async function expandSkillTemplates(skillNames) {
|
|
|
55
69
|
const skillMdPath = path.join(root, name, 'SKILL.md')
|
|
56
70
|
try {
|
|
57
71
|
const original = await fs.readFile(skillMdPath, 'utf-8')
|
|
58
|
-
const expanded = expandTemplateVars(original,
|
|
72
|
+
const expanded = expandTemplateVars(original, mergedVars)
|
|
59
73
|
if (expanded !== original) {
|
|
60
74
|
originals.set(`${root}::${name}`, { skillMdPath, original })
|
|
61
75
|
await fs.writeFile(skillMdPath, expanded, 'utf-8')
|
package/core/routes/variables.js
CHANGED
|
@@ -32,14 +32,26 @@ function isValidValue(value) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function variableRoutes(fastify, _opts, done) {
|
|
35
|
-
|
|
35
|
+
function readWorkspaceId(request) {
|
|
36
|
+
const rawWs = request.query?.workspace_id
|
|
37
|
+
return (typeof rawWs === 'string') ? rawWs : ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Variables (non-sensitive, workspace-scoped) ──────────────────────
|
|
41
|
+
//
|
|
42
|
+
// Variables are scoped per workspace just like secrets. Pass
|
|
43
|
+
// ?workspace_id=<uuid> to target a specific workspace's bucket; omit it
|
|
44
|
+
// (or pass '') for the minion-wide bucket. At skill execution time the
|
|
45
|
+
// template-expander merges minion-wide + current-workspace variables, with
|
|
46
|
+
// WS-scoped values overriding minion-wide ones.
|
|
36
47
|
|
|
37
48
|
fastify.get('/api/variables', async (request, reply) => {
|
|
38
49
|
if (!verifyToken(request)) {
|
|
39
50
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
40
51
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
52
|
+
const workspaceId = readWorkspaceId(request)
|
|
53
|
+
const variables = variableStore.getVariablesForScope(workspaceId)
|
|
54
|
+
return { success: true, workspace_id: workspaceId, variables }
|
|
43
55
|
})
|
|
44
56
|
|
|
45
57
|
fastify.get('/api/variables/:key', async (request, reply) => {
|
|
@@ -47,11 +59,12 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
47
59
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
48
60
|
}
|
|
49
61
|
const { key } = request.params
|
|
50
|
-
const
|
|
62
|
+
const workspaceId = readWorkspaceId(request)
|
|
63
|
+
const value = variableStore.getVariable(workspaceId, key)
|
|
51
64
|
if (value === null) {
|
|
52
65
|
return reply.code(404).send({ error: `Variable not found: ${key}` })
|
|
53
66
|
}
|
|
54
|
-
return { success: true, key, value }
|
|
67
|
+
return { success: true, key, value, workspace_id: workspaceId }
|
|
55
68
|
})
|
|
56
69
|
|
|
57
70
|
fastify.put('/api/variables/:key', async (request, reply) => {
|
|
@@ -60,6 +73,7 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
60
73
|
}
|
|
61
74
|
const { key } = request.params
|
|
62
75
|
const { value } = request.body || {}
|
|
76
|
+
const workspaceId = readWorkspaceId(request)
|
|
63
77
|
|
|
64
78
|
if (!isValidKey(key)) {
|
|
65
79
|
return reply.code(400).send({ error: 'Invalid key. Use alphanumeric characters and underscores (1-100 chars).' })
|
|
@@ -68,8 +82,8 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
68
82
|
return reply.code(400).send({ error: 'Invalid value. Must be a string, no newlines, max 2000 chars.' })
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
variableStore.
|
|
72
|
-
return { success: true, key, value }
|
|
85
|
+
variableStore.setVariable(workspaceId, key, value)
|
|
86
|
+
return { success: true, key, value, workspace_id: workspaceId }
|
|
73
87
|
})
|
|
74
88
|
|
|
75
89
|
fastify.delete('/api/variables/:key', async (request, reply) => {
|
|
@@ -77,22 +91,45 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
77
91
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
78
92
|
}
|
|
79
93
|
const { key } = request.params
|
|
80
|
-
const
|
|
94
|
+
const workspaceId = readWorkspaceId(request)
|
|
95
|
+
const removed = variableStore.removeVariable(workspaceId, key)
|
|
81
96
|
if (!removed) {
|
|
82
97
|
return reply.code(404).send({ error: `Variable not found: ${key}` })
|
|
83
98
|
}
|
|
84
|
-
return { success: true, key }
|
|
99
|
+
return { success: true, key, workspace_id: workspaceId }
|
|
85
100
|
})
|
|
86
101
|
|
|
87
|
-
//
|
|
102
|
+
// List the workspace scopes (workspace_ids) that currently hold any
|
|
103
|
+
// variable. '' represents the minion-wide bucket.
|
|
104
|
+
fastify.get('/api/variables/scopes', async (request, reply) => {
|
|
105
|
+
if (!verifyToken(request)) {
|
|
106
|
+
return reply.code(401).send({ error: 'Unauthorized' })
|
|
107
|
+
}
|
|
108
|
+
return { success: true, scopes: variableStore.listVariableScopes() }
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ─── Secrets (sensitive, workspace-scoped) ────────────────────────────
|
|
112
|
+
//
|
|
113
|
+
// Secrets are scoped per workspace. Pass ?workspace_id=<uuid> to target a
|
|
114
|
+
// specific workspace's bucket; omit it (or pass '') for the minion-wide
|
|
115
|
+
// bucket. At skill execution time, the runner merges minion-wide +
|
|
116
|
+
// current-workspace secrets, with WS-scoped values overriding.
|
|
117
|
+
//
|
|
118
|
+
// Values are never returned via the API by design — only key names. Secrets
|
|
119
|
+
// never leave the minion: the HQ proxy is a pure pass-through.
|
|
120
|
+
|
|
121
|
+
function readWorkspaceId(request) {
|
|
122
|
+
const rawWs = request.query?.workspace_id
|
|
123
|
+
return (typeof rawWs === 'string') ? rawWs : ''
|
|
124
|
+
}
|
|
88
125
|
|
|
89
126
|
fastify.get('/api/secrets', async (request, reply) => {
|
|
90
127
|
if (!verifyToken(request)) {
|
|
91
128
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
92
129
|
}
|
|
93
|
-
|
|
94
|
-
const keys = variableStore.
|
|
95
|
-
return { success: true, keys }
|
|
130
|
+
const workspaceId = readWorkspaceId(request)
|
|
131
|
+
const keys = variableStore.listSecretKeys(workspaceId)
|
|
132
|
+
return { success: true, workspace_id: workspaceId, keys }
|
|
96
133
|
})
|
|
97
134
|
|
|
98
135
|
fastify.put('/api/secrets/:key', async (request, reply) => {
|
|
@@ -101,6 +138,7 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
101
138
|
}
|
|
102
139
|
const { key } = request.params
|
|
103
140
|
const { value } = request.body || {}
|
|
141
|
+
const workspaceId = readWorkspaceId(request)
|
|
104
142
|
|
|
105
143
|
if (!isValidKey(key)) {
|
|
106
144
|
return reply.code(400).send({ error: 'Invalid key. Use alphanumeric characters and underscores (1-100 chars).' })
|
|
@@ -109,8 +147,8 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
109
147
|
return reply.code(400).send({ error: 'Invalid value. Must be a string, no newlines, max 2000 chars.' })
|
|
110
148
|
}
|
|
111
149
|
|
|
112
|
-
variableStore.
|
|
113
|
-
return { success: true, key }
|
|
150
|
+
variableStore.setSecret(workspaceId, key, value)
|
|
151
|
+
return { success: true, key, workspace_id: workspaceId }
|
|
114
152
|
})
|
|
115
153
|
|
|
116
154
|
fastify.delete('/api/secrets/:key', async (request, reply) => {
|
|
@@ -118,11 +156,23 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
118
156
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
119
157
|
}
|
|
120
158
|
const { key } = request.params
|
|
121
|
-
const
|
|
159
|
+
const workspaceId = readWorkspaceId(request)
|
|
160
|
+
const removed = variableStore.removeSecret(workspaceId, key)
|
|
122
161
|
if (!removed) {
|
|
123
162
|
return reply.code(404).send({ error: `Secret not found: ${key}` })
|
|
124
163
|
}
|
|
125
|
-
return { success: true, key }
|
|
164
|
+
return { success: true, key, workspace_id: workspaceId }
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// List the scopes (workspace_ids) that currently hold any secret.
|
|
168
|
+
// '' represents the minion-wide bucket. Useful for the dashboard secrets
|
|
169
|
+
// editor to populate the workspace selector even when a workspace has no
|
|
170
|
+
// active minion membership yet.
|
|
171
|
+
fastify.get('/api/secrets/scopes', async (request, reply) => {
|
|
172
|
+
if (!verifyToken(request)) {
|
|
173
|
+
return reply.code(401).send({ error: 'Unauthorized' })
|
|
174
|
+
}
|
|
175
|
+
return { success: true, scopes: variableStore.listSecretScopes() }
|
|
126
176
|
})
|
|
127
177
|
|
|
128
178
|
done()
|