Files
CST-agent/tests/test_registry.c
T
WHC 5ba215b36c v0.8.7: tool capability metadata, in-process engine, Qt app, Android engine
The engine can be embedded now instead of only driven from a terminal: the
same objects link into a Qt front end (app/) that owns a cst_agent_t inside
its own process -- no port, no token, no second process -- and that front end
now runs on the desktop and, engine-side, on the phone.

Capability metadata (include/cst_tools.h, src/tools/registry.c):
- cst_tool_def_t gains perm_name, platforms and available (a probe that
  returns a reason), so a permission is addressable by name and a tool can
  say why it cannot run here.
- cst_platform_name(), cst_tool_platform_ok(), cst_tool_available(),
  cst_tool_meta_reload(), cst_tool_perm_name() and CST_PLAT_DESKTOP[].
- cst_tool_register() may take over a built-in name that is not runnable on
  this platform, so a front end replaces it with its own implementation.
- the registry entries carry their class (delete_file -> "delete"), the
  desktop clipboard tools carry a clipboard probe; find/active/run gate on it.
- /api/settings.tools[] exposes perm_name, platforms, available, why and the
  declared parameters.

Engine as a library (Makefile, CMakeLists.txt):
- libcstcore.a from the CORE_OBJ set, and a CMake cstcore target shared by
  the CLI and by app/ (-DCST_BUILD_APP=ON): the engine is compiled once.
- Android branch for the vendored sashidown: a per-ABI archive built with the
  NDK's aarch64-linux-android24-clang wrapper, because CMake's compiler is
  plain clang and keeps --target/--sysroot in its own variables.

Qt front end (app/, new):
- AgentCore wraps one cst_agent_t: turns run on a worker thread, every
  cst_agent_hooks_t callback becomes a Qt signal, and the two hooks that need
  an answer (on_permission, on_ask_user) block the worker until the GUI
  replies; mode, approval and plan mode go through the engine's config.
- ToolProvider hands the front end's tools to the engine (owner "app:*"); the
  Linux one is notification_send with a probe for notify-send/D-Bus.
- Main.qml: transcript, tool blocks, consent dialog (Deny/Once/Always ->
  CST_PERM_*), ask_user, mode switch, Esc to cancel; --selftest stays
  headless and runs the embedding checks without QML.

Android (scripts/, docs/android.md, new):
- scripts/build_third_party_android.sh builds static mbedTLS + libcurl for an
  ABI (CA path /system/etc/security/cacerts); build_android.sh builds
  cst-agent, libcstcore.a and the test binaries for the phone, can push them
  and run the whole suite there.
- sandbox_exec.c keeps the plain exec path on Android: the seccomp supervisor
  compiled in and the probe reported "seccomp", but writes outside the
  workspace were not denied on the device -- claiming a sandbox that does not
  hold is worse than reporting none.
- tests/test_diff.c compares layouts only against real GNU diffutils: toybox's
  diff prints the same hunk header with its own hunk/context rules.

Tests: test_tool_meta (metadata, platform gate, probe reasons, takeover of a
built-in name) and test_embed (in-process embedding contract) are new; the
existing registry/llm_body assertions now follow availability. 38 test
binaries pass on the host, 36/36 on arm64-v8a Android 16, cst-app-selftest
passes in both the standalone app/ build and the repo build.

Version 0.8.7.
2026-10-11 11:42:04 +08:00

216 lines
8.2 KiB
C

