Init Project
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s

This commit is contained in:
enkly
2026-10-30 18:06:55 +01:00
parent da8af794bd
commit 79ce8dc4c0
211 changed files with 44434 additions and 1 deletions

View File

@@ -0,0 +1,159 @@
// Package agent is the Agent Orchestration bounded context: the agentic
// run loop (prompt -> vLLM -> tool calls -> final reply), the chat-run
// quota/concurrency guard, and the in-flight/just-finished run registry
// that used to live in internal/handlers/chat.go's StreamManager.
//
// This is a saga coordinator, not a data owner: it holds no SQL table of
// its own. It calls billing (usage quota), conversation (persisting
// turns) and knowledge (the search_codebase tool) over gRPC, and
// workspace (every filesystem/bash tool) over gRPC, exactly replacing
// internal/llm/agent.go + internal/llm/tools.go's direct, in-process
// calls into internal/db and the local filesystem.
//
// Rendering (Markdown, syntax highlighting, deliverable cards) stays out
// of this service on purpose — see internal/render's doc comment in the
// gateway service — so this API deals in plain text and structured tool
// results, never HTML.
syntax = "proto3";
package enkly.agent.v1;
option go_package = "enkly/gen/agent/v1;agentv1";
service AgentService {
// StartTurn replaces the synchronous prefix of Handlers.Chat: the
// per-user throttle (ChatLimiter), the billing quota check, loading
// the conversation, persisting the user's prompt, auto-titling, and
// registering a new run (StreamManager.create) before handing off to
// a background goroutine. Returns immediately with a stream_id; the
// reply itself arrives via StreamTurn.
rpc StartTurn(StartTurnRequest) returns (StartTurnResponse);
// StreamTurn replaces Handlers.StreamChat: a server-streaming view of
// one run's progress. resume_from_tool_index mirrors the monolith's
// SSE Last-Event-ID handling, so a reconnecting client doesn't see
// tool cards it already has.
rpc StreamTurn(StreamTurnRequest) returns (stream TurnEvent);
// StopTurn replaces Handlers.StopStream.
rpc StopTurn(StopTurnRequest) returns (StopTurnResponse);
}
enum StartTurnOutcome {
START_TURN_OUTCOME_UNSPECIFIED = 0;
START_TURN_OUTCOME_OK = 1;
START_TURN_OUTCOME_CONVERSATION_NOT_FOUND = 2;
START_TURN_OUTCOME_CONVERSATION_BUSY = 3;
START_TURN_OUTCOME_TOO_MANY_STREAMS = 4;
START_TURN_OUTCOME_RATE_LIMITED = 5;
START_TURN_OUTCOME_QUOTA_EXCEEDED = 6;
START_TURN_OUTCOME_INTERNAL_ERROR = 7;
}
// SystemEntry is a mid-conversation instruction/permission/budget
// update — see agentrun.SystemEntry (services/agent/internal/
// infrastructure/agentrun/effort.go) for what each kind means and how
// it's folded into the system prompt.
message SystemEntry {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_INSTRUCTION = 1;
KIND_PERMISSION = 2;
KIND_BUDGET = 3;
}
Kind kind = 1;
string content = 2;
}
message StartTurnRequest {
int64 user_id = 1;
int64 conversation_id = 2;
string prompt = 3;
// --- Fields below this line are the TARGET structured API surface
// for effort control, fast mode and live system-entry updates — see
// pkg/turnopts' package doc comment for why they aren't wired up yet
// (this environment has no protoc/go toolchain to regenerate
// gen/agent/v1 from this file) and what the working fallback is
// today (leading /effort, /fast, /system directive lines parsed out
// of `prompt` itself, by pkg/turnopts.Parse — see
// application.Service.StartTurn). Once `make proto` is run against
// this file, a follow-up change points StartTurn at these fields
// directly, with turnopts kept on as the fallback path for any
// client that still sends plain-text directives instead. Proto3
// fields are purely additive and backward compatible to add, so
// declaring them now costs nothing even before that regeneration —
// every field below defaults to its zero value for any client built
// against today's compiled stubs.
// effort selects one of pkg/effort's five levels ("low", "medium",
// "high", "extra", "max") for this turn; empty means "use the
// deployment's configured default" (see agentrun.Agent.Effort).
string effort = 4;
// fast_mode requests fast mode for this turn (see
// application.Service.FastAgent) — available on every plan, unlike
// dynamic_workflows below.
bool fast_mode = 5;
// system_entries are folded into the system prompt for this turn
// onward (see agentrun.WithSystemEntries) — appended, never
// replacing anything, so earlier turns' prefix stays stable.
repeated SystemEntry system_entries = 6;
}
message StartTurnResponse {
StartTurnOutcome outcome = 1;
string message = 2; // user-facing text for any non-OK outcome
string stream_id = 3; // set when outcome == OK
}
message StreamTurnRequest {
int64 user_id = 1;
string stream_id = 2;
int32 resume_from_tool_index = 3;
}
message ToolCall {
string name = 1;
string arguments_json = 2;
string result = 3;
bool is_error = 4;
}
// ThinkingRound is one agent round's own reasoning trace, standalone
// rather than folded into the running total thinking_delta carries —
// the same per-step granularity Claude's interleaved thinking exposes
// (one distinct thinking block per tool-use step) rather than one
// cumulative blob for the whole turn. round is 0-indexed and matches
// the round the agent service's internal run loop was on when this
// trace was produced; a client can use it to render reasoning as
// discrete, collapsible steps next to the tool call each one preceded,
// instead of a single growing "thinking" pane.
message ThinkingRound {
int32 round = 1;
string thinking = 2;
}
message TurnEvent {
oneof event {
string thinking_delta = 1; // cumulative reasoning text so far, plain
string token_delta = 2; // cumulative reply text so far, plain
ToolCall tool_call = 3; // one completed tool call
TurnDone done = 4;
ThinkingRound thinking_round = 5; // one round's own reasoning trace, standalone
}
}
message TurnDone {
string final_text = 1; // plain Markdown source, not yet rendered
// every tool call made during the turn, successful or not — the
// gateway uses this to build deliverable (download) cards exactly
// like internal/handlers/deliverables.go.attachDeliverables.
repeated ToolCall tool_calls = 2;
}
message StopTurnRequest {
int64 user_id = 1;
string stream_id = 2;
}
message StopTurnResponse {}

