commit - 3a70517b1b3371da4e8c33fc991db7fea4f84caf
commit + 4bf2f54fab51f79f5a7f751e2d342775a53fd316
blob - e07e1ac9c829c800190315fad645eabdf26200f9
blob + 2ae919e3ed5c0a1593f3c0a37fa16803d625169b
--- regress/Makefile
+++ regress/Makefile
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 <bsd.subdir.mk>
blob - 0c773c935909293f3137f33b22513da99a2e8bb1
blob + 7f5775119f0531b36755b44131b49acece141b17
--- regress/README
+++ regress/README
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);
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
+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 <bsd.regress.mk>
blob - /dev/null
blob + 2fee78d2b5bbccbdb5ea42da729ae616a7d0f7b2 (mode 644)
--- /dev/null
+++ regress/tool_display/tool_display_test.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * 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 <sys/types.h>
+
+#include <string.h>
+
+#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
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
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * 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 <sys/types.h>
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * 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 <sys/types.h>
+
+#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 */