/* deliver_file: a finished file handed to the person, not to the model. * * The three things that must hold: * - the stored copy is the file itself (any bytes, its own extension and a * mime taken from the name), not the text reduction an attachment gets; * - it carries `for_user`, so no request ever contains it -- and a document * attachment on the same session still travels; * - the tool is in the engine's registry, takes a workspace path and refuses * a directory or a path that leaves the workspace. */ #define _POSIX_C_SOURCE 200809L #include "cJSON.h" #include "cst_agent.h" #include "cst_attach.h" #include "cst_test_util.h" #include "cst_tools.h" #include "os_compat.h" #include #include #include #include static int nfail; static char g_ws[1024]; #define EXPECT(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) static void wfile(const char *rel, const void *data, size_t n) { char p[1200]; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); FILE *f = fopen(p, "wb"); if (!f) { fprintf(stderr, "cannot write %s\n", p); exit(1); } if (n && fwrite(data, 1, n, f) != n) { fprintf(stderr, "short write %s\n", p); exit(1); } fclose(f); } static void full_path(char *out, size_t n, const char *rel) { snprintf(out, n, "%s/%s", g_ws, rel); } /* name, then key/value pairs, NULL-terminated (the ctl_tool_call_t a turn * builds from a model's call). */ static ctl_tool_call_t mk(const char *name, ...) { ctl_tool_call_t c; memset(&c, 0, sizeof(c)); snprintf(c.id, sizeof(c.id), "t"); snprintf(c.name, sizeof(c.name), "%s", name); va_list ap; va_start(ap, name); const char *k; while ((k = va_arg(ap, const char *)) != NULL) { const char *v = va_arg(ap, const char *); ctl_arg_t *a = &c.args[c.nargs++]; snprintf(a->key, sizeof(a->key), "%s", k); a->value = (char *)v; a->value_len = strlen(v); } va_end(ap); return c; } static void test_registry(void) { const cst_tool_def_t *t = cst_tool_find("deliver_file"); EXPECT(t != NULL, "deliver_file is in the registry"); if (!t) return; char why[160] = ""; EXPECT(cst_tool_available(t, why, sizeof(why)) == 0, "deliver_file is available here"); /* It copies out of the workspace and never writes it: a read. */ EXPECT(strcmp(cst_tool_perm_name(t), "read") == 0, "deliver_file is a read-policy tool"); EXPECT(t->owner == NULL, "deliver_file is an engine tool"); } static void test_import(const char *session) { const char *md = "# Report\n\n- one\n- two\n"; wfile("out/report.md", md, strlen(md)); char src[1200]; full_path(src, sizeof(src), "out/report.md"); char err[256] = ""; cst_attachment_t *a = cst_attach_import_artifact(session, src, NULL, err, sizeof(err)); EXPECT(a != NULL, "a markdown file is delivered"); if (!a) { fprintf(stderr, " (%s)\n", err); return; } EXPECT(strcmp(a->mime, "text/markdown") == 0, "mime comes from the name"); EXPECT(strcmp(a->name, "report.md") == 0, "display name is the file name"); EXPECT(a->for_user == 1, "marked for the user"); EXPECT(!cst_attach_is_text(a), "a delivered file is not inlined as text"); EXPECT(!cst_attach_is_image(a), "and it is not an image part either"); EXPECT(strncmp(a->rel, "art-", 4) == 0, "stored under an artifact id"); size_t rl = strlen(a->rel); EXPECT(rl > 4 && strcmp(a->rel + rl - 3, ".md") == 0, "the stored copy keeps the extension"); EXPECT(a->bytes == (long long)strlen(md), "size is the file's size"); size_t n = 0; char *back = cst_attach_read(session, a, &n, err, sizeof(err)); EXPECT(back && n == strlen(md) && memcmp(back, md, n) == 0, "the payload is byte for byte"); free(back); /* The copy lives beside the session, not in the workspace. */ char stored[1200]; EXPECT(cst_attach_full_path(session, a, stored, sizeof(stored)) == 0, "stored path resolves"); os_stat_t st; EXPECT(os_lstat(stored, &st) == 0 && st.size == (long long)strlen(md), "the stored file is on disk"); /* A title wins over the file name, and its extension decides the type. */ cst_attachment_t *b = cst_attach_import_artifact(session, src, "summary.html", err, sizeof(err)); EXPECT(b && strcmp(b->name, "summary.html") == 0, "title is the display name"); EXPECT(b && strcmp(b->mime, "text/html") == 0, "the title's extension decides"); cst_attach_free(b); cst_attach_free(a); } static void test_any_bytes(const char *session) { /* A zip is bytes, not text: an attachment would refuse it (the content * decides), a delivered file is copied as it is. */ char zip[512]; for (size_t i = 0; i < sizeof(zip); i++) zip[i] = (char)(i % 251); memcpy(zip, "PK\003\004", 4); wfile("out/build.zip", zip, sizeof(zip)); char src[1200]; full_path(src, sizeof(src), "out/build.zip"); char err[256] = ""; cst_attachment_t *a = cst_attach_import_artifact(session, src, NULL, err, sizeof(err)); EXPECT(a != NULL, "a binary file is delivered"); EXPECT(a && strcmp(a->mime, "application/zip") == 0, "zip mime"); if (a) { size_t n = 0; char *back = cst_attach_read(session, a, &n, err, sizeof(err)); EXPECT(back && n == sizeof(zip) && memcmp(back, zip, n) == 0, "binary payload survives"); free(back); cst_attach_free(a); } /* The same file through the attachment path: refused, as before. */ cst_attachment_t *as_attach = cst_attach_import_file(session, src, err, sizeof(err)); EXPECT(as_attach == NULL, "an attachment still refuses binary garbage"); cst_attach_free(as_attach); /* Bigger than an attachment may be (16 MB) but inside the artifact cap: a * report is allowed to be large. */ char big[1200]; full_path(big, sizeof(big), "out/big.log"); FILE *f = fopen(big, "wb"); if (f) { char line[64]; memset(line, 'x', sizeof(line) - 1); line[sizeof(line) - 1] = '\n'; /* 17 MB: past the 16 MB attachment cap, well inside the artifact one. */ for (int i = 0; i < 17 * 1024 * 1024 / (int)sizeof(line); i++) fwrite(line, 1, sizeof(line), f); fclose(f); } os_stat_t st; EXPECT(os_lstat(big, &st) == 0 && st.size > CST_ATTACH_MAX_BYTES, "the scratch file is larger than the attachment cap"); cst_attachment_t *large = cst_attach_import_artifact(session, big, NULL, err, sizeof(err)); EXPECT(large != NULL, "a file past the attachment cap is delivered"); EXPECT(large && large->bytes == (long long)st.size, "all of it is stored"); EXPECT(large && strcmp(large->mime, "text/plain") == 0, "log mime"); cst_attach_free(large); cst_attachment_t *small = cst_attach_import_file(session, big, err, sizeof(err)); EXPECT(small == NULL, "the attachment path still caps at 16 MB"); cst_attach_free(small); } static void test_json(const char *session) { char src[1200]; full_path(src, sizeof(src), "out/report.md"); char err[256] = ""; cst_attachment_t *a = cst_attach_import_artifact(session, src, NULL, err, sizeof(err)); EXPECT(a != NULL, "artifact for the json round trip"); if (!a) return; char *json = cst_attachments_to_json(a, 1); EXPECT(json && strstr(json, "\"for_user\":1") != NULL, "the JSONL says it is the user's"); cJSON *parsed = json ? cJSON_Parse(json) : NULL; int n = 0; cst_attachment_t *back = parsed ? cst_attachments_from_json(parsed, &n) : NULL; EXPECT(back && n == 1, "one entry parses back"); if (back) { EXPECT(back[0].for_user == 1, "for_user survives the JSONL"); EXPECT(strcmp(back[0].mime, "text/markdown") == 0, "mime survives"); EXPECT(strcmp(back[0].rel, a->rel) == 0, "stored name survives"); cst_attachments_free(back, n); } cJSON_Delete(parsed); free(json); cst_attach_free(a); } /* The session file is what a replayed transcript is read from: a delivered * file that loses `for_user` on the way through it would be handed back to the * model (and never shown as a card) on the next start. */ static void test_session_file(void) { char src[1200], path[1200]; full_path(src, sizeof(src), "out/report.md"); snprintf(path, sizeof(path), "%s/delivered.jsonl", g_ws); char err[256] = ""; cst_attachment_t *a = cst_attach_import_artifact(path, src, NULL, err, sizeof(err)); EXPECT(a != NULL, "the file is stored for a session message"); cst_session_t *s = cst_session_open(path); EXPECT(s != NULL, "the session opens"); if (!s) { cst_attach_free(a); return; } cst_message_t m; memset(&m, 0, sizeof(m)); m.role = CST_ROLE_TOOL; m.content = (char *)"delivered report.md"; snprintf(m.tool_call_id, sizeof(m.tool_call_id), "c1"); snprintf(m.tool_name, sizeof(m.tool_name), "deliver_file"); m.attach = a; m.nattach = a ? 1 : 0; EXPECT(cst_session_append(s, &m) == 0, "the tool result is appended"); cst_session_close(s); cst_attach_free(a); /* Read it back the way cst_agent_open() does. */ s = cst_session_open(path); EXPECT(s && cst_session_count(s) == 1, "the message is in the file"); const cst_message_t *back = s ? cst_session_get(s, 0) : NULL; EXPECT(back && back->nattach == 1, "its attachment comes back"); EXPECT(back && back->attach && back->attach[0].for_user == 1, "and is still the user's, not the model's"); EXPECT(back && back->attach && !cst_attach_is_text(&back->attach[0]), "so it is never inlined"); EXPECT(back && back->attach && !cst_attach_is_image(&back->attach[0]), "and it is not an image part either"); cst_session_close(s); } /* The tool through the registry, the way a turn calls it. */ static void test_tool(const char *session) { cst_agent_config_t cfg; memset(&cfg, 0, sizeof(cfg)); snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", g_ws); snprintf(cfg.session_path, sizeof(cfg.session_path), "%s", session); cst_agent_t *ag = cst_agent_create(&cfg); EXPECT(ag != NULL, "agent for the tool call"); if (!ag) return; cst_tool_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.workspace_root = g_ws; ctx.agent = ag; cst_tool_result_t r; memset(&r, 0, sizeof(r)); ctl_tool_call_t c = mk("deliver_file", "path", "out/report.md", NULL); cst_tool_run(&ctx, &c, &r); /* by name, through the registry */ EXPECT(r.exit_code == 0, "deliver_file runs"); EXPECT(r.out && strstr(r.out, "delivered report.md") != NULL, "the result names the file"); EXPECT(r.nattach == 1 && r.attach && r.attach[0].for_user == 1, "the tool hands the file to the front end"); EXPECT(r.attach && !cst_attach_is_text(&r.attach[0]), "and never to a request"); cst_tool_result_free(&r); memset(&r, 0, sizeof(r)); c = mk("deliver_file", "path", "out", NULL); /* a directory */ cst_tool_run(&ctx, &c, &r); EXPECT(r.exit_code == 1 && r.out && strstr(r.out, "is a directory") != NULL, "a directory is refused"); cst_tool_result_free(&r); memset(&r, 0, sizeof(r)); c = mk("deliver_file", "path", "../outside.txt", NULL); cst_tool_run(&ctx, &c, &r); EXPECT(r.exit_code == 1 && r.out && strstr(r.out, "outside workspace") != NULL, "a path outside the workspace is refused"); cst_tool_result_free(&r); memset(&r, 0, sizeof(r)); c = mk("deliver_file", NULL); cst_tool_run(&ctx, &c, &r); EXPECT(r.exit_code == 1 && r.out && strstr(r.out, "path") != NULL, "a missing path is refused"); cst_tool_result_free(&r); cst_agent_destroy(ag); } /* What actually goes out: the delivered file is not in the body, a document * attachment on the same transcript still is. */ static void test_wire(const char *session) { char md_src[1200], txt_src[1200]; full_path(md_src, sizeof(md_src), "out/report.md"); const char *note = "the build log lives in out/build.log\n"; wfile("notes.txt", note, strlen(note)); full_path(txt_src, sizeof(txt_src), "notes.txt"); char err[256] = ""; cst_attachment_t *art = cst_attach_import_artifact(session, md_src, NULL, err, sizeof(err)); cst_attachment_t *doc = cst_attach_import_file(session, txt_src, err, sizeof(err)); EXPECT(art && doc, "both a delivered file and a document"); cst_message_t msgs[4]; memset(msgs, 0, sizeof(msgs)); msgs[0].role = CST_ROLE_USER; msgs[0].content = (char *)"what did you build?"; msgs[1].role = CST_ROLE_ASSISTANT; msgs[1].tool_calls_json = (char *)"[{\"id\":\"c1\",\"type\":\"function\"," "\"function\":{\"name\":\"deliver_file\"," "\"arguments\":\"{}\"}}]"; /* Two tool results of one turn: the first delivered a file for the person, * the second came back with a document the model is meant to read. The * delivered file must add nothing (not even a "not sent" note); the * document must still be inlined. */ msgs[2].role = CST_ROLE_TOOL; msgs[2].content = (char *)"the finished report is stored for the user"; snprintf(msgs[2].tool_call_id, sizeof(msgs[2].tool_call_id), "c1"); snprintf(msgs[2].tool_name, sizeof(msgs[2].tool_name), "deliver_file"); msgs[2].attach = art; msgs[2].nattach = art ? 1 : 0; msgs[3].role = CST_ROLE_TOOL; msgs[3].content = (char *)"read the build log"; snprintf(msgs[3].tool_call_id, sizeof(msgs[3].tool_call_id), "c2"); snprintf(msgs[3].tool_name, sizeof(msgs[3].tool_name), "read_file"); msgs[3].attach = doc; msgs[3].nattach = doc ? 1 : 0; cst_llm_request_t req; memset(&req, 0, sizeof(req)); req.model = "test-model"; req.msgs = msgs; req.nmsgs = 4; req.session_path = session; /* a document is read back from the store */ char *body = cst_llm_build_body(&req); EXPECT(body != NULL, "body built"); if (body) { EXPECT(art && strstr(body, art->rel) == NULL, "the delivered file's stored name is not in the request"); EXPECT(strstr(body, "for_user") == NULL, "and nothing marks it as an attachment either"); EXPECT(strstr(body, "the build log lives in") != NULL, "a document attachment still travels as text"); cJSON *j = cJSON_Parse(body); const cJSON *m = j ? cJSON_GetObjectItemCaseSensitive(j, "messages") : NULL; EXPECT(m && cJSON_GetArraySize(m) == 5, "four messages plus the one deferred document message"); cJSON_Delete(j); free(body); } cst_attachments_free(art, art ? 1 : 0); cst_attachments_free(doc, doc ? 1 : 0); } int main(void) { if (test_tmpdir(g_ws, sizeof(g_ws), "cst-artifacts") != 0) { perror("tmpdir"); return 1; } char mk1[1200]; snprintf(mk1, sizeof(mk1), "%s/out", g_ws); test_mkdir(mk1, 0755); char session[1200]; snprintf(session, sizeof(session), "%s/s.jsonl", g_ws); test_registry(); test_import(session); test_any_bytes(session); test_json(session); test_session_file(); test_tool(session); test_wire(session); test_rmrf(g_ws); if (nfail) { fprintf(stderr, "%d check(s) failed\n", nfail); return 1; } printf("test_artifacts: OK\n"); return 0; }