diff --git a/.changeset/systems-language-templates.md b/.changeset/systems-language-templates.md new file mode 100644 index 0000000..c299281 --- /dev/null +++ b/.changeset/systems-language-templates.md @@ -0,0 +1,7 @@ +--- +"agentic-debug-mode": minor +--- + +Add probe templates for five systems languages: C, C++, Rust, Java, and Kotlin. Each renders a +hypothesis-tagged, at-source-redacting fire-and-forget helper that appends bounded NDJSON evidence, +bringing the advertised helper/runtime pairs from nine to fourteen. diff --git a/DESIGN.md b/DESIGN.md index 4f7a0e4..f62dbe3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -115,9 +115,14 @@ The first supported combinations are: - PowerShell + file - C# + file - Swift + file - -Every advertised combination must pass a live end-to-end test with its real runtime. Rust, Java, -Kotlin, C, C++, and shell are not advertised until a safe serializer contract is defined. +- Rust + file +- C++ + file +- C + file +- Java + file +- Kotlin + file + +Every advertised combination must pass a live end-to-end test with its real runtime. Shell is +not advertised until a safe serializer contract is defined. ### `debug-mode reset --session ` diff --git a/README.md b/README.md index be06eec..8eb9254 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ reproduces the failure, and proves the root cause with runtime evidence before i Inspired from [Cursor's debug mode](https://cursor.com/blog/debug-mode) and built upon it: the same hypothesis-driven core, made agent-agnostic (any coding agent that can run a CLI), and extended -with a session-scoped evidence store, hypothesis-tagged probes across nine languages, embedded +with a session-scoped evidence store, hypothesis-tagged probes across fourteen languages, embedded structured queries over captured events, at-source secret redaction, and bounded reads that keep large evidence sets token-cheap. @@ -86,6 +86,11 @@ runtime: | PowerShell | file | | C# | file | | Swift | file | +| Rust | file | +| C++ | file | +| C | file | +| Java | file | +| Kotlin | file | ## Development diff --git a/skills/agentic-debug-mode/REFERENCE.md b/skills/agentic-debug-mode/REFERENCE.md index b3da0a4..18f8540 100644 --- a/skills/agentic-debug-mode/REFERENCE.md +++ b/skills/agentic-debug-mode/REFERENCE.md @@ -31,6 +31,11 @@ schema** for one language and transport. Templates never take `--session`. | PowerShell | `powershell` (`pwsh`) | `file` | | C# | `csharp` (`cs`, `c#`) | `file` | | Swift | `swift` | `file` | +| Rust | `rust` (`rs`) | `file` | +| C++ | `cpp` (`c++`, `cxx`) | `file` | +| C | `c` | `file` | +| Java | `java` | `file` | +| Kotlin | `kotlin` (`kt`) | `file` | HTTP templates use an ingest-URL placeholder; file templates use an append-path placeholder. Other languages are unsupported until a safe serializer contract is defined. diff --git a/skills/agentic-debug-mode/SKILL.md b/skills/agentic-debug-mode/SKILL.md index 868768a..615eac8 100644 --- a/skills/agentic-debug-mode/SKILL.md +++ b/skills/agentic-debug-mode/SKILL.md @@ -114,7 +114,9 @@ with the Append Path. Advertised combinations, each verified end-to-end: | TypeScript | http | PHP | file | | Python | file | PowerShell | file | | Go | file | C# | file | -| Swift | file | | | +| Swift | file | Rust | file | +| C++ | file | C | file | +| Java | file | Kotlin | file | The output has four sections: **HELPER TEMPLATE**, **CALL TEMPLATE**, **PLACEHOLDERS**, and **EVENT SCHEMA**. diff --git a/specs/building-a-debug-mode-agent.md b/specs/building-a-debug-mode-agent.md index 496ea74..ab7dcde 100644 --- a/specs/building-a-debug-mode-agent.md +++ b/specs/building-a-debug-mode-agent.md @@ -146,6 +146,11 @@ advertised, end-to-end-tested combinations are: - PowerShell + file - C# + file - Swift + file +- Rust + file +- C++ + file +- C + file +- Java + file +- Kotlin + file Other languages are not advertised until a safe serializer contract is defined. @@ -266,7 +271,7 @@ visually distinct, foldable, mechanically removable, and auditable as complete b | Language/file type | Open | Close | | --------------------------------- | ---------------------- | --------------- | -| JavaScript, TypeScript, Go, Swift | `// #region agent log` | `// #endregion` | +| JavaScript, TypeScript, Go, Swift, Rust, C++, C, Java, Kotlin | `// #region agent log` | `// #endregion` | | C#, PowerShell | `#region agent log` | `#endregion` | | Python, Ruby | `# region agent log` | `# endregion` | | PHP | `// #region agent log` | `// #endregion` | diff --git a/src/probes/c.ts b/src/probes/c.ts new file mode 100644 index 0000000..c745115 --- /dev/null +++ b/src/probes/c.ts @@ -0,0 +1,587 @@ +import type { ProbeTemplates } from "./render"; + +export function renderCTemplate(): ProbeTemplates { + return { + callTemplate: [ + "// #region agent log", + '__agentDebugEmit("__HYPOTHESIS_ID__", "__LOCATION__", "__MESSAGE__", __DATA_EXPRESSION__);', + "// #endregion", + ].join("\n"), + helperTemplate: [ + "// #region agent log", + "#define _POSIX_C_SOURCE 200809L", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#ifndef _WIN32", + "#include ", + "#include ", + "#endif", + "", + "enum {", + " AGENT_KIND_NULL,", + " AGENT_KIND_BOOL,", + " AGENT_KIND_INT,", + " AGENT_KIND_DOUBLE,", + " AGENT_KIND_STR,", + " AGENT_KIND_ARRAY,", + " AGENT_KIND_OBJECT", + "};", + "", + "typedef struct AgentValue AgentValue;", + "struct AgentValue {", + " int kind;", + " int boolValue;", + " long long intValue;", + " double doubleValue;", + " char *strValue;", + " char **keys;", + " AgentValue **items;", + " size_t count;", + "};", + "", + "typedef struct {", + " char *buffer;", + " size_t length;", + " size_t capacity;", + " int overflow;", + "} AgentDebugBuffer;", + "", + "static const char *const AGENT_DEBUG_SECRET_KEYS[] = {", + ' "authorization", "authorization_header", "cookie", "set_cookie",', + ' "password", "passwd", "pwd", "private_key", "secret", "token",', + ' "credential", "credentials"};', + "", + "static const char *const AGENT_DEBUG_SECRET_SUFFIXES[] = {", + ' "api_key", "api_token", "oauth_token", "o_auth_token", "private_key",', + ' "client_secret", "access_token", "refresh_token", "id_token",', + ' "auth_token", "bearer_token"};', + "", + "static AgentValue *__agent_debug_new(int kind) {", + " AgentValue *value = (AgentValue *)malloc(sizeof(AgentValue));", + " if (value == NULL) {", + " return NULL;", + " }", + " value->kind = kind;", + " value->boolValue = 0;", + " value->intValue = 0;", + " value->doubleValue = 0.0;", + " value->strValue = NULL;", + " value->keys = NULL;", + " value->items = NULL;", + " value->count = 0;", + " return value;", + "}", + "", + "static char *__agent_debug_strdup(const char *text) {", + " if (text == NULL) {", + " return NULL;", + " }", + " size_t n = strlen(text);", + " char *copy = (char *)malloc(n + 1);", + " if (copy == NULL) {", + " return NULL;", + " }", + " for (size_t i = 0; i <= n; i += 1) {", + " copy[i] = text[i];", + " }", + " return copy;", + "}", + "", + "static void __agent_debug_free(AgentValue *value) {", + " if (value == NULL) {", + " return;", + " }", + " if (value->strValue != NULL) {", + " free(value->strValue);", + " }", + " if (value->keys != NULL) {", + " for (size_t i = 0; i < value->count; i += 1) {", + " if (value->keys[i] != NULL) {", + " free(value->keys[i]);", + " }", + " }", + " free(value->keys);", + " }", + " if (value->items != NULL) {", + " for (size_t i = 0; i < value->count; i += 1) {", + " __agent_debug_free(value->items[i]);", + " }", + " free(value->items);", + " }", + " free(value);", + "}", + "", + "static AgentValue *adbg_null(void) {", + " return __agent_debug_new(AGENT_KIND_NULL);", + "}", + "", + "static AgentValue *adbg_bool(int flag) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_BOOL);", + " if (value != NULL) {", + " value->boolValue = flag ? 1 : 0;", + " }", + " return value;", + "}", + "", + "static AgentValue *adbg_int(long long number) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_INT);", + " if (value != NULL) {", + " value->intValue = number;", + " }", + " return value;", + "}", + "", + "static AgentValue *adbg_double(double number) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_DOUBLE);", + " if (value != NULL) {", + " value->doubleValue = number;", + " }", + " return value;", + "}", + "", + "static AgentValue *adbg_str(const char *text) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_STR);", + " if (value != NULL) {", + " value->strValue = __agent_debug_strdup(text);", + " }", + " return value;", + "}", + "", + "static AgentValue *adbg_arr(AgentValue *first, ...) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_ARRAY);", + " va_list args;", + " va_start(args, first);", + " AgentValue *current = first;", + " while (current != NULL) {", + " if (value != NULL) {", + " AgentValue **grown = (AgentValue **)realloc(value->items, (value->count + 1) * sizeof(AgentValue *));", + " if (grown != NULL) {", + " value->items = grown;", + " value->items[value->count] = current;", + " value->count += 1;", + " } else {", + " __agent_debug_free(current);", + " }", + " } else {", + " __agent_debug_free(current);", + " }", + " current = va_arg(args, AgentValue *);", + " }", + " va_end(args);", + " return value;", + "}", + "", + "static AgentValue *adbg_obj(const char *first_key, ...) {", + " AgentValue *value = __agent_debug_new(AGENT_KIND_OBJECT);", + " va_list args;", + " va_start(args, first_key);", + " const char *key = first_key;", + " while (key != NULL) {", + " AgentValue *child = va_arg(args, AgentValue *);", + " if (value != NULL) {", + " char **grown_keys = (char **)realloc(value->keys, (value->count + 1) * sizeof(char *));", + " if (grown_keys != NULL) {", + " value->keys = grown_keys;", + " }", + " AgentValue **grown_items = (AgentValue **)realloc(value->items, (value->count + 1) * sizeof(AgentValue *));", + " if (grown_items != NULL) {", + " value->items = grown_items;", + " }", + " if (grown_keys != NULL && grown_items != NULL) {", + " value->keys[value->count] = __agent_debug_strdup(key);", + " value->items[value->count] = child;", + " value->count += 1;", + " } else {", + " __agent_debug_free(child);", + " }", + " } else {", + " __agent_debug_free(child);", + " }", + " key = va_arg(args, const char *);", + " }", + " va_end(args);", + " return value;", + "}", + "", + "static char *__agent_debug_split_acronym(const char *input) {", + " size_t n = strlen(input);", + " char *result = (char *)malloc(n * 2 + 1);", + " if (result == NULL) {", + " return NULL;", + " }", + " size_t out = 0;", + " size_t i = 0;", + " while (i < n) {", + " unsigned char ci = (unsigned char)input[i];", + " if (ci >= 'A' && ci <= 'Z') {", + " size_t j = i;", + " while (j < n) {", + " unsigned char cj = (unsigned char)input[j];", + " if (cj >= 'A' && cj <= 'Z') {", + " j += 1;", + " } else {", + " break;", + " }", + " }", + " unsigned char after = (j < n) ? (unsigned char)input[j] : 0;", + " if (j - i >= 2 && j < n && after >= 'a' && after <= 'z') {", + " for (size_t k = i; k < j - 1; k += 1) {", + " result[out] = input[k];", + " out += 1;", + " }", + " result[out] = '_';", + " out += 1;", + " result[out] = input[j - 1];", + " out += 1;", + " result[out] = input[j];", + " out += 1;", + " i = j + 1;", + " continue;", + " }", + " for (size_t k = i; k < j; k += 1) {", + " result[out] = input[k];", + " out += 1;", + " }", + " i = j;", + " continue;", + " }", + " result[out] = input[i];", + " out += 1;", + " i += 1;", + " }", + " result[out] = 0;", + " return result;", + "}", + "", + "static char *__agent_debug_split_camel(const char *input) {", + " size_t n = strlen(input);", + " char *result = (char *)malloc(n * 2 + 1);", + " if (result == NULL) {", + " return NULL;", + " }", + " size_t out = 0;", + " for (size_t i = 0; i < n; i += 1) {", + " unsigned char ci = (unsigned char)input[i];", + " result[out] = input[i];", + " out += 1;", + " int boundary = (ci >= 'a' && ci <= 'z') || (ci >= '0' && ci <= '9');", + " if (boundary && i + 1 < n) {", + " unsigned char next = (unsigned char)input[i + 1];", + " if (next >= 'A' && next <= 'Z') {", + " result[out] = '_';", + " out += 1;", + " }", + " }", + " }", + " result[out] = 0;", + " return result;", + "}", + "", + "static char *__agent_debug_normalize_key(const char *key) {", + " char *acronym = __agent_debug_split_acronym(key);", + " if (acronym == NULL) {", + " return NULL;", + " }", + " char *split = __agent_debug_split_camel(acronym);", + " free(acronym);", + " if (split == NULL) {", + " return NULL;", + " }", + " size_t n = strlen(split);", + " char *normalized = (char *)malloc(n + 1);", + " if (normalized == NULL) {", + " free(split);", + " return NULL;", + " }", + " size_t out = 0;", + " int pending_underscore = 0;", + " for (size_t i = 0; i < n; i += 1) {", + " unsigned char c = (unsigned char)split[i];", + " int alnum = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');", + " if (alnum) {", + " if (pending_underscore && out > 0) {", + " normalized[out] = '_';", + " out += 1;", + " }", + " pending_underscore = 0;", + " if (c >= 'A' && c <= 'Z') {", + " normalized[out] = (char)(c - 'A' + 'a');", + " out += 1;", + " } else {", + " normalized[out] = (char)c;", + " out += 1;", + " }", + " } else {", + " pending_underscore = 1;", + " }", + " }", + " normalized[out] = 0;", + " free(split);", + " return normalized;", + "}", + "", + "static int __agent_debug_is_secret_key(const char *key) {", + " if (key == NULL) {", + " return 0;", + " }", + " char *normalized = __agent_debug_normalize_key(key);", + " if (normalized == NULL) {", + " return 0;", + " }", + " size_t norm_len = strlen(normalized);", + " int secret = 0;", + " size_t exact_count = sizeof(AGENT_DEBUG_SECRET_KEYS) / sizeof(AGENT_DEBUG_SECRET_KEYS[0]);", + " for (size_t i = 0; i < exact_count; i += 1) {", + " if (strcmp(normalized, AGENT_DEBUG_SECRET_KEYS[i]) == 0) {", + " secret = 1;", + " break;", + " }", + " }", + " if (!secret) {", + " size_t suffix_count = sizeof(AGENT_DEBUG_SECRET_SUFFIXES) / sizeof(AGENT_DEBUG_SECRET_SUFFIXES[0]);", + " for (size_t i = 0; i < suffix_count; i += 1) {", + " const char *suffix = AGENT_DEBUG_SECRET_SUFFIXES[i];", + " size_t suffix_len = strlen(suffix);", + " if (norm_len >= suffix_len && strcmp(normalized + (norm_len - suffix_len), suffix) == 0) {", + " size_t boundary = norm_len - suffix_len;", + " if (boundary == 0 || normalized[boundary - 1] == '_') {", + " secret = 1;", + " break;", + " }", + " }", + " }", + " }", + " free(normalized);", + " return secret;", + "}", + "", + "static void __agent_debug_append(AgentDebugBuffer *buf, const char *text, size_t n) {", + " if (buf->overflow) {", + " return;", + " }", + " if (n > buf->capacity - buf->length) {", + " buf->overflow = 1;", + " return;", + " }", + " for (size_t i = 0; i < n; i += 1) {", + " buf->buffer[buf->length + i] = text[i];", + " }", + " buf->length += n;", + "}", + "", + "static void __agent_debug_append_char(AgentDebugBuffer *buf, char c) {", + " __agent_debug_append(buf, &c, 1);", + "}", + "", + "static void __agent_debug_append_ll(AgentDebugBuffer *buf, long long number) {", + " char temp[32];", + ' int written = snprintf(temp, sizeof(temp), "%lld", number);', + " if (written < 0) {", + " buf->overflow = 1;", + " return;", + " }", + " __agent_debug_append(buf, temp, (size_t)written);", + "}", + "", + "static void __agent_debug_append_double(AgentDebugBuffer *buf, double number) {", + " char temp[64];", + ' int written = snprintf(temp, sizeof(temp), "%.17g", number);', + " if (written < 0) {", + " buf->overflow = 1;", + " return;", + " }", + " __agent_debug_append(buf, temp, (size_t)written);", + "}", + "", + "static void __agent_debug_write_string(AgentDebugBuffer *buf, const char *text) {", + " char backslash = (char)0x5C;", + " char quote = (char)0x22;", + " __agent_debug_append_char(buf, quote);", + " if (text != NULL) {", + " for (const unsigned char *p = (const unsigned char *)text; *p != 0; p += 1) {", + " unsigned int code = (unsigned int)(*p);", + " char c = (char)(*p);", + " if (c == quote) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, quote);", + " } else if (c == backslash) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, backslash);", + " } else if (code == 8) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 'b');", + " } else if (code == 9) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 't');", + " } else if (code == 10) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 'n');", + " } else if (code == 12) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 'f');", + " } else if (code == 13) {", + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 'r');", + " } else if (code < 32) {", + ' const char *digits = "0123456789abcdef";', + " __agent_debug_append_char(buf, backslash);", + " __agent_debug_append_char(buf, 'u');", + " __agent_debug_append_char(buf, '0');", + " __agent_debug_append_char(buf, '0');", + " __agent_debug_append_char(buf, digits[(code >> 4) & 15]);", + " __agent_debug_append_char(buf, digits[code & 15]);", + " } else {", + " __agent_debug_append_char(buf, c);", + " }", + " }", + " }", + " __agent_debug_append_char(buf, quote);", + "}", + "", + "static int __agent_debug_write_value(AgentDebugBuffer *buf, const AgentValue *value, int depth) {", + " if (depth > 64) {", + " return 0;", + " }", + " if (value == NULL) {", + " return 0;", + " }", + " switch (value->kind) {", + " case AGENT_KIND_NULL:", + ' __agent_debug_append(buf, "null", 4);', + " break;", + " case AGENT_KIND_BOOL:", + " if (value->boolValue) {", + ' __agent_debug_append(buf, "true", 4);', + " } else {", + ' __agent_debug_append(buf, "false", 5);', + " }", + " break;", + " case AGENT_KIND_INT:", + " __agent_debug_append_ll(buf, value->intValue);", + " break;", + " case AGENT_KIND_DOUBLE:", + " if (!isfinite(value->doubleValue)) {", + " return 0;", + " }", + " __agent_debug_append_double(buf, value->doubleValue);", + " break;", + " case AGENT_KIND_STR:", + " __agent_debug_write_string(buf, value->strValue);", + " break;", + " case AGENT_KIND_ARRAY: {", + " __agent_debug_append_char(buf, '[');", + " for (size_t i = 0; i < value->count; i += 1) {", + " if (i > 0) {", + " __agent_debug_append_char(buf, ',');", + " }", + " if (!__agent_debug_write_value(buf, value->items[i], depth + 1)) {", + " return 0;", + " }", + " }", + " __agent_debug_append_char(buf, ']');", + " break;", + " }", + " case AGENT_KIND_OBJECT: {", + " __agent_debug_append_char(buf, '{');", + " for (size_t i = 0; i < value->count; i += 1) {", + " if (i > 0) {", + " __agent_debug_append_char(buf, ',');", + " }", + " __agent_debug_write_string(buf, value->keys[i]);", + " __agent_debug_append_char(buf, ':');", + " if (__agent_debug_is_secret_key(value->keys[i])) {", + ' __agent_debug_write_string(buf, "[REDACTED]");', + " } else if (!__agent_debug_write_value(buf, value->items[i], depth + 1)) {", + " return 0;", + " }", + " }", + " __agent_debug_append_char(buf, '}');", + " break;", + " }", + " }", + " return 1;", + "}", + "", + "static long long __agent_debug_timestamp_ms(void) {", + "#ifdef _WIN32", + " return (long long)time(NULL) * 1000;", + "#else", + " struct timespec ts;", + " if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {", + " return 0;", + " }", + " return (long long)ts.tv_sec * 1000 + (long long)ts.tv_nsec / 1000000;", + "#endif", + "}", + "", + "static void __agentDebugEmit(const char *hypothesisId, const char *location,", + " const char *message, AgentValue *data) {", + " char storage[65536 + 4096];", + " AgentDebugBuffer buf;", + " buf.buffer = storage;", + " buf.length = 0;", + " buf.capacity = sizeof(storage);", + " buf.overflow = 0;", + " int serialized = 1;", + " __agent_debug_append_char(&buf, '{');", + ' __agent_debug_write_string(&buf, "hypothesisId");', + " __agent_debug_append_char(&buf, ':');", + " __agent_debug_write_string(&buf, hypothesisId);", + " __agent_debug_append_char(&buf, ',');", + ' __agent_debug_write_string(&buf, "location");', + " __agent_debug_append_char(&buf, ':');", + " __agent_debug_write_string(&buf, location);", + " __agent_debug_append_char(&buf, ',');", + ' __agent_debug_write_string(&buf, "message");', + " __agent_debug_append_char(&buf, ':');", + " __agent_debug_write_string(&buf, message);", + " __agent_debug_append_char(&buf, ',');", + ' __agent_debug_write_string(&buf, "data");', + " __agent_debug_append_char(&buf, ':');", + " if (!__agent_debug_write_value(&buf, data, 0)) {", + " serialized = 0;", + " }", + " if (serialized) {", + " __agent_debug_append_char(&buf, ',');", + ' __agent_debug_write_string(&buf, "timestamp");', + " __agent_debug_append_char(&buf, ':');", + " __agent_debug_append_ll(&buf, __agent_debug_timestamp_ms());", + " __agent_debug_append_char(&buf, '}');", + " __agent_debug_append_char(&buf, (char)0x0A);", + " }", + " if (serialized && !buf.overflow && buf.length <= 65536) {", + "#ifdef _WIN32", + ' FILE *file = fopen("__APPEND_PATH__", "ab");', + " if (file != NULL) {", + " size_t wrote = fwrite(storage, 1, buf.length, file);", + " (void)wrote;", + " fclose(file);", + " }", + "#else", + ' int fd = open("__APPEND_PATH__", O_WRONLY | O_APPEND | O_CREAT, 0600);', + " if (fd >= 0) {", + " ssize_t wrote = write(fd, storage, buf.length);", + " (void)wrote;", + " close(fd);", + " }", + "#endif", + " }", + " __agent_debug_free(data);", + "}", + "// #endregion", + ].join("\n"), + ingest: "file", + language: "c", + placeholders: { + __APPEND_PATH__: "Replace with the appendPath returned by debug-mode create.", + __DATA_EXPRESSION__: "Replace with a JSON-compatible C AgentValue that has no secrets.", + __HYPOTHESIS_ID__: "Replace with the hypothesis label.", + __LOCATION__: "Replace with the observed source location.", + __MESSAGE__: "Replace with a constant observation message.", + }, + }; +} diff --git a/src/probes/cpp.ts b/src/probes/cpp.ts new file mode 100644 index 0000000..d3032b8 --- /dev/null +++ b/src/probes/cpp.ts @@ -0,0 +1,332 @@ +import type { ProbeTemplates } from "./render"; + +export function renderCppTemplate(): ProbeTemplates { + return { + callTemplate: [ + "// #region agent log", + '__agentDebugEmit("__HYPOTHESIS_ID__", "__LOCATION__", "__MESSAGE__", __DATA_EXPRESSION__);', + "// #endregion", + ].join("\n"), + helperTemplate: [ + "// #region agent log", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#include ", + "#ifndef _WIN32", + "#include ", + "#endif", + "", + "enum class AgentKind { Null, Bool, Int, Double, Str, Array, Object };", + "", + "struct AgentValue {", + " AgentKind kind = AgentKind::Null;", + " bool boolValue = false;", + " long long intValue = 0;", + " double doubleValue = 0.0;", + " std::string strValue;", + " std::vector arrayValue;", + " std::vector> objectValue;", + "", + " AgentValue() {}", + " AgentValue(bool v) { kind = AgentKind::Bool; boolValue = v; }", + " AgentValue(int v) { kind = AgentKind::Int; intValue = v; }", + " AgentValue(long long v) { kind = AgentKind::Int; intValue = v; }", + " AgentValue(double v) { kind = AgentKind::Double; doubleValue = v; }", + " AgentValue(const char* v) { kind = AgentKind::Str; strValue = v; }", + " AgentValue(const std::string& v) { kind = AgentKind::Str; strValue = v; }", + " AgentValue(std::initializer_list items) {", + " bool isObject = items.size() > 0;", + " for (const auto& item : items) {", + " if (item.kind != AgentKind::Array || item.arrayValue.size() != 2 ||", + " item.arrayValue[0].kind != AgentKind::Str) {", + " isObject = false;", + " break;", + " }", + " }", + " if (isObject) {", + " kind = AgentKind::Object;", + " for (const auto& item : items) {", + " objectValue.emplace_back(item.arrayValue[0].strValue, item.arrayValue[1]);", + " }", + " } else {", + " kind = AgentKind::Array;", + " arrayValue = std::vector(items);", + " }", + " }", + "};", + "", + "static const char* const AGENT_DEBUG_SECRET_KEYS[] = {", + ' "authorization", "authorization_header", "cookie", "set_cookie",', + ' "password", "passwd", "pwd", "private_key", "secret", "token",', + ' "credential", "credentials"};', + "", + "static const char* const AGENT_DEBUG_SECRET_SUFFIXES[] = {", + ' "api_key", "api_token", "oauth_token", "o_auth_token", "private_key",', + ' "client_secret", "access_token", "refresh_token", "id_token",', + ' "auth_token", "bearer_token"};', + "", + "static std::string __agent_debug_split_acronym(const std::string& input) {", + " std::string result;", + " size_t n = input.size();", + " size_t i = 0;", + " while (i < n) {", + " unsigned char ci = static_cast(input[i]);", + " if (ci >= 'A' && ci <= 'Z') {", + " size_t j = i;", + " while (j < n) {", + " unsigned char cj = static_cast(input[j]);", + " if (cj >= 'A' && cj <= 'Z') {", + " j += 1;", + " } else {", + " break;", + " }", + " }", + " unsigned char after = (j < n) ? static_cast(input[j]) : 0;", + " if (j - i >= 2 && j < n && after >= 'a' && after <= 'z') {", + " for (size_t k = i; k < j - 1; k += 1) {", + " result.push_back(input[k]);", + " }", + " result.push_back('_');", + " result.push_back(input[j - 1]);", + " result.push_back(input[j]);", + " i = j + 1;", + " continue;", + " }", + " for (size_t k = i; k < j; k += 1) {", + " result.push_back(input[k]);", + " }", + " i = j;", + " continue;", + " }", + " result.push_back(input[i]);", + " i += 1;", + " }", + " return result;", + "}", + "", + "static std::string __agent_debug_split_camel(const std::string& input) {", + " std::string result;", + " size_t n = input.size();", + " for (size_t i = 0; i < n; i += 1) {", + " unsigned char ci = static_cast(input[i]);", + " result.push_back(input[i]);", + " bool boundary = (ci >= 'a' && ci <= 'z') || (ci >= '0' && ci <= '9');", + " if (boundary && i + 1 < n) {", + " unsigned char next = static_cast(input[i + 1]);", + " if (next >= 'A' && next <= 'Z') {", + " result.push_back('_');", + " }", + " }", + " }", + " return result;", + "}", + "", + "static std::string __agent_debug_normalize_key(const std::string& key) {", + " std::string split = __agent_debug_split_camel(__agent_debug_split_acronym(key));", + " std::string normalized;", + " bool pending_underscore = false;", + " for (unsigned char c : split) {", + " bool alnum = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');", + " if (alnum) {", + " if (pending_underscore && !normalized.empty()) {", + " normalized.push_back('_');", + " }", + " pending_underscore = false;", + " if (c >= 'A' && c <= 'Z') {", + " normalized.push_back(static_cast(c - 'A' + 'a'));", + " } else {", + " normalized.push_back(static_cast(c));", + " }", + " } else {", + " pending_underscore = true;", + " }", + " }", + " return normalized;", + "}", + "", + "static bool __agent_debug_is_secret_key(const std::string& key) {", + " std::string normalized = __agent_debug_normalize_key(key);", + " for (const auto& exact : AGENT_DEBUG_SECRET_KEYS) {", + " if (normalized == exact) {", + " return true;", + " }", + " }", + " for (const auto& suffix : AGENT_DEBUG_SECRET_SUFFIXES) {", + " std::string tail(suffix);", + " if (normalized.size() >= tail.size() &&", + " normalized.compare(normalized.size() - tail.size(), tail.size(), tail) == 0) {", + " size_t boundary = normalized.size() - tail.size();", + " if (boundary == 0 || normalized[boundary - 1] == '_') {", + " return true;", + " }", + " }", + " }", + " return false;", + "}", + "", + "static void __agent_debug_write_string(std::ostringstream& out, const std::string& text) {", + " const char backslash = static_cast(0x5C);", + " const char quote = static_cast(0x22);", + " out << quote;", + " for (unsigned char c : text) {", + " unsigned int code = static_cast(c);", + " if (c == quote) {", + " out << backslash << quote;", + " } else if (c == backslash) {", + " out << backslash << backslash;", + " } else if (code == 8) {", + " out << backslash << 'b';", + " } else if (code == 9) {", + " out << backslash << 't';", + " } else if (code == 10) {", + " out << backslash << 'n';", + " } else if (code == 12) {", + " out << backslash << 'f';", + " } else if (code == 13) {", + " out << backslash << 'r';", + " } else if (code < 32) {", + ' const char* digits = "0123456789abcdef";', + " out << backslash << 'u' << '0' << '0';", + " out << digits[(code >> 4) & 15] << digits[code & 15];", + " } else {", + " out << static_cast(c);", + " }", + " }", + " out << quote;", + "}", + "", + "static bool __agent_debug_write_value(std::ostringstream& out, const AgentValue& value, int depth) {", + " if (depth > 64) {", + " return false;", + " }", + " switch (value.kind) {", + " case AgentKind::Null:", + ' out << "null";', + " break;", + " case AgentKind::Bool:", + ' out << (value.boolValue ? "true" : "false");', + " break;", + " case AgentKind::Int:", + " out << value.intValue;", + " break;", + " case AgentKind::Double: {", + " if (!std::isfinite(value.doubleValue)) {", + " return false;", + " }", + " std::ostringstream number;", + " number << std::setprecision(17) << value.doubleValue;", + " out << number.str();", + " break;", + " }", + " case AgentKind::Str:", + " __agent_debug_write_string(out, value.strValue);", + " break;", + " case AgentKind::Array: {", + " out << '[';", + " int index = 0;", + " for (const auto& item : value.arrayValue) {", + " if (index > 0) {", + " out << ',';", + " }", + " index += 1;", + " if (!__agent_debug_write_value(out, item, depth + 1)) {", + " return false;", + " }", + " }", + " out << ']';", + " break;", + " }", + " case AgentKind::Object: {", + " out << '{';", + " int index = 0;", + " for (const auto& entry : value.objectValue) {", + " if (index > 0) {", + " out << ',';", + " }", + " index += 1;", + " __agent_debug_write_string(out, entry.first);", + " out << ':';", + " if (__agent_debug_is_secret_key(entry.first)) {", + ' __agent_debug_write_string(out, "[REDACTED]");', + " } else if (!__agent_debug_write_value(out, entry.second, depth + 1)) {", + " return false;", + " }", + " }", + " out << '}';", + " break;", + " }", + " }", + " return true;", + "}", + "", + "static long long __agent_debug_timestamp_ms() {", + " return std::chrono::duration_cast(", + " std::chrono::system_clock::now().time_since_epoch())", + " .count();", + "}", + "", + "static void __agentDebugEmit(const std::string& hypothesisId, const std::string& location,", + " const std::string& message, const AgentValue& data) {", + " try {", + " std::ostringstream out;", + " out << '{';", + ' __agent_debug_write_string(out, "hypothesisId");', + " out << ':';", + " __agent_debug_write_string(out, hypothesisId);", + " out << ',';", + ' __agent_debug_write_string(out, "location");', + " out << ':';", + " __agent_debug_write_string(out, location);", + " out << ',';", + ' __agent_debug_write_string(out, "message");', + " out << ':';", + " __agent_debug_write_string(out, message);", + " out << ',';", + ' __agent_debug_write_string(out, "data");', + " out << ':';", + " if (!__agent_debug_write_value(out, data, 0)) {", + " return;", + " }", + " out << ',';", + ' __agent_debug_write_string(out, "timestamp");', + " out << ':';", + " out << __agent_debug_timestamp_ms();", + " out << '}';", + " out << static_cast(0x0A);", + " std::string payload = out.str();", + " if (payload.size() > 65536) {", + " return;", + " }", + ' std::ofstream file("__APPEND_PATH__", std::ios::out | std::ios::app | std::ios::binary);', + " if (!file.is_open()) {", + " return;", + " }", + " file << payload;", + " file.close();", + "#ifndef _WIN32", + ' chmod("__APPEND_PATH__", 0600);', + "#endif", + " } catch (...) {", + " return;", + " }", + "}", + "// #endregion", + ].join("\n"), + ingest: "file", + language: "cpp", + placeholders: { + __APPEND_PATH__: "Replace with the appendPath returned by debug-mode create.", + __DATA_EXPRESSION__: "Replace with a JSON-compatible C++ AgentValue that has no secrets.", + __HYPOTHESIS_ID__: "Replace with the hypothesis label.", + __LOCATION__: "Replace with the observed source location.", + __MESSAGE__: "Replace with a constant observation message.", + }, + }; +} diff --git a/src/probes/java.ts b/src/probes/java.ts new file mode 100644 index 0000000..a26c887 --- /dev/null +++ b/src/probes/java.ts @@ -0,0 +1,274 @@ +import type { ProbeTemplates } from "./render"; + +export function renderJavaTemplate(): ProbeTemplates { + return { + callTemplate: [ + "// #region agent log", + 'AgentDebugLog.Emit("__HYPOTHESIS_ID__", "__LOCATION__", "__MESSAGE__", __DATA_EXPRESSION__);', + "// #endregion", + ].join("\n"), + helperTemplate: [ + "// #region agent log", + "class AgentDebugLog {", + " private static final java.util.Set SECRET_KEYS = new java.util.HashSet<>(java.util.Arrays.asList(", + ' "authorization", "authorization_header", "cookie", "set_cookie",', + ' "password", "passwd", "pwd", "private_key", "secret", "token",', + ' "credential", "credentials"));', + "", + " private static final java.util.regex.Pattern SECRET_SUFFIX = java.util.regex.Pattern.compile(", + ' "(^|_)(api_key|api_token|oauth_token|o_auth_token|private_key|client_secret|access_token|refresh_token|id_token|auth_token|bearer_token)$");', + "", + " private static boolean isSecretKey(String key) {", + ' String normalized = key.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");', + ' normalized = normalized.replaceAll("([a-z0-9])([A-Z])", "$1_$2");', + ' normalized = normalized.replaceAll("[^a-zA-Z0-9]+", "_").toLowerCase(java.util.Locale.ROOT);', + ' normalized = normalized.replaceAll("^_+", "").replaceAll("_+$", "");', + " return SECRET_KEYS.contains(normalized) || SECRET_SUFFIX.matcher(normalized).find();", + " }", + "", + " private static String decapitalize(String name) {", + " if (name.isEmpty()) {", + " return name;", + " }", + " if (name.length() > 1 && Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) {", + " return name;", + " }", + " char[] letters = name.toCharArray();", + " letters[0] = Character.toLowerCase(letters[0]);", + " return new String(letters);", + " }", + "", + " private static Object redact(Object value, java.util.IdentityHashMap active, int depth) {", + " if (depth > 64) {", + ' throw new IllegalStateException("Observation graph is too deep");', + " }", + " if (value == null) {", + " return null;", + " }", + " if (value instanceof CharSequence || value instanceof Character) {", + " return value.toString();", + " }", + " if (value instanceof Number || value instanceof Boolean) {", + " return value;", + " }", + " if (value instanceof Enum) {", + " return ((Enum) value).name();", + " }", + " if (active.put(value, Boolean.TRUE) != null) {", + ' throw new IllegalStateException("Cyclic observation graph");', + " }", + " try {", + " if (value instanceof java.util.Map) {", + " java.util.LinkedHashMap redacted = new java.util.LinkedHashMap<>();", + " for (java.util.Map.Entry entry : ((java.util.Map) value).entrySet()) {", + " String key = String.valueOf(entry.getKey());", + ' redacted.put(key, isSecretKey(key) ? "[REDACTED]" : redact(entry.getValue(), active, depth + 1));', + " }", + " return redacted;", + " }", + " if (value instanceof Iterable) {", + " java.util.ArrayList redacted = new java.util.ArrayList<>();", + " for (Object item : (Iterable) value) {", + " redacted.add(redact(item, active, depth + 1));", + " }", + " return redacted;", + " }", + " if (value.getClass().isArray()) {", + " java.util.ArrayList redacted = new java.util.ArrayList<>();", + " int length = java.lang.reflect.Array.getLength(value);", + " for (int index = 0; index < length; index += 1) {", + " redacted.add(redact(java.lang.reflect.Array.get(value, index), active, depth + 1));", + " }", + " return redacted;", + " }", + " java.util.LinkedHashMap members = new java.util.LinkedHashMap<>();", + " Class type = value.getClass();", + " for (java.lang.reflect.Method method : type.getMethods()) {", + " if (method.getParameterCount() != 0 || method.getReturnType() == void.class) {", + " continue;", + " }", + " if (java.lang.reflect.Modifier.isStatic(method.getModifiers()) || method.getDeclaringClass() == Object.class) {", + " continue;", + " }", + " String name = method.getName();", + " String property;", + ' if (name.startsWith("get") && name.length() > 3) {', + " property = decapitalize(name.substring(3));", + ' } else if (name.startsWith("is") && name.length() > 2 && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)) {', + " property = decapitalize(name.substring(2));", + " } else {", + " continue;", + " }", + " Object raw;", + " try {", + " method.setAccessible(true);", + " raw = method.invoke(value);", + " } catch (Throwable failure) {", + " continue;", + " }", + ' members.put(property, isSecretKey(property) ? "[REDACTED]" : redact(raw, active, depth + 1));', + " }", + " for (java.lang.reflect.Field field : type.getFields()) {", + " if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {", + " continue;", + " }", + " Object raw;", + " try {", + " field.setAccessible(true);", + " raw = field.get(value);", + " } catch (Throwable failure) {", + " continue;", + " }", + ' members.put(field.getName(), isSecretKey(field.getName()) ? "[REDACTED]" : redact(raw, active, depth + 1));', + " }", + " return members;", + " } finally {", + " active.remove(value);", + " }", + " }", + "", + " private static void writeString(StringBuilder out, String text) {", + " char backslash = (char) 92;", + " char quote = (char) 34;", + " out.append(quote);", + " for (int index = 0; index < text.length(); index += 1) {", + " char symbol = text.charAt(index);", + " if (symbol == quote) {", + " out.append(backslash).append(quote);", + " } else if (symbol == backslash) {", + " out.append(backslash).append(backslash);", + " } else if (symbol == (char) 8) {", + " out.append(backslash).append('b');", + " } else if (symbol == (char) 9) {", + " out.append(backslash).append('t');", + " } else if (symbol == (char) 10) {", + " out.append(backslash).append('n');", + " } else if (symbol == (char) 12) {", + " out.append(backslash).append('f');", + " } else if (symbol == (char) 13) {", + " out.append(backslash).append('r');", + " } else if (symbol < (char) 32) {", + ' String digits = "0123456789abcdef";', + " out.append(backslash).append('u').append('0').append('0');", + " out.append(digits.charAt((symbol >> 4) & 15)).append(digits.charAt(symbol & 15));", + " } else {", + " out.append(symbol);", + " }", + " }", + " out.append(quote);", + " }", + "", + " private static void writeValue(StringBuilder out, Object value) {", + " if (value == null) {", + ' out.append("null");', + " return;", + " }", + " if (value instanceof String) {", + " writeString(out, (String) value);", + " return;", + " }", + " if (value instanceof Boolean) {", + ' out.append(((Boolean) value).booleanValue() ? "true" : "false");', + " return;", + " }", + " if (value instanceof Double || value instanceof Float) {", + " double number = ((Number) value).doubleValue();", + " if (Double.isNaN(number) || Double.isInfinite(number)) {", + ' throw new IllegalStateException("Non-finite number");', + " }", + " out.append(Double.toString(number));", + " return;", + " }", + " if (value instanceof Number) {", + " out.append(value.toString());", + " return;", + " }", + " if (value instanceof java.util.Map) {", + " out.append('{');", + " boolean first = true;", + " for (java.util.Map.Entry entry : ((java.util.Map) value).entrySet()) {", + " if (!first) {", + " out.append(',');", + " }", + " first = false;", + " writeString(out, String.valueOf(entry.getKey()));", + " out.append(':');", + " writeValue(out, entry.getValue());", + " }", + " out.append('}');", + " return;", + " }", + " if (value instanceof java.util.List) {", + " out.append('[');", + " boolean first = true;", + " for (Object item : (java.util.List) value) {", + " if (!first) {", + " out.append(',');", + " }", + " first = false;", + " writeValue(out, item);", + " }", + " out.append(']');", + " return;", + " }", + " writeString(out, String.valueOf(value));", + " }", + "", + " static void Emit(String hypothesisId, String location, String message, Object data) {", + " try {", + " StringBuilder out = new StringBuilder();", + " out.append('{');", + ' writeString(out, "hypothesisId");', + " out.append(':');", + " writeString(out, hypothesisId);", + " out.append(',');", + ' writeString(out, "location");', + " out.append(':');", + " writeString(out, location);", + " out.append(',');", + ' writeString(out, "message");', + " out.append(':');", + " writeString(out, message);", + " out.append(',');", + ' writeString(out, "data");', + " out.append(':');", + " writeValue(out, redact(data, new java.util.IdentityHashMap(), 0));", + " out.append(',');", + ' writeString(out, "timestamp");', + " out.append(':');", + " out.append(System.currentTimeMillis());", + " out.append('}');", + " out.append((char) 10);", + " byte[] payload = out.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);", + " if (payload.length > 65536) {", + " return;", + " }", + ' java.nio.file.Path path = java.nio.file.Paths.get("__APPEND_PATH__");', + " try {", + ' java.nio.file.Files.createFile(path, java.nio.file.attribute.PosixFilePermissions.asFileAttribute(java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")));', + " } catch (UnsupportedOperationException nonPosix) {", + " try {", + " java.nio.file.Files.createFile(path);", + " } catch (Throwable ignored) {", + " }", + " } catch (Throwable ignored) {", + " }", + " java.nio.file.Files.write(path, payload, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);", + " } catch (Throwable failure) {", + " // Observations must never change application behavior.", + " }", + " }", + "}", + "// #endregion", + ].join("\n"), + ingest: "file", + language: "java", + placeholders: { + __APPEND_PATH__: "Replace with the appendPath returned by debug-mode create.", + __DATA_EXPRESSION__: "Replace with a JSON-compatible Java value that has no secrets.", + __HYPOTHESIS_ID__: "Replace with the hypothesis label.", + __LOCATION__: "Replace with the observed source location.", + __MESSAGE__: "Replace with a constant observation message.", + }, + }; +} diff --git a/src/probes/kotlin.ts b/src/probes/kotlin.ts new file mode 100644 index 0000000..defa3fa --- /dev/null +++ b/src/probes/kotlin.ts @@ -0,0 +1,280 @@ +import type { ProbeTemplates } from "./render"; + +export function renderKotlinTemplate(): ProbeTemplates { + return { + callTemplate: [ + "// #region agent log", + 'AgentDebugLog.emit("__HYPOTHESIS_ID__", "__LOCATION__", "__MESSAGE__", __DATA_EXPRESSION__)', + "// #endregion", + ].join("\n"), + helperTemplate: [ + "// #region agent log", + "object AgentDebugLog {", + " private val SECRET_KEYS = java.util.HashSet(java.util.Arrays.asList(", + ' "authorization", "authorization_header", "cookie", "set_cookie",', + ' "password", "passwd", "pwd", "private_key", "secret", "token",', + ' "credential", "credentials"))', + "", + ' private val SECRET_BOUNDARY_ONE = java.util.regex.Pattern.compile("([A-Z]+)([A-Z][a-z])")', + ' private val SECRET_BOUNDARY_TWO = java.util.regex.Pattern.compile("([a-z0-9])([A-Z])")', + ' private val SECRET_NON_ALNUM = java.util.regex.Pattern.compile("[^a-zA-Z0-9]+")', + ' private val SECRET_TRIM_LEADING = java.util.regex.Pattern.compile("^_+")', + ' private val SECRET_TRIM_TRAILING = java.util.regex.Pattern.compile("_+\\$")', + " private val SECRET_SUFFIX = java.util.regex.Pattern.compile(", + ' "(^|_)(api_key|api_token|oauth_token|o_auth_token|private_key|client_secret|access_token|refresh_token|id_token|auth_token|bearer_token)\\$")', + "", + " private fun isSecretKey(key: String): Boolean {", + ' var normalized = SECRET_BOUNDARY_ONE.matcher(key).replaceAll("\\$1_\\$2")', + ' normalized = SECRET_BOUNDARY_TWO.matcher(normalized).replaceAll("\\$1_\\$2")', + ' normalized = SECRET_NON_ALNUM.matcher(normalized).replaceAll("_").lowercase()', + ' normalized = SECRET_TRIM_LEADING.matcher(normalized).replaceAll("")', + ' normalized = SECRET_TRIM_TRAILING.matcher(normalized).replaceAll("")', + " return SECRET_KEYS.contains(normalized) || SECRET_SUFFIX.matcher(normalized).find()", + " }", + "", + " private fun decapitalize(name: String): String {", + " if (name.isEmpty()) {", + " return name", + " }", + " if (name.length > 1 && name[0].isUpperCase() && name[1].isUpperCase()) {", + " return name", + " }", + " val letters = name.toCharArray()", + " letters[0] = letters[0].lowercaseChar()", + " return String(letters)", + " }", + "", + " private fun redact(value: Any?, active: java.util.IdentityHashMap, depth: Int): Any? {", + " if (depth > 64) {", + ' throw IllegalStateException("Observation graph is too deep")', + " }", + " if (value == null) {", + " return null", + " }", + " if (value is CharSequence || value is Char) {", + " return value.toString()", + " }", + " if (value is Number || value is Boolean) {", + " return value", + " }", + " if (value is Enum<*>) {", + " return value.name", + " }", + " if (active.put(value, true) != null) {", + ' throw IllegalStateException("Cyclic observation graph")', + " }", + " try {", + " if (value is Map<*, *>) {", + " val redacted = java.util.LinkedHashMap()", + " for (entry in value.entries) {", + ' val key = entry.key?.toString() ?: "null"', + ' redacted[key] = if (isSecretKey(key)) "[REDACTED]" else redact(entry.value, active, depth + 1)', + " }", + " return redacted", + " }", + " if (value is Iterable<*>) {", + " val redacted = java.util.ArrayList()", + " for (item in value) {", + " redacted.add(redact(item, active, depth + 1))", + " }", + " return redacted", + " }", + " if (value.javaClass.isArray) {", + " val redacted = java.util.ArrayList()", + " val length = java.lang.reflect.Array.getLength(value)", + " for (index in 0 until length) {", + " redacted.add(redact(java.lang.reflect.Array.get(value, index), active, depth + 1))", + " }", + " return redacted", + " }", + " val members = java.util.LinkedHashMap()", + " val type = value.javaClass", + " for (method in type.methods) {", + " if (method.parameterCount != 0 || method.returnType == java.lang.Void.TYPE) {", + " continue", + " }", + " if (java.lang.reflect.Modifier.isStatic(method.modifiers) || method.declaringClass == java.lang.Object::class.java) {", + " continue", + " }", + " val name = method.name", + " val property: String", + ' if (name.startsWith("get") && name.length > 3) {', + " property = decapitalize(name.substring(3))", + ' } else if (name.startsWith("is") && name.length > 2 && (method.returnType == java.lang.Boolean.TYPE || method.returnType == java.lang.Boolean::class.java)) {', + " property = decapitalize(name.substring(2))", + " } else {", + " continue", + " }", + " val raw: Any?", + " try {", + " method.isAccessible = true", + " raw = method.invoke(value)", + " } catch (methodFailure: Throwable) {", + " continue", + " }", + ' members[property] = if (isSecretKey(property)) "[REDACTED]" else redact(raw, active, depth + 1)', + " }", + " for (field in type.fields) {", + " if (java.lang.reflect.Modifier.isStatic(field.modifiers)) {", + " continue", + " }", + " val raw: Any?", + " try {", + " field.isAccessible = true", + " raw = field.get(value)", + " } catch (fieldFailure: Throwable) {", + " continue", + " }", + ' members[field.name] = if (isSecretKey(field.name)) "[REDACTED]" else redact(raw, active, depth + 1)', + " }", + " return members", + " } finally {", + " active.remove(value)", + " }", + " }", + "", + " private fun writeString(out: StringBuilder, text: String) {", + " val backslash = 92.toChar()", + " val quote = 34.toChar()", + " out.append(quote)", + " for (index in 0 until text.length) {", + " val symbol = text[index]", + " if (symbol == quote) {", + " out.append(backslash).append(quote)", + " } else if (symbol == backslash) {", + " out.append(backslash).append(backslash)", + " } else if (symbol == 8.toChar()) {", + " out.append(backslash).append('b')", + " } else if (symbol == 9.toChar()) {", + " out.append(backslash).append('t')", + " } else if (symbol == 10.toChar()) {", + " out.append(backslash).append('n')", + " } else if (symbol == 12.toChar()) {", + " out.append(backslash).append('f')", + " } else if (symbol == 13.toChar()) {", + " out.append(backslash).append('r')", + " } else if (symbol < 32.toChar()) {", + ' val digits = "0123456789abcdef"', + " out.append(backslash).append('u').append('0').append('0')", + " out.append(digits[(symbol.code shr 4) and 15]).append(digits[symbol.code and 15])", + " } else {", + " out.append(symbol)", + " }", + " }", + " out.append(quote)", + " }", + "", + " private fun writeValue(out: StringBuilder, value: Any?) {", + " if (value == null) {", + ' out.append("null")', + " return", + " }", + " if (value is String) {", + " writeString(out, value)", + " return", + " }", + " if (value is Boolean) {", + ' out.append(if (value) "true" else "false")', + " return", + " }", + " if (value is Double || value is Float) {", + " val number = (value as Number).toDouble()", + " if (number.isNaN() || number.isInfinite()) {", + ' throw IllegalStateException("Non-finite number")', + " }", + " out.append(number.toString())", + " return", + " }", + " if (value is Number) {", + " out.append(value.toString())", + " return", + " }", + " if (value is Map<*, *>) {", + " out.append('{')", + " var first = true", + " for (entry in value.entries) {", + " if (!first) {", + " out.append(',')", + " }", + " first = false", + ' writeString(out, entry.key?.toString() ?: "null")', + " out.append(':')", + " writeValue(out, entry.value)", + " }", + " out.append('}')", + " return", + " }", + " if (value is List<*>) {", + " out.append('[')", + " var first = true", + " for (item in value) {", + " if (!first) {", + " out.append(',')", + " }", + " first = false", + " writeValue(out, item)", + " }", + " out.append(']')", + " return", + " }", + " writeString(out, value.toString())", + " }", + "", + " fun emit(hypothesisId: String, location: String, message: String, data: Any?) {", + " try {", + " val out = StringBuilder()", + " out.append('{')", + ' writeString(out, "hypothesisId")', + " out.append(':')", + " writeString(out, hypothesisId)", + " out.append(',')", + ' writeString(out, "location")', + " out.append(':')", + " writeString(out, location)", + " out.append(',')", + ' writeString(out, "message")', + " out.append(':')", + " writeString(out, message)", + " out.append(',')", + ' writeString(out, "data")', + " out.append(':')", + " writeValue(out, redact(data, java.util.IdentityHashMap(), 0))", + " out.append(',')", + ' writeString(out, "timestamp")', + " out.append(':')", + " out.append(System.currentTimeMillis())", + " out.append('}')", + " out.append(10.toChar())", + " val payload = out.toString().toByteArray(java.nio.charset.StandardCharsets.UTF_8)", + " if (payload.size > 65536) {", + " return", + " }", + ' val path = java.nio.file.Paths.get("__APPEND_PATH__")', + " try {", + ' java.nio.file.Files.createFile(path, java.nio.file.attribute.PosixFilePermissions.asFileAttribute(java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")))', + " } catch (nonPosix: UnsupportedOperationException) {", + " try {", + " java.nio.file.Files.createFile(path)", + " } catch (ignored: Throwable) {", + " }", + " } catch (ignored: Throwable) {", + " }", + " java.nio.file.Files.write(path, payload, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND)", + " } catch (failure: Throwable) {", + " // Observations must never change application behavior.", + " }", + " }", + "}", + "// #endregion", + ].join("\n"), + ingest: "file", + language: "kotlin", + placeholders: { + __APPEND_PATH__: "Replace with the appendPath returned by debug-mode create.", + __DATA_EXPRESSION__: "Replace with a JSON-compatible Kotlin value that has no secrets.", + __HYPOTHESIS_ID__: "Replace with the hypothesis label.", + __LOCATION__: "Replace with the observed source location.", + __MESSAGE__: "Replace with a constant observation message.", + }, + }; +} diff --git a/src/probes/render.ts b/src/probes/render.ts index 549cf95..c622045 100644 --- a/src/probes/render.ts +++ b/src/probes/render.ts @@ -1,10 +1,15 @@ +import { renderCTemplate } from "./c"; +import { renderCppTemplate } from "./cpp"; import { renderCSharpTemplate } from "./csharp"; import { renderGoTemplate } from "./go"; +import { renderJavaTemplate } from "./java"; import { renderJavaScriptTemplate } from "./javascript"; +import { renderKotlinTemplate } from "./kotlin"; import { renderPhpTemplate } from "./php"; import { renderPowerShellTemplate } from "./powershell"; import { renderPythonTemplate } from "./python"; import { renderRubyTemplate } from "./ruby"; +import { renderRustTemplate } from "./rust"; import { renderSwiftTemplate } from "./swift"; import { renderTypeScriptTemplate } from "./typescript"; @@ -17,7 +22,12 @@ export type TemplateLanguage = | "php" | "powershell" | "csharp" - | "swift"; + | "swift" + | "rust" + | "cpp" + | "c" + | "java" + | "kotlin"; export type IngestMethod = "http" | "file"; @@ -77,6 +87,20 @@ function normalizeLanguage(language: string): TemplateLanguage | undefined { return "csharp"; case "swift": return "swift"; + case "rust": + case "rs": + return "rust"; + case "cpp": + case "c++": + case "cxx": + return "cpp"; + case "c": + return "c"; + case "java": + return "java"; + case "kotlin": + case "kt": + return "kotlin"; default: return undefined; } @@ -109,6 +133,16 @@ export function renderTemplate(language: string, ingest: string): ProbeTemplates return renderCSharpTemplate(); case "swift:file": return renderSwiftTemplate(); + case "rust:file": + return renderRustTemplate(); + case "cpp:file": + return renderCppTemplate(); + case "c:file": + return renderCTemplate(); + case "java:file": + return renderJavaTemplate(); + case "kotlin:file": + return renderKotlinTemplate(); default: throw new UnsupportedTemplateError(language, ingest); } diff --git a/src/probes/rust.ts b/src/probes/rust.ts new file mode 100644 index 0000000..72bf09a --- /dev/null +++ b/src/probes/rust.ts @@ -0,0 +1,294 @@ +import type { ProbeTemplates } from "./render"; + +export function renderRustTemplate(): ProbeTemplates { + return { + callTemplate: [ + "// #region agent log", + '__agentDebugEmit("__HYPOTHESIS_ID__", "__LOCATION__", "__MESSAGE__", __DATA_EXPRESSION__);', + "// #endregion", + ].join("\n"), + helperTemplate: [ + "// #region agent log", + "#[derive(Clone)]", + "#[allow(dead_code)]", + "enum AgentValue {", + " Null,", + " Bool(bool),", + " Int(i64),", + " Float(f64),", + " Str(String),", + " Array(Vec),", + " Object(Vec<(String, AgentValue)>),", + "}", + "", + "impl From for AgentValue { fn from(value: bool) -> Self { AgentValue::Bool(value) } }", + "impl From for AgentValue { fn from(value: i64) -> Self { AgentValue::Int(value) } }", + "impl From for AgentValue { fn from(value: f64) -> Self { AgentValue::Float(value) } }", + "impl From<&str> for AgentValue { fn from(value: &str) -> Self { AgentValue::Str(value.to_string()) } }", + "impl From for AgentValue { fn from(value: String) -> Self { AgentValue::Str(value) } }", + "", + "macro_rules! adbg {", + " ({ $($key:tt : $value:tt),* $(,)? }) => {", + " AgentValue::Object(vec![ $( ($key.to_string(), adbg!($value)) ),* ])", + " };", + " ([ $($value:tt),* $(,)? ]) => {", + " AgentValue::Array(vec![ $( adbg!($value) ),* ])", + " };", + " ($other:expr) => {", + " AgentValue::from($other)", + " };", + "}", + "", + "const AGENT_DEBUG_SECRET_KEYS: [&str; 12] = [", + ' "authorization", "authorization_header", "cookie", "set_cookie",', + ' "password", "passwd", "pwd", "private_key", "secret", "token",', + ' "credential", "credentials",', + "];", + "", + "const AGENT_DEBUG_SECRET_SUFFIXES: [&str; 11] = [", + ' "api_key", "api_token", "oauth_token", "o_auth_token", "private_key",', + ' "client_secret", "access_token", "refresh_token", "id_token",', + ' "auth_token", "bearer_token",', + "];", + "", + "fn __agent_debug_split_acronym(input: &str) -> String {", + " let chars: Vec = input.chars().collect();", + " let mut result = String::new();", + " let mut i = 0;", + " while i < chars.len() {", + " if chars[i].is_ascii_uppercase() {", + " let mut j = i;", + " while j < chars.len() && chars[j].is_ascii_uppercase() {", + " j += 1;", + " }", + " if j - i >= 2 && j < chars.len() && chars[j].is_ascii_lowercase() {", + " for k in i..(j - 1) {", + " result.push(chars[k]);", + " }", + " result.push('_');", + " result.push(chars[j - 1]);", + " result.push(chars[j]);", + " i = j + 1;", + " continue;", + " }", + " for k in i..j {", + " result.push(chars[k]);", + " }", + " i = j;", + " continue;", + " }", + " result.push(chars[i]);", + " i += 1;", + " }", + " result", + "}", + "", + "fn __agent_debug_split_camel(input: &str) -> String {", + " let chars: Vec = input.chars().collect();", + " let mut result = String::new();", + " let mut i = 0;", + " while i < chars.len() {", + " result.push(chars[i]);", + " let boundary = chars[i].is_ascii_lowercase() || chars[i].is_ascii_digit();", + " if boundary && i + 1 < chars.len() && chars[i + 1].is_ascii_uppercase() {", + " result.push('_');", + " }", + " i += 1;", + " }", + " result", + "}", + "", + "fn __agent_debug_normalize_key(key: &str) -> String {", + " let split = __agent_debug_split_camel(&__agent_debug_split_acronym(key));", + " let mut normalized = String::new();", + " let mut pending_underscore = false;", + " for c in split.chars() {", + " if c.is_ascii_alphanumeric() {", + " if pending_underscore && !normalized.is_empty() {", + " normalized.push('_');", + " }", + " pending_underscore = false;", + " for lower in c.to_lowercase() {", + " normalized.push(lower);", + " }", + " } else {", + " pending_underscore = true;", + " }", + " }", + " normalized", + "}", + "", + "fn __agent_debug_is_secret_key(key: &str) -> bool {", + " let normalized = __agent_debug_normalize_key(key);", + " for exact in AGENT_DEBUG_SECRET_KEYS.iter() {", + " if normalized == *exact {", + " return true;", + " }", + " }", + " let bytes = normalized.as_bytes();", + " for suffix in AGENT_DEBUG_SECRET_SUFFIXES.iter() {", + " if normalized.ends_with(suffix) {", + " let boundary = normalized.len() - suffix.len();", + " if boundary == 0 || bytes[boundary - 1] == b'_' {", + " return true;", + " }", + " }", + " }", + " false", + "}", + "", + "fn __agent_debug_write_string(out: &mut String, text: &str) {", + " let backslash = char::from(92u8);", + " let quote = char::from(34u8);", + " out.push(quote);", + " for c in text.chars() {", + " let code = c as u32;", + " if c == quote {", + " out.push(backslash);", + " out.push(quote);", + " } else if c == backslash {", + " out.push(backslash);", + " out.push(backslash);", + " } else if code == 8 {", + " out.push(backslash);", + " out.push(char::from(98u8));", + " } else if code == 9 {", + " out.push(backslash);", + " out.push(char::from(116u8));", + " } else if code == 10 {", + " out.push(backslash);", + " out.push(char::from(110u8));", + " } else if code == 12 {", + " out.push(backslash);", + " out.push(char::from(102u8));", + " } else if code == 13 {", + " out.push(backslash);", + " out.push(char::from(114u8));", + " } else if code < 32 {", + ' let digits = "0123456789abcdef".as_bytes();', + " out.push(backslash);", + " out.push(char::from(117u8));", + " out.push(char::from(48u8));", + " out.push(char::from(48u8));", + " out.push(char::from(digits[((code >> 4) & 15) as usize]));", + " out.push(char::from(digits[(code & 15) as usize]));", + " } else {", + " out.push(c);", + " }", + " }", + " out.push(quote);", + "}", + "", + "fn __agent_debug_write_value(out: &mut String, value: &AgentValue, depth: usize) -> bool {", + " if depth > 64 {", + " return false;", + " }", + " match value {", + ' AgentValue::Null => out.push_str("null"),', + ' AgentValue::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),', + " AgentValue::Int(number) => out.push_str(&number.to_string()),", + " AgentValue::Float(number) => {", + " if !number.is_finite() {", + " return false;", + " }", + " out.push_str(&number.to_string());", + " }", + " AgentValue::Str(text) => __agent_debug_write_string(out, text),", + " AgentValue::Array(items) => {", + " out.push('[');", + " let mut index = 0;", + " for item in items.iter() {", + " if index > 0 {", + " out.push(',');", + " }", + " index += 1;", + " if !__agent_debug_write_value(out, item, depth + 1) {", + " return false;", + " }", + " }", + " out.push(']');", + " }", + " AgentValue::Object(entries) => {", + " out.push('{');", + " let mut index = 0;", + " for entry in entries.iter() {", + " if index > 0 {", + " out.push(',');", + " }", + " index += 1;", + " __agent_debug_write_string(out, &entry.0);", + " out.push(':');", + " if __agent_debug_is_secret_key(&entry.0) {", + ' __agent_debug_write_string(out, "[REDACTED]");', + " } else if !__agent_debug_write_value(out, &entry.1, depth + 1) {", + " return false;", + " }", + " }", + " out.push('}');", + " }", + " }", + " true", + "}", + "", + "fn __agent_debug_timestamp_ms() -> i64 {", + " match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {", + " Ok(elapsed) => elapsed.as_millis() as i64,", + " Err(_) => 0,", + " }", + "}", + "", + "#[allow(non_snake_case)]", + "fn __agentDebugEmit(hypothesis_id: &str, location: &str, message: &str, data: AgentValue) {", + " let mut out = String::new();", + " out.push('{');", + ' __agent_debug_write_string(&mut out, "hypothesisId");', + " out.push(':');", + " __agent_debug_write_string(&mut out, hypothesis_id);", + " out.push(',');", + ' __agent_debug_write_string(&mut out, "location");', + " out.push(':');", + " __agent_debug_write_string(&mut out, location);", + " out.push(',');", + ' __agent_debug_write_string(&mut out, "message");', + " out.push(':');", + " __agent_debug_write_string(&mut out, message);", + " out.push(',');", + ' __agent_debug_write_string(&mut out, "data");', + " out.push(':');", + " if !__agent_debug_write_value(&mut out, &data, 0) {", + " return;", + " }", + " out.push(',');", + ' __agent_debug_write_string(&mut out, "timestamp");', + " out.push(':');", + " out.push_str(&__agent_debug_timestamp_ms().to_string());", + " out.push('}');", + " out.push(char::from(10u8));", + " if out.len() > 65536 {", + " return;", + " }", + " let mut options = std::fs::OpenOptions::new();", + " options.create(true).append(true);", + " #[cfg(unix)]", + " {", + " use std::os::unix::fs::OpenOptionsExt;", + " options.mode(0o600);", + " }", + ' if let Ok(mut file) = options.open("__APPEND_PATH__") {', + " use std::io::Write;", + " let _ = file.write_all(out.as_bytes());", + " }", + "}", + "// #endregion", + ].join("\n"), + ingest: "file", + language: "rust", + placeholders: { + __APPEND_PATH__: "Replace with the appendPath returned by debug-mode create.", + __DATA_EXPRESSION__: "Replace with a JSON-compatible Rust AgentValue that has no secrets.", + __HYPOTHESIS_ID__: "Replace with the hypothesis label.", + __LOCATION__: "Replace with the observed source location.", + __MESSAGE__: "Replace with a constant observation message.", + }, + }; +} diff --git a/tests/contract/template-renderers.test.ts b/tests/contract/template-renderers.test.ts index 0c7192f..e4f5b41 100644 --- a/tests/contract/template-renderers.test.ts +++ b/tests/contract/template-renderers.test.ts @@ -19,6 +19,11 @@ const supported = [ ["powershell", "file"], ["csharp", "file"], ["swift", "file"], + ["rust", "file"], + ["cpp", "file"], + ["c", "file"], + ["java", "file"], + ["kotlin", "file"], ] as const satisfies readonly (readonly [TemplateLanguage, IngestMethod])[]; const callPlaceholders = [ @@ -109,6 +114,17 @@ describe("session-independent template renderers", () => { expect(renderTemplate("C#", "file").language).toBe("csharp"); expect(renderTemplate("cs", "file").language).toBe("csharp"); expect(renderTemplate("SWIFT", "FILE").language).toBe("swift"); + expect(renderTemplate("Rust", "FILE").language).toBe("rust"); + expect(renderTemplate("rs", "file").language).toBe("rust"); + expect(renderTemplate("CPP", "FILE").language).toBe("cpp"); + expect(renderTemplate("C++", "file").language).toBe("cpp"); + expect(renderTemplate("cxx", "file").language).toBe("cpp"); + expect(renderTemplate("C", "FILE").language).toBe("c"); + expect(renderTemplate("c", "file").language).toBe("c"); + expect(renderTemplate("Java", "FILE").language).toBe("java"); + expect(renderTemplate("java", "file").language).toBe("java"); + expect(renderTemplate("Kotlin", "FILE").language).toBe("kotlin"); + expect(renderTemplate("kt", "file").language).toBe("kotlin"); }); test("rejects every unadvertised language and ingest pair with a typed error", () => { @@ -122,7 +138,11 @@ describe("session-independent template renderers", () => { ["powershell", "http"], ["csharp", "http"], ["swift", "http"], - ["rust", "file"], + ["rust", "http"], + ["cpp", "http"], + ["c", "http"], + ["java", "http"], + ["kotlin", "http"], ["javascript", "socket"], ] as const) { try { diff --git a/tests/e2e/languages/live-probes.test.ts b/tests/e2e/languages/live-probes.test.ts index 49c3aa4..8504bdc 100644 --- a/tests/e2e/languages/live-probes.test.ts +++ b/tests/e2e/languages/live-probes.test.ts @@ -206,6 +206,117 @@ const fixtures: Fixture[] = [ sharedData: '["left": __agentShared, "right": __agentShared]', sharedPrelude: 'let __agentShared: [String: Any] = ["APIKey": "source-shared-secret"]', }, + { + callData: + 'adbg!({ "value": 42i64, "designToken": "visible-design-token", "fortuneCookie": "visible-fortune-cookie", "secretSauceName": "visible-secret-sauce", "tokenCount": 7i64, "passwordPolicy": "visible-password-policy", "password": "source-password-secret", "APIKey": "source-api-acronym-secret", "APIToken": "source-api-token-secret", "IDToken": "source-id-token-secret", "OAuthToken": "source-oauth-token-secret", "Client Secret": "source-client-secret", "nested": { "apiKey": "source-api-secret", "items": [ { "refresh-token": "source-refresh-secret" }, { "credentials": "source-credentials-secret" } ] } })', + // Rust's AgentValue is an owned tree, so a reference cycle is unrepresentable. + // The depth-64 cap is the analogous unbounded-structure rejection: a value + // nested past the cap is dropped without emitting, exactly like a cycle would be. + command: (path) => { + const rustc = Bun.which("rustc") ?? ""; + const binary = path.replace(/\.rs$/, process.platform === "win32" ? ".exe" : ".out"); + const script = `"${rustc}" -A warnings "${path}" -o "${binary}" && "${binary}"`; + return process.platform === "win32" ? ["cmd", "/c", script] : ["sh", "-c", script]; + }, + cycleData: "__agent_cycle", + cyclePrelude: + "let mut __agent_cycle = AgentValue::Int(0); let mut __agent_depth = 0; while __agent_depth < 100 { __agent_cycle = AgentValue::Array(vec![__agent_cycle]); __agent_depth += 1; }", + file: "rust-file.rs", + ingest: "file", + language: "rust", + runtime: Bun.which("rustc"), + sharedData: + 'AgentValue::Object(vec![("left".to_string(), __agent_shared.clone()), ("right".to_string(), __agent_shared)])', + sharedPrelude: + 'let __agent_shared = AgentValue::Object(vec![("APIKey".to_string(), AgentValue::from("source-shared-secret"))]);', + }, + { + callData: + 'AgentValue{ {"value", 42LL}, {"designToken", "visible-design-token"}, {"fortuneCookie", "visible-fortune-cookie"}, {"secretSauceName", "visible-secret-sauce"}, {"tokenCount", 7LL}, {"passwordPolicy", "visible-password-policy"}, {"password", "source-password-secret"}, {"APIKey", "source-api-acronym-secret"}, {"APIToken", "source-api-token-secret"}, {"IDToken", "source-id-token-secret"}, {"OAuthToken", "source-oauth-token-secret"}, {"Client Secret", "source-client-secret"}, {"nested", AgentValue{ {"apiKey", "source-api-secret"}, {"items", AgentValue{ AgentValue{ {"refresh-token", "source-refresh-secret"} }, AgentValue{ {"credentials", "source-credentials-secret"} } }} }} }', + // C++'s AgentValue is an owned value tree, so a reference cycle is + // unrepresentable. The depth-64 cap is the analogous unbounded-structure + // rejection: a value nested past the cap is dropped without emitting, + // exactly like a cycle would be. + command: (path) => { + const compiler = Bun.which("clang++") ?? Bun.which("g++") ?? ""; + const binary = path.replace(/\.cpp$/, process.platform === "win32" ? ".exe" : ".out"); + const script = `"${compiler}" -std=c++17 "${path}" -o "${binary}" && "${binary}"`; + return process.platform === "win32" ? ["cmd", "/c", script] : ["sh", "-c", script]; + }, + cycleData: "__agent_cycle", + cyclePrelude: + "AgentValue __agent_cycle = AgentValue(0LL); for (int __agent_depth = 0; __agent_depth < 100; __agent_depth += 1) { __agent_cycle = AgentValue{ __agent_cycle }; }", + file: "cpp-file.cpp", + ingest: "file", + language: "cpp", + runtime: Bun.which("clang++") ?? Bun.which("g++"), + sharedData: 'AgentValue{ {"left", __agent_shared}, {"right", __agent_shared} }', + sharedPrelude: 'AgentValue __agent_shared = AgentValue{ {"APIKey", "source-shared-secret"} };', + }, + { + callData: + 'adbg_obj("value", adbg_int(42), "designToken", adbg_str("visible-design-token"), "fortuneCookie", adbg_str("visible-fortune-cookie"), "secretSauceName", adbg_str("visible-secret-sauce"), "tokenCount", adbg_int(7), "passwordPolicy", adbg_str("visible-password-policy"), "password", adbg_str("source-password-secret"), "APIKey", adbg_str("source-api-acronym-secret"), "APIToken", adbg_str("source-api-token-secret"), "IDToken", adbg_str("source-id-token-secret"), "OAuthToken", adbg_str("source-oauth-token-secret"), "Client Secret", adbg_str("source-client-secret"), "nested", adbg_obj("apiKey", adbg_str("source-api-secret"), "items", adbg_arr(adbg_obj("refresh-token", adbg_str("source-refresh-secret"), NULL), adbg_obj("credentials", adbg_str("source-credentials-secret"), NULL), NULL), NULL), NULL)', + // C's AgentValue is an owned pointer tree, so a reference cycle is + // unrepresentable. The depth-64 cap is the analogous unbounded-structure + // rejection: a value nested past the cap is dropped without emitting, + // exactly like a cycle would be. + command: (path) => { + const compiler = Bun.which("clang") ?? Bun.which("gcc") ?? ""; + const binary = path.replace(/\.c$/, process.platform === "win32" ? ".exe" : ".out"); + const script = `"${compiler}" -std=c99 "${path}" -o "${binary}" && "${binary}"`; + return process.platform === "win32" ? ["cmd", "/c", script] : ["sh", "-c", script]; + }, + cycleData: "__agent_cycle", + cyclePrelude: + "AgentValue *__agent_cycle = adbg_int(0); for (int __agent_depth = 0; __agent_depth < 100; __agent_depth += 1) { __agent_cycle = adbg_arr(__agent_cycle, NULL); }", + file: "c-file.c", + ingest: "file", + language: "c", + runtime: Bun.which("clang") ?? Bun.which("gcc"), + // C stores an owned pointer tree; sharing one pointer twice would double-free + // on cleanup, so the shared-reference case builds two independent equal + // subtrees (the test compares output equality, not pointer identity). + sharedData: + 'adbg_obj("left", adbg_obj("APIKey", adbg_str("source-shared-secret"), NULL), "right", adbg_obj("APIKey", adbg_str("source-shared-secret"), NULL), NULL)', + sharedPrelude: "", + }, + { + callData: + 'java.util.Map.ofEntries(java.util.Map.entry("value", 42), java.util.Map.entry("designToken", "visible-design-token"), java.util.Map.entry("fortuneCookie", "visible-fortune-cookie"), java.util.Map.entry("secretSauceName", "visible-secret-sauce"), java.util.Map.entry("tokenCount", 7), java.util.Map.entry("passwordPolicy", "visible-password-policy"), java.util.Map.entry("password", "source-password-secret"), java.util.Map.entry("APIKey", "source-api-acronym-secret"), java.util.Map.entry("APIToken", "source-api-token-secret"), java.util.Map.entry("IDToken", "source-id-token-secret"), java.util.Map.entry("OAuthToken", "source-oauth-token-secret"), java.util.Map.entry("Client Secret", "source-client-secret"), java.util.Map.entry("nested", java.util.Map.ofEntries(java.util.Map.entry("apiKey", "source-api-secret"), java.util.Map.entry("items", java.util.List.of(java.util.Map.of("refresh-token", "source-refresh-secret"), java.util.Map.of("credentials", "source-credentials-secret"))))))', + command: (path) => [Bun.which("java") ?? "", path], + cycleData: "__agentCycle", + cyclePrelude: + 'java.util.Map __agentCycle = new java.util.LinkedHashMap<>(); __agentCycle.put("self", __agentCycle);', + file: "java-file.java", + ingest: "file", + language: "java", + runtime: Bun.which("java"), + sharedData: 'java.util.Map.of("left", __agentShared, "right", __agentShared)', + sharedPrelude: + 'java.util.Map __agentShared = new java.util.LinkedHashMap<>(); __agentShared.put("APIKey", "source-shared-secret");', + }, + { + callData: + 'mapOf("value" to 42, "designToken" to "visible-design-token", "fortuneCookie" to "visible-fortune-cookie", "secretSauceName" to "visible-secret-sauce", "tokenCount" to 7, "passwordPolicy" to "visible-password-policy", "password" to "source-password-secret", "APIKey" to "source-api-acronym-secret", "APIToken" to "source-api-token-secret", "IDToken" to "source-id-token-secret", "OAuthToken" to "source-oauth-token-secret", "Client Secret" to "source-client-secret", "nested" to mapOf("apiKey" to "source-api-secret", "items" to listOf(mapOf("refresh-token" to "source-refresh-secret"), mapOf("credentials" to "source-credentials-secret"))))', + command: (path) => { + const kotlinc = Bun.which("kotlinc") ?? ""; + const java = Bun.which("java") ?? ""; + const jar = path.replace(/\.kt$/, ".jar"); + const script = `"${kotlinc}" -include-runtime -d "${jar}" "${path}" && "${java}" -jar "${jar}"`; + return process.platform === "win32" ? ["cmd", "/c", script] : ["sh", "-c", script]; + }, + cycleData: "__agentCycle", + cyclePrelude: + 'val __agentCycle = java.util.LinkedHashMap(); __agentCycle.put("self", __agentCycle)', + file: "kotlin-file.kt", + ingest: "file", + language: "kotlin", + runtime: + Bun.which("kotlinc") !== null && Bun.which("java") !== null ? Bun.which("kotlinc") : null, + sharedData: 'mapOf("left" to __agentShared, "right" to __agentShared)', + sharedPrelude: + 'val __agentShared = java.util.LinkedHashMap(); __agentShared.put("APIKey", "source-shared-secret")', + }, ]; async function run(command: string[], env: Record = process.env) { @@ -495,7 +606,7 @@ describe("live language templates", () => { await runCli(home, ["stop", "--json"]); } }, - 90_000, + 180_000, ); runtimeTest( @@ -525,7 +636,7 @@ describe("live language templates", () => { expect(executed.exitCode, executed.stderr).toBe(0); expect(`${executed.stdout}\n${executed.stderr}`).toContain("application-completed"); }, - 90_000, + 180_000, ); runtimeTest( @@ -580,7 +691,7 @@ describe("live language templates", () => { await runCli(home, ["stop", "--json"]); } }, - 90_000, + 180_000, ); runtimeTest( @@ -631,7 +742,7 @@ describe("live language templates", () => { await runCli(home, ["stop", "--json"]); } }, - 90_000, + 180_000, ); } @@ -726,10 +837,89 @@ describe("live language templates", () => { }, 120_000); } - for (const language of ["csharp", "powershell"]) { + // Per-language custom-object coverage: C# and PowerShell exercise value types + // (structs) so the redactor inspects their members instead of treating them as + // atomic; Java has no value types, so it exercises the reflection path over a + // plain object's public fields — the only case that reaches the reflection + // branch, since the shared policy matrix uses maps and lists throughout. + const valueTypeCases: Record< + string, + { dataExpression: string; prelude: string; extraTypes?: string } + > = { + csharp: { + dataExpression: "__agentValue", + prelude: [ + "var __agentValue = new AgentCustomValue", + "{", + ' APIKey = "source-value-api-secret",', + ' designToken = "visible-value-design-token",', + ' Nested = new Dictionary { ["OAuthToken"] = "source-value-oauth-secret" },', + "};", + ].join("\n"), + extraTypes: [ + "internal struct AgentCustomValue", + "{", + " public string APIKey { get; init; }", + " public Dictionary Nested;", + " public string designToken;", + "}", + ].join("\n"), + }, + java: { + dataExpression: "__agentValue", + prelude: [ + "AgentCustomValue __agentValue = new AgentCustomValue();", + '__agentValue.APIKey = "source-value-api-secret";', + '__agentValue.designToken = "visible-value-design-token";', + '__agentValue.Nested = java.util.Map.of("OAuthToken", "source-value-oauth-secret");', + ].join("\n"), + extraTypes: [ + "static class AgentCustomValue {", + " public String APIKey;", + " public Object Nested;", + " public String designToken;", + "}", + ].join("\n"), + }, + kotlin: { + dataExpression: "__agentValue", + prelude: [ + "val __agentValue = AgentCustomValue()", + '__agentValue.APIKey = "source-value-api-secret"', + '__agentValue.designToken = "visible-value-design-token"', + '__agentValue.Nested = java.util.Map.of("OAuthToken", "source-value-oauth-secret")', + ].join("\n"), + extraTypes: [ + "class AgentCustomValue {", + ' @JvmField var APIKey: String = ""', + ' @JvmField var Nested: Any = ""', + ' @JvmField var designToken: String = ""', + "}", + ].join("\n"), + }, + powershell: { + dataExpression: "$__agentValue", + prelude: [ + "Add-Type -TypeDefinition @'", + "public struct AgentCustomValue {", + " public string APIKey { get; set; }", + " public object Nested { get; set; }", + " public string designToken;", + "}", + "'@", + "$__agentValue = [AgentCustomValue]::new()", + '$__agentValue.APIKey = "source-value-api-secret"', + '$__agentValue.designToken = "visible-value-design-token"', + '$__agentValue.Nested = @{ OAuthToken = "source-value-oauth-secret" }', + ].join("\n"), + }, + }; + + for (const language of ["csharp", "powershell", "java", "kotlin"]) { const fixture = fixtures.find((candidate) => candidate.language === language); const unavailable = fixture?.runtime === null; const runtimeTest = unavailable && !requireRuntimes ? test.skip : test; + const valueTypeCase = valueTypeCases[language]; runtimeTest( `${language} redacts custom value-type members`, @@ -737,6 +927,9 @@ describe("live language templates", () => { if (!fixture) { throw new Error(`Missing ${language} fixture`); } + if (!valueTypeCase) { + throw new Error(`Missing ${language} value-type case`); + } expect(fixture.runtime, `${language} runtime must be installed`).not.toBeNull(); const home = await mkdtemp(join(tmpdir(), "debug-mode-home-")); const workspace = await mkdtemp(join(tmpdir(), `debug-mode-${language}-value-type-`)); @@ -749,41 +942,8 @@ describe("live language templates", () => { ); await fixture.setup?.(workspace); const fixturePath = join(workspace, fixture.file); - const isCSharp = language === "csharp"; - const prelude = isCSharp - ? [ - "var __agentValue = new AgentCustomValue", - "{", - ' APIKey = "source-value-api-secret",', - ' designToken = "visible-value-design-token",', - ' Nested = new Dictionary { ["OAuthToken"] = "source-value-oauth-secret" },', - "};", - ].join("\n") - : [ - "Add-Type -TypeDefinition @'", - "public struct AgentCustomValue {", - " public string APIKey { get; set; }", - " public object Nested { get; set; }", - " public string designToken;", - "}", - "'@", - "$__agentValue = [AgentCustomValue]::new()", - '$__agentValue.APIKey = "source-value-api-secret"', - '$__agentValue.designToken = "visible-value-design-token"', - '$__agentValue.Nested = @{ OAuthToken = "source-value-oauth-secret" }', - ].join("\n"); - if (isCSharp) { - source = source.replace( - "/* __EXTRA_TYPES__ */", - [ - "internal struct AgentCustomValue", - "{", - " public string APIKey { get; init; }", - " public Dictionary Nested;", - " public string designToken;", - "}", - ].join("\n"), - ); + if (valueTypeCase.extraTypes) { + source = source.replace("/* __EXTRA_TYPES__ */", valueTypeCase.extraTypes); } await writeFile( fixturePath, @@ -793,8 +953,8 @@ describe("live language templates", () => { fixture, created.data.appendPath.replaceAll("\\", "\\\\"), 1, - isCSharp ? "__agentValue" : "$__agentValue", - prelude, + valueTypeCase.dataExpression, + valueTypeCase.prelude, ), ); @@ -817,7 +977,7 @@ describe("live language templates", () => { await runCli(home, ["stop", "--json"]); } }, - 90_000, + 180_000, ); } @@ -863,7 +1023,7 @@ describe("live language templates", () => { expect(() => JSON.parse(line)).not.toThrow(); } }, - 90_000, + 180_000, ); } }); diff --git a/tests/fixtures/languages/c-file.c b/tests/fixtures/languages/c-file.c new file mode 100644 index 0000000..2c5e1d5 --- /dev/null +++ b/tests/fixtures/languages/c-file.c @@ -0,0 +1,7 @@ +__HELPER_TEMPLATE__ + +int main(void) { + __CALL_TEMPLATE__ + printf("application-completed\n"); + return 0; +} diff --git a/tests/fixtures/languages/cpp-file.cpp b/tests/fixtures/languages/cpp-file.cpp new file mode 100644 index 0000000..b4ffe3e --- /dev/null +++ b/tests/fixtures/languages/cpp-file.cpp @@ -0,0 +1,7 @@ +__HELPER_TEMPLATE__ + +int main() { + __CALL_TEMPLATE__ + std::cout << "application-completed" << std::endl; + return 0; +} diff --git a/tests/fixtures/languages/java-file.java b/tests/fixtures/languages/java-file.java new file mode 100644 index 0000000..640aa0e --- /dev/null +++ b/tests/fixtures/languages/java-file.java @@ -0,0 +1,9 @@ +class JavaFile { + /* __EXTRA_TYPES__ */ + public static void main(String[] args) { + __CALL_TEMPLATE__ + System.out.println("application-completed"); + } +} + +__HELPER_TEMPLATE__ diff --git a/tests/fixtures/languages/kotlin-file.kt b/tests/fixtures/languages/kotlin-file.kt new file mode 100644 index 0000000..ad43917 --- /dev/null +++ b/tests/fixtures/languages/kotlin-file.kt @@ -0,0 +1,7 @@ +/* __EXTRA_TYPES__ */ +fun main() { + __CALL_TEMPLATE__ + println("application-completed") +} + +__HELPER_TEMPLATE__ diff --git a/tests/fixtures/languages/rust-file.rs b/tests/fixtures/languages/rust-file.rs new file mode 100644 index 0000000..da5268b --- /dev/null +++ b/tests/fixtures/languages/rust-file.rs @@ -0,0 +1,6 @@ +__HELPER_TEMPLATE__ + +fn main() { + __CALL_TEMPLATE__ + println!("application-completed"); +}