Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ idf_component_register(
"tools_persona.c"
"tools_cron.c"
"tools_system.c"
"tools_email.c"
"bridge_client.c"
"memory.c"
"json_util.c"
"telegram.c"
Expand Down
194 changes: 194 additions & 0 deletions main/bridge_client.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#include "bridge_client.h"
#include "memory.h"
#include "nvs_keys.h"
#include "text_buffer.h"
#include "esp_crt_bundle.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BRIDGE_URL_MAX 256
#define BRIDGE_KEY_MAX 128
#define BRIDGE_ENDPOINT_MAX 320
#define BRIDGE_HTTP_TIMEOUT_MS 15000

typedef struct {
char *buf;
size_t len;
size_t max;
bool truncated;
} bridge_client_http_ctx_t;

static const char *TAG = "bridge_client";

static void normalize_bridge_url(const char *raw, char *out, size_t out_len)
{
size_t len;

if (!raw || !out || out_len == 0) {
return;
}

strncpy(out, raw, out_len - 1);
out[out_len - 1] = '\0';

len = strlen(out);
while (len > 0 && out[len - 1] == '/') {
out[len - 1] = '\0';
len--;
}
}

static bool load_bridge_config(char *url_out,
size_t url_out_len,
char *key_out,
size_t key_out_len)
{
char raw_url[BRIDGE_URL_MAX] = {0};

if (!memory_get(NVS_KEY_BRIDGE_URL, raw_url, sizeof(raw_url)) || raw_url[0] == '\0') {
return false;
}
if (!memory_get(NVS_KEY_BRIDGE_KEY, key_out, key_out_len) || key_out[0] == '\0') {
return false;
}

normalize_bridge_url(raw_url, url_out, url_out_len);
return url_out[0] != '\0';
}

static esp_err_t bridge_client_http_event_handler(esp_http_client_event_t *evt)
{
bridge_client_http_ctx_t *ctx = (bridge_client_http_ctx_t *)evt->user_data;

if (!ctx) {
return ESP_OK;
}

if (evt->event_id == HTTP_EVENT_ON_DATA && evt->data && evt->data_len > 0) {
bool ok = text_buffer_append(ctx->buf, &ctx->len, ctx->max, (const char *)evt->data, evt->data_len);
if (!ok && !ctx->truncated) {
ctx->truncated = true;
ESP_LOGW(TAG, "Bridge response truncated at %d bytes", (int)(ctx->max - 1));
}
}

return ESP_OK;
}

bool bridge_client_is_configured(void)
{
char url[BRIDGE_URL_MAX] = {0};
char key[BRIDGE_KEY_MAX] = {0};
return load_bridge_config(url, sizeof(url), key, sizeof(key));
}

esp_err_t bridge_client_post_json(const char *path,
const cJSON *payload,
char *response_out,
size_t response_out_len,
int *status_out,
bool *truncated_out)
{
char bridge_url[BRIDGE_URL_MAX] = {0};
char bridge_key[BRIDGE_KEY_MAX] = {0};
char auth_header[BRIDGE_KEY_MAX + 16] = {0};
char endpoint[BRIDGE_ENDPOINT_MAX] = {0};
char *payload_json = NULL;
const char *payload_body = "{}";
esp_http_client_handle_t client = NULL;
int status = -1;
esp_err_t err;
bridge_client_http_ctx_t ctx = {
.buf = response_out,
.len = 0,
.max = response_out_len,
.truncated = false,
};

if (!response_out || response_out_len == 0 || !path || path[0] == '\0') {
return ESP_ERR_INVALID_ARG;
}

response_out[0] = '\0';
if (status_out) {
*status_out = -1;
}
if (truncated_out) {
*truncated_out = false;
}

if (!load_bridge_config(bridge_url, sizeof(bridge_url), bridge_key, sizeof(bridge_key))) {
return ESP_ERR_INVALID_STATE;
}

if (path[0] == '/') {
if (snprintf(endpoint, sizeof(endpoint), "%s%s", bridge_url, path) >= (int)sizeof(endpoint)) {
return ESP_ERR_INVALID_SIZE;
}
} else {
if (snprintf(endpoint, sizeof(endpoint), "%s/%s", bridge_url, path) >= (int)sizeof(endpoint)) {
return ESP_ERR_INVALID_SIZE;
}
}

if (payload) {
payload_json = cJSON_PrintUnformatted((cJSON *)payload);
if (!payload_json) {
return ESP_ERR_NO_MEM;
}
payload_body = payload_json;
}

esp_http_client_config_t cfg = {
.url = endpoint,
.event_handler = bridge_client_http_event_handler,
.user_data = &ctx,
.timeout_ms = BRIDGE_HTTP_TIMEOUT_MS,
.crt_bundle_attach = esp_crt_bundle_attach,
};

client = esp_http_client_init(&cfg);
if (!client) {
free(payload_json);
return ESP_FAIL;
}

esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
if (snprintf(auth_header, sizeof(auth_header), "Bearer %s", bridge_key) >= (int)sizeof(auth_header)) {
esp_http_client_cleanup(client);
free(payload_json);
return ESP_ERR_INVALID_SIZE;
}
esp_http_client_set_header(client, "Authorization", auth_header);
esp_http_client_set_header(client, "X-Zclaw-Bridge-Key", bridge_key);
esp_http_client_set_post_field(client, payload_body, (int)strlen(payload_body));

err = esp_http_client_perform(client);
status = esp_http_client_get_status_code(client);

if (status_out) {
*status_out = status;
}
if (truncated_out) {
*truncated_out = ctx.truncated;
}

esp_http_client_cleanup(client);
free(payload_json);

if (ctx.truncated) {
return ESP_ERR_NO_MEM;
}
if (err != ESP_OK) {
return err;
}
if (status < 200 || status >= 300) {
return ESP_FAIL;
}

return ESP_OK;
}
21 changes: 21 additions & 0 deletions main/bridge_client.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef BRIDGE_CLIENT_H
#define BRIDGE_CLIENT_H

#include "cJSON.h"
#include "esp_err.h"
#include <stdbool.h>
#include <stddef.h>

// Returns true when both bridge URL and key are provisioned.
bool bridge_client_is_configured(void);

// POST JSON payload to configured bridge endpoint path (e.g. "/v1/email/send").
// response_out always receives a null-terminated string (possibly empty).
esp_err_t bridge_client_post_json(const char *path,
const cJSON *payload,
char *response_out,
size_t response_out_len,
int *status_out,
bool *truncated_out);

#endif // BRIDGE_CLIENT_H
14 changes: 14 additions & 0 deletions main/builtin_tools.def
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ TOOL_ENTRY("get_diagnostics",
"{\"type\":\"object\",\"properties\":{\"scope\":{\"type\":\"string\",\"enum\":[\"quick\",\"runtime\",\"memory\",\"rates\",\"time\",\"all\"],\"description\":\"Optional diagnostics scope (default quick)\"},\"verbose\":{\"type\":\"boolean\",\"description\":\"Include extra details (default false)\"}}}",
tools_get_diagnostics_handler)

// Email Bridge
TOOL_ENTRY("email_send",
"Send an email through the configured email bridge service. Requires bridge provisioning.",
"{\"type\":\"object\",\"properties\":{\"to\":{\"type\":\"string\",\"description\":\"Recipient email address\"},\"subject\":{\"type\":\"string\",\"description\":\"Email subject\"},\"body\":{\"type\":\"string\",\"description\":\"Plain-text email body\"}},\"required\":[\"to\",\"subject\",\"body\"]}",
tools_email_send_handler)
TOOL_ENTRY("email_list",
"List recent emails through the configured email bridge service. Supports optional unread filtering.",
"{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"Optional mailbox label (default INBOX)\"},\"max\":{\"type\":\"integer\",\"description\":\"Max emails to return (1-20, default 5)\"},\"unread_only\":{\"type\":\"boolean\",\"description\":\"When true, return unread email only\"}}}",
tools_email_list_handler)
TOOL_ENTRY("email_read",
"Read a specific email by message id through the configured email bridge service.",
"{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Message identifier from email_list\"},\"max_chars\":{\"type\":\"integer\",\"description\":\"Optional max body characters to return (200-4000, default 1200)\"}},\"required\":[\"id\"]}",
tools_email_read_handler)

// User Tool Management
TOOL_ENTRY("create_tool",
"Create a custom tool. Provide a short name (no spaces), brief description, and the action to perform when called.",
Expand Down
2 changes: 2 additions & 0 deletions main/memory_keys.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ bool memory_keys_is_sensitive(const char *key)
NVS_KEY_TG_TOKEN,
NVS_KEY_TG_CHAT_ID,
NVS_KEY_TG_CHAT_IDS,
NVS_KEY_BRIDGE_URL,
NVS_KEY_BRIDGE_KEY,
NVS_KEY_WIFI_PASS,
NVS_KEY_LLM_BACKEND,
NVS_KEY_LLM_MODEL,
Expand Down
2 changes: 2 additions & 0 deletions main/nvs_keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#define NVS_KEY_TG_TOKEN "tg_token"
#define NVS_KEY_TG_CHAT_ID "tg_chat_id"
#define NVS_KEY_TG_CHAT_IDS "tg_chat_ids"
#define NVS_KEY_BRIDGE_URL "bridge_url"
#define NVS_KEY_BRIDGE_KEY "bridge_key"
#define NVS_KEY_TIMEZONE "timezone"
#define NVS_KEY_PERSONA "persona"

Expand Down
Loading