Files
Hermit/include/cst_attach.h
T
WHC 60cbce3c5c v0.9.2: files the agent delivers come back as cards, and the saved key is used
A turn could write a report and say "here it is" -- and there was nothing to
open. The engine had no way to hand a finished file to the person, and the
front end had no way to show one. This release adds the whole path: a tool to
deliver, a store that keeps the copy, a card in the transcript, and open /
share through the platform. It also fixes two bugs the phone exposed while it
was being tried there.

deliver_file (engine)
- cst_attachment_t.for_user marks a copy that belongs to the person, not to
  the model; cst_attach_import_artifact() stores any bytes up to
  CST_ARTIFACT_MAX_BYTES (64 MB) with the mime taken from the name's
  extension. Attachments are unchanged (content decides the type, 16 MB).
- It is a read-policy engine tool: path resolves inside the workspace, a
  directory is refused, title (optional) is the display name.
- The wire is what keeps it out of the conversation: parts_add_attachments()
  skips for_user entries without even a "not sent" note, and a turn that only
  delivered a file no longer invents an empty user message. tests/test_artifacts.c
  pins that down end to end, including a session-file round trip.

The card (Qt front end)
- filesText() replaced imagesText(): one line per file -- kind, stored name,
  name, origin, mime, bytes, pixels -- decoded by ImageStrip.qml. A for_user
  entry is a file card (extension badge, name, type and size, Open, Share);
  a picture stays a thumbnail and opens on tap. Both shapes are read, so
  sessions written by an older version still draw.
- AgentCore::openArtifact() / shareArtifact() accept a name only when one of
  the session's own messages carries it, and hand the display name to the
  platform layer for the status line.
- Android opens through the app's FileProvider (content:// + ACTION_VIEW,
  resolveActivity() checked, read permission granted) and shares through the
  share sheet; a desktop uses QDesktopServices / the clipboard.
  CST_APP_NO_OPEN=1 makes --uitest run without opening anything.

Two bugs the device found
- The session writer had its own copy of the attachment JSON and forgot
  `for_user`: the file survived the file, but came back as an attachment --
  no card, and put back in front of the model. It now uses the one writer
  (cst_attachments_to_json()), with the round trip covered by a test.
- engineToolImages() looked the card up with findTool(), which only finds a
  card that is still running -- and the engine reports the result *before*
  the files, so the card was already complete and the files were dropped.
  A live turn therefore showed no card while a replay of the same session
  did. It now matches on the call id.