View File

@@ -0,0 +1,153 @@
// Package billing is the Billing & Subscription bounded context: plans,
// Stripe checkout/portal, webhook ingestion, and the per-plan daily
// message quota. System of record for `subscriptions`, `stripe_events`
// and `usage_daily` (see services/billing/migrations/0001_init.sql).
//
// Mirrors internal/handlers/billing.go + internal/billing/*.go +
// internal/db/subscriptions.go.
syntax = "proto3";
package enkly.billing.v1;
option go_package = "enkly/gen/billing/v1;billingv1";
import "google/protobuf/timestamp.proto";
service BillingService {
// GetOrCreateSubscription replaces db.GetOrCreateSubscription: lazily
// inserts the default free-plan row the first time a user's
// subscription is looked up.
rpc GetOrCreateSubscription(GetOrCreateSubscriptionRequest) returns (Subscription);
// GetPlanCatalog replaces billing.Catalog + billing.PlanPriceIDs for
// rendering /subscription — returns every plan and whether its Stripe
// price is currently configured (so an unconfigured plan renders as
// "Unavailable", same as today).
rpc GetPlanCatalog(GetPlanCatalogRequest) returns (GetPlanCatalogResponse);
// CreateCheckoutSession replaces Handlers.CreateCheckout.
rpc CreateCheckoutSession(CreateCheckoutSessionRequest) returns (CreateCheckoutSessionResponse);
// SyncCheckoutSession replaces Handlers.syncCheckoutSession, the
// fallback path SubscriptionSuccess uses when the webhook hasn't
// landed yet.
rpc SyncCheckoutSession(SyncCheckoutSessionRequest) returns (Subscription);
// CreateBillingPortalSession replaces Handlers.BillingPortal.
rpc CreateBillingPortalSession(CreateBillingPortalSessionRequest) returns (CreateBillingPortalSessionResponse);
// IngestStripeWebhook replaces Handlers.StripeWebhook /
// applyStripeEvent. The gateway still terminates the public
// POST /webhooks/stripe route (so the URL Stripe is configured with
// never changes) but forwards the raw body and signature header here
// untouched — signature verification needs STRIPE_WEBHOOK_SECRET,
// which only billing holds.
rpc IngestStripeWebhook(IngestStripeWebhookRequest) returns (IngestStripeWebhookResponse);
// CheckAndIncrementUsage replaces the daily-cap check + db.
// IncrementDailyUsage call in Handlers.Chat. Called by the agent
// service once per chat turn, after the message is accepted and
// before the agent run starts (same ordering as the monolith).
rpc CheckAndIncrementUsage(CheckAndIncrementUsageRequest) returns (CheckAndIncrementUsageResponse);
// GetDailyUsage replaces db.DailyUsage — a read-only lookup (no
// increment) used by the gateway's /subscription page to show
// today's count without miscounting it as a sent message.
rpc GetDailyUsage(GetDailyUsageRequest) returns (GetDailyUsageResponse);
}
enum Plan {
PLAN_UNSPECIFIED = 0;
PLAN_FREE = 1;
PLAN_BASIC = 2;
PLAN_PRO = 3;
PLAN_PREMIUM = 4;
}
message PlanInfo {
Plan plan = 1;
string name = 2;
int32 price_eur = 3;
int32 daily_message_limit = 4; // 0 = unlimited
bool available = 5; // Stripe price configured (always true for free)
}
message Subscription {
int64 user_id = 1;
Plan plan = 2;
string status = 3; // Stripe's subscription status, or "active" for free
google.protobuf.Timestamp current_period_end = 4;
bool cancel_at_period_end = 5;
// has_billing_account reports whether a Stripe customer exists for
// this user (so the gateway knows whether to show "Manage billing"),
// without exposing the actual customer ID. The raw
// stripe_customer_id / stripe_subscription_id / stripe_price_id are
// deliberately NOT exposed here, mirroring models.Subscription's
// json:"-" — they never need to leave billing.
bool has_billing_account = 6;
}
message GetOrCreateSubscriptionRequest {
int64 user_id = 1;
}
message GetPlanCatalogRequest {}
message GetPlanCatalogResponse {
repeated PlanInfo plans = 1;
string publishable_key = 2; // Stripe publishable key, for Checkout.js
}
message CreateCheckoutSessionRequest {
int64 user_id = 1;
string email = 2;
Plan plan = 3;
string success_url = 4;
string cancel_url = 5;
}
message CreateCheckoutSessionResponse {
string checkout_url = 1;
}
message SyncCheckoutSessionRequest {
int64 user_id = 1;
string stripe_session_id = 2;
}
message CreateBillingPortalSessionRequest {
int64 user_id = 1;
string return_url = 2;
}
message CreateBillingPortalSessionResponse {
string portal_url = 1;
}
message IngestStripeWebhookRequest {
bytes payload = 1;
string signature_header = 2;
}
message IngestStripeWebhookResponse {
bool accepted = 1;
}
message CheckAndIncrementUsageRequest {
int64 user_id = 1;
}
message CheckAndIncrementUsageResponse {
bool allowed = 1;
int32 used = 2;
int32 limit = 3; // 0 = unlimited
string plan_name = 4;
}
message GetDailyUsageRequest {
int64 user_id = 1;
}
message GetDailyUsageResponse {
int32 used = 1;
}

