Merge pull request 'fix: vision(#623): scope Claude chat working directory to project staging checkout (#1027)' (#1089) from fix/issue-1027-1 into main
This commit is contained in:
commit
65df00ea6a
3 changed files with 64 additions and 4 deletions
|
|
@ -20,6 +20,12 @@ OAuth flow:
|
||||||
6. Redirects to /chat/
|
6. Redirects to /chat/
|
||||||
|
|
||||||
The claude binary is expected to be mounted from the host at /usr/local/bin/claude.
|
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
|
import asyncio
|
||||||
|
|
@ -46,6 +52,10 @@ UI_DIR = "/var/chat/ui"
|
||||||
STATIC_DIR = os.path.join(UI_DIR, "static")
|
STATIC_DIR = os.path.join(UI_DIR, "static")
|
||||||
CLAUDE_BIN = "/usr/local/bin/claude"
|
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
|
# OAuth configuration
|
||||||
FORGE_URL = os.environ.get("FORGE_URL", "http://localhost:3000")
|
FORGE_URL = os.environ.get("FORGE_URL", "http://localhost:3000")
|
||||||
CHAT_OAUTH_CLIENT_ID = os.environ.get("CHAT_OAUTH_CLIENT_ID", "")
|
CHAT_OAUTH_CLIENT_ID = os.environ.get("CHAT_OAUTH_CLIENT_ID", "")
|
||||||
|
|
@ -491,12 +501,18 @@ class _WebSocketHandler:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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
|
# 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(
|
proc = subprocess.Popen(
|
||||||
[CLAUDE_BIN, "--print", "--output-format", "stream-json", message],
|
claude_args,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
|
cwd=cwd,
|
||||||
bufsize=1,
|
bufsize=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1040,12 +1056,18 @@ class ChatHandler(BaseHTTPRequestHandler):
|
||||||
# Save user message to history
|
# Save user message to history
|
||||||
_write_message(user, conv_id, "user", message)
|
_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)
|
# 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(
|
proc = subprocess.Popen(
|
||||||
[CLAUDE_BIN, "--print", "--output-format", "stream-json", message],
|
claude_args,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
|
cwd=cwd,
|
||||||
bufsize=1, # Line buffered
|
bufsize=1, # Line buffered
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,9 @@ COMPOSEEOF
|
||||||
- chat-config:/var/chat/config
|
- chat-config:/var/chat/config
|
||||||
# Chat history persistence: per-user NDJSON files on bind-mounted host volume
|
# Chat history persistence: per-user NDJSON files on bind-mounted host volume
|
||||||
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
|
- ${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:
|
environment:
|
||||||
CHAT_HOST: "0.0.0.0"
|
CHAT_HOST: "0.0.0.0"
|
||||||
CHAT_PORT: "8080"
|
CHAT_PORT: "8080"
|
||||||
|
|
@ -718,6 +721,8 @@ COMPOSEEOF
|
||||||
# Shared secret for Caddy forward_auth verify endpoint (#709)
|
# Shared secret for Caddy forward_auth verify endpoint (#709)
|
||||||
FORWARD_AUTH_SECRET: ${FORWARD_AUTH_SECRET:-}
|
FORWARD_AUTH_SECRET: ${FORWARD_AUTH_SECRET:-}
|
||||||
# Rate limiting removed (#1084)
|
# Rate limiting removed (#1084)
|
||||||
|
# Workspace directory for Claude code access (#1027)
|
||||||
|
CHAT_WORKSPACE_DIR: ${CHAT_WORKSPACE_DIR:-./workspace}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,17 @@
|
||||||
# FORWARD_AUTH_SECRET from kv/disinto/shared/chat
|
# FORWARD_AUTH_SECRET from kv/disinto/shared/chat
|
||||||
# - Seeded on fresh boxes by tools/vault-seed-chat.sh
|
# - Seeded on fresh boxes by tools/vault-seed-chat.sh
|
||||||
#
|
#
|
||||||
# Host volume:
|
# Host volumes:
|
||||||
# - chat-history → /var/lib/chat/history (persists conversation history)
|
# - 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
|
# 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
|
# until cutover. This file exists so CI can validate it and S5.2 can wire
|
||||||
|
|
@ -61,6 +70,21 @@ job "chat" {
|
||||||
read_only = false
|
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 policy ───────────────────────────────────────────────────────
|
||||||
restart {
|
restart {
|
||||||
attempts = 3
|
attempts = 3
|
||||||
|
|
@ -115,11 +139,20 @@ job "chat" {
|
||||||
read_only = false
|
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) ──────────────────────────────
|
# ── Environment: secrets from Vault (S5.2) ──────────────────────────────
|
||||||
# CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, FORWARD_AUTH_SECRET
|
# CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, FORWARD_AUTH_SECRET
|
||||||
# rendered from kv/disinto/shared/chat via template stanza.
|
# rendered from kv/disinto/shared/chat via template stanza.
|
||||||
env {
|
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) ─────────────────────────
|
# ── Vault-templated secrets (S5.2, issue #989) ─────────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue