Merge pull request 'fix: vision(#623): scope Claude chat working directory to project staging checkout (#1027)' (#1089) from fix/issue-1027-1 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/nomad-validate Pipeline was successful

This commit is contained in:
dev-qwen2 2026-04-20 18:26:41 +00:00
commit 65df00ea6a
3 changed files with 64 additions and 4 deletions

View file

@ -20,6 +20,12 @@ OAuth flow:
6. Redirects to /chat/
The claude binary is expected to be mounted from the host at /usr/local/bin/claude.
Workspace access:
- CHAT_WORKSPACE_DIR environment variable: bind-mounted project working tree
- Claude invocation uses --permission-mode acceptEdits for code modification
- CWD is set to workspace directory when configured, enabling Claude to
inspect, explain, or modify code scoped to that tree only
"""
import asyncio
@ -46,6 +52,10 @@ UI_DIR = "/var/chat/ui"
STATIC_DIR = os.path.join(UI_DIR, "static")
CLAUDE_BIN = "/usr/local/bin/claude"
# Workspace directory: bind-mounted project working tree for Claude access
# Defaults to empty; when set, Claude can read/write to this directory
WORKSPACE_DIR = os.environ.get("CHAT_WORKSPACE_DIR", "")
# OAuth configuration
FORGE_URL = os.environ.get("FORGE_URL", "http://localhost:3000")
CHAT_OAUTH_CLIENT_ID = os.environ.get("CHAT_OAUTH_CLIENT_ID", "")
@ -491,12 +501,18 @@ class _WebSocketHandler:
return
try:
# Build claude command with permission mode (acceptEdits allows file edits)
claude_args = [CLAUDE_BIN, "--print", "--output-format", "stream-json", "--permission-mode", "acceptEdits", message]
# Spawn claude --print with stream-json for streaming output
# Set cwd to workspace directory if configured, allowing Claude to access project code
cwd = WORKSPACE_DIR if WORKSPACE_DIR else None
proc = subprocess.Popen(
[CLAUDE_BIN, "--print", "--output-format", "stream-json", message],
claude_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=cwd,
bufsize=1,
)
@ -1040,12 +1056,18 @@ class ChatHandler(BaseHTTPRequestHandler):
# Save user message to history
_write_message(user, conv_id, "user", message)
# Build claude command with permission mode (acceptEdits allows file edits)
claude_args = [CLAUDE_BIN, "--print", "--output-format", "stream-json", "--permission-mode", "acceptEdits", message]
# Spawn claude --print with stream-json for token tracking (#711)
# Set cwd to workspace directory if configured, allowing Claude to access project code
cwd = WORKSPACE_DIR if WORKSPACE_DIR else None
proc = subprocess.Popen(
[CLAUDE_BIN, "--print", "--output-format", "stream-json", message],
claude_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=cwd,
bufsize=1, # Line buffered
)

View file

@ -705,6 +705,9 @@ COMPOSEEOF
- chat-config:/var/chat/config
# Chat history persistence: per-user NDJSON files on bind-mounted host volume
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
# Workspace directory: bind-mounted project working tree for Claude access (#1027)
# Mounted when CHAT_WORKSPACE_DIR is set (defaults to ./workspace)
- ${CHAT_WORKSPACE_DIR:-./workspace}:/var/workspace
environment:
CHAT_HOST: "0.0.0.0"
CHAT_PORT: "8080"
@ -718,6 +721,8 @@ COMPOSEEOF
# Shared secret for Caddy forward_auth verify endpoint (#709)
FORWARD_AUTH_SECRET: ${FORWARD_AUTH_SECRET:-}
# Rate limiting removed (#1084)
# Workspace directory for Claude code access (#1027)
CHAT_WORKSPACE_DIR: ${CHAT_WORKSPACE_DIR:-./workspace}
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
interval: 30s

View file

@ -21,8 +21,17 @@
# FORWARD_AUTH_SECRET from kv/disinto/shared/chat
# - Seeded on fresh boxes by tools/vault-seed-chat.sh
#
# Host volume:
# Host volumes:
# - chat-history /var/lib/chat/history (persists conversation history)
# - workspace /var/workspace (project working tree for Claude access, #1027)
#
# Client-side host_volume registration (operator prerequisite):
# In nomad/client.hcl on each Nomad node:
# host_volume "chat-workspace" {
# path = "/var/disinto/chat-workspace"
# read_only = false
# }
# Nodes without the host_volume registered will not schedule the workspace mount.
#
# Not the runtime yet: docker-compose.yml is still the factory's live stack
# until cutover. This file exists so CI can validate it and S5.2 can wire
@ -61,6 +70,21 @@ job "chat" {
read_only = false
}
# Workspace volume: bind-mounted project working tree for Claude access (#1027)
# Source is a fixed logical name resolved by client-side host_volume registration.
volume "workspace" {
type = "host"
source = "chat-workspace"
read_only = false
}
# Metadata (per-dispatch env var via NOMAD_META_*)
# CHAT_WORKSPACE_DIR: project working tree path, injected into task env
# as NOMAD_META_CHAT_WORKSPACE_DIR for the workspace volume mount target.
meta {
CHAT_WORKSPACE_DIR = "/var/workspace"
}
# Restart policy
restart {
attempts = 3
@ -115,11 +139,20 @@ job "chat" {
read_only = false
}
# Mount workspace directory for Claude code access (#1027)
# Binds project working tree so Claude can inspect/modify code
volume_mount {
volume = "workspace"
destination = "/var/workspace"
read_only = false
}
# Environment: secrets from Vault (S5.2)
# CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, FORWARD_AUTH_SECRET
# rendered from kv/disinto/shared/chat via template stanza.
env {
FORGE_URL = "http://forgejo:3000"
FORGE_URL = "http://forgejo:3000"
CHAT_WORKSPACE_DIR = "${NOMAD_META_CHAT_WORKSPACE_DIR}"
}
# Vault-templated secrets (S5.2, issue #989)