View File

@@ -0,0 +1,28 @@
// Package common holds message types shared across every enkly bounded
// context's gRPC API, so each context's own .proto only needs to import
// this one file instead of depending on each other directly.
syntax = "proto3";
package enkly.common.v1;
option go_package = "enkly/gen/common/v1;commonv1";
// Money is a decimal amount in the smallest unit of currency (cents),
// matching how internal/billing.Info prices plans today (EUR integer
// amounts) without pulling in a floating point rounding bug.
message Money {
int64 minor_units = 1; // e.g. 500 = 5.00
string currency_code = 2; // ISO 4217, e.g. "EUR"
}
// PageRequest / PageResponse: not used by today's list endpoints (the
// monolith never paginates conversations or messages), reserved so a
// service can add pagination later without a breaking proto change.
message PageRequest {
int32 page_size = 1;
string page_token = 2;
}
message PageResponse {
string next_page_token = 1;
}

View File

@@ -0,0 +1,136 @@
// Package conversation is the Conversation bounded context: chat threads
// and their messages/tool-call audit log. System of record for
// `conversations`, `messages` and `tool_calls` (see
// services/conversation/migrations/0001_init.sql).
//
// Mirrors internal/db/conversations.go, called both by the gateway
// (listing/renaming/deleting threads, loading history to render) and by
// the agent service (appending messages as a run proceeds).
syntax = "proto3";
package enkly.conversation.v1;
option go_package = "enkly/gen/conversation/v1;conversationv1";
import "google/protobuf/timestamp.proto";
service ConversationService {
rpc CreateConversation(CreateConversationRequest) returns (Conversation);
rpc GetConversation(GetConversationRequest) returns (Conversation);
rpc ListConversations(ListConversationsRequest) returns (ListConversationsResponse);
rpc MostRecentConversation(MostRecentConversationRequest) returns (Conversation);
rpc RenameConversation(RenameConversationRequest) returns (Conversation);
// AutoTitleConversation replaces db.AutoTitleConversation: sets the
// title from the first prompt only if the conversation still has the
// default title (returns retitled=true when it actually changed it).
rpc AutoTitleConversation(AutoTitleConversationRequest) returns (AutoTitleConversationResponse);
rpc DeleteConversation(DeleteConversationRequest) returns (DeleteConversationResponse);
rpc CountMessages(CountMessagesRequest) returns (CountMessagesResponse);
rpc AddMessage(AddMessageRequest) returns (Message);
rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse);
rpc AddToolCall(AddToolCallRequest) returns (AddToolCallResponse);
}
message Conversation {
int64 id = 1;
int64 user_id = 2;
string title = 3;
google.protobuf.Timestamp created_at = 4;
google.protobuf.Timestamp last_message_at = 5;
}
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_USER = 1;
ROLE_ASSISTANT = 2;
ROLE_SYSTEM = 3;
ROLE_TOOL = 4;
}
message Message {
int64 id = 1;
int64 conversation_id = 2;
Role role = 3;
string content = 4;
string tool_name = 5;
google.protobuf.Timestamp created_at = 6;
}
message CreateConversationRequest {
int64 user_id = 1;
string title = 2;
}
message GetConversationRequest {
int64 user_id = 1;
int64 conversation_id = 2;
}
message ListConversationsRequest {
int64 user_id = 1;
}
message ListConversationsResponse {
repeated Conversation conversations = 1;
}
message MostRecentConversationRequest {
int64 user_id = 1;
}
message RenameConversationRequest {
int64 user_id = 1;
int64 conversation_id = 2;
string title = 3;
}
message AutoTitleConversationRequest {
int64 user_id = 1;
int64 conversation_id = 2;
string title = 3;
}
message AutoTitleConversationResponse {
bool retitled = 1;
}
message DeleteConversationRequest {
int64 user_id = 1;
int64 conversation_id = 2;
}
message DeleteConversationResponse {}
message CountMessagesRequest {
int64 conversation_id = 1;
}
message CountMessagesResponse {
int32 count = 1;
}
message AddMessageRequest {
int64 conversation_id = 1;
Role role = 2;
string content = 3;
string tool_name = 4;
}
message ListMessagesRequest {
int64 conversation_id = 1;
}
message ListMessagesResponse {
repeated Message messages = 1;
}
message AddToolCallRequest {
int64 message_id = 1;
string tool_name = 2;
string arguments_json = 3;
string result = 4;
bool is_error = 5;
}
message AddToolCallResponse {}

