Skip to content

Commit 9f2feca

Browse files
committed
WIP.
1 parent 1802cad commit 9f2feca

4 files changed

Lines changed: 86 additions & 5 deletions

File tree

src/chat/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ChatProviders } from '../providers/chat_providers'
55
import { EmbeddingsProviders } from '../providers/embeddings_providers'
66
import { Preferences } from '../providers/preferences'
77
import { Rule } from '../rules/types'
8+
import { Container } from '../util/docker/container'
89
import { InterruptHandler } from '../util/interrupts/interrupts'
910
import { Prompter } from '../util/prompter/prompter'
1011
import { UsageTracker } from '../util/usage/tracker'
@@ -21,6 +22,7 @@ export type ChatContext = {
2122
contextStateManager: ContextStateManager
2223
yolo: boolean
2324
tools: string[]
25+
container?: Container
2426
}
2527

2628
export async function swapProvider(context: ChatContext, modelName: string): Promise<void> {

src/cli.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Rule } from './rules/types'
1818
import { buildSystemPrompt } from './system'
1919
import { seedAllowedCommands } from './tools/shell/shell_execute'
2020
import { removeDisabledTools } from './tools/tools'
21+
import { startContainer } from './util/docker/container'
2122
import { createInterruptHandler, InterruptHandlerOptions } from './util/interrupts/interrupts'
2223
import { createPrompter } from './util/prompter/prompter'
2324
import { createLimiter } from './util/ratelimits/limiter'
@@ -61,13 +62,17 @@ async function main() {
6162
const disableToolsFlags = '--disable-tools <string>'
6263
const disableToolsDescription = 'Comma-separated list of tool names to disable.'
6364

65+
const sandboxedFlags = '--sandboxed'
66+
const sandboxedDescription = 'Run shell commands in a Docker container (alpine image).'
67+
6468
program
6569
.option(historyFlags, historyDescription)
6670
.option(portFlags, portDescription)
6771
.option(cwdFlags, cwdDescription)
6872
.option(yoloFlags, yoloDescription)
6973
.option(oneShotFlags, oneShotDescription)
7074
.option(disableToolsFlags, disableToolsDescription)
75+
.option(sandboxedFlags, sandboxedDescription)
7176
.action(options => {
7277
if (options.cwd) {
7378
process.chdir(options.cwd)
@@ -84,6 +89,7 @@ async function main() {
8489
options.oneShot,
8590
options.yolo,
8691
options.disableTools,
92+
options.sandboxed,
8793
)
8894
})
8995

@@ -101,6 +107,7 @@ async function chat(
101107
oneShot?: string,
102108
yolo: boolean = false,
103109
disabledTools?: string,
110+
sandboxed: boolean = false,
104111
) {
105112
if (!process.stdin.setRawMode) {
106113
throw new Error('chat command is not supported in this environment.')
@@ -136,14 +143,20 @@ async function chat(
136143
await registerTools(client)
137144
await seedAllowedCommands()
138145

146+
const interruptHandler = createInterruptHandler(rl)
147+
const interruptInputOptions = rootInterruptHandlerOptions(rl)
148+
149+
const container = sandboxed
150+
? await startContainer({ interruptHandler, preferences, contextStateManager })
151+
: undefined
152+
139153
try {
140-
const interruptHandler = createInterruptHandler(rl)
141-
const interruptInputOptions = rootInterruptHandlerOptions(rl)
142154
const prompter = createPrompter(
143155
rl,
144156
interruptHandler,
145157
attentionGetter(preferences.attentionCommand ?? defaultAttentionCommand),
146158
)
159+
147160
const provider = await providers.createProvider({
148161
contextState: contextStateManager,
149162
modelName: preferences.defaultModel,
@@ -164,6 +177,7 @@ async function chat(
164177
contextStateManager,
165178
yolo,
166179
tools: allowedToolNames,
180+
container,
167181
}
168182

169183
await registerContextListeners(context, client)
@@ -180,6 +194,7 @@ async function chat(
180194
interruptInputOptions,
181195
)
182196
} finally {
197+
await container?.stop()
183198
process.stdin.unpipe(filter)
184199
filter.destroy()
185200
rl.close()

src/util/docker/container.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import chalk from 'chalk'
2+
import { ExecContext, execOnHost } from '../shell/exec'
3+
4+
export type Container = Awaited<ReturnType<typeof startContainer>>
5+
6+
export async function startContainer(context: ExecContext) {
7+
const imageName = 'alpine'
8+
let containerId = ''
9+
10+
console.log(`${chalk.dim('ℹ')} Starting Docker container...`)
11+
12+
try {
13+
const output = await execOnHost(
14+
context,
15+
`docker run -d -v "${process.cwd()}:/workspace" -w /workspace ${imageName} tail -f /dev/null`,
16+
)
17+
for (const line of output) {
18+
containerId = line.content.trim()
19+
}
20+
21+
console.log(`${chalk.green('✓')} Docker container ${containerId} started`)
22+
} catch (error: any) {
23+
throw new Error(`Failed to start Docker container: ${error.message}`)
24+
}
25+
26+
const stop = async () => {
27+
console.log(`${chalk.dim('ℹ')} Stopping Docker container...`)
28+
29+
try {
30+
await execOnHost(context, `docker stop ${containerId}`)
31+
await execOnHost(context, `docker rm ${containerId}`)
32+
console.log(`${chalk.green('✓')} Docker container stopped and removed`)
33+
} catch (error: any) {
34+
console.warn(`${chalk.yellow('⚠')} Failed to clean up container: ${error.message}`)
35+
}
36+
}
37+
38+
return {
39+
containerId: () => containerId,
40+
stop,
41+
}
42+
}

src/util/shell/exec.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ export type OutputLine = {
1515
content: string
1616
}
1717

18+
export type ExecContext = Pick<ChatContext, 'interruptHandler' | 'preferences' | 'contextStateManager' | 'container'>
19+
1820
export async function executeCommand(
19-
context: ChatContext,
21+
context: ExecContext,
2022
command: string,
23+
signal?: AbortSignal, // TODO - check callers
2124
): Promise<{ result?: ShellResult; error?: Error; canceled?: boolean }> {
22-
const response = await withProgress<OutputLine[]>(update => runCommand(context, command, update), {
25+
const response = await withProgress<OutputLine[]>(update => runCommand(context, command, update, signal), {
2326
progress: prefixFormatter('Executing command...', formatOutput),
2427
success: prefixFormatter('Command succeeded.', formatOutput),
2528
failure: prefixFormatter('Command failed.', formatOutput),
@@ -43,7 +46,26 @@ export async function executeCommand(
4346
}
4447
}
4548

46-
async function runCommand(context: ChatContext, command: string, update: Updater<OutputLine[]>): Promise<OutputLine[]> {
49+
async function runCommand(
50+
context: ExecContext,
51+
command: string,
52+
update: Updater<OutputLine[]> = () => {},
53+
signal?: AbortSignal,
54+
): Promise<OutputLine[]> {
55+
const containerId = context.container?.containerId
56+
if (containerId) {
57+
command = `docker exec ${containerId} sh -c ${command}`
58+
}
59+
60+
return execOnHost(context, command, update, signal)
61+
}
62+
63+
export async function execOnHost(
64+
context: ExecContext,
65+
command: string,
66+
update: Updater<OutputLine[]> = () => {},
67+
_signal?: AbortSignal, // TODO - use
68+
): Promise<OutputLine[]> {
4769
return context.interruptHandler.withInterruptHandler(signal => {
4870
return new Promise((resolve, reject) => {
4971
const output: OutputLine[] = []

0 commit comments

Comments
 (0)