From 749f7a2b08b20aa9a5c212102b29a9ba5602fa31 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Wed, 11 Mar 2026 09:44:57 +0100 Subject: [PATCH 1/4] fix: convert service Dockerfiles to multi-stage builds for ARM64 compatibility Add python3 and build-essential in builder stage so better-sqlite3 can compile from source when ARM64 prebuilt binaries fail. Production image remains slim with only dumb-init and wget. Co-Authored-By: Claude --- .../services/contact-intelligence/Dockerfile | 31 ++++++++++++++++--- .../services/sentiment-analysis/Dockerfile | 31 ++++++++++++++++--- .../services/writing-style/Dockerfile | 31 ++++++++++++++++--- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/tests/ag-ui-app/services/contact-intelligence/Dockerfile b/tests/ag-ui-app/services/contact-intelligence/Dockerfile index 0556ae4d..007cec08 100644 --- a/tests/ag-ui-app/services/contact-intelligence/Dockerfile +++ b/tests/ag-ui-app/services/contact-intelligence/Dockerfile @@ -3,12 +3,12 @@ # Using Debian slim for better native module compatibility # ============================================================================= -FROM node:20-slim +FROM node:20-slim AS builder -# Install dumb-init for proper signal handling +# Install build dependencies for native modules (better-sqlite3) RUN apt-get update && apt-get install -y --no-install-recommends \ - dumb-init \ - wget \ + python3 \ + build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -26,6 +26,29 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build +# ============================================================================= +# Production stage - minimal runtime image +# ============================================================================= +FROM node:20-slim + +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ + dumb-init \ + wget \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Enable pnpm for runtime +RUN corepack enable pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml* .npmrc* ./ + +# Copy built artifacts and node_modules from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules + # Create non-root user for security RUN groupadd --system --gid 1001 nodejs && \ useradd --system --uid 1001 --gid nodejs appuser && \ diff --git a/tests/ag-ui-app/services/sentiment-analysis/Dockerfile b/tests/ag-ui-app/services/sentiment-analysis/Dockerfile index 6dedebf6..4f95ff90 100644 --- a/tests/ag-ui-app/services/sentiment-analysis/Dockerfile +++ b/tests/ag-ui-app/services/sentiment-analysis/Dockerfile @@ -3,12 +3,12 @@ # Using Debian slim for better native module compatibility # ============================================================================= -FROM node:20-slim +FROM node:20-slim AS builder -# Install dumb-init for proper signal handling +# Install build dependencies for native modules (better-sqlite3) RUN apt-get update && apt-get install -y --no-install-recommends \ - dumb-init \ - wget \ + python3 \ + build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -26,6 +26,29 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build +# ============================================================================= +# Production stage - minimal runtime image +# ============================================================================= +FROM node:20-slim + +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ + dumb-init \ + wget \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Enable pnpm for runtime +RUN corepack enable pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml* .npmrc* ./ + +# Copy built artifacts and node_modules from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules + # Create non-root user for security RUN groupadd --system --gid 1001 nodejs && \ useradd --system --uid 1001 --gid nodejs appuser && \ diff --git a/tests/ag-ui-app/services/writing-style/Dockerfile b/tests/ag-ui-app/services/writing-style/Dockerfile index e294cdfc..33a72bc5 100644 --- a/tests/ag-ui-app/services/writing-style/Dockerfile +++ b/tests/ag-ui-app/services/writing-style/Dockerfile @@ -3,12 +3,12 @@ # Using Debian slim for better native module compatibility # ============================================================================= -FROM node:20-slim +FROM node:20-slim AS builder -# Install dumb-init for proper signal handling +# Install build dependencies for native modules (better-sqlite3) RUN apt-get update && apt-get install -y --no-install-recommends \ - dumb-init \ - wget \ + python3 \ + build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -26,6 +26,29 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build +# ============================================================================= +# Production stage - minimal runtime image +# ============================================================================= +FROM node:20-slim + +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ + dumb-init \ + wget \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Enable pnpm for runtime +RUN corepack enable pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml* .npmrc* ./ + +# Copy built artifacts and node_modules from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules + # Create non-root user for security RUN groupadd --system --gid 1001 nodejs && \ useradd --system --uid 1001 --gid nodejs appuser && \ From aab4bf30cdd9be8b8346f3a0a4015698d532198c Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Wed, 11 Mar 2026 10:18:29 +0100 Subject: [PATCH 2/4] fix(agent): restore write/shell tools on follow-up confirmation turns is_generation_query() only checked the current user input, so short follow-up messages like "sure go ahead fix all then" didn't match any keyword and caused the agent to be built without WriteFileTool, WriteFilesTool, and ShellTool. The model then tried to call those tools (they were in conversation history) and got ToolNotFoundError. Two fixes: 1. Add commonly missing generation verbs to is_generation_query: fix, update, add, change, modify, edit, configure, setup, patch, install - these clearly imply file modification intent. 2. Track last_was_generation in ChatSession. For short inputs (< 60 chars) that aren't planning mode, inherit generation mode from the previous turn so confirmations like "sure", "yes", "go ahead" keep write/shell tools active. Co-Authored-By: Claude --- src/agent/mod.rs | 11 ++++++++++- src/agent/prompts/mod.rs | 12 ++++++++++++ src/agent/session/mod.rs | 5 +++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index d46250ec..ab75eeb1 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -690,8 +690,13 @@ pub async fn run_interactive( Some(¤t_input), session.plan_mode, ); - let is_generation = prompts::is_generation_query(¤t_input); let is_planning = session.plan_mode.is_planning(); + // Inherit generation mode for short follow-up messages ("sure", "yes", "go ahead", + // etc.) so the write/shell tool set is not lost between turns. + let is_generation = prompts::is_generation_query(¤t_input) + || (!is_planning + && session.last_was_generation + && current_input.trim().len() < 60); // Note: using raw_chat_history directly which preserves Reasoning blocks // This is needed for extended thinking to work with multi-turn conversations @@ -1138,6 +1143,10 @@ pub async fn run_interactive( // Add to conversation history with tool call records conversation_history.add_turn(input.clone(), text.clone(), tool_calls.clone()); + // Remember whether this turn had generation tools active so short follow-up + // messages ("sure", "go ahead", etc.) don't lose write/shell access. + session.last_was_generation = is_generation; + // Check if this heavy turn requires immediate compaction // This helps prevent context overflow in subsequent requests if conversation_history.needs_compaction() { diff --git a/src/agent/prompts/mod.rs b/src/agent/prompts/mod.rs index e25e72e6..2a49f658 100644 --- a/src/agent/prompts/mod.rs +++ b/src/agent/prompts/mod.rs @@ -705,6 +705,18 @@ pub fn is_generation_query(query: &str) -> bool { "new feature", "develop", "code", + // Common modification verbs (previously missing) + "fix", + "update", + "add", + "change", + "modify", + "edit", + "configure", + "setup", + "set up", + "patch", + "install", // Plan execution keywords - needed for plan continuation "plan", "continue", diff --git a/src/agent/session/mod.rs b/src/agent/session/mod.rs index 8b60caff..d6bd0b7e 100644 --- a/src/agent/session/mod.rs +++ b/src/agent/session/mod.rs @@ -34,6 +34,10 @@ pub struct ChatSession { pub token_usage: TokenUsage, /// Current planning mode state pub plan_mode: PlanMode, + /// Whether the previous turn used generation mode (write/shell tools active). + /// Used so short follow-up messages ("sure", "go ahead", "yes") inherit the + /// tool set from the previous turn instead of losing write/shell access. + pub last_was_generation: bool, /// Session loaded via /resume command, to be processed by main loop pub pending_resume: Option, /// Platform session state (selected project/org context) @@ -58,6 +62,7 @@ impl ChatSession { history: Vec::new(), token_usage: TokenUsage::new(), plan_mode: PlanMode::default(), + last_was_generation: false, pending_resume: None, platform_session, } From 024a22b07a3fa75bdac04bfece8d0be9fc60786d Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Fri, 13 Mar 2026 14:36:38 +0100 Subject: [PATCH 3/4] bug: hallucinated project_id The agent created a hallucinated project_id when deploying for the syncable platform. Instead of relying context, the project_id is now ALWAYS derrived directly from the session, where users attach them selves to a project. --- .../platform/create_deployment_config.rs | 39 +++++++------- .../tools/platform/trigger_deployment.rs | 51 ++++++++++--------- src/platform/api/client.rs | 2 +- src/platform/api/types.rs | 2 +- src/wizard/orchestrator.rs | 3 +- 5 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/agent/tools/platform/create_deployment_config.rs b/src/agent/tools/platform/create_deployment_config.rs index 790e44f3..b825152d 100644 --- a/src/agent/tools/platform/create_deployment_config.rs +++ b/src/agent/tools/platform/create_deployment_config.rs @@ -13,13 +13,12 @@ use crate::platform::api::types::{ build_cloud_runner_config_v2, }; use crate::platform::api::{PlatformApiClient, PlatformApiError}; +use crate::platform::session::PlatformSession; use std::str::FromStr; /// Arguments for the create deployment config tool #[derive(Debug, Deserialize)] pub struct CreateDeploymentConfigArgs { - /// The project UUID - pub project_id: String, /// Service name for the deployment pub service_name: String, /// Repository ID from GitHub integration @@ -102,7 +101,6 @@ A deployment config defines how to build and deploy a service, including: - Auto-deploy settings **Required Parameters:** -- project_id: The project UUID - service_name: Name for the service (lowercase, hyphens allowed) - repository_id: GitHub repository ID (from platform GitHub integration) - repository_full_name: Full repo name like "owner/repo" @@ -138,10 +136,6 @@ A deployment config defines how to build and deploy a service, including: parameters: json!({ "type": "object", "properties": { - "project_id": { - "type": "string", - "description": "The UUID of the project" - }, "service_name": { "type": "string", "description": "Name for the service (lowercase, hyphens allowed)" @@ -218,7 +212,7 @@ A deployment config defines how to build and deploy a service, including: } }, "required": [ - "project_id", "service_name", "repository_id", "repository_full_name", + "service_name", "repository_id", "repository_full_name", "port", "branch", "target_type", "provider", "environment_id" ] }), @@ -226,19 +220,30 @@ A deployment config defines how to build and deploy a service, including: } async fn call(&self, args: Self::Args) -> Result { - // Validate required fields - if args.project_id.trim().is_empty() { + // Load project_id from session (authoritative source — prevents stale IDs from LLM context) + let session = match PlatformSession::load() { + Ok(s) => s, + Err(_) => { + return Ok(format_error_for_llm( + "create_deployment_config", + ErrorCategory::InternalError, + "Failed to load platform session", + Some(vec!["Try authenticating with `sync-ctl auth login`"]), + )); + } + }; + + if !session.is_project_selected() { return Ok(format_error_for_llm( "create_deployment_config", ErrorCategory::ValidationFailed, - "project_id cannot be empty", - Some(vec![ - "Use list_projects to find valid project IDs", - "Use current_context to get the selected project", - ]), + "No project selected", + Some(vec!["Use select_project to choose a project first"]), )); } + let project_id = session.project_id.clone().unwrap_or_default(); + if args.service_name.trim().is_empty() { return Ok(format_error_for_llm( "create_deployment_config", @@ -316,7 +321,7 @@ A deployment config defines how to build and deploy a service, including: if let Some(ref provider) = provider_enum { if matches!(provider, CloudProvider::Gcp | CloudProvider::Azure) { if let Ok(credential) = client - .check_provider_connection(provider, &args.project_id) + .check_provider_connection(provider, &project_id) .await { if let Some(cred) = credential { @@ -351,7 +356,7 @@ A deployment config defines how to build and deploy a service, including: // Note: Send both field name variants (dockerfile/dockerfilePath, context/buildContext) // for backend compatibility - different endpoints may expect different field names let request = CreateDeploymentConfigRequest { - project_id: args.project_id.clone(), + project_id, service_name: args.service_name.clone(), repository_id: args.repository_id, repository_full_name: args.repository_full_name.clone(), diff --git a/src/agent/tools/platform/trigger_deployment.rs b/src/agent/tools/platform/trigger_deployment.rs index 0bc138d7..262d8b38 100644 --- a/src/agent/tools/platform/trigger_deployment.rs +++ b/src/agent/tools/platform/trigger_deployment.rs @@ -9,12 +9,11 @@ use serde_json::json; use crate::agent::tools::error::{ErrorCategory, format_error_for_llm}; use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest}; +use crate::platform::session::PlatformSession; /// Arguments for the trigger deployment tool #[derive(Debug, Deserialize)] pub struct TriggerDeploymentArgs { - /// The project ID for the deployment - pub project_id: String, /// The deployment config ID to use pub config_id: String, /// Optional specific commit SHA to deploy @@ -56,13 +55,12 @@ Starts a new deployment for the specified config. Returns a task ID that can be used to monitor deployment progress with `get_deployment_status`. **Parameters:** -- project_id: The project UUID -- config_id: The deployment config ID (get from list_deployment_configs) +- config_id: The deployment config ID (get from list_deployment_configs or create_deployment_config) - commit_sha: Optional specific commit to deploy (defaults to latest on branch) **Prerequisites:** - User must be authenticated via `sync-ctl auth login` -- A deployment config must exist for the project +- A deployment config must exist (use create_deployment_config first if needed) **Use Cases:** - Deploy the latest code from a branch @@ -77,50 +75,57 @@ used to monitor deployment progress with `get_deployment_status`. parameters: json!({ "type": "object", "properties": { - "project_id": { - "type": "string", - "description": "The UUID of the project" - }, "config_id": { "type": "string", - "description": "The deployment config ID (from list_deployment_configs)" + "description": "The deployment config ID (from list_deployment_configs or create_deployment_config)" }, "commit_sha": { "type": "string", "description": "Optional: specific commit SHA to deploy (defaults to latest)" } }, - "required": ["project_id", "config_id"] + "required": ["config_id"] }), } } async fn call(&self, args: Self::Args) -> Result { - // Validate project_id - if args.project_id.trim().is_empty() { + // Validate config_id + if args.config_id.trim().is_empty() { return Ok(format_error_for_llm( "trigger_deployment", ErrorCategory::ValidationFailed, - "project_id cannot be empty", + "config_id cannot be empty", Some(vec![ - "Use list_projects to find valid project IDs", - "Use select_project to set the current project context", + "Use list_deployment_configs to find available deployment configs", ]), )); } - // Validate config_id - if args.config_id.trim().is_empty() { + // Load project_id from session (authoritative source) + let session = match PlatformSession::load() { + Ok(s) => s, + Err(_) => { + return Ok(format_error_for_llm( + "trigger_deployment", + ErrorCategory::InternalError, + "Failed to load platform session", + Some(vec!["Try authenticating with `sync-ctl auth login`"]), + )); + } + }; + + if !session.is_project_selected() { return Ok(format_error_for_llm( "trigger_deployment", ErrorCategory::ValidationFailed, - "config_id cannot be empty", - Some(vec![ - "Use list_deployment_configs to find available deployment configs", - ]), + "No project selected", + Some(vec!["Use select_project to choose a project first"]), )); } + let project_id = session.project_id.clone().unwrap_or_default(); + // Create the API client let client = match PlatformApiClient::new() { Ok(c) => c, @@ -131,7 +136,7 @@ used to monitor deployment progress with `get_deployment_status`. // Build the request let request = TriggerDeploymentRequest { - project_id: args.project_id.clone(), + project_id, config_id: args.config_id.clone(), commit_sha: args.commit_sha.clone(), }; diff --git a/src/platform/api/client.rs b/src/platform/api/client.rs index 6d09fc73..99b560c0 100644 --- a/src/platform/api/client.rs +++ b/src/platform/api/client.rs @@ -722,7 +722,7 @@ impl PlatformApiClient { request: &TriggerDeploymentRequest, ) -> Result { log::debug!( - "Triggering deployment: POST /api/deployment-configs/deploy with projectId={}, configId={}", + "Triggering deployment: POST /api/deployment-configs/deploy with projectId={} configId={}", request.project_id, request.config_id ); diff --git a/src/platform/api/types.rs b/src/platform/api/types.rs index 32f03f96..64d2cb95 100644 --- a/src/platform/api/types.rs +++ b/src/platform/api/types.rs @@ -335,7 +335,7 @@ pub struct CreateDeploymentConfigResponse { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TriggerDeploymentRequest { - /// Project ID for the deployment + /// The project UUID this config belongs to pub project_id: String, /// Deployment config ID to use pub config_id: String, diff --git a/src/wizard/orchestrator.rs b/src/wizard/orchestrator.rs index f551259d..912d9597 100644 --- a/src/wizard/orchestrator.rs +++ b/src/wizard/orchestrator.rs @@ -525,8 +525,7 @@ pub async fn run_wizard( // Debug: Show trigger request log::debug!( - "Trigger request: projectId={}, configId={}", - trigger_request.project_id, + "Trigger request: configId={}", trigger_request.config_id ); From e80b452da120d52c8fd936cb2ae788e64745fa31 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Sun, 15 Mar 2026 17:07:47 +0100 Subject: [PATCH 4/4] --- Cargo.lock | 30 +++++++++++++++--------------- slate.json | 8 ++++++++ src/agent/mod.rs | 4 +--- src/wizard/orchestrator.rs | 5 +---- 4 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 slate.json diff --git a/Cargo.lock b/Cargo.lock index 963d5659..c727296f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,7 +966,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1423,7 +1423,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1564,7 +1564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3084,7 +3084,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -3343,7 +3343,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3380,7 +3380,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4449,7 +4449,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.6.2", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -4458,9 +4458,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -4487,9 +4487,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4883,7 +4883,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4954,7 +4954,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5684,7 +5684,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6577,7 +6577,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/slate.json b/slate.json new file mode 100644 index 00000000..a57770c9 --- /dev/null +++ b/slate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://randomlabs.ai/config.json", + "permission": { + "*": "allow", + "bash": "ask", + "edit": "ask" + } +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs index ab75eeb1..cbcb2f46 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -694,9 +694,7 @@ pub async fn run_interactive( // Inherit generation mode for short follow-up messages ("sure", "yes", "go ahead", // etc.) so the write/shell tool set is not lost between turns. let is_generation = prompts::is_generation_query(¤t_input) - || (!is_planning - && session.last_was_generation - && current_input.trim().len() < 60); + || (!is_planning && session.last_was_generation && current_input.trim().len() < 60); // Note: using raw_chat_history directly which preserves Reasoning blocks // This is needed for extended thinking to work with multi-turn conversations diff --git a/src/wizard/orchestrator.rs b/src/wizard/orchestrator.rs index 912d9597..1a558ee5 100644 --- a/src/wizard/orchestrator.rs +++ b/src/wizard/orchestrator.rs @@ -524,10 +524,7 @@ pub async fn run_wizard( }; // Debug: Show trigger request - log::debug!( - "Trigger request: configId={}", - trigger_request.config_id - ); + log::debug!("Trigger request: configId={}", trigger_request.config_id); match client.trigger_deployment(&trigger_request).await { Ok(response) => {