arkaos 3.69.0 → 3.70.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/VERSION +1 -1
- package/dashboard/app/components/Terminal.vue +12 -24
- package/dashboard/app/composables/useTerminalThemes.ts +190 -0
- package/dashboard/app/pages/terminal.vue +71 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +2 -210
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.70.0
|
|
@@ -9,10 +9,12 @@ import { FitAddon } from '@xterm/addon-fit'
|
|
|
9
9
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
10
10
|
import { SearchAddon } from '@xterm/addon-search'
|
|
11
11
|
import '@xterm/xterm/css/xterm.css'
|
|
12
|
+
import type { XtermTheme } from '~/composables/useTerminalThemes'
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
session?: ReturnType<typeof useTerminalSession>
|
|
15
16
|
onInputLine?: (line: string) => void
|
|
17
|
+
theme?: XtermTheme
|
|
16
18
|
}
|
|
17
19
|
const props = defineProps<Props>()
|
|
18
20
|
|
|
@@ -24,29 +26,10 @@ const search = shallowRef<SearchAddon | null>(null)
|
|
|
24
26
|
|
|
25
27
|
const decoder = new TextDecoder('utf-8', { fatal: false })
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
cursorAccent: '#0a0a0f',
|
|
32
|
-
selectionBackground: '#1e3a5f',
|
|
33
|
-
black: '#0a0a0f',
|
|
34
|
-
red: '#f87171',
|
|
35
|
-
green: '#86efac',
|
|
36
|
-
yellow: '#fde68a',
|
|
37
|
-
blue: '#7dd3fc',
|
|
38
|
-
magenta: '#f0abfc',
|
|
39
|
-
cyan: '#67e8f9',
|
|
40
|
-
white: '#e6e6f0',
|
|
41
|
-
brightBlack: '#3f3f46',
|
|
42
|
-
brightRed: '#fca5a5',
|
|
43
|
-
brightGreen: '#bbf7d0',
|
|
44
|
-
brightYellow: '#fef3c7',
|
|
45
|
-
brightBlue: '#bae6fd',
|
|
46
|
-
brightMagenta: '#f5d0fe',
|
|
47
|
-
brightCyan: '#a5f3fc',
|
|
48
|
-
brightWhite: '#fafafa',
|
|
49
|
-
}
|
|
29
|
+
// PR99d v3.70.0 — theme comes from prop or from the composable
|
|
30
|
+
// default (operator's choice stored in localStorage).
|
|
31
|
+
const { activeTheme } = useTerminalThemes()
|
|
32
|
+
const effectiveTheme = computed(() => props.theme ?? activeTheme.value)
|
|
50
33
|
|
|
51
34
|
let unsubscribeOutput: (() => void) | null = null
|
|
52
35
|
let resizeObserver: ResizeObserver | null = null
|
|
@@ -60,9 +43,14 @@ onMounted(async () => {
|
|
|
60
43
|
fontSize: 13,
|
|
61
44
|
lineHeight: 1.2,
|
|
62
45
|
scrollback: 5000,
|
|
63
|
-
theme:
|
|
46
|
+
theme: effectiveTheme.value,
|
|
64
47
|
allowProposedApi: true,
|
|
65
48
|
})
|
|
49
|
+
|
|
50
|
+
// React to theme switches without remounting.
|
|
51
|
+
watch(effectiveTheme, (next) => {
|
|
52
|
+
if (term.value) term.value.options.theme = next
|
|
53
|
+
}, { deep: true })
|
|
66
54
|
const fitAddon = new FitAddon()
|
|
67
55
|
const searchAddon = new SearchAddon()
|
|
68
56
|
t.loadAddon(fitAddon)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// PR99d v3.70.0 — theme presets for xterm.js.
|
|
2
|
+
//
|
|
3
|
+
// Stored as plain xterm.js ITheme objects. The active theme name lives
|
|
4
|
+
// in localStorage so it survives reloads. ArkaOS Dark is the default
|
|
5
|
+
// and is tuned to the dashboard's primary color.
|
|
6
|
+
|
|
7
|
+
export interface XtermTheme {
|
|
8
|
+
background: string
|
|
9
|
+
foreground: string
|
|
10
|
+
cursor: string
|
|
11
|
+
cursorAccent: string
|
|
12
|
+
selectionBackground: string
|
|
13
|
+
black: string
|
|
14
|
+
red: string
|
|
15
|
+
green: string
|
|
16
|
+
yellow: string
|
|
17
|
+
blue: string
|
|
18
|
+
magenta: string
|
|
19
|
+
cyan: string
|
|
20
|
+
white: string
|
|
21
|
+
brightBlack: string
|
|
22
|
+
brightRed: string
|
|
23
|
+
brightGreen: string
|
|
24
|
+
brightYellow: string
|
|
25
|
+
brightBlue: string
|
|
26
|
+
brightMagenta: string
|
|
27
|
+
brightCyan: string
|
|
28
|
+
brightWhite: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const TERMINAL_THEMES: Record<string, XtermTheme> = {
|
|
32
|
+
'arkaos-dark': {
|
|
33
|
+
background: '#0a0a0f',
|
|
34
|
+
foreground: '#e6e6f0',
|
|
35
|
+
cursor: '#7dd3fc',
|
|
36
|
+
cursorAccent: '#0a0a0f',
|
|
37
|
+
selectionBackground: '#1e3a5f',
|
|
38
|
+
black: '#0a0a0f',
|
|
39
|
+
red: '#f87171',
|
|
40
|
+
green: '#86efac',
|
|
41
|
+
yellow: '#fde68a',
|
|
42
|
+
blue: '#7dd3fc',
|
|
43
|
+
magenta: '#f0abfc',
|
|
44
|
+
cyan: '#67e8f9',
|
|
45
|
+
white: '#e6e6f0',
|
|
46
|
+
brightBlack: '#3f3f46',
|
|
47
|
+
brightRed: '#fca5a5',
|
|
48
|
+
brightGreen: '#bbf7d0',
|
|
49
|
+
brightYellow: '#fef3c7',
|
|
50
|
+
brightBlue: '#bae6fd',
|
|
51
|
+
brightMagenta: '#f5d0fe',
|
|
52
|
+
brightCyan: '#a5f3fc',
|
|
53
|
+
brightWhite: '#fafafa',
|
|
54
|
+
},
|
|
55
|
+
dracula: {
|
|
56
|
+
background: '#282a36',
|
|
57
|
+
foreground: '#f8f8f2',
|
|
58
|
+
cursor: '#f8f8f2',
|
|
59
|
+
cursorAccent: '#282a36',
|
|
60
|
+
selectionBackground: '#44475a',
|
|
61
|
+
black: '#21222c',
|
|
62
|
+
red: '#ff5555',
|
|
63
|
+
green: '#50fa7b',
|
|
64
|
+
yellow: '#f1fa8c',
|
|
65
|
+
blue: '#bd93f9',
|
|
66
|
+
magenta: '#ff79c6',
|
|
67
|
+
cyan: '#8be9fd',
|
|
68
|
+
white: '#f8f8f2',
|
|
69
|
+
brightBlack: '#6272a4',
|
|
70
|
+
brightRed: '#ff6e6e',
|
|
71
|
+
brightGreen: '#69ff94',
|
|
72
|
+
brightYellow: '#ffffa5',
|
|
73
|
+
brightBlue: '#d6acff',
|
|
74
|
+
brightMagenta: '#ff92df',
|
|
75
|
+
brightCyan: '#a4ffff',
|
|
76
|
+
brightWhite: '#ffffff',
|
|
77
|
+
},
|
|
78
|
+
'solarized-dark': {
|
|
79
|
+
background: '#002b36',
|
|
80
|
+
foreground: '#839496',
|
|
81
|
+
cursor: '#93a1a1',
|
|
82
|
+
cursorAccent: '#002b36',
|
|
83
|
+
selectionBackground: '#073642',
|
|
84
|
+
black: '#073642',
|
|
85
|
+
red: '#dc322f',
|
|
86
|
+
green: '#859900',
|
|
87
|
+
yellow: '#b58900',
|
|
88
|
+
blue: '#268bd2',
|
|
89
|
+
magenta: '#d33682',
|
|
90
|
+
cyan: '#2aa198',
|
|
91
|
+
white: '#eee8d5',
|
|
92
|
+
brightBlack: '#586e75',
|
|
93
|
+
brightRed: '#cb4b16',
|
|
94
|
+
brightGreen: '#586e75',
|
|
95
|
+
brightYellow: '#657b83',
|
|
96
|
+
brightBlue: '#839496',
|
|
97
|
+
brightMagenta: '#6c71c4',
|
|
98
|
+
brightCyan: '#93a1a1',
|
|
99
|
+
brightWhite: '#fdf6e3',
|
|
100
|
+
},
|
|
101
|
+
'solarized-light': {
|
|
102
|
+
background: '#fdf6e3',
|
|
103
|
+
foreground: '#657b83',
|
|
104
|
+
cursor: '#586e75',
|
|
105
|
+
cursorAccent: '#fdf6e3',
|
|
106
|
+
selectionBackground: '#eee8d5',
|
|
107
|
+
black: '#073642',
|
|
108
|
+
red: '#dc322f',
|
|
109
|
+
green: '#859900',
|
|
110
|
+
yellow: '#b58900',
|
|
111
|
+
blue: '#268bd2',
|
|
112
|
+
magenta: '#d33682',
|
|
113
|
+
cyan: '#2aa198',
|
|
114
|
+
white: '#eee8d5',
|
|
115
|
+
brightBlack: '#002b36',
|
|
116
|
+
brightRed: '#cb4b16',
|
|
117
|
+
brightGreen: '#586e75',
|
|
118
|
+
brightYellow: '#657b83',
|
|
119
|
+
brightBlue: '#839496',
|
|
120
|
+
brightMagenta: '#6c71c4',
|
|
121
|
+
brightCyan: '#93a1a1',
|
|
122
|
+
brightWhite: '#fdf6e3',
|
|
123
|
+
},
|
|
124
|
+
nord: {
|
|
125
|
+
background: '#2e3440',
|
|
126
|
+
foreground: '#d8dee9',
|
|
127
|
+
cursor: '#d8dee9',
|
|
128
|
+
cursorAccent: '#2e3440',
|
|
129
|
+
selectionBackground: '#434c5e',
|
|
130
|
+
black: '#3b4252',
|
|
131
|
+
red: '#bf616a',
|
|
132
|
+
green: '#a3be8c',
|
|
133
|
+
yellow: '#ebcb8b',
|
|
134
|
+
blue: '#81a1c1',
|
|
135
|
+
magenta: '#b48ead',
|
|
136
|
+
cyan: '#88c0d0',
|
|
137
|
+
white: '#e5e9f0',
|
|
138
|
+
brightBlack: '#4c566a',
|
|
139
|
+
brightRed: '#bf616a',
|
|
140
|
+
brightGreen: '#a3be8c',
|
|
141
|
+
brightYellow: '#ebcb8b',
|
|
142
|
+
brightBlue: '#81a1c1',
|
|
143
|
+
brightMagenta: '#b48ead',
|
|
144
|
+
brightCyan: '#8fbcbb',
|
|
145
|
+
brightWhite: '#eceff4',
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const THEME_LABELS: Record<string, string> = {
|
|
150
|
+
'arkaos-dark': 'ArkaOS Dark',
|
|
151
|
+
dracula: 'Dracula',
|
|
152
|
+
'solarized-dark': 'Solarized Dark',
|
|
153
|
+
'solarized-light': 'Solarized Light',
|
|
154
|
+
nord: 'Nord',
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const STORAGE_KEY = 'arka-terminal-theme'
|
|
158
|
+
const DEFAULT_THEME = 'arkaos-dark'
|
|
159
|
+
|
|
160
|
+
export function useTerminalThemes() {
|
|
161
|
+
const themeName = useState<string>('terminal-theme', () => {
|
|
162
|
+
if (typeof localStorage === 'undefined') return DEFAULT_THEME
|
|
163
|
+
return localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
function setTheme(name: string) {
|
|
167
|
+
if (!TERMINAL_THEMES[name]) return
|
|
168
|
+
themeName.value = name
|
|
169
|
+
try {
|
|
170
|
+
localStorage.setItem(STORAGE_KEY, name)
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore quota
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const activeTheme = computed<XtermTheme>(
|
|
177
|
+
() => TERMINAL_THEMES[themeName.value] ?? TERMINAL_THEMES[DEFAULT_THEME]!,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const options = computed(() =>
|
|
181
|
+
Object.entries(THEME_LABELS).map(([value, label]) => ({ value, label })),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
themeName,
|
|
186
|
+
activeTheme,
|
|
187
|
+
setTheme,
|
|
188
|
+
options,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -55,6 +55,29 @@ function recordCommand(cmd: string) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// PR99d v3.70.0 — theme picker + Ctrl+R history search.
|
|
59
|
+
const { themeName, setTheme, options: themeOptions } = useTerminalThemes()
|
|
60
|
+
const searchOpen = ref(false)
|
|
61
|
+
const searchQuery = ref('')
|
|
62
|
+
|
|
63
|
+
const searchResults = computed(() => {
|
|
64
|
+
const q = searchQuery.value.trim().toLowerCase()
|
|
65
|
+
if (!q) return history.value.slice(0, 30)
|
|
66
|
+
return history.value
|
|
67
|
+
.filter((h) => h.cmd.toLowerCase().includes(q))
|
|
68
|
+
.slice(0, 30)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function openSearch() {
|
|
72
|
+
searchOpen.value = true
|
|
73
|
+
searchQuery.value = ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pickFromSearch(cmd: string) {
|
|
77
|
+
activeTab.value?.session.sendInput(cmd)
|
|
78
|
+
searchOpen.value = false
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
const editingTabId = ref<string | null>(null)
|
|
59
82
|
const renameDraft = ref('')
|
|
60
83
|
|
|
@@ -94,6 +117,7 @@ defineShortcuts({
|
|
|
94
117
|
},
|
|
95
118
|
usingInput: false,
|
|
96
119
|
},
|
|
120
|
+
ctrl_r: { handler: openSearch, usingInput: false },
|
|
97
121
|
meta_1: { handler: () => switchByIndex(0), usingInput: false },
|
|
98
122
|
meta_2: { handler: () => switchByIndex(1), usingInput: false },
|
|
99
123
|
meta_3: { handler: () => switchByIndex(2), usingInput: false },
|
|
@@ -137,6 +161,22 @@ const showHistory = ref(false)
|
|
|
137
161
|
<UIcon name="i-lucide-shield" class="size-3 mr-1" />
|
|
138
162
|
localhost only
|
|
139
163
|
</UBadge>
|
|
164
|
+
<USelect
|
|
165
|
+
:model-value="themeName"
|
|
166
|
+
:items="themeOptions"
|
|
167
|
+
size="xs"
|
|
168
|
+
class="w-44"
|
|
169
|
+
@update:model-value="setTheme($event as string)"
|
|
170
|
+
/>
|
|
171
|
+
<UButton
|
|
172
|
+
size="xs"
|
|
173
|
+
variant="ghost"
|
|
174
|
+
icon="i-lucide-search"
|
|
175
|
+
title="Ctrl+R — search history"
|
|
176
|
+
@click="openSearch"
|
|
177
|
+
>
|
|
178
|
+
⌃R
|
|
179
|
+
</UButton>
|
|
140
180
|
<UButton
|
|
141
181
|
size="xs"
|
|
142
182
|
variant="ghost"
|
|
@@ -239,7 +279,37 @@ const showHistory = ref(false)
|
|
|
239
279
|
|
|
240
280
|
<footer class="text-xs text-muted">
|
|
241
281
|
Sessions live on the backend until you close them or 30 min idle.
|
|
242
|
-
History stays in this browser only.
|
|
282
|
+
History stays in this browser only. Ctrl+R to search history.
|
|
243
283
|
</footer>
|
|
284
|
+
|
|
285
|
+
<UModal v-model:open="searchOpen" :title="`Search history (${history.length})`">
|
|
286
|
+
<template #body>
|
|
287
|
+
<div class="space-y-2">
|
|
288
|
+
<UInput
|
|
289
|
+
v-model="searchQuery"
|
|
290
|
+
placeholder="type to filter…"
|
|
291
|
+
autofocus
|
|
292
|
+
icon="i-lucide-search"
|
|
293
|
+
@keydown.enter="searchResults[0] && pickFromSearch(searchResults[0].cmd)"
|
|
294
|
+
/>
|
|
295
|
+
<div class="max-h-80 overflow-y-auto rounded-md border border-default divide-y divide-default">
|
|
296
|
+
<button
|
|
297
|
+
v-for="(entry, i) in searchResults"
|
|
298
|
+
:key="i"
|
|
299
|
+
class="w-full text-left px-3 py-2 text-sm font-mono hover:bg-elevated/40 truncate"
|
|
300
|
+
@click="pickFromSearch(entry.cmd)"
|
|
301
|
+
>
|
|
302
|
+
{{ entry.cmd }}
|
|
303
|
+
</button>
|
|
304
|
+
<div v-if="searchResults.length === 0" class="px-3 py-4 text-muted text-center text-sm">
|
|
305
|
+
No matches
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
<p class="text-xs text-muted">
|
|
309
|
+
Enter sends the top match to the active session.
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
</template>
|
|
313
|
+
</UModal>
|
|
244
314
|
</div>
|
|
245
315
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -2221,216 +2221,8 @@ def _iso_duration_s(start_iso: str, end_iso: str) -> Optional[int]:
|
|
|
2221
2221
|
return None
|
|
2222
2222
|
|
|
2223
2223
|
|
|
2224
|
-
#
|
|
2225
|
-
|
|
2226
|
-
# Allowlist of commands the dashboard terminal can run. Each entry is
|
|
2227
|
-
# {id, label, cmd, description}. Server-side enforcement: any request
|
|
2228
|
-
# whose `command_id` isn't in this list is rejected. No shell expansion,
|
|
2229
|
-
# no globbing, no pipes — subprocess.run with explicit argv only.
|
|
2230
|
-
TERMINAL_ALLOWLIST: list[dict] = [
|
|
2231
|
-
{
|
|
2232
|
-
"id": "arka-status",
|
|
2233
|
-
"label": "ArkaOS status",
|
|
2234
|
-
"cmd": ["arkaos", "status"],
|
|
2235
|
-
"description": "System status (version, departments, agents, projects).",
|
|
2236
|
-
},
|
|
2237
|
-
{
|
|
2238
|
-
"id": "git-status",
|
|
2239
|
-
"label": "git status",
|
|
2240
|
-
"cmd": ["git", "status", "--short"],
|
|
2241
|
-
"description": "Working tree status in short form.",
|
|
2242
|
-
},
|
|
2243
|
-
{
|
|
2244
|
-
"id": "git-log",
|
|
2245
|
-
"label": "git log",
|
|
2246
|
-
"cmd_template": ["git", "log", "-{count}", "--oneline"],
|
|
2247
|
-
"args": [
|
|
2248
|
-
{
|
|
2249
|
-
"name": "count",
|
|
2250
|
-
"label": "Commits to show",
|
|
2251
|
-
"choices": ["5", "10", "20", "50"],
|
|
2252
|
-
"default": "10",
|
|
2253
|
-
},
|
|
2254
|
-
],
|
|
2255
|
-
"description": "Most recent N commits (operator picks N).",
|
|
2256
|
-
},
|
|
2257
|
-
{
|
|
2258
|
-
"id": "npm-version",
|
|
2259
|
-
"label": "npm view arkaos version",
|
|
2260
|
-
"cmd": ["npm", "view", "arkaos", "version"],
|
|
2261
|
-
"description": "Latest published ArkaOS version on npm.",
|
|
2262
|
-
},
|
|
2263
|
-
{
|
|
2264
|
-
"id": "pytest-collect",
|
|
2265
|
-
"label": "pytest --collect-only",
|
|
2266
|
-
"cmd": ["python3", "-m", "pytest", "--collect-only", "-q", "tests/python/"],
|
|
2267
|
-
"description": "List every Python test without running.",
|
|
2268
|
-
},
|
|
2269
|
-
{
|
|
2270
|
-
"id": "ls",
|
|
2271
|
-
"label": "ls",
|
|
2272
|
-
"cmd": ["ls", "-la"],
|
|
2273
|
-
"description": "List files in the project root.",
|
|
2274
|
-
},
|
|
2275
|
-
{
|
|
2276
|
-
"id": "arka-costs",
|
|
2277
|
-
"label": "ArkaOS costs",
|
|
2278
|
-
"cmd_template": ["python3", "-m", "core.runtime.llm_cost_telemetry_cli", "{period}"],
|
|
2279
|
-
"args": [
|
|
2280
|
-
{
|
|
2281
|
-
"name": "period",
|
|
2282
|
-
"label": "Period",
|
|
2283
|
-
"choices": ["today", "week", "month", "all"],
|
|
2284
|
-
"default": "today",
|
|
2285
|
-
},
|
|
2286
|
-
],
|
|
2287
|
-
"description": "LLM cost summary for the selected period.",
|
|
2288
|
-
},
|
|
2289
|
-
]
|
|
2290
|
-
|
|
2291
|
-
_TERMINAL_TIMEOUT_S = 15
|
|
2292
|
-
_TERMINAL_MAX_OUTPUT = 20_000 # chars — both stdout and stderr capped
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
@app.get("/api/terminal/commands")
|
|
2296
|
-
def terminal_commands():
|
|
2297
|
-
"""List allowlisted commands the dashboard terminal may run.
|
|
2298
|
-
|
|
2299
|
-
PR96b v3.56.0 — includes the `args` schema (label / choices /
|
|
2300
|
-
default) when an entry has parameters. The raw cmd / cmd_template
|
|
2301
|
-
is NEVER returned — defence in depth.
|
|
2302
|
-
"""
|
|
2303
|
-
out: list[dict] = []
|
|
2304
|
-
for c in TERMINAL_ALLOWLIST:
|
|
2305
|
-
out.append({
|
|
2306
|
-
"id": c["id"],
|
|
2307
|
-
"label": c["label"],
|
|
2308
|
-
"description": c["description"],
|
|
2309
|
-
"args": _safe_args_schema(c.get("args")),
|
|
2310
|
-
})
|
|
2311
|
-
return {"commands": out, "total": len(out)}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
def _safe_args_schema(args) -> list[dict]:
|
|
2315
|
-
"""Sanitised arg schema for the public API — never leaks cmd/template."""
|
|
2316
|
-
if not isinstance(args, list):
|
|
2317
|
-
return []
|
|
2318
|
-
out: list[dict] = []
|
|
2319
|
-
for a in args:
|
|
2320
|
-
if not isinstance(a, dict):
|
|
2321
|
-
continue
|
|
2322
|
-
choices = a.get("choices")
|
|
2323
|
-
if not isinstance(choices, list) or not choices:
|
|
2324
|
-
continue
|
|
2325
|
-
out.append({
|
|
2326
|
-
"name": str(a.get("name") or ""),
|
|
2327
|
-
"label": str(a.get("label") or a.get("name") or ""),
|
|
2328
|
-
"choices": [str(x) for x in choices],
|
|
2329
|
-
"default": str(a.get("default") or choices[0]),
|
|
2330
|
-
})
|
|
2331
|
-
return out
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
@app.post("/api/terminal/exec")
|
|
2335
|
-
def terminal_exec(body: dict):
|
|
2336
|
-
"""PR95a v3.51.0 — run an allowlisted command and return the output.
|
|
2337
|
-
|
|
2338
|
-
Body: ``{"command_id": "<id>"}``. Unknown ids are rejected.
|
|
2339
|
-
Returns ``{stdout, stderr, exit_code, duration_ms, command}``.
|
|
2340
|
-
Output is capped at ``_TERMINAL_MAX_OUTPUT`` chars per stream.
|
|
2341
|
-
"""
|
|
2342
|
-
if not isinstance(body, dict):
|
|
2343
|
-
return {"error": "body must be an object"}
|
|
2344
|
-
cid = (body.get("command_id") or "").strip()
|
|
2345
|
-
if not cid:
|
|
2346
|
-
return {"error": "command_id is required"}
|
|
2347
|
-
entry = next((c for c in TERMINAL_ALLOWLIST if c["id"] == cid), None)
|
|
2348
|
-
if entry is None:
|
|
2349
|
-
return {"error": f"command '{cid}' is not on the allowlist"}
|
|
2350
|
-
|
|
2351
|
-
# PR96b v3.56.0 — resolve template + args, or fall back to fixed cmd.
|
|
2352
|
-
if "cmd_template" in entry:
|
|
2353
|
-
argv, err = _resolve_cmd_template(entry, body.get("args") or {})
|
|
2354
|
-
if err:
|
|
2355
|
-
return {"error": err}
|
|
2356
|
-
else:
|
|
2357
|
-
argv = entry["cmd"]
|
|
2358
|
-
|
|
2359
|
-
import time
|
|
2360
|
-
started = time.monotonic()
|
|
2361
|
-
try:
|
|
2362
|
-
result = subprocess.run(
|
|
2363
|
-
argv,
|
|
2364
|
-
cwd=str(ARKAOS_ROOT),
|
|
2365
|
-
capture_output=True,
|
|
2366
|
-
text=True,
|
|
2367
|
-
timeout=_TERMINAL_TIMEOUT_S,
|
|
2368
|
-
shell=False,
|
|
2369
|
-
)
|
|
2370
|
-
except subprocess.TimeoutExpired:
|
|
2371
|
-
return {
|
|
2372
|
-
"stdout": "",
|
|
2373
|
-
"stderr": f"command timed out after {_TERMINAL_TIMEOUT_S}s",
|
|
2374
|
-
"exit_code": -1,
|
|
2375
|
-
"duration_ms": int(_TERMINAL_TIMEOUT_S * 1000),
|
|
2376
|
-
"command": " ".join(argv),
|
|
2377
|
-
}
|
|
2378
|
-
except OSError as exc:
|
|
2379
|
-
return {
|
|
2380
|
-
"stdout": "",
|
|
2381
|
-
"stderr": f"command failed to launch: {exc}",
|
|
2382
|
-
"exit_code": -1,
|
|
2383
|
-
"duration_ms": int((time.monotonic() - started) * 1000),
|
|
2384
|
-
"command": " ".join(argv),
|
|
2385
|
-
}
|
|
2386
|
-
duration_ms = int((time.monotonic() - started) * 1000)
|
|
2387
|
-
return {
|
|
2388
|
-
"stdout": (result.stdout or "")[:_TERMINAL_MAX_OUTPUT],
|
|
2389
|
-
"stderr": (result.stderr or "")[:_TERMINAL_MAX_OUTPUT],
|
|
2390
|
-
"exit_code": result.returncode,
|
|
2391
|
-
"duration_ms": duration_ms,
|
|
2392
|
-
"command": " ".join(argv),
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
def _resolve_cmd_template(entry: dict, supplied: dict) -> tuple[list[str], "str | None"]:
|
|
2397
|
-
"""Substitute `{name}` placeholders in cmd_template with validated args.
|
|
2398
|
-
|
|
2399
|
-
Returns (argv, None) on success, ([], error_msg) on validation failure.
|
|
2400
|
-
Anything supplied that isn't in the schema's choices is rejected.
|
|
2401
|
-
Unknown arg names are also rejected — no silent passthrough.
|
|
2402
|
-
"""
|
|
2403
|
-
schema = entry.get("args") or []
|
|
2404
|
-
if not isinstance(schema, list):
|
|
2405
|
-
return [], "command has invalid args schema"
|
|
2406
|
-
by_name: dict[str, dict] = {a["name"]: a for a in schema if isinstance(a, dict)}
|
|
2407
|
-
chosen: dict[str, str] = {}
|
|
2408
|
-
for arg_def in schema:
|
|
2409
|
-
name = arg_def["name"]
|
|
2410
|
-
s = supplied.get(name)
|
|
2411
|
-
if s is None:
|
|
2412
|
-
chosen[name] = str(arg_def.get("default") or arg_def["choices"][0])
|
|
2413
|
-
continue
|
|
2414
|
-
s_str = str(s)
|
|
2415
|
-
if s_str not in arg_def["choices"]:
|
|
2416
|
-
return [], (
|
|
2417
|
-
f"arg '{name}'='{s_str}' is not in the allowed choices "
|
|
2418
|
-
f"({', '.join(arg_def['choices'])})"
|
|
2419
|
-
)
|
|
2420
|
-
chosen[name] = s_str
|
|
2421
|
-
for k in supplied:
|
|
2422
|
-
if k not in by_name:
|
|
2423
|
-
return [], f"unknown arg '{k}'"
|
|
2424
|
-
argv: list[str] = []
|
|
2425
|
-
for tok in entry["cmd_template"]:
|
|
2426
|
-
if not isinstance(tok, str):
|
|
2427
|
-
continue
|
|
2428
|
-
out_tok = tok
|
|
2429
|
-
for name, val in chosen.items():
|
|
2430
|
-
out_tok = out_tok.replace(f"{{{name}}}", val)
|
|
2431
|
-
argv.append(out_tok)
|
|
2432
|
-
return argv, None
|
|
2433
|
-
|
|
2224
|
+
# Allowlist runner (PR95a v3.51.0) removed in PR99d v3.70.0 —
|
|
2225
|
+
# replaced by PTY WebSocket from PR99a/b/c.
|
|
2434
2226
|
|
|
2435
2227
|
@app.put("/api/workflows/{workflow_id}/yaml")
|
|
2436
2228
|
def workflow_update_yaml(workflow_id: str, body: dict):
|