Commit Diff


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 <bsd.subdir.mk>
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 <bsd.regress.mk>
blob - /dev/null
blob + 2fee78d2b5bbccbdb5ea42da729ae616a7d0f7b2 (mode 644)
--- /dev/null
+++ regress/tool_display/tool_display_test.c
@@ -0,0 +1,109 @@
+/*
+ * 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
@@ -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 <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
@@ -0,0 +1,29 @@
+/*
+ * 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 */