165
api/proto/iam/v1/iam.proto Normal file
View File

@@ -0,0 +1,165 @@
// Package iam is the Identity & Access bounded context: accounts,
// sessions, email verification and password reset. It is the system of
// record for `users`, `sessions`, `email_verification_codes` and
// `password_reset_tokens` (see services/iam/migrations/0001_init.sql) —
// no other service may read or write those tables directly.
//
// Mirrors the monolith's internal/handlers/auth.go + internal/db/users.go
// + internal/auth/*.go one-for-one; see each rpc's comment for the
// handler function it replaces.
syntax = "proto3";
package enkly.iam.v1;
option go_package = "enkly/gen/iam/v1;iamv1";
import "google/protobuf/timestamp.proto";
service IAMService {
// SignUp replaces Handlers.Signup. Validates and normalizes the email,
// hashes the password (PBKDF2, see internal/auth/password.go), creates
// the account (or silently reuses an existing unverified one — same
// anti-enumeration behavior as the monolith), issues a verification
// code and publishes an iam.verification_code.issued event for the
// notification service to deliver it. Never returns whether the email
// already existed.
rpc SignUp(SignUpRequest) returns (SignUpResponse);
// ResendVerification replaces Handlers.ResendVerification.
rpc ResendVerification(ResendVerificationRequest) returns (ResendVerificationResponse);
// VerifyEmail replaces Handlers.VerifyEmail.
rpc VerifyEmail(VerifyEmailRequest) returns (VerifyEmailResponse);
// Login replaces Handlers.Login: verifies credentials (constant-time
// against a dummy hash for unknown emails) and, on success, mints a
// session and returns its raw token for the gateway to set as a
// cookie. Never persists the raw token — only its SHA-256 hash.
rpc Login(LoginRequest) returns (LoginResponse);
// Logout replaces Handlers.Logout: deletes the session row for the
// given raw session token.
rpc Logout(LogoutRequest) returns (LogoutResponse);
// ValidateSession replaces the session half of Handlers.RequireAuth /
// db.GetSessionUser. Called by the gateway on every authenticated
// request.
rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse);
// ForgotPassword replaces Handlers.ForgotPassword.
rpc ForgotPassword(ForgotPasswordRequest) returns (ForgotPasswordResponse);
// CheckPasswordResetToken replaces the GET-time validity check in
// Handlers.ResetPasswordPage (db.PasswordResetTokenValid).
rpc CheckPasswordResetToken(CheckPasswordResetTokenRequest) returns (CheckPasswordResetTokenResponse);
// ResetPassword replaces Handlers.ResetPassword. Also revokes every
// session for the account (db.UpdatePassword's cascade delete).
rpc ResetPassword(ResetPasswordRequest) returns (ResetPasswordResponse);
// GetUser is used by other services (e.g. billing, when it only has a
// user_id) to resolve an email address. Never returns PasswordHash.
rpc GetUser(GetUserRequest) returns (User);
}
message User {
int64 id = 1;
string email = 2;
bool email_verified = 3;
google.protobuf.Timestamp created_at = 4;
}
message SignUpRequest {
string email = 1;
string password = 2;
string password_confirm = 3;
string client_ip = 4; // for the shared signup/resend rate limiter
}
message SignUpResponse {
// Always true unless a hard infrastructure error occurred — the
// monolith never reveals whether the address was already registered.
bool check_inbox = 1;
string email = 2;
// mail_configured is echoed back so the gateway can show the same
// "written to the server log instead" notice the monolith shows.
bool mail_configured = 3;
}
message ResendVerificationRequest {
string email = 1;
string client_ip = 2;
}
message ResendVerificationResponse {
bool mail_configured = 1;
}
message VerifyEmailRequest {
string email = 1;
string code = 2;
string client_ip = 3;
}
message VerifyEmailResponse {
bool verified = 1;
}
message LoginRequest {
string email = 1;
string password = 2;
string client_ip = 3;
}
message LoginResponse {
bool ok = 1;
// Only one of these is meaningful depending on `ok` / `needs_verification`.
bool needs_verification = 2;
string session_token = 3; // raw token; gateway stores only this, in a cookie
google.protobuf.Timestamp session_expires_at = 4;
User user = 5;
}
message LogoutRequest {
string session_token = 1;
}
message LogoutResponse {}
message ValidateSessionRequest {
string session_token = 1;
}
message ValidateSessionResponse {
bool valid = 1;
User user = 2;
}
message ForgotPasswordRequest {
string email = 1;
string client_ip = 2;
}
message ForgotPasswordResponse {}
message CheckPasswordResetTokenRequest {
string token = 1;
}
message CheckPasswordResetTokenResponse {
bool valid = 1;
}
message ResetPasswordRequest {
string token = 1;
string password = 2;
string password_confirm = 3;
}
message ResetPasswordResponse {
bool ok = 1;
}
message GetUserRequest {
int64 user_id = 1;
}