/* Tool registry: schema / prompt generation, profiles and /tools switches. */
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_tools.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
static int schema_has(cJSON *arr, const char *name) {
const cJSON *t = NULL;
cJSON_ArrayForEach(t, arr) {
const cJSON *fn = cJSON_GetObjectItemCaseSensitive(t, "function");
const cJSON *nm = cJSON_GetObjectItemCaseSensitive(fn, "name");
if (cJSON_IsString(nm) && strcmp(nm->valuestring, name) == 0)
return 1;
}
return 0;
}
static int echo_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r, void *ud) {
(void)ctx;
const cst_tool_def_t *self = ud;
char *j = cst_tool_args_json(self, c);
r->out = j;
r->out_len = j ? strlen(j) : 0;
r->exit_code = 0;
return 0;
}
static void dynamic_tools(void) {
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
int base = cst_tool_count();
cst_tool_def_t d;
memset(&d, 0, sizeof(d));
d.name = "dyn_echo";
d.desc = "Echo the arguments.";
d.schema_json = "{\"type\":\"object\",\"properties\":{\"text\":{"
"\"type\":\"string\"},\"n\":{\"type\":\"integer\"},"
"\"opts\":{\"type\":\"object\"}},\"required\":[\"text\"]}";
d.perm = CST_TP_READ;
d.run_ex = echo_run;
d.owner = "test:echo";
EXPECT(cst_tool_register(&d) == 0, "register");
EXPECT(cst_tool_register(&d) != 0, "duplicate name refused");
EXPECT(cst_tool_count() == base + 1, "count grows");
const cst_tool_def_t *t = cst_tool_find("dyn_echo");
EXPECT(t && t != &d && strcmp(t->owner, "test:echo") == 0, "copied def");
/* the runner gets its own def as ud for typing */
((cst_tool_def_t *)t)->ud = (void *)t;
cJSON *sc = cst_tools_schema_cfg(&cfg);
char *ss = cJSON_PrintUnformatted(sc);
EXPECT(ss && strstr(ss, "\"name\":\"dyn_echo\"") &&
strstr(ss, "\"required\":[\"text\"]"),
"schema passes the JSON Schema through");
free(ss);
cJSON_Delete(sc);
char *pr = cst_tools_prompt(&cfg);
EXPECT(pr && strstr(pr, "- dyn_echo(text, n?, opts?): Echo the arguments."),
"text prompt lists schema params");
free(pr);
ctl_tool_call_t c;
memset(&c, 0, sizeof(c));
snprintf(c.name, sizeof(c.name), "dyn_echo");
cst_tool_ctx_t ctx;
memset(&ctx, 0, sizeof(ctx));
cst_tool_result_t r;
memset(&r, 0, sizeof(r));
cst_tool_run(&ctx, &c, &r);
EXPECT(r.exit_code == 1 && r.out && strstr(r.out, "needs argument \"text\""),
"schema required checked");
cst_tool_result_free(&r);
c.nargs = 3;
snprintf(c.args[0].key, sizeof(c.args[0].key), "text");
c.args[0].value = "42";
c.args[0].value_len = 2;
snprintf(c.args[1].key, sizeof(c.args[1].key), "n");
c.args[1].value = "7";
c.args[1].value_len = 1;
snprintf(c.args[2].key, sizeof(c.args[2].key), "opts");
c.args[2].value = "{\"a\":true}";
c.args[2].value_len = strlen(c.args[2].value);
memset(&r, 0, sizeof(r));
cst_tool_run(&ctx, &c, &r);
EXPECT(r.exit_code == 0 && r.out &&
strcmp(r.out, "{\"text\":\"42\",\"n\":7,\"opts\":{\"a\":true}}") == 0,
"args typed by schema");
cst_tool_result_free(&r);
cst_tool_set_disabled(&cfg, "dyn_echo", 1);
cst_tool_set_disabled(&cfg, "grep", 1);
EXPECT(!cst_tool_active(&cfg, t) &&
!cst_tool_active(&cfg, cst_tool_find("grep")),
"disable by name");
cst_tool_set_disabled(&cfg, "dyn_echo", 0);
EXPECT(cst_tool_active(&cfg, t) &&
!cst_tool_active(&cfg, cst_tool_find("grep")),
"re-enable one name");
cst_tool_set_disabled(&cfg, "grep", 0);
EXPECT(cfg.tools_off[0] == 0, "set empties");
char nm[64];
cst_tool_sanitize_name("mcp__my server__do.it", nm, sizeof(nm));
EXPECT(strcmp(nm, "mcp__my_server__do_it") == 0, "sanitize");
EXPECT(cst_tool_unregister_owner("test:echo") == 1 &&
!cst_tool_find("dyn_echo") && cst_tool_count() == base,
"unregister by owner");
}
int main(void) {
dynamic_tools();
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
/* every tool has a runner and a well-formed parameter list */
EXPECT(cst_tool_count() >= 13, "registry size");
for (int i = 0; i < cst_tool_count(); i++) {
const cst_tool_def_t *t = cst_tool_at(i);
EXPECT(t->run != NULL, t->name);
EXPECT((t->perm == CST_TP_WRITE) == (t->preview != NULL),
"write tools (and only those) have previews");
EXPECT(cst_tool_find(t->name) == t, "find by name");
}
cJSON *all = cst_tools_schema_cfg(&cfg);
/* Every tool the host can actually run is offered in the default profile;
* one whose platform probe fails (the clipboard without a helper) is not. */
int usable = 0;
for (int i = 0; i < cst_tool_count(); i++)
if (cst_tool_available(cst_tool_at(i), NULL, 0) == 0)
usable++;
EXPECT(cJSON_GetArraySize(all) == usable, "default: every usable tool");
EXPECT(schema_has(all, "edit_file") && schema_has(all, "grep") &&
schema_has(all, "web_fetch"),
"default schema has new tools");
cJSON_Delete(all);
cfg.tool_profile = CST_TOOLS_MINIMAL;
cJSON *min = cst_tools_schema_cfg(&cfg);
EXPECT(cJSON_GetArraySize(min) == 4, "minimal: four tools");
EXPECT(schema_has(min, "read_file") && schema_has(min, "run_shell") &&
schema_has(min, "write_file") && schema_has(min, "apply_patch"),
"minimal: the original four");
char *ms = cJSON_PrintUnformatted(min);
EXPECT(ms && strstr(ms, "\"description\":\"Read a UTF-8 text file under the "
"workspace.\""),
"minimal keeps the original wording");
EXPECT(ms && !strstr(ms, "offset"), "minimal hides optional params");
free(ms);
cJSON_Delete(min);
char *pmin = cst_agent_build_prompt(&cfg, CST_PROTO_NATIVE_EOS, NULL);
EXPECT(pmin && strcmp(pmin, cst_agent_system_prompt_for(
CST_PROTO_NATIVE_EOS)) == 0,
"minimal prompt == legacy prompt");
EXPECT(pmin && strstr(pmin, "ls/grep via run_shell"), "legacy workflow");
free(pmin);
char *pctl = cst_agent_build_prompt(&cfg, CST_PROTO_CTL, NULL);
EXPECT(pctl && !strstr(pctl, "must be quoted"), "minimal ctl: no new rule");
free(pctl);
cfg.tool_profile = CST_TOOLS_DEFAULT;
char *pdef = cst_agent_build_prompt(&cfg, CST_PROTO_CTL_HEREDOC,
"Always run `make test`.");
EXPECT(pdef && strstr(pdef, "- edit_file(path, old_string, new_string, "
"replace_all?):"),
"default prompt lists edit_file with optional marker");
EXPECT(pdef && strstr(pdef, "list_dir, glob, grep"), "default workflow");
EXPECT(pdef && strstr(pdef, "must be quoted"), "ctl numeric rule");
EXPECT(pdef && strstr(pdef, "Always run `make test`."), "project notes");
free(pdef);
/* /tools off: grep disappears everywhere */
cst_tool_set_disabled(&cfg, "grep", 1);
EXPECT(!cst_tool_active(&cfg, cst_tool_find("grep")), "grep off");
cJSON *off = cst_tools_schema_cfg(&cfg);
EXPECT(!schema_has(off, "grep") && schema_has(off, "glob"), "schema off");
cJSON_Delete(off);
char *names = cst_tools_names(&cfg);
EXPECT(names && !strstr(names, "grep") && strstr(names, "glob"),
"names off");
free(names);
char *poff = cst_agent_build_prompt(&cfg, CST_PROTO_NATIVE_EOS, NULL);
EXPECT(poff && !strstr(poff, "- grep(") && strstr(poff, "list_dir, glob)"),
"prompt off");
free(poff);
int prof = -1;
EXPECT(cst_tool_profile_parse("minimal", &prof) == 0 &&
prof == CST_TOOLS_MINIMAL,
"parse minimal");
EXPECT(cst_tool_profile_parse("bogus", &prof) != 0, "parse bogus");
if (nfail) {
fprintf(stderr, "test_registry: %d failure(s)\n", nfail);
return 1;
}
printf("test_registry: OK\n");
return 0;
}