Merge origin/main into fix/dotd-depinfo-sniff

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-27 20:02:57 +01:00
commit 3f4afec3ee
35 files changed
+1230 -183

No files matched your search

+67 -25
View File
@@ -22,6 +22,7 @@
#include "foundation/compat.h"
#include "foundation/platform.h"
#include "foundation/constants.h"
#include "foundation/json_writer.h"
#include "foundation/log.h"
#include "foundation/mem_core.h"
#include "foundation/sha256.h"
@@ -4198,7 +4199,8 @@ enum { COPILOT_HOOK_TIMEOUT_SEC = 5 };
static int cbm_build_copilot_hook_command(const char *binary_path, const char *event,
bool powershell, char *out, size_t out_size) {
if (!binary_path || !event || !out ||
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0)) {
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0 &&
strcmp(event, "PostToolUse") != 0)) {
return CLI_ERR;
}
char quoted[CLI_BUF_8K];
@@ -4215,11 +4217,12 @@ static int cbm_build_copilot_hook_command(const char *binary_path, const char *e
}
static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hooks,
const char *event_key, const char *bash_command,
const char *powershell_command) {
const char *event_key, const char *matcher,
const char *bash_command, const char *powershell_command) {
yyjson_mut_val *entries = yyjson_mut_arr(doc);
yyjson_mut_val *entry = yyjson_mut_obj(doc);
if (!entries || !entry || !yyjson_mut_obj_add_str(doc, entry, "type", "command") ||
(matcher && !yyjson_mut_obj_add_str(doc, entry, "matcher", matcher)) ||
!yyjson_mut_obj_add_str(doc, entry, "bash", bash_command) ||
!yyjson_mut_obj_add_str(doc, entry, "powershell", powershell_command) ||
!yyjson_mut_obj_add_int(doc, entry, "timeoutSec", COPILOT_HOOK_TIMEOUT_SEC) ||
@@ -4230,11 +4233,25 @@ static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hook
return true;
}
static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
/* Copilot CLI's preToolUse cannot add model context (its output is only a
* permission decision) and fails closed on hook errors, so searches are
* augmented after they run: postToolUse additionalContext is appended to the
* tool result the model sees. The PascalCase event name selects the
* Claude-format payload (tool_name Bash/Grep/Glob, tool_input), and the matcher
* names the Claude and the runtime tool names so either matcher reading fires. */
#define COPILOT_SEARCH_MATCHER "Bash|Grep|Glob|bash|powershell|grep|rg|glob"
/* include_posttool renders the current manifest; without it the function
* renders the previous lifecycle-only manifest, which installs up to v0.12 wrote
* and which upgrades and uninstalls must still recognize as owned. The result is
* released with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
static char *cbm_build_copilot_hook_manifest(const char *binary_path, bool include_posttool) {
char session_bash[CLI_BUF_8K];
char session_powershell[CLI_BUF_8K];
char subagent_bash[CLI_BUF_8K];
char subagent_powershell[CLI_BUF_8K];
char post_bash[CLI_BUF_8K];
char post_powershell[CLI_BUF_8K];
if (cbm_build_copilot_hook_command(binary_path, "SessionStart", false, session_bash,
sizeof(session_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "SessionStart", true, session_powershell,
@@ -4242,7 +4259,11 @@ static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
cbm_build_copilot_hook_command(binary_path, "SubagentStart", false, subagent_bash,
sizeof(subagent_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "SubagentStart", true, subagent_powershell,
sizeof(subagent_powershell)) != CLI_OK) {
sizeof(subagent_powershell)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", false, post_bash,
sizeof(post_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", true, post_powershell,
sizeof(post_powershell)) != CLI_OK) {
return NULL;
}
@@ -4256,39 +4277,59 @@ static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
return NULL;
}
yyjson_mut_doc_set_root(doc, root);
bool ok =
yyjson_mut_obj_add_int(doc, root, "version", 1) &&
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", session_bash, session_powershell) &&
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", subagent_bash,
subagent_powershell) &&
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
char *manifest = ok ? yyjson_mut_write(doc, YYJSON_WRITE_PRETTY, NULL) : NULL;
bool ok = yyjson_mut_obj_add_int(doc, root, "version", 1) &&
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", NULL, session_bash,
session_powershell) &&
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", NULL, subagent_bash,
subagent_powershell) &&
(!include_posttool ||
cbm_copilot_add_hook_event(doc, hooks, "PostToolUse", COPILOT_SEARCH_MATCHER,
post_bash, post_powershell)) &&
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
char *manifest = ok ? yyjson_mut_write_opts(doc, YYJSON_WRITE_PRETTY,
cbm_json_writer_allocator(), NULL, NULL)
: NULL;
yyjson_mut_doc_free(doc);
return manifest;
}
#ifdef CBM_CLI_ENABLE_TEST_API
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool) {
return cbm_build_copilot_hook_manifest(binary_path, include_posttool);
}
#endif
/* Copilot loads every manifest in $COPILOT_HOME/hooks. Treat our dedicated
* filename as an exact-owned document: a foreign collision fails closed, and
* uninstall accepts only the complete generated lifecycle schema. */
* filename as an exact-owned document: a foreign collision fails closed. The
* current manifest or the exact previous one is ours, so install upgrades the
* previous one in place and uninstall removes either. */
static int cbm_upsert_copilot_hooks(const char *binary_path, const char *manifest_path) {
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
if (!manifest || !manifest_path) {
free(manifest);
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
if (!manifest || !previous || !manifest_path) {
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return CLI_ERR;
}
int rc = cbm_text_ensure_owned_document(manifest_path, manifest);
free(manifest);
const char *const released[] = {previous};
int rc = cbm_text_migrate_owned_document(manifest_path, manifest, released, 1U);
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return rc == 0 ? CLI_OK : CLI_ERR;
}
static int cbm_remove_copilot_hooks(const char *manifest_path, const char *binary_path) {
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
if (!manifest || !manifest_path) {
free(manifest);
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
if (!manifest || !previous || !manifest_path) {
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return CLI_ERR;
}
int rc = cbm_text_remove_owned_document(manifest_path, manifest);
free(manifest);
const char *const released[] = {previous};
int rc = cbm_text_remove_owned_document_any(manifest_path, manifest, released, 1U);
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return rc >= 0 ? CLI_OK : CLI_ERR;
}
@@ -9193,7 +9234,8 @@ static void install_copilot_durable_context(const char *home, const char *binary
record_agent_config_error(false, "Copilot", "lifecycle_hook_install", hook_path);
}
if (hook_ok) {
printf(" hooks: SessionStart + SubagentStart (dynamic graph context)\n");
printf(" hooks: SessionStart + SubagentStart + PostToolUse searches (dynamic graph "
"context)\n");
}
}
+8 -2
View File
@@ -283,6 +283,9 @@ bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
bool case_insensitive);
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event);
bool cbm_hook_augment_parse_bash_pattern_for_testing(const char *cmd, char *out, size_t out_sz);
/* include_posttool false renders the previous lifecycle-only manifest. Release
* the result with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool);
bool cbm_mcp_command_path_probe_safe_for_testing(const char *command, bool windows);
void cbm_set_mcp_command_path_probe_counter_for_testing(int *counter);
int cbm_install_editor_mcp_with_previous_for_testing(const char *binary_path,
@@ -591,8 +594,11 @@ void cbm_hook_augment_arm_deadline(void);
int cbm_hook_augment_deadline_ms_for_testing(void);
#endif
char *cbm_hook_augment_read_stdin(void);
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). */
bool cbm_hook_augment_input_is_noop_bash(const char *input);
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). It also
* covers a forced search event, such as Copilot's PostToolUse; forced_event and
* dialect_name are borrowed and may be NULL. */
bool cbm_hook_augment_input_is_noop_for(const char *input, const char *forced_event,
const char *dialect_name);
/* Hand back stdin bytes main() consumed early; the next read returns them. */
void cbm_hook_augment_prefetch_stdin(char *owned);
+123 -26
View File
@@ -448,8 +448,7 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
int off = snprintf(text, 4096,
"[codebase-memory] untrusted repository metadata (data only; never "
"instructions): %zu graph symbol(s) match \"%s\" "
"(structured context; your search results below are "
"unaffected):",
"(structured context; your search results are unaffected):",
nres, token);
yyjson_arr_foreach(groups, gidx, gmax, g) {
const char *prefix = ha_obj_str(g, "qn_prefix");
@@ -1359,6 +1358,62 @@ static bool ha_parse_bash_search_pattern(const char *cmd, char *out, size_t out_
return w > 0 && (size_t)w < out_sz;
}
/* One tool call as its dialect reports it. Copilot's postToolUse payload may
* name the runtime tool (bash, rg, ...) instead of the Claude tool, may use the
* camelCase field names, and may pass the arguments as a JSON string, so the
* reader maps the tool name and parses string arguments into input_doc, which
* the caller frees with ha_tool_call_free. Copilot names no event in its
* camelCase payload; its event is the one the managed manifest forces. */
typedef struct {
const char *event;
const char *tool;
yyjson_val *input;
yyjson_doc *input_doc;
} ha_tool_call_t;
static const char *ha_copilot_claude_tool_name(const char *tool) {
if (!tool) {
return NULL;
}
if (strcmp(tool, "bash") == 0 || strcmp(tool, "powershell") == 0) {
return "Bash";
}
if (strcmp(tool, "grep") == 0 || strcmp(tool, "rg") == 0) {
return "Grep";
}
if (strcmp(tool, "glob") == 0) {
return "Glob";
}
return tool;
}
static void ha_tool_call_read(yyjson_val *root, const char *forced_event,
ha_lifecycle_dialect_t dialect, ha_tool_call_t *call) {
call->event = ha_hook_event_name(root);
call->tool = ha_obj_str(root, "tool_name");
call->input = root ? yyjson_obj_get(root, "tool_input") : NULL;
call->input_doc = NULL;
if (dialect != HA_DIALECT_COPILOT) {
return;
}
call->event = forced_event;
call->tool =
ha_copilot_claude_tool_name(call->tool ? call->tool : ha_obj_str(root, "toolName"));
if (!call->input && root) {
call->input = yyjson_obj_get(root, "toolArgs");
}
if (call->input && yyjson_is_str(call->input)) {
call->input_doc = yyjson_read(yyjson_get_str(call->input), yyjson_get_len(call->input), 0);
call->input = call->input_doc ? yyjson_doc_get_root(call->input_doc) : NULL;
}
}
static void ha_tool_call_free(ha_tool_call_t *call) {
yyjson_doc_free(call->input_doc);
call->input_doc = NULL;
call->input = NULL;
}
static bool ha_tool_event_supported(ha_lifecycle_dialect_t dialect, const char *event,
const char *tool, bool *coverage) {
if (coverage) {
@@ -1367,6 +1422,13 @@ static bool ha_tool_event_supported(ha_lifecycle_dialect_t dialect, const char *
if (!event || !tool) {
return false;
}
/* Copilot's preToolUse can only decide permissions, so its searches are
* augmented after they run, through postToolUse additionalContext. */
if (dialect == HA_DIALECT_COPILOT) {
return strcmp(event, "PostToolUse") == 0 &&
(strcmp(tool, "Grep") == 0 || strcmp(tool, "Glob") == 0 ||
strcmp(tool, "Bash") == 0);
}
if (dialect == HA_DIALECT_EVENT) {
if (strcmp(event, "PreToolUse") == 0 &&
(strcmp(tool, "Grep") == 0 || strcmp(tool, "Glob") == 0 || strcmp(tool, "Bash") == 0)) {
@@ -1478,6 +1540,9 @@ static bool ha_invocation_supported(ha_lifecycle_dialect_t dialect, const char *
if (dialect == HA_DIALECT_COPILOT && !forced_event) {
return false;
}
if (dialect == HA_DIALECT_COPILOT && strcmp(forced_event, "PostToolUse") == 0) {
return true;
}
return !forced_event || ha_dialect_event_supported(dialect, forced_event);
}
@@ -1683,15 +1748,18 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
yyjson_doc_free(doc);
return lifecycle;
}
const char *event = ha_hook_event_name(root);
const char *tool = ha_obj_str(root, "tool_name");
ha_tool_call_t call;
ha_tool_call_read(root, forced_event, dialect, &call);
const char *event = call.event;
const char *tool = call.tool;
bool coverage = false;
if (!ha_tool_event_supported(dialect, event, tool, &coverage)) {
ha_tool_call_free(&call);
yyjson_doc_free(doc);
return NULL;
}
yyjson_val *tin = yyjson_obj_get(root, "tool_input");
yyjson_val *tin = call.input;
/* Post-read coverage adapters warn only when the exact path is not fully
* represented. They never block or rewrite the completed tool call. */
@@ -1702,10 +1770,12 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
if (note) {
char *output = ha_build_event_json(event, note);
free(note);
ha_tool_call_free(&call);
yyjson_doc_free(doc);
return output;
}
}
ha_tool_call_free(&call);
yyjson_doc_free(doc);
return NULL;
}
@@ -1715,6 +1785,7 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
if (strcmp(tool, "Bash") == 0) {
const char *cmd = ha_obj_str(tin, "command");
if (!ha_parse_bash_search_pattern(cmd, bash_pattern, sizeof(bash_pattern))) {
ha_tool_call_free(&call);
yyjson_doc_free(doc);
return NULL;
}
@@ -1723,7 +1794,9 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
pattern = ha_obj_str(tin, "pattern");
}
char token[HA_MAX_TOKEN + 1];
if (!ha_extract_token(pattern, token, sizeof(token))) {
bool have_token = ha_extract_token(pattern, token, sizeof(token));
ha_tool_call_free(&call);
if (!have_token) {
yyjson_doc_free(doc);
return NULL;
}
@@ -1736,7 +1809,11 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
}
char *ctx = ha_resolve_and_query(srv, cwd, token);
char *output = ctx ? ha_build_event_json(event, ctx) : NULL;
char *output = NULL;
if (ctx) {
output = dialect == HA_DIALECT_COPILOT ? ha_build_copilot_json(ctx)
: ha_build_event_json(event, ctx);
}
free(ctx);
yyjson_doc_free(doc);
return output;
@@ -1765,18 +1842,27 @@ char *cbm_hook_augment_process_for(cbm_mcp_server_t *srv, const char *input_json
return ha_process(srv, input_json, forced_event, dialect);
}
/* TRUE iff `input` is an un-forced PreToolUse Bash event whose command can
* never produce augmentation output: not a recognised search, or no queryable
* token. Pure string work — safe to run BEFORE executable-identity hashing,
* cohort admission, or any daemon contact, which is the point: agents issue
* far more Bash calls than Grep/Glob and nearly all are not searches, while
* the identity hash alone costs ~1.1 s of user CPU per invocation on a
* production binary (measured 2026-08-28). Uses the exact parser pair
* ha_process uses, so the gate and the augmenter cannot disagree about what
* counts as a search. Anything uncertain returns false and pays full fare:
* lifecycle events, other tools, coverage adapters, oversized stdin. */
bool cbm_hook_augment_input_is_noop_bash(const char *input) {
if (!input || strlen(input) > HA_STDIN_CAP) {
/* TRUE iff `input` is a search event that can never produce augmentation
* output: a Bash command that is not a recognised search or has no queryable
* token. That event is an un-forced PreToolUse Bash call, or Copilot's forced
* PostToolUse. For Copilot it is also any tool the augmenter does not read:
* VS Code's local hook parser ignores matchers, so the managed PostToolUse
* entry can fire after every tool. Pure string work — safe to run BEFORE
* executable-identity hashing, cohort admission, or any daemon contact, which
* is the point: agents issue far more Bash calls than Grep/Glob and nearly all
* are not searches, while the identity hash alone costs ~1.1 s of user CPU per
* invocation on a production binary (measured 2026-08-28). Uses the exact
* reader and parser pair ha_process uses, so the gate and the augmenter cannot
* disagree about what counts as a search. Anything uncertain returns false and
* pays full fare: lifecycle events, other tools, coverage adapters, oversized
* stdin. */
static bool ha_input_is_noop(const char *input, const char *forced_event,
ha_lifecycle_dialect_t dialect) {
bool copilot = dialect == HA_DIALECT_COPILOT;
const char *search_event = copilot ? "PostToolUse" : "PreToolUse";
if (!input || strlen(input) > HA_STDIN_CAP ||
(copilot ? !forced_event || strcmp(forced_event, search_event) != 0
: forced_event != NULL)) {
return false;
}
yyjson_doc *doc = yyjson_read(input, strlen(input), 0);
@@ -1784,21 +1870,32 @@ bool cbm_hook_augment_input_is_noop_bash(const char *input) {
return false;
}
bool noop = false;
yyjson_val *root = yyjson_doc_get_root(doc);
const char *event = ha_hook_event_name(root);
const char *tool = ha_obj_str(root, "tool_name");
if (event && tool && strcmp(event, "PreToolUse") == 0 && strcmp(tool, "Bash") == 0) {
yyjson_val *tin = yyjson_obj_get(root, "tool_input");
const char *cmd = ha_obj_str(tin, "command");
ha_tool_call_t call;
ha_tool_call_read(yyjson_doc_get_root(doc), forced_event, dialect, &call);
if (copilot && !ha_tool_event_supported(dialect, call.event, call.tool, NULL)) {
noop = true;
} else if (call.event && call.tool && strcmp(call.event, search_event) == 0 &&
strcmp(call.tool, "Bash") == 0) {
const char *cmd = ha_obj_str(call.input, "command");
char pat[HA_BASH_TOK_SZ];
char tok[HA_MAX_TOKEN + 1];
noop = !ha_parse_bash_search_pattern(cmd, pat, sizeof(pat)) ||
!ha_extract_token(pat, tok, sizeof(tok));
}
ha_tool_call_free(&call);
yyjson_doc_free(doc);
return noop;
}
bool cbm_hook_augment_input_is_noop_for(const char *input, const char *forced_event,
const char *dialect_name) {
ha_lifecycle_dialect_t dialect = HA_DIALECT_EVENT;
if (dialect_name && dialect_name[0] && !ha_dialect_from_name(dialect_name, &dialect)) {
return false;
}
return ha_input_is_noop(input, forced_event, dialect);
}
int cbm_cmd_hook_augment(int argc, char **argv) {
cbm_hook_augment_arm_deadline();
@@ -1824,7 +1921,7 @@ int cbm_cmd_hook_augment(int argc, char **argv) {
if (!input) {
return 0;
}
if (!forced_event && cbm_hook_augment_input_is_noop_bash(input)) {
if (ha_input_is_noop(input, forced_event, dialect)) {
free(input);
return 0;
}
+3 -1
View File
@@ -2581,10 +2581,12 @@ static void application_background_initialize_impl(cbm_daemon_application_sessio
cbm_mcp_auto_index_within_file_limit(root_path, auto_index_limit, &tracked_files);
if (auto_index_candidate && !within_auto_index_limit) {
char files[32];
char limit[32];
(void)snprintf(files, sizeof(files), "%d", tracked_files);
(void)snprintf(limit, sizeof(limit), "%d", auto_index_limit);
cbm_log_warn("daemon.autoindex.skipped", "project", project, "reason",
tracked_files >= 0 ? "too_many_files" : "unsafe_or_unavailable_path", "files",
files);
files, "limit", limit);
}
bool args_required = auto_index_candidate && within_auto_index_limit;
char *args = args_required ? application_auto_index_args(application, root_path) : NULL;
+54 -2
View File
@@ -346,6 +346,56 @@ static bool resolve_global_excludes_path(char *out, size_t out_sz) {
return false;
}
/* JVM source-set language roots: src/<set>/<lang>/ in Maven and Gradle layouts. */
static const char *JVM_SOURCE_LANG_DIRS[] = {"java", "kotlin", "scala", "groovy", NULL};
static bool component_equals(const char *start, size_t len, const char *word) {
return strlen(word) == len && strncmp(start, word, len) == 0;
}
/* True when rel_path names a directory strictly below a JVM source root
* ("src/main/java/com/acme/target", "app/src/test/kotlin/x/tmp"). There every
* directory is a package, so a built-in skip name such as "target", "tmp" or
* "vendor" is an ordinary package name, not build output (#2661). The root
* itself and directories beside it ("src/main/target") are not packages. */
bool cbm_discover_path_in_jvm_package(const char *rel_path) {
if (!rel_path) {
return false;
}
enum { JVM_MAX_COMPONENTS = 256 };
const char *starts[JVM_MAX_COMPONENTS];
size_t lens[JVM_MAX_COMPONENTS];
size_t count = 0;
const char *p = rel_path;
while (*p && count < JVM_MAX_COMPONENTS) {
while (*p == '/' || *p == '\\') {
p++;
}
if (!*p) {
break;
}
const char *start = p;
while (*p && *p != '/' && *p != '\\') {
p++;
}
starts[count] = start;
lens[count] = (size_t)(p - start);
count++;
}
/* The language root sits at i + 2; the directory must be below it. */
for (size_t i = 0; i + 3 < count; i++) {
if (!component_equals(starts[i], lens[i], "src")) {
continue;
}
for (int l = 0; JVM_SOURCE_LANG_DIRS[l]; l++) {
if (component_equals(starts[i + 2], lens[i + 2], JVM_SOURCE_LANG_DIRS[l])) {
return true;
}
}
}
return false;
}
/* True when rel_path equals a suffix rule or ends with "/" + the rule. */
bool cbm_discover_path_has_skip_suffix(const char *rel_path) {
if (!rel_path) {
@@ -803,8 +853,10 @@ static dir_verdict_t directory_verdict(const char *entry_name, const char *rel_p
const gitignore_link_t *ignore_chain,
const cbm_gitignore_t *global_gi,
const cbm_gitignore_t *cbmignore) {
if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) ||
cbm_discover_path_has_skip_suffix(rel_path)) {
bool builtin_skip = cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) &&
(cbm_discover_is_safety_core_directory(entry_name) ||
!cbm_discover_path_in_jvm_package(rel_path));
if (builtin_skip || cbm_discover_path_has_skip_suffix(rel_path)) {
/* #500: a .cbmignore negation (e.g. "!obj/") whose rule is the last
* match for this dir un-skips a built-in skip-list dir — except the
* non-negatable safety core. Fall through so .gitignore/global/local
+1
View File
@@ -5,5 +5,6 @@
/* Pure legacy policy predicates; callers provide live terminated strings. */
bool cbm_discover_path_has_skip_suffix(const char *relative_path);
bool cbm_discover_is_safety_core_directory(const char *name);
bool cbm_discover_path_in_jvm_package(const char *relative_path);
#endif
+3 -1
View File
@@ -102,7 +102,9 @@ static bool cif_directory_reason(cif_context *c, size_t parent, cbm_inventory_pa
if (!cif_poll(c)) {
return false;
}
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL);
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL) &&
(cbm_discover_is_safety_core_directory(name) ||
!cbm_discover_path_in_jvm_package((const char *)path.data));
bool suffix = !builtin && cbm_discover_path_has_skip_suffix((const char *)path.data);
bool safety =
(builtin || suffix) && c->owner->cbm_program && cbm_discover_is_safety_core_directory(name);
+17 -5
View File
@@ -363,17 +363,29 @@ static void copy_path_without_query(const char *path, char *out, size_t outsz) {
}
void cbm_log_mcp_request(const char *method, const char *tool_name, bool is_error,
int64_t duration_us) {
int64_t duration_us, int64_t handler_duration_us,
int64_t response_serialization_us, bool cancelled, bool timed_out) {
char duration_ms[CBM_SZ_32];
char handler_duration_ms[CBM_SZ_32];
char response_serialization_ms[CBM_SZ_32];
snprintf(duration_ms, sizeof(duration_ms), "%" PRId64, duration_us / 1000);
snprintf(handler_duration_ms, sizeof(handler_duration_ms), "%" PRId64,
handler_duration_us / 1000);
snprintf(response_serialization_ms, sizeof(response_serialization_ms), "%" PRId64,
response_serialization_us / 1000);
const char *status = timed_out ? "timeout" : (is_error ? "error" : "ok");
if (tool_name && tool_name[0] != '\0') {
cbm_log(is_error ? CBM_LOG_WARN : CBM_LOG_INFO, "mcp.request", "protocol", "jsonrpc",
"method", method ? method : "", "tool", tool_name, "status",
is_error ? "error" : "ok", "duration_ms", duration_ms, NULL);
"method", method ? method : "", "tool", tool_name, "status", status, "duration_ms",
duration_ms, "handler_duration_ms", handler_duration_ms,
"response_serialization_ms", response_serialization_ms, "cancelled",
cancelled ? "true" : "false", "timed_out", timed_out ? "true" : "false", NULL);
} else {
cbm_log(is_error ? CBM_LOG_WARN : CBM_LOG_INFO, "mcp.request", "protocol", "jsonrpc",
"method", method ? method : "", "status", is_error ? "error" : "ok", "duration_ms",
duration_ms, NULL);
"method", method ? method : "", "status", status, "duration_ms", duration_ms,
"handler_duration_ms", handler_duration_ms, "response_serialization_ms",
response_serialization_ms, "cancelled", cancelled ? "true" : "false", "timed_out",
timed_out ? "true" : "false", NULL);
}
}
+2 -1
View File
@@ -95,7 +95,8 @@ void cbm_log_int(CBMLogLevel level, const char *msg, const char *key, int64_t va
/* Operational event helpers. They deliberately avoid request bodies, headers,
* arguments, and query strings. */
void cbm_log_mcp_request(const char *method, const char *tool_name, bool is_error,
int64_t duration_us);
int64_t duration_us, int64_t handler_duration_us,
int64_t response_serialization_us, bool cancelled, bool timed_out);
void cbm_log_http_request(const char *component, const char *method, const char *path, int status,
int64_t duration_ms, size_t request_bytes, size_t response_bytes);
+5 -4
View File
@@ -2880,18 +2880,19 @@ int main(int argc, char **argv) {
cbm_hook_augment_arm_deadline();
#endif
/* Read stdin now and bail before executable-identity hashing when the
* event can never produce output (an un-forced PreToolUse Bash call
* that is not a search). The identity hash costs ~1.1 s of user CPU
* event can never produce output (an un-forced PreToolUse Bash call,
* or Copilot's forced PostToolUse after a shell call, that is not a
* search). The identity hash costs ~1.1 s of user CPU
* per invocation, and with Bash in the installed matcher nearly every
* agent command would pay it for nothing. A no-op touches no shared
* state, so it needs no identity. Anything else is handed back via
* the prefetch so downstream readers see stdin exactly once. */
if (!hook_event) {
if (!hook_event || strcmp(hook_event, "PostToolUse") == 0) {
char *hook_input = cbm_hook_augment_read_stdin();
if (!hook_input) {
return EXIT_SUCCESS; /* fail open, nothing to augment */
}
if (cbm_hook_augment_input_is_noop_bash(hook_input)) {
if (cbm_hook_augment_input_is_noop_for(hook_input, hook_event, hook_dialect)) {
free(hook_input);
return EXIT_SUCCESS;
}
+50 -25
View File
@@ -20936,6 +20936,11 @@ bool cbm_mcp_jsonrpc_response_prepend_notice(char **response_io, const char *not
return true;
}
static long long mcp_elapsed_us(struct timespec start, struct timespec end) {
return ((long long)(end.tv_sec - start.tv_sec) * MCP_S_TO_US) +
((long long)(end.tv_nsec - start.tv_nsec) / MCP_MS_TO_US);
}
char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
cbm_jsonrpc_request_t req = {0};
if (cbm_jsonrpc_parse(line, &req) < 0) {
@@ -20966,7 +20971,11 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
cbm_clock_gettime(CLOCK_MONOTONIC, &req_t0);
char *result_json = NULL;
char *request_error_json = NULL;
bool request_logged = false;
char *request_tool_name = NULL;
long long handler_duration_us = 0;
bool request_is_error = false;
bool request_cancelled = false;
bool request_timed_out = false;
if (strcmp(req.method, "initialize") == 0) {
result_json = cbm_mcp_initialize_response_for_profile(req.params_raw, srv->tool_profile);
@@ -20993,6 +21002,7 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
char *tool_name = req.params_raw ? cbm_mcp_get_tool_name(req.params_raw) : NULL;
char *tool_args =
req.params_raw ? cbm_mcp_get_arguments(req.params_raw) : heap_strdup("{}");
request_tool_name = tool_name;
srv->active_request_id = req.id;
free(srv->active_request_id_str);
srv->active_request_id_str = req.id_str ? heap_strdup(req.id_str) : NULL;
@@ -21005,16 +21015,15 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
srv->active_request_id_str = NULL;
struct timespec t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
long long dur_us = ((long long)(t1.tv_sec - t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - t0.tv_nsec) / MCP_MS_TO_US);
long long dur_us = mcp_elapsed_us(t0, t1);
bool is_err = (result_json != NULL) && (strstr(result_json, "\"isError\":true") != NULL);
cbm_diag_record_query(dur_us, is_err);
long long request_dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
cbm_log_mcp_request(req.method, tool_name, is_err, request_dur_us);
request_logged = true;
handler_duration_us = mcp_elapsed_us(req_t0, t1);
request_is_error = is_err;
request_cancelled = mcp_request_cancelled(srv);
request_timed_out =
result_json && strstr(result_json, "\"code\":\"request_timeout\"") != NULL;
free(tool_name);
free(tool_args);
} else {
/* Echo the original id (string or numeric, issue #253) on the error. */
@@ -21026,14 +21035,20 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
.id_str = req.id_str,
.error_json = err_obj,
};
char *err = cbm_jsonrpc_format_response(&err_resp);
struct timespec t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
long long handler_us = mcp_elapsed_us(req_t0, t1);
struct timespec serialize_t0;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
char *err = cbm_jsonrpc_format_response(&err_resp);
struct timespec serialize_t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
/* server/discover is the modern connect probe. -32601 stays so the
* client falls back to initialize; that miss is not an error (#2433). */
cbm_log_mcp_request(req.method, NULL, strcmp(req.method, "server/discover") != 0, dur_us);
cbm_log_mcp_request(req.method, NULL, strcmp(req.method, "server/discover") != 0,
duration_us, handler_us, serialization_us, false, false);
cbm_mcp_server_request_scope_end(srv);
cbm_jsonrpc_request_free(&req);
return err;
@@ -21045,32 +21060,42 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
.id_str = req.id_str,
.error_json = request_error_json,
};
struct timespec serialize_t0;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
char *err = cbm_jsonrpc_format_response(&err_resp);
struct timespec t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
cbm_log_mcp_request(req.method, NULL, true, dur_us);
struct timespec serialize_t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
long long handler_us = mcp_elapsed_us(req_t0, serialize_t0);
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
cbm_log_mcp_request(req.method, NULL, true, duration_us, handler_us, serialization_us,
false, false);
free(request_error_json);
cbm_mcp_server_request_scope_end(srv);
cbm_jsonrpc_request_free(&req);
return err;
}
if (!request_logged) {
struct timespec t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
cbm_log_mcp_request(req.method, NULL, false, dur_us);
}
cbm_jsonrpc_response_t resp = {
.id = req.id,
.id_str = req.id_str,
.result_json = result_json,
};
struct timespec serialize_t0;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
char *out = cbm_jsonrpc_format_response(&resp);
struct timespec serialize_t1;
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
request_is_error = request_is_error || out == NULL;
if (!request_tool_name) {
handler_duration_us = duration_us - serialization_us;
}
cbm_log_mcp_request(req.method, request_tool_name, request_is_error, duration_us,
handler_duration_us, serialization_us, request_cancelled,
request_timed_out);
free(request_tool_name);
free(result_json);
cbm_mcp_server_request_scope_end(srv);
cbm_jsonrpc_request_free(&req);
+46 -34
View File
@@ -2037,6 +2037,49 @@ static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *c
return NULL;
}
static bool explicit_relative_path(const char *p) {
return strncmp(p, "./", PAIR_LEN) == 0 || strncmp(p, "../", PAIR_LEN + SKIP_ONE) == 0;
}
enum { SIBLING_MAX_CANDS = 4 };
/* The paths resolve_sibling_file tries for module_path, relative to the
* importer's directory `dir`, in priority order. Returns how many it wrote. */
static int sibling_file_candidates(const char *dir, const char *module_path,
char cands[SIBLING_MAX_CANDS][PKGMAP_PATH_BUF]) {
int ncand = 0;
const char *sep = dir[0] ? "/" : "";
const char *base = module_path;
/* Skip a leading "./". */
if (strncmp(base, "./", PAIR_LEN) == 0) {
base += PAIR_LEN;
}
const char *slash = strrchr(base, '/');
/* 1. Direct sibling: dir/<module_path>. */
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, sep, base);
/* 2. SCSS partial: dir/[subdir/]_<basename>.scss (underscore-prefixed). */
{
const char *bn = slash ? slash + SKIP_ONE : base;
char *dpart = slash ? cbm_strndup(base, (size_t)(slash - base)) : strdup("");
if (dpart && bn[0] != '_') {
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s%s_%s.scss", dir, sep, dpart,
dpart[0] ? "/" : "", bn);
}
free(dpart);
}
/* 3. Meson subdir: dir/<module_path>/meson.build. */
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s/meson.build", dir, sep, base);
/* 4. Basename sibling: dir/<basename(module_path)>. Covers include paths
* that carry a non-relative prefix (Hyprlang `source = ~/.config/.../x.conf`,
* absolute include paths) but reference a file sitting beside the importer.
* An explicit `./` or `../` path names one location, which Strategy 1c
* resolves exactly; its basename beside the importer is another file (#2797). */
if (slash && slash[SKIP_ONE] && !explicit_relative_path(module_path)) {
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, sep, slash + SKIP_ONE);
}
return ncand;
}
/* Resolve a sibling-file import: a bare path/name (no leading "./") that names
* a file relative to the importer's directory. This covers build/markup
* grammars whose import string is a sibling filename or directory rather than a
@@ -2063,38 +2106,8 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
return NULL;
}
/* Candidate relative paths, in priority order. */
char cands[5][PKGMAP_PATH_BUF];
int ncand = 0;
const char *base = module_path;
/* Skip a leading "./". */
if (base[0] == '.' && base[1] == '/') {
base += 2;
}
/* 1. Direct sibling: dir/<module_path>. */
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, dir[0] ? "/" : "", base);
/* 2. SCSS partial: dir/[subdir/]_<basename>.scss (underscore-prefixed). */
{
const char *slash = strrchr(base, '/');
const char *bn = slash ? slash + 1 : base;
char *dpart = slash ? cbm_strndup(base, (size_t)(slash - base)) : strdup("");
if (dpart && bn[0] != '_') {
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s%s_%s.scss", dir, dir[0] ? "/" : "",
dpart[0] ? dpart : "", dpart[0] ? "/" : "", bn);
}
free(dpart);
}
/* 3. Meson subdir: dir/<module_path>/meson.build. */
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s/meson.build", dir, dir[0] ? "/" : "", base);
/* 4. Basename sibling: dir/<basename(module_path)>. Covers include paths
* that carry a non-relative prefix (Hyprlang `source = ~/.config/.../x.conf`,
* absolute include paths) but reference a file sitting beside the importer. */
{
const char *slash = strrchr(base, '/');
if (slash && slash[1]) {
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, dir[0] ? "/" : "", slash + 1);
}
}
char cands[SIBLING_MAX_CANDS][PKGMAP_PATH_BUF];
int ncand = sibling_file_candidates(dir, module_path, cands);
const cbm_gbuf_node_t *found = NULL;
for (int i = 0; i < ncand; i++) {
@@ -2123,8 +2136,7 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
static const cbm_gbuf_node_t *resolve_relative_file(const cbm_pipeline_ctx_t *ctx,
const char *source_rel, const char *module_path,
bool php_only) {
if (!source_rel || !module_path || module_path[0] != '.' ||
!(module_path[1] == '/' || (module_path[1] == '.' && module_path[2] == '/'))) {
if (!source_rel || !module_path || !explicit_relative_path(module_path)) {
return NULL;
}
char dir[PKGMAP_PATH_BUF];