84 || !fs_cfg.outside_root_allow.empty()) {
85 logger->info(
"workspace servers confine to their root (host "
86 "mcp.filesystem.allow_outside_root, outside_root_allow "
87 "({} entries) and the path approver apply to the "
88 "default set only)", fs_cfg.outside_root_allow.size());
91 fs_cfg.outside_root_allow.clear();
113 void* cb_data =
nullptr;
115 std::lock_guard<std::mutex> lock(h->path_approval_mutex);
116 cb = h->path_approval_cb;
117 cb_data = h->path_approval_data;
125 ? ENT_PATH_ACCESS_WRITE : ENT_PATH_ACCESS_READ,
127 verdict = cb(&c_req, cb_data) == ENT_DECISION_ACCEPT
161 bool approved =
false;
162 if (h->hook_registry.hook_count(kPoint) > 0) {
163 char* ignored =
nullptr;
164 const int rc = h->hook_registry.fire_pre(kPoint, context_json.c_str(),
193 std::filesystem::path sock;
211 const std::optional<std::string>& app_context) {
213 h->
engine->clear_workspace_prompts(workspace);
216 const auto data_dir = entropic::config::resolve_data_dir(h->
config);
217 std::string constitution, handle_ctx;
218 entropic::prompts::load_shared_prompt_sources(h->
config, data_dir,
219 constitution, handle_ctx);
221 if (!constitution.empty()) { prefix += constitution +
"\n\n"; }
222 if (!app_context->empty()) { prefix += *app_context +
"\n\n"; }
227 + entropic::prompts::resolve_tier_identity_full(tier, name, data_dir).
body;
231 h->
engine->set_workspace_prompts(workspace, std::move(prompts));
244 bool with_declared) {
284 const std::string& session_key,
285 const std::filesystem::path& path,
286 bool entering,
void* ud) {
289 if (servers ==
nullptr) {
return; }
290 logger->info(
"delegation dir swap: session='{}' -> {} ({})",
291 session_key, path.string(),
292 entering ?
"enter" :
"restore");
294 servers->enter_working_dir(path);
296 servers->leave_working_dir(path);
316 const std::string& key) {
317 std::size_t released = 0;
318 if (h ==
nullptr) {
return released; }
323 for (
const auto& [name, ws] : h->
workspaces) {
324 if (ws && ws->servers) {
325 released += ws->servers->release_session(key);
358 logger->info(
"path approval callback {}",
359 cb !=
nullptr ?
"registered" :
"cleared");
382 const std::filesystem::path& dir) {
383 auto ws = std::make_unique<EntropicWorkspace>();
386 ws->servers = std::make_unique<entropic::ServerManager>(
388 std::vector<std::string> tier_names;
389 std::vector<std::string> require_context;
392 tier_names.push_back(tier);
394 if (cfg.requires_context) { require_context.push_back(tier); }
396 auto data_dir = entropic::config::resolve_data_dir(h->
config);
398 ws->servers->init_builtins(mcp, tier_names,
399 data_dir.string(), require_context);
400 ws->servers->load_plugins(mcp);
428 std::filesystem::path& root) {
429 if (name ==
nullptr || *name ==
'\0' || dir ==
nullptr
434 root = std::filesystem::absolute(dir, ec);
436 if (ec || !std::filesystem::is_directory(root, ec)) {
437 err = std::string(
"workspace dir is not a directory: ") + dir;
441 err = std::string(
"workspace already exists: ") + name;
471 std::optional<std::string>& text) {
472 namespace fs = std::filesystem;
475 if (rel.empty() || !fs::exists(root / rel, ec)) {
return ENTROPIC_OK; }
476 const auto file = fs::weakly_canonical(root / rel, ec);
477 const auto base = fs::weakly_canonical(root, ec);
478 std::ifstream in(file, std::ios::binary);
480 if (file.lexically_relative(base).string().rfind(
"..", 0) == 0) {
481 handle->
last_error =
"workspace_app_context " + (root / rel).
string()
482 +
" resolves outside the workspace root (" + file.string() +
")";
485 handle->
last_error =
"cannot read workspace_app_context " + file.string();
488 text = std::string(std::istreambuf_iterator<char>(in), {});
489 logger->info(
"workspace app_context loaded from {} ({} bytes)",
490 file.string(), text->size());
514 std::filesystem::path root;
515 std::optional<std::string> ctx;
521 ws->app_context = ctx;
522 ws->servers->initialize();
526 logger->info(
"workspace '{}' created at {} (app_context: {})",
527 name, root.string(), ctx ?
"its own" :
"the handle's");
528 }
catch (
const std::exception& e) {
550 const char* session_key,
554 if (name ==
nullptr || *name ==
'\0' || !handle->
engine) {
558 std::string key = session_key !=
nullptr ? session_key :
"";
564 if (handle->
engine->message_count_for(key) > 0) {
566 "session '" + key +
"' already holds messages; bind a "
567 "workspace before its first turn";
572 handle->
last_error = std::string(
"unknown workspace: ") + name;
576 handle->
engine->bind_session_workspace(key, name);
577 logger->info(
"session '{}' bound to workspace '{}'", key, name);
597 const char* app_context) {
602 std::optional<std::string> ctx;
603 if (app_context !=
nullptr) { ctx = app_context; }
608 handle->
last_error = std::string(
"unknown workspace: ") + name;
611 it->second->app_context = ctx;
617 logger->info(
"workspace '{}' app_context {}", name,
618 ctx ?
"set (" + std::to_string(ctx->size()) +
" bytes)"
619 : std::string(
"reset to the handle's"));
621 }
catch (
const std::exception& e) {
666 const char* name,
const nlohmann::json& j) {
669 spec.
command = j.value(
"command",
"");
670 spec.
url = j.value(
"url",
"");
672 spec.
url.empty() ?
"stdio" :
"sse");
673 if (j.contains(
"args") && j[
"args"].is_array()) {
674 spec.
args = j[
"args"].get<std::vector<std::string>>();
676 if (j.contains(
"env") && j[
"env"].is_object()) {
677 for (
auto& [key, val] : j[
"env"].items()) {
679 logger->warn(
"register_mcp_server '{}': blocked env var "
680 "'{}' — skipping", name, key);
683 if (val.is_string()) {
684 spec.
env[key] = val.get<std::string>();
713 if (it == handle->
workspaces.end()) {
return nullptr; }
715 return it->second->servers.get();
733 const char* config_json) {
741 auto j = nlohmann::json::parse(config_json);
747 auto ws_name = j.value(
"workspace", std::string{});
749 if (servers ==
nullptr) {
750 handle->
last_error =
"unknown workspace: " + ws_name;
753 servers->connect_external_server(spec);
754 logger->info(
"register_mcp_server: name='{}' env_keys={} "
755 "workspace='{}'", name, spec.env.size(), ws_name);
757 }
catch (
const std::exception& e) {
787 logger->info(
"deregister_mcp_server: name='{}'", name);
789 }
catch (
const std::exception& e) {
806extern "C" ENTROPIC_EXPORT
char*
812 nlohmann::json arr = nlohmann::json::array();
815 for (
const auto& [name, s] : servers) {
816 arr.push_back({{
"name", s.name},
817 {
"transport", s.transport},
818 {
"status", s.status},
819 {
"source", s.source}});
822 return strdup(arr.dump().c_str());
823 }
catch (
const std::exception& e) {
gh#59 (v2.3.1): RAII guard combining api_mutex + log scope.
Manages MCP server instances and routes tool calls.
void set_discovered_server_approver(DiscoveredServerApprover fn, void *user_data)
Install the discovered-server approver (v2.14.0, gh#199).
void set_mcp_config(const MCPConfig &config)
Set MCP config for external server initialization.
void set_own_bridge_socket(const std::filesystem::path &socket)
The socket this handle's external bridge binds (v2.14.0, gh#200).
gh#59 (v2.3.1): RAII guard — sets thread's current handle_id.
Private definition of the entropic_engine struct.
Public C API for the Entropic inference engine.
ent_decision_t(* ent_path_approval_cb)(const ent_path_approval_request_t *req, void *user_data)
Host decision for one outside-root filesystem access.
ENTROPIC_EXPORT entropic_error_t entropic_workspace_create(entropic_handle_t handle, const char *name, const char *dir)
Create a named workspace (gh#166) — see entropic.h.
ENTROPIC_EXPORT entropic_error_t entropic_register_mcp_server(entropic_handle_t handle, const char *name, const char *config_json)
Register an external MCP server from JSON config (C ABI).
static entropic::ExternalServerConfig parse_external_server_spec(const char *name, const nlohmann::json &j)
Register an external MCP server at runtime.
static entropic_error_t check_server_mgr(entropic_handle_t h)
Check handle prerequisites for MCP server APIs.
static entropic_error_t validate_workspace_args(entropic_handle_t handle, const char *name, const char *dir, std::filesystem::path &root)
Validate workspace-create arguments and resolve the root.
static entropic::ServerManager * resolve_registration_target(entropic_handle_t handle, const std::string &ws_name, entropic::ExternalServerConfig &spec)
Servers a registration targets, and the cwd it spawns in (gh#166).
ENTROPIC_EXPORT entropic_error_t entropic_workspace_set_app_context(entropic_handle_t handle, const char *name, const char *app_context)
Give a workspace its own app_context (gh#179) — see entropic.h .
static std::unique_ptr< EntropicWorkspace > build_workspace(entropic_handle_t h, const std::string &name, const std::filesystem::path &dir)
Build a workspace's own server instances rooted at dir.
static entropic::MCPConfig confined_mcp_config(entropic_handle_t h)
The handle's MCP config, confined to a workspace root (gh#166).
ENTROPIC_EXPORT entropic_error_t entropic_session_bind_workspace(entropic_handle_t handle, const char *session_key, const char *name)
Bind a session to a workspace (gh#166) — see entropic.h.
ENTROPIC_EXPORT entropic_error_t entropic_set_path_approval_callback(entropic_handle_t handle, ent_path_approval_cb cb, void *user_data)
Register the outside-root path approver — see entropic.h.
ENTROPIC_EXPORT char * entropic_list_mcp_servers(entropic_handle_t handle)
List all MCP servers as JSON array.
static entropic::OutsideRootVerdict outside_root_thunk(const entropic::OutsideRootRequest &req, void *ud)
FilesystemServer → consumer bridge for outside-root approval.
static bool discovered_server_thunk(const std::string &context_json, void *ud)
Ask the handle's PRE_MCP_SERVER_CONNECT hooks about one server.
static entropic_error_t load_workspace_context_file(entropic_handle_t handle, const std::filesystem::path &root, std::optional< std::string > &text)
Read a new workspace's opt-in app_context file (gh#179).
ENTROPIC_EXPORT entropic_error_t entropic_deregister_mcp_server(entropic_handle_t handle, const char *name)
Deregister an external MCP server.
entropic_error_t
Error codes returned by all C API functions.
@ ENTROPIC_ERROR_INTERNAL
Unexpected internal error (bug)
@ ENTROPIC_ERROR_SERVER_NOT_FOUND
MCP server name not found (v1.8.7)
@ ENTROPIC_ERROR_CONNECTION_FAILED
Transport connection failed (v1.8.7)
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
@ ENTROPIC_ERROR_INVALID_HANDLE
NULL or destroyed handle (v1.8.9)
@ ENTROPIC_ERROR_IO
File/network I/O error.
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
std::string err
size_exceeded, when set
@ ENTROPIC_HOOK_PRE_MCP_SERVER_CONNECT
A discovered .mcp.json server is about to be launched (v2.14.0).
JSON serialization helpers for the facade.
Config loader — YAML to C++ structs with validation.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Prompt manager — frontmatter parsing, identity loading, assembly.
Discovers external MCP servers from .mcp.json files.
std::filesystem::path compute_socket_path(const std::filesystem::path &project_dir)
Compute project-unique Unix socket path for self-detection.
void wire_external_server_policy(entropic_handle_t h, ServerManager &servers, bool with_declared)
Give a server set the handle's external-server policy (v2.14.0).
OutsideRootVerdict
The host's answer to an OutsideRootRequest (v2.13.0).
@ approved
Serve this one call.
@ rejected
The approver said no.
@ no_approver
No approver is registered — refuse, fail loud.
void apply_workspace_app_context(entropic_handle_t h, const std::string &workspace, const std::optional< std::string > &app_context)
Give a workspace's sessions prompts built on its own app_context (gh#179), or return them to the hand...
@ refuse
false: every escape refused ("Path escapes project root")
bool is_blocked_env_var(const std::string &key)
Environment variable blocklist for .mcp.json env field.
std::filesystem::path bridge_project_dir(entropic_handle_t h)
The project dir the external bridge derives its socket from.
ENTROPIC_EXPORT std::string current_run_session()
The session key of the run executing on this thread (gh#166).
@ write
write_file, edit_file
ENTROPIC_EXPORT void swap_session_tool_dir(const std::string &session_key, const std::filesystem::path &path, bool entering, void *ud)
The facade's gh#160 ScopedSandbox swap callback.
std::filesystem::path expected_bridge_socket(entropic_handle_t h)
The socket this handle's external bridge binds, or empty when the handle runs no bridge (v2....
void wire_outside_root_approver(entropic_handle_t h)
Point the DEFAULT server set's filesystem server at the handle's path-approval slot (v2....
std::size_t release_session_tool_state(entropic_handle_t h, const std::string &key)
Release a session's per-session tool state on every server set (gh#158).
entropic::ServerManager * workspace_servers(entropic_handle_t h, const std::string &key)
The MCP servers a session's tools must run against (gh#166).
One filesystem access OUTSIDE the project root, awaiting the host's decision.
std::optional< std::filesystem::path > socket_path
Socket path (nullopt = derived)
Parsed server entry from .mcp.json.
std::string name
Server name.
std::map< std::string, std::string > env
Stdio env vars.
std::string command
Stdio command (empty for SSE)
std::string working_dir
gh#166 (v2.13.0): cwd for the spawned child ("" = inherit).
std::string transport
"stdio" | "sse"
std::vector< std::string > args
Stdio command args.
std::string url
SSE URL (empty for stdio)
MCP server configuration.
FilesystemConfig filesystem
Filesystem server config.
std::unordered_map< std::string, ExternalServerEntry > external_servers
Named external servers the operator declared (v1.8.7).
ExternalMCPConfig external
External MCP server config (Entropic-as-server)
std::unordered_map< std::string, TierConfig > tiers
Tier name → config.
std::string default_tier
Default tier name.
One outside-root access awaiting the host's decision (v2.13.0).
PathAccess access
Read or write.
std::string root
Canonical root the path lies outside.
std::string tool
Fully-qualified tool ("filesystem.write_file")
std::string path
Canonical absolute path the tool resolved.
PermissionsConfig permissions
Tool permissions.
ModelsConfig models
Tiers + router.
std::filesystem::path workspace_app_context
Per-workspace app_context file, relative to each workspace root (gh#179, v2.14.0).
MCPConfig mcp
MCP server settings.
std::filesystem::path config_dir
Config dir — base for bundled data discovery.
One workspace's assembled system prompts (gh#179, v2.14.0).
std::unordered_map< std::string, std::string > tier_prompts
Tier → system prompt.
std::string default_prompt
Untiered first turn.
std::string body
Markdown system prompt body.
Engine handle struct — owns all subsystems.
int log_id
gh#59 (v2.3.1): unique handle id for per-handle log routing via entropic::log::HandleAwareSink.
std::mutex workspace_mutex
gh#166: guards both maps above. Held for lookups only.
std::map< std::string, std::unique_ptr< EntropicWorkspace > > workspaces
gh#166: named workspaces, by name.
std::atomic< bool > configured
True after configure()
std::map< std::string, std::string > session_workspace
gh#166: session key → workspace name.
std::string last_error
Per-handle error message.
std::unique_ptr< entropic::AgentEngine > engine
Agentic loop (owns conversation state)
void * path_approval_data
Forwarded to the approver.
entropic::ParsedConfig config
Parsed config.
std::unique_ptr< entropic::ServerManager > server_manager
MCP server lifecycle.
std::mutex path_approval_mutex
Guards the pair above: the thunk reads it from run threads while the setter may write it from any thr...
ent_path_approval_cb path_approval_cb
Consumer's approver for filesystem access outside the root.