View File

@@ -0,0 +1,75 @@
// Package knowledge is the Code Intelligence (RAG) bounded context:
// chunking, embedding (via the knowledge service's vLLM-backed embed client) and
// cosine-similarity search over workspace source files. System of
// record for the pgvector-backed `documents` table (see
// services/knowledge/migrations/0001_init.sql).
//
// Mirrors internal/rag/*.go. IndexDirectory (cmd/indexer) now walks the
// workspace through the workspace service instead of the local
// filesystem — see tools/indexer.
syntax = "proto3";
package enkly.knowledge.v1;
option go_package = "enkly/gen/knowledge/v1;knowledgev1";
service KnowledgeService {
// IndexFile replaces rag.Store.IndexFile: chunks the given file
// content, embeds each new/changed chunk, and upserts it.
rpc IndexFile(IndexFileRequest) returns (IndexFileResponse);
// PruneMissing replaces rag.Store.pruneMissing, called by the indexer
// after a full directory walk to drop rows for files no longer
// present.
rpc PruneMissing(PruneMissingRequest) returns (PruneMissingResponse);
// Search replaces rag.Store.Search — the search_codebase agent tool.
rpc Search(SearchRequest) returns (SearchResponse);
// ListIndexedFiles replaces db.ListIndexedFiles, used by the gateway's
// sidebar "indexed files" panel (Handlers.renderConversation).
rpc ListIndexedFiles(ListIndexedFilesRequest) returns (ListIndexedFilesResponse);
}
message CodeChunk {
int64 id = 1;
string file_path = 2;
int32 chunk_index = 3;
string content = 4;
double distance = 5; // cosine distance; lower = more relevant
}
message IndexFileRequest {
string file_path = 1;
string content = 2;
}
message IndexFileResponse {
int32 chunks_indexed = 1;
int32 chunks_skipped = 2; // unchanged content_hash
}
message PruneMissingRequest {
repeated string keep_file_paths = 1;
}
message PruneMissingResponse {
int32 rows_deleted = 1;
}
message SearchRequest {
string query = 1;
int32 top_k = 2;
}
message SearchResponse {
repeated CodeChunk chunks = 1;
}
message ListIndexedFilesRequest {
int32 limit = 1; // 0 = default (200)
}
message ListIndexedFilesResponse {
repeated string file_paths = 1;
}