- A tap that could not work said nothing: ImageStrip.qml's errorText was
  never drawn. The card now carries the reason ("no app on the phone opens
  report.md").

Also
- The saved provider key reaches the engine on every start: AgentCore::open()
  was handed an empty key and the engine never resolves credentials itself
  (only the CLI did), so every start asked for it again. It resolves the same
  order main.c uses (saved key, provider environment, CST_API_KEY) and wipes
  the copy it made; --selftest checks it against a scratch credentials file.
- Android: notification_send reads the Java exception text into its result,
  and keeps failing on the NX789J -- the build() returns null and Qt clears
  the exception before C++ can read it, so the call has to be wrapped in Java
  next (docs/android.md says so).
- --uitest grew the delivered-file case (card, Open, Share, a forged name
  refused), the live order of result-then-files, and the startup row count.

Version 0.9.2.
2026-10-11 16:34:47 +08:00

104 lines
5.1 KiB
C

#ifndef CST_ATTACH_H
#define CST_ATTACH_H
#include "cst_agent.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* --- attachments ---------------------------------------------------------
* An attachment is a validated copy of a file, stored beside the session so
* what a model is shown stays what was picked. Images are stored as they are
* and encoded into the request when it is built; a PDF is reduced to its text
* layer and a text file is stored as is, both of which travel inline as text
* (so a text-only model still gets them). Only the metadata goes in the
* JSONL. */
/* Supported: PNG, JPEG, GIF, WebP, BMP (static images), PDF, UTF-8 text. */
#define CST_ATTACH_MAX_BYTES (16 * 1024 * 1024) /* one picked file */
#define CST_ATTACH_MAX_TOTAL (32 * 1024 * 1024) /* per request */
#define CST_ATTACH_MAX_COUNT 8 /* per message */
/* Text stored and inlined per document (a PDF also caps its text here). */
#define CST_ATTACH_TEXT_MAX (4 * 1024 * 1024)
/* A delivered file (deliver_file) is the opposite of an attachment: it is
* handed to the person, not to the model, so any bytes are allowed up to this
* size, the extension names the type, and no request ever shows it. */
#define CST_ARTIFACT_MAX_BYTES (64 * 1024 * 1024)
/* Images go on the wire as image parts; everything else is inlined as text. */
int cst_attach_is_image(const cst_attachment_t *a);
int cst_attach_is_text(const cst_attachment_t *a);
/* Magic-byte test: 0 and a canonical mime when data holds a supported
* image, -1 otherwise (an extension alone never decides). */
int cst_attach_sniff(const void *data, size_t n, char *mime, size_t mime_sz);
/* Pixel size from the header when it is readable; 0 for unknown. */
int cst_attach_dimensions(const void *data, size_t n, int *w, int *h);
/* A stored file name as store() makes it ("<id>.<ext>"); rejects anything
* that could leave the attachment directory. */
int cst_attach_rel_ok(const char *rel);
/* Directory holding this session's stored images (created on demand). */
int cst_attach_dir(const char *session_path, char *out, size_t out_sz);
/* Absolute path of a stored image (0 / -1). */
int cst_attach_full_path(const char *session_path, const cst_attachment_t *a,
char *out, size_t out_sz);
/* Read src_path and copy it into the session's attachment directory: an image
* as it is, a PDF as its extracted text, anything else only when it is text.
* Returns a malloc'd entry the caller owns (cst_attach_free), or NULL with
* err filled. */
cst_attachment_t *cst_attach_import_file(const char *session_path,
const char *src_path, char *err,
size_t err_sz);
/* Store already-read image bytes (clipboard, downloads); name is
* display-only. */
cst_attachment_t *cst_attach_import_bytes(const char *session_path,
const char *name, const char *data,
size_t n, char *err, size_t err_sz);
/* Copy src_path into the session's store as a finished file for the user:
* any bytes up to CST_ARTIFACT_MAX_BYTES, mime from the name's extension, and
* `for_user` set so it stays out of every request. title (optional) is the
* display name, the source's file name the default. */
cst_attachment_t *cst_attach_import_artifact(const char *session_path,
const char *src_path,
const char *title, char *err,
size_t err_sz);
/* Validate and read back a stored payload, or NULL when it is gone. */
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
size_t *out_len, char *err, size_t err_sz);
/* "data:image/png;base64,...." for the wire; images only, NULL otherwise. */
char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a);
/* "1920x1080 png 412 KB" / "pdf 1.2 MB, 12 pages" one-liner for prompts. */
void cst_attach_describe(const cst_attachment_t *a, char *out, size_t out_sz);
void cst_attach_free(cst_attachment_t *a);
/* Free a list of n entries allocated as separate blocks. */
void cst_attachments_free(cst_attachment_t *list, int n);
/* Deep copy (NULL on OOM or when n <= 0). */
cst_attachment_t *cst_attachments_dup(const cst_attachment_t *list, int n);
/* Append src to *list (grows with realloc). 0 / -1. */
int cst_attachments_push(cst_attachment_t **list, int *n, int *cap,
const cst_attachment_t *src);
/* JSON array of n attachments for the session JSONL (malloc'd), or NULL. */
char *cst_attachments_to_json(const cst_attachment_t *list, int n);
/* Parse a JSON array; caller frees with cst_attachments_free. */
cst_attachment_t *cst_attachments_from_json(const void *array_json, int *n_out);
/* Base64 of n bytes (malloc'd, no line breaks). */
char *cst_base64_encode(const void *data, size_t n);
/* Standard or URL-safe base64 (whitespace ignored) to bytes; malloc'd,
* NULL on a bad character. */
char *cst_base64_decode(const char *s, size_t n, size_t *out_len);
#ifdef __cplusplus
}
#endif
#endif