commit 4bf2f54fab51f79f5a7f751e2d342775a53fd316 from: Isaac Meerleo date: Fri Jul 17 14:00:06 2026 UTC add bounded tool-call display summaries commit - 3a70517b1b3371da4e8c33fc991db7fea4f84caf commit + 4bf2f54fab51f79f5a7f751e2d342775a53fd316 blob - e07e1ac9c829c800190315fad645eabdf26200f9 blob + 2ae919e3ed5c0a1593f3c0a37fa16803d625169b --- regress/Makefile +++ regress/Makefile @@ -1,5 +1,5 @@ SUBDIR= agentcfg anthropic buf claude_subscription conf deploy diff editor generation http imsgev journal json local_port log markdown \ - model_window openai output print sandbox skills sse term tools turn \ + model_window openai output print sandbox skills sse term tool_display tools turn \ turn_mechanics turn_txn ui_config web xmalloc .include blob - 0c773c935909293f3137f33b22513da99a2e8bb1 blob + 7f5775119f0531b36755b44131b49acece141b17 --- regress/README +++ regress/README @@ -127,6 +127,8 @@ provider codec: anthropic anthropic (builder + stream provider codec: openai openai (builder, adversarial stream, Bearer-auth full worker mesh) tools (behavior.md 5) agentcfg (agent schema and routing); + tool_display (bounded, control-safe progress + summaries with secret-prone fields omitted); tools (executors, caps, freshness, NUL rejection, bounded background draining, supervisor/liveness cleanup); @@ -204,6 +206,7 @@ src/fugu-tool (tools.c, job.c, CTX read/append, ephemeral agent loop) src/fugu/tooldefs.c turn (tools offered per gates, the skill tool) +src/fugu/tool_display.c tool_display; turn (line-mode wiring) src/fugu/skills.c skills (load/parse); turn (dispatch) src/fugu/output.c output, print src/fugu-web (fetch.c, main.c) web (hermetic TLS flows); sandbox blob - /dev/null blob + 272e18743e40ede8d02a30b0098eefae861bf99a (mode 644) --- /dev/null +++ regress/tool_display/Makefile @@ -0,0 +1,7 @@ +PROG= tool_display_test +SRCS= tool_display_test.c tool_display.c json.c buf.c log.c xmalloc.c + +CFLAGS+= -I${.CURDIR}/../../src/fugu +.PATH: ${.CURDIR}/../../src/fugu + +.include blob - /dev/null blob + 2fee78d2b5bbccbdb5ea42da729ae616a7d0f7b2 (mode 644) --- /dev/null +++ regress/tool_display/tool_display_test.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include "buf.h" +#include "tool_display.h" +#include "regress.h" + +static int +format_is(const char *name, const char *input, const char *want) +{ + struct buf out; + int ok; + + buf_init(&out); + tool_display_format(&out, name, input, strlen(input)); + ok = out.len == strlen(want) && + memcmp(out.data, want, out.len) == 0 && + out.len <= TOOL_DISPLAY_MAX && + memchr(out.data, '\n', out.len) == NULL && + memchr(out.data, '\0', out.len) == NULL; + buf_free(&out); + return (ok); +} + +static void +test_read_and_search(void) +{ + CHECK(format_is("read", "{\"path\":\"src/fugu/coord.c\"," + "\"offset\":1017,\"limit\":20,\"content\":\"not shown\"}", + "read path=\"src/fugu/coord.c\" offset=1017 limit=20")); + CHECK(format_is("grep", "{\"pattern\":\"tool:\\nread\"," + "\"path\":\"src/fugu\"}", + "grep pattern=\"tool:\\nread\" path=\"src/fugu\"")); + CHECK(format_is("find", "{\"path\":\"src\",\"name\":\"*.c\"}", + "find path=\"src\" name=\"*.c\"")); + CHECK(format_is("ls", "{}", "ls")); +} + +static void +test_safe_allowlist(void) +{ + CHECK(format_is("edit", "{\"path\":\"notes.txt\"," + "\"old_string\":\"private old text\"," + "\"new_string\":\"private new text\"}", + "edit path=\"notes.txt\"")); + CHECK(format_is("http_request", "{\"method\":\"POST\"," + "\"url\":\"https://example.com/v1\"," + "\"headers\":{\"Authorization\":\"secret\"}," + "\"body\":\"private body\"}", + "http_request method=\"POST\" url=\"https://example.com/v1\"")); + CHECK(format_is("agent", "{\"label\":\"reader\"," + "\"model\":\"fast\",\"prompt\":\"private prompt\"}", + "agent label=\"reader\" model=\"fast\"")); + CHECK(format_is("shell", "{\"command\":\"printf 'a\\nb'\"}", + "shell command=\"printf 'a\\nb'\"")); +} + +static void +test_hostile_and_invalid_input(void) +{ + struct buf input, out; + int i; + + CHECK(format_is("read", "{\"path\":\"a\\u0000b\\tc\"}", + "read path=\"a\\x00b\\tc\"")); + CHECK(format_is("read", "not json", "read")); + CHECK(format_is("future_tool", "{\"secret\":\"not shown\"}", + "future_tool")); + + buf_init(&input); + buf_addstr(&input, "{\"path\":\""); + for (i = 0; i < 1000; i++) + buf_addc(&input, 'x'); + buf_addstr(&input, "\"}"); + buf_init(&out); + tool_display_format(&out, "read", input.data, input.len); + CHECK(out.len <= TOOL_DISPLAY_MAX); + CHECK(memmem(out.data, out.len, "...\"", 4) != NULL); + CHECK(memchr(out.data, '\n', out.len) == NULL); + CHECK(memchr(out.data, '\0', out.len) == NULL); + buf_free(&out); + buf_free(&input); +} + +int +main(void) +{ + test_read_and_search(); + test_safe_allowlist(); + test_hostile_and_invalid_input(); + REGRESS_END(); +} blob - 148f52fe64099fd853425940c49bbd069d86533e blob + d0db4e7302520e7d4488499539269b62fbac6239 --- src/fugu/Makefile +++ src/fugu/Makefile @@ -1,6 +1,6 @@ PROG= fugu SRCS= main.c coord.c priv.c conf.c parse.y \ - journal.c sysprompt.c tooldefs.c agentcfg.c skills.c output.c \ + journal.c sysprompt.c tooldefs.c tool_display.c agentcfg.c skills.c output.c \ turn_txn.c turn_mechanics.c anthropic_req.c claude_sub_req.c \ claude_sub_profile.c openai_req.c generation.c msg.c \ json.c model_window.c buf.c imsgev.c log.c xmalloc.c blob - /dev/null blob + 70688281e28cba8bd06ce1b47a11dea3791b2c43 (mode 644) --- /dev/null +++ src/fugu/tool_display.c @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "buf.h" +#include "json.h" +#include "tool_display.h" + +#define DISPLAY_NAME_MAX 64 +#define DISPLAY_VALUE_MAX 160 +#define DISPLAY_FIELDS_MAX 3 + +enum display_type { DISPLAY_STRING, DISPLAY_NUMBER }; + +struct display_field { + const char *name; + int type; +}; + +struct display_tool { + const char *name; + struct display_field fields[DISPLAY_FIELDS_MAX]; +}; + +/* + * This is deliberately an allowlist. Locator and action fields explain + * what ran; bulk or secret-prone fields (content, replacements, headers, + * bodies, and prompts) never become presentation text. + */ +static const struct display_tool tools[] = { + { "read", { + { "path", DISPLAY_STRING }, + { "offset", DISPLAY_NUMBER }, + { "limit", DISPLAY_NUMBER } } }, + { "write", { { "path", DISPLAY_STRING } } }, + { "edit", { { "path", DISPLAY_STRING } } }, + { "multi_edit", { { "path", DISPLAY_STRING } } }, + { "shell", { { "command", DISPLAY_STRING } } }, + { "grep", { + { "pattern", DISPLAY_STRING }, + { "path", DISPLAY_STRING } } }, + { "find", { + { "path", DISPLAY_STRING }, + { "name", DISPLAY_STRING } } }, + { "ls", { { "path", DISPLAY_STRING } } }, + { "web_search", { { "query", DISPLAY_STRING } } }, + { "web_fetch", { { "url", DISPLAY_STRING } } }, + { "http_request", { + { "method", DISPLAY_STRING }, + { "url", DISPLAY_STRING } } }, + { "shell_bg", { { "command", DISPLAY_STRING } } }, + { "shell_output", { { "id", DISPLAY_NUMBER } } }, + { "shell_kill", { { "id", DISPLAY_NUMBER } } }, + { "skill", { { "name", DISPLAY_STRING } } }, + { "agent", { + { "label", DISPLAY_STRING }, + { "model", DISPLAY_STRING } } } +}; + +static void +add_tool_name(struct buf *out, const char *name) +{ + size_t i; + + if (name == NULL || name[0] == '\0') { + buf_addstr(out, "tool"); + return; + } + for (i = 0; name[i] != '\0' && i < DISPLAY_NAME_MAX; i++) { + unsigned char c = (unsigned char)name[i]; + + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') + buf_addc(out, c); + else + buf_addc(out, '?'); + } + if (name[i] != '\0') + buf_addstr(out, "..."); +} + +static size_t +utf8_len(unsigned char c) +{ + if (c < 0x80) + return (1); + if ((c & 0xe0) == 0xc0) + return (2); + if ((c & 0xf0) == 0xe0) + return (3); + return (4); +} + +/* Add a decoded JSON string without allowing terminal controls or new lines. */ +static void +add_quoted(struct buf *out, const unsigned char *s, size_t len) +{ + static const char hex[] = "0123456789abcdef"; + size_t i = 0, shown = 0; + + buf_addc(out, '"'); + while (i < len) { + unsigned char c = s[i]; + const char *escaped = NULL; + size_t n = 1, add = 1; + + switch (c) { + case '"': escaped = "\\\""; add = 2; break; + case '\\': escaped = "\\\\"; add = 2; break; + case '\n': escaped = "\\n"; add = 2; break; + case '\r': escaped = "\\r"; add = 2; break; + case '\t': escaped = "\\t"; add = 2; break; + default: + if (c < 0x20 || c == 0x7f) + add = 4; + else if (c >= 0x80) + n = add = utf8_len(c); + break; + } + if (shown + add > DISPLAY_VALUE_MAX) + break; + if (escaped != NULL) + buf_add(out, escaped, add); + else if (c < 0x20 || c == 0x7f) { + buf_addstr(out, "\\x"); + buf_addc(out, hex[c >> 4]); + buf_addc(out, hex[c & 0x0f]); + } else + buf_add(out, s + i, n); + shown += add; + i += n; + } + if (i < len) + buf_addstr(out, "..."); + buf_addc(out, '"'); +} + +static const struct display_tool * +find_tool(const char *name) +{ + size_t i; + + if (name == NULL) + return (NULL); + for (i = 0; i < sizeof(tools) / sizeof(tools[0]); i++) + if (strcmp(name, tools[i].name) == 0) + return (&tools[i]); + return (NULL); +} + +static void +add_field(struct buf *out, const struct json *j, int root, + const struct display_field *field) +{ + char *s; + size_t len; + int64_t number; + int token; + + if (field->name == NULL || + (token = json_obj_get(j, root, field->name)) == -1) + return; + if (field->type == DISPLAY_STRING) { + if ((s = json_get_str(j, token, &len)) == NULL) + return; + buf_addc(out, ' '); + buf_addstr(out, field->name); + buf_addc(out, '='); + add_quoted(out, (const unsigned char *)s, len); + free(s); + } else { + if (json_get_num(j, token, &number) == -1) + return; + buf_addc(out, ' '); + buf_addstr(out, field->name); + buf_addf(out, "=%lld", (long long)number); + } +} + +void +tool_display_format(struct buf *out, const char *name, const void *input, + size_t inputlen) +{ + const struct display_tool *tool; + struct buf summary; + struct json json; + int root; + size_t i; + + buf_reset(out); + buf_init(&summary); + add_tool_name(&summary, name); + tool = find_tool(name); + if (tool == NULL || input == NULL || inputlen == 0 || + json_parse(&json, input, inputlen, 0) == -1) + goto done; + root = json_root(&json); + if (json_is_object(&json, root)) + for (i = 0; i < DISPLAY_FIELDS_MAX; i++) + add_field(&summary, &json, root, &tool->fields[i]); + json_done(&json); + +done: + /* The allowlist and per-value cap should fit; fail terse if they drift. */ + if (summary.len > TOOL_DISPLAY_MAX) { + buf_reset(&summary); + add_tool_name(&summary, name); + } + buf_add(out, summary.data, summary.len); + buf_free(&summary); +} blob - /dev/null blob + ccab4611f2c3f1fd53fcee6e2068c961970d39bb (mode 644) --- /dev/null +++ src/fugu/tool_display.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Isaac + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef TOOL_DISPLAY_H +#define TOOL_DISPLAY_H + +#include + +#include "buf.h" + +/* One bounded, single-line tool-call summary for stderr or the transcript. */ +#define TOOL_DISPLAY_MAX 512 + +void tool_display_format(struct buf *, const char *, const void *, size_t); + +#endif /* TOOL_DISPLAY_H */