View File

@@ -0,0 +1,196 @@
// Package workspace is the Workspace (agent sandbox / files) bounded
// context: the single shared ENKLY_WORKSPACE directory the coding agent
// reads, writes and runs commands in, and that the "Files" panel in the
// UI lets a human upload into / download from. Every path-jailing rule
// from internal/llm/sandbox.go (ResolvePath, the bash allowlist, the
// filesystem guards) is enforced here and nowhere else — no other
// service is allowed to touch the workspace volume directly.
//
// Mirrors internal/llm/tools.go (filesystem + bash tools) and
// internal/handlers/files.go / ui.go (upload, download, tree).
syntax = "proto3";
package enkly.workspace.v1;
option go_package = "enkly/gen/workspace/v1;workspacev1";
import "google/protobuf/timestamp.proto";
service WorkspaceService {
// --- agent filesystem/bash tools (internal/llm/tools.go, sandbox.go) ---
rpc ReadFile(ReadFileRequest) returns (ReadFileResponse);
rpc WriteFile(WriteFileRequest) returns (WriteFileResponse);
rpc EditFile(EditFileRequest) returns (EditFileResponse);
rpc MultiEditFile(MultiEditFileRequest) returns (MultiEditFileResponse);
rpc ListDirectory(ListDirectoryRequest) returns (ListDirectoryResponse);
rpc GlobSearch(GlobSearchRequest) returns (GlobSearchResponse);
rpc Grep(GrepRequest) returns (GrepResponse);
rpc RunBashCommand(RunBashCommandRequest) returns (RunBashCommandResponse);
// --- human-facing files panel (internal/handlers/files.go, ui.go) ---
rpc UploadFile(UploadFileRequest) returns (UploadFileResponse);
rpc DownloadFile(DownloadFileRequest) returns (DownloadFileResponse);
rpc Tree(TreeRequest) returns (TreeResponse);
rpc StatFile(StatFileRequest) returns (StatFileResponse); // backs deliverable cards
}
message ReadFileRequest {
string path = 1;
int32 offset = 2; // 0-based line offset
int32 limit = 3; // 0 = default (maxReadLines)
}
message ReadFileResponse {
string content = 1;
bool truncated = 2;
bool is_error = 3;
string error_message = 4;
}
message WriteFileRequest {
string path = 1;
string content = 2;
}
message WriteFileResponse {
int64 bytes_written = 1;
bool is_error = 2;
string error_message = 3;
}
message EditFileRequest {
string path = 1;
string old_string = 2;
string new_string = 3;
bool replace_all = 4;
}
message EditFileResponse {
string diff_preview = 1;
int32 occurrences = 2;
bool is_error = 3;
string error_message = 4;
}
message TextEdit {
string old_string = 1;
string new_string = 2;
bool replace_all = 3;
}
message MultiEditFileRequest {
string path = 1;
repeated TextEdit edits = 2;
}
message MultiEditFileResponse {
string diff_preview = 1;
bool is_error = 2;
string error_message = 3;
}
message ListDirectoryRequest {
string path = 1;
}
message ListDirectoryResponse {
string listing = 1; // pre-rendered, matching the monolith's tool output format
bool is_error = 2;
string error_message = 3;
}
message GlobSearchRequest {
string pattern = 1;
string path = 2;
}
message GlobSearchResponse {
// Pre-rendered exactly like every other tool's text output (a
// newline-separated path list, "no files matched that pattern", or a
// truncation notice) — deliberately NOT a repeated string, since that
// would require guessing which lines are paths vs. a status message,
// a lossy transformation the original tool output never needed.
string output = 1;
bool is_error = 2;
string error_message = 3;
}
message GrepRequest {
string pattern = 1;
string path = 2;
string glob = 3;
bool case_insensitive = 4;
}
message GrepResponse {
string output = 1;
bool is_error = 2;
string error_message = 3;
}
message RunBashCommandRequest {
string command = 1;
int32 timeout_seconds = 2; // 0 = service default
}
message RunBashCommandResponse {
string output = 1;
int32 exit_code = 2;
bool is_error = 3;
string error_message = 4;
}
message UploadFileRequest {
string path = 1; // destination relative path
bytes content = 2;
}
message UploadFileResponse {
int64 bytes_written = 1;
}
message DownloadFileRequest {
string path = 1;
}
message DownloadFileResponse {
bytes content = 1;
string filename = 2;
}
message TreeRequest {
string path = 1; // "" = workspace root
// one_level: list only the immediate children of path (both files
// and directories, directories included as entries in their own
// right) instead of recursing — backs the gateway's lazy-expanding
// file explorer panel (internal/handlers/ui.go's FileTree), where
// each folder click fetches just that folder. false (the default)
// recurses fully and returns only files, for the flat "Workspace
// files" upload/download panel (internal/handlers/files.go).
bool one_level = 2;
}
message TreeNode {
string name = 1;
string rel_path = 2;
bool is_dir = 3;
int64 size = 4;
google.protobuf.Timestamp modified_at = 5;
// download_url is left empty by this service — the gateway owns URL
// construction (it knows its own public route prefix), not workspace.
}
message TreeResponse {
repeated TreeNode nodes = 1;
}
message StatFileRequest {
string path = 1;
}
message StatFileResponse {
bool exists = 1;
bool is_dir = 2;
int64 size = 3;
google.protobuf.Timestamp modified_at = 4;
}