Repository navigation
chore: sync fork with upstream main - #1
Merged
Merged
Conversation
…eusData#963) scripts/setup-windows.ps1 has 326 lines and its parse-coverage report read "113-113,113-113,245-327" — the same line named twice, and an end line that does not exist. Two separate faults, both in cbm_error_regions_push. Past-EOF end line. A tree-sitter node that ends at column 0 stopped right after the previous line's newline, so it holds no text on the row it points at. Adding 1 to that row named a line past the end of the file whenever the region ran to EOF. The node here is start=(244,2) end=(326,0). Clamp the end to the row above when the end column is 0 and the node spans more than one row. Duplicate range. Line 113 carries two separate ERROR nodes, at columns 25-29 and 31-32, and each pushed its own range. A line range says nothing new the second time. Drop a range that exactly repeats the one already open. The drop runs BEFORE the cap check, so a repeat is never miscounted as a range the cap threw away. Only an EXACT repeat is dropped, never a range that merely overlaps. Each range is judged separately afterwards by cbm_region_is_recovered, which asks whether definitions starting inside that range cover it. Two ranges holding the same numbers always get the same verdict, so dropping the repeat changes nothing. Two different ranges do not. Merging 3-3 into 2-3 hands the wider range's covering definition to an error that definition does not explain, and a real parse failure then vanishes from the report. That is not hypothetical. An earlier version of this commit merged on overlap and broke perl_malformed_source_remains_partial_issue1838, the test added with the Perl grammar refresh in 17b5a43. The malformed fixture produces two ERROR nodes, at lines 2-3 and 3-3. Merged, the 2-3 range looks fully covered by before_error and is removed, so parse_incomplete comes back false on a file that plainly does not parse. That test now pins this boundary. The real file reports "113-113,245-326". Two tests, both proved RED first with the exact expected text: coverage_repeated_error_line_reports_one_range_issue963 "2-2,2-2" != "2-2" coverage_range_never_ends_past_the_last_line_issue963 "1-5" != "1-4" Suites run on this change: parse_coverage 34, extraction 325, pipeline 264, mcp 246, index_resilience 7 — all passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…ge-duplicate-and-eof fix(coverage): stop reporting a duplicate range and a line past EOF (DeusData#963)
…n-variable-count fix(cypher): refuse a query naming more variables than a binding holds
…rt-null-guard fix(pipeline): skip the function sort when the graph has no functions
…chdog fix(main): exit Windows stdio servers when the spawning client dies (DeusData#914)
…ous-recovery fix: recover markerless stale rendezvous sockets during activation
…shallowest try_load_nested_gitignore() returned NULL as soon as a walk frame already carried a local matcher, and that matcher was inherited by every child frame. So only the SHALLOWEST nested .gitignore on a path was ever loaded; any deeper one was silently dropped — an EMPTY storage/.gitignore was enough to disable storage/dump/.gitignore's "*". On the standard Laravel layout (storage/.gitignore above storage/<dir>/.gitignore) that meant thousands of git-ignored cache/debugbar dumps were discovered and indexed, which on the reporter's app grew to 25,638 discovered files against 6,682 git-visible ones and ended in an OOM kill (DeusData#1973, discovery half; also the §2 root cause of DeusData#530). Discovery now keeps a root->leaf chain of .gitignore links per directory frame: every directory below the root loads its own .gitignore, a frame without one shares its parent's link, and a path is judged by walking the chain leaf->root with git's precedence — the deepest file with an opinion wins (so a deeper negation re-includes what an ancestor ignores), and inside one file the last matching pattern wins. The repository root's .gitignore (merged with info/exclude) is the chain head, so the same precedence holds between root and nested files. Cost is one match per .gitignore on the path — O(depth) per entry, no rescans; the walk frame shrinks by the 4 KB local-prefix buffer it no longer needs. Tests pin the reporter's layout (empty ancestor + deeper "*"), a deeper negation overriding an ancestor pattern, and three nesting levels with an empty middle file. Measured on a Laravel app (koel) seeded with typical compiled-view/cache artifacts: 2,885 discovered files with 450 git-ignored -> 2,435 with 0; clean clones of linux, dotnet/runtime, koel and redis discover byte-identical file sets before and after. Fixes the discovery half of DeusData#1973. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Hermes `install` failed with `op=pre_llm_hook_install` whenever config.yaml held a PLAIN scalar with a `>` or `|` anywhere after the value start — `probe: kaomoji face >w< here` — while the identical string double-quoted, or a mid-value `*`, installed fine. An agent-config failure aborts activation, so the whole install exited 1 and user-PATH registration was skipped although eleven other agents had configured cleanly. Bisected by @ukind, key by key and persona by persona, down to two persona strings (DeusData#1924, continuing DeusData#1631/DeusData#1656). Root cause: yaml_sequence_line_has_unsupported — the document validator on the mapping-sequence path that the hook upsert/remove uses — ran a second scan after the shared yaml_range_has_unsupported_ex and returned "unsupported" for `[`, `]`, `|` and `>` at ANY position of a plain scalar. The mapping-body path (mcp_servers) never had that check, which is exactly why the mcp op succeeded and only the hook op failed on the same file. Fix: the positional rule the `*`/`&` fix (v0.10.4) and the quote fix (DeusData#1631, v0.10.6) already use — an indicator counts only where a NODE begins (range start, after a mapping colon, after a block-sequence dash). Interior ones are text. `probe: >`, `probe: |`, `probe: >-` and `probe: [a, b]` still begin a value and stay unsupported, byte-identically. `[`/`]` get the same treatment on purpose: in block context they are ordinary characters inside a plain scalar (`a: b[0]` parses as the string `b[0]`), a value-start `[` still opens a flow sequence and stays refused (the DeusData#1631 line: exact `[]` accepted, non-empty flow sequences unsupported), and the mapping-body path already accepted them mid-value — leaving the sequence scan stricter would keep the mcp-op-passes / hook-op-fails asymmetry this issue is about. Known conservative residue, shared with the `*` rule: a plain-scalar CONTINUATION line that itself starts with one of these characters is still refused (nothing precedes it on its line). Tests (tests/test_config_yaml_edit.c): three accept cases run both real Hermes ops (mcp_servers + pre_llm_call) against a persona corpus with a mid-value `>`, `|` and `[]` and require the original document to survive as an untouched prefix; two pins keep value-start block scalars and a non-empty flow sequence refused with the file byte-identical. RED on the unfixed editor: 61 passed, 3 failed (`yaml_hermes_hook_upsert(&fixture) == -1`); GREEN: 64 passed; RED again with the production change reverted. config_yaml_edit 64 · yaml 73 · cli 306 — 443 passed, 0 failed (macOS, ASan/UBSan test build). Not in this change: the refusal still reaches the user as `op=pre_llm_hook_install path=... (target: regular file, N bytes)` with no reason. The editor collapses every one of its ~236 YAML_ERROR sites into CBM_YAML_IDENTITY_EDIT_ERROR, so naming the refusing rule needs a detailed variant with a typed failure out-parameter, the shape the Codex TOML hook path already has (cbm_toml_codex_hook_failure_t + record_agent_config_error_with_reason). Follow-up. Fixes DeusData#1924. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ignature After every publish the pipeline re-exports <root>/.codebase-memory/ (graph.db.zst + artifact.json) whenever an artifact already lives there: once a session indexed with persistence, or when the team committed it and every linked worktree checks it out. The watcher's dirty signature folded those files' (size, mtime) in, so each successful reindex rewrote them, the next poll saw a new dirty state, and the daemon re-triggered itself forever: index.supervisor.reap outcome=clean immediately followed by watcher.changed strategy=git, 100+ times in 15 minutes with index workers pinned (DeusData#1953). Nothing under .git moved and no source changed, which is why the reporters blamed their long hyphenated worktree branch names (the DeusData#1254 theory its own reporter retracted); the regression test builds that exact worktree and shows the branch is irrelevant. Scope git status with the same :(exclude).codebase-memory pathspec the exporter's own clean-tree probe already uses for the same reason. Nothing under that directory is ever an index input (discovery skips it), so a change there can never require a reindex; the pathspec is CWD-relative, so a monorepo sub-package watch excludes its own artifact directory. Untracked and committed artifacts both looped before; a real edit and a real HEAD move are still detected. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…terministic-order fix(pipeline): make the complexity pass independent of worker id order
…agents-cleanup fix(codex): replace global guidance with an activation pointer
…i-attribute-decorates fix(extract): scan every attribute_list sibling, not just the first (DeusData#1692)
The outer run rewrites the pipeline's db_path to its own stage, so the inner publication (gbuf dump and the delta clone) minted ITS stage from that stage: <db>.stage.A.stage.B, with -wal/-shm beside it under WAL. Every such copy is a full-size generation, so one project could occupy three times its database on disk (DeusData#1839). Mint from the stage root instead. The root is recognised only by the exact minted shape "<name>.stage.<6 alphanumerics>" in the basename, so a database whose own name merely contains ".stage." keeps its full name, and the marker is never matched across a path separator. The inner finalize still renames B over A; the unpredictable-name contract of the mkstemp helper is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…t-overshooting The extraction memory gate was advisory: when a full back-pressure cycle ended still over budget it latched "futile" and kept admitting files, so a host that could not hold the graph ran on until the kernel killed the worker (DeusData#1997, DeusData#832) and the supervisor then quarantined innocent files. Now the worker that latches futility runs ONE confirmation cycle (collect + bounded naps). If resident memory drained, the gate re-arms as before. If it is still over budget, the attempt fails whole: every worker stops pulling, extract returns the new CBM_PIPELINE_ABORT_OVER_BUDGET, the orchestrator discards the staging DB on that non-zero code like any other, and the previously serving generation is left byte-identical. The verdict is a plain return value so it travels as a clean-exit worker response, never a kill. The abort flag is its own atomic, not the shared cancel token, which belongs to client cancellation. The abort/persist codes move to pipeline.h alongside the new one (callers legitimately distinguish them); FORCE_FULL_REINDEX stays internal. Tests: the futility test now asserts the new code (the confirmation adds one cycle to its bound), and a new test proves fail-whole preserves the live DB byte-for-byte, keeps its node count, and leaves no stage residue. Refs DeusData#1997 DeusData#832 DeusData#1973 DeusData#1864 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A full /tmp made every daemon start die at pending publication, and the only durable trace was `daemon.ipc.listen_failed stage=pending_publication` -- no syscall, no errno, no path. The reporter needed hours (and a `df` on the wrong mount) to find the cause (DeusData#1828). Every listener failure now records the stage, the errno the failing step reported, and the artifact path it was operating on, and the log line carries all three: daemon.ipc.listen_failed stage=pending_publication errno=ENOSPC path=/tmp/cbm-daemon-1000/cbm-<key>.pending.tmp The record publication helper notes which step refused (create, write, link, unlink, sync, close, read-back) and on which artifact, so the listener can name it. The bind failure keeps its errno but joins the same shape (stage=socket_bind errno=... path=...) instead of a bare numeric bind_errno. cbm_errno_name() maps the portable errno set to its symbol. cbm_daemon_ipc_listen_failure_detail() exposes the same triple to the daemon host for the fail-fast record that follows. Test: daemon_ipc_listen_failure_names_errno_and_path forces the record write to fail with ENOSPC through a narrow inert seam (the socket binds, the file is created, only its data write is refused -- the reporter's exact shape) and asserts the log line names the errno and the temp path, and that the namespace is left clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A single agent configuration the editors refuse to touch (the reporter's ~/.cursor/mcp.json was a symlink into a dotfiles checkout) aborted the uninstall activation BEFORE index and binary removal. `uninstall -y` exited 1 and left a 300 MB executable plus the whole cache behind — the data-loss half of DeusData#1954 — while the transcript still read "removed". Agent-config failures are now collected instead of gating: every cleanup runs, the indexes and the executable go, and the command ends with one list of the files that could not be cleaned (agent, operation, path, observed target) and a non-zero exit. The existing per-error stderr line is unchanged. Two tests encoded the old hostage contract ("binary must survive failed cleanup") and move to the new one. The new test drives `uninstall -y` against a sandbox HOME with a dangling ~/.cursor/mcp.json link and asserts binary + index gone, exit non-zero, and the file named on stderr; on main it fails at ASSERT(binary_gone). (DeusData#1954) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A stage was recognisable only by its name: the mkstemp descriptor was closed at once and nothing marked who was writing it. A worker killed mid-run -- the daemon cancels with SIGTERM and SIGKILLs after one second of grace, which a gigabyte backup or delta clone never finishes inside -- left its full-size stage behind for good, and no later run could tell a dead stage from a live one, so none tried (DeusData#1839). Ownership is now an exclusive kernel lock on the sidecar "<stage>.lock", taken when the stage is minted and dropped by the same helpers that discard the stage or rename it into place. The kernel releases it on any death, so "can I take this lock?" is exactly "is this stage dead?" -- no pid, no mtime, no grace period. The lock lives on a sidecar rather than the stage file itself because on macOS an flock conflicts with SQLite's fcntl byte locks on the same file (verified: flock(LOCK_EX) then fcntl(F_WRLCK) on another descriptor fails with EAGAIN, both directions). The primitive is cbm_lockfile_open: POSIX open(O_CLOEXEC|O_NOFOLLOW) plus flock(LOCK_EX|LOCK_NB); Windows _wsopen with _SH_DENYRW and _O_NOINHERIT. Neither is inherited by spawned children. A backup that fails keeps the outer stage NAME owned: the rebuilt generation is renamed over it by the inner finalize and published from it, so its lock is held to the end. The sweep that uses this ownership follows separately. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
index_repository rendered every non-zero pipeline code as "Pipeline failed. Check repo_path exists and contains source files", which sent people debugging a path that was fine (DeusData#2020). Three outcomes now say what actually happened: - CBM_PIPELINE_ABORT_OVER_BUDGET (DeusData#1997, DeusData#832): status "error" with reason "over_memory_budget", previous_index "preserved", the budget and peak RSS in MB, and a hint naming CBM_MEM_BUDGET_MB / CBM_WORKERS / excludes. The handler logs index.abort reason=over_memory_budget. The CLI prints the same JSON and exits 1; a supervised worker still exits 0 with the response written, so the daemon passes it through verbatim instead of entering skip-and-continue recovery. - CBM_PIPELINE_ABORT_PRESERVE_DB: status "aborted_previous_preserved" (retry; the previous index still serves). - CBM_PIPELINE_PERSIST_FAILED: status "persist_failed" (disk/permissions). The incremental test helper goes fail-closed on error responses so a transient pre-publication abort surfaces once at the source instead of cascading into dozens of downstream count failures. Test: an in-process index over 64 files at a 1 MiB budget seam returns the named error, leaves the project DB the same size on disk, and index_status still reports the previous node count. Refs DeusData#2020 DeusData#1997 DeusData#832 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… run Nothing on the worker-death path ever cleaned a stage up: the supervisor reaps only an exit status, and a cancel is SIGTERM then SIGKILL after one second, which a gigabyte backup or clone never finishes inside. Every daemon-side cancel or OOM kill therefore left a full-size <db>.stage.X (plus -wal/-shm) behind for good, and with auto_index a large project filled the disk with copies of itself (DeusData#1839). Each run now sweeps before minting its own stage. Only names of the exact minted shape for THIS database are considered -- "<basename>.stage.<6 alphanumerics>" and that stage's -wal/-shm/-journal/.lock sidecars -- never the live database, a quarantined .corrupt, or another project's files. Liveness is the ownership lock and nothing else: a stage whose sidecar lock can be taken has no writer and is removed (logged as pipeline.stage action=orphan_removed bytes=N); one whose lock a live writer holds is kept (orphan_kept reason=live_writer). No pid, no mtime, no grace period, so the sweep is deterministic and never races a writer. A stage minted by an older binary carries no lock and is swept too; that writer's final rename then fails and it discards its stage. The live database is never named on either path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…e job limit The daemon handed every index worker aggregate/physical_job_limit (a fixed quarter by default) regardless of how many jobs were running, so a lone worker on a host like DeusData#1864 was capped at a quarter of the memory the daemon itself was allowed — and with the budget now failing whole (decision A) that fixed slice would have turned an idle host into a guaranteed over_memory_budget. Decision 2 (DeusData#1997, DeusData#832): the slice is decided at spawn time as aggregate / jobs active right then (the job being spawned included), so a lone worker may use the whole aggregate and concurrent jobs split it. The explicit CBM_MEM_BUDGET_MB override keeps its precedence inside the worker (bf46b91); the sub-byte-per-slot guard is unchanged. The spawn line logs project, active_jobs, aggregate_mb and slice_mb so a fail-whole verdict can be read against the budget the worker really had. Tests: sequenced admission proves first=aggregate, second=aggregate/2; the racing 4-way admission test accepts any aggregate/1..4 slice (the divisor is scheduling-dependent there); a scripted CLEAN worker returning the over_memory_budget error response is passed through verbatim on the first attempt — no recovery loop, no quarantine. Refs DeusData#1997 DeusData#832 DeusData#1864 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A daemon that died at listener publication (a full /tmp) took the lifetime reservation, failed, and released it; the waiting client saw the generation vanish, respawned it, and repeated that for its whole 30 s deadline before reporting "CBM daemon is active or starting but could not accept this client" -- the opposite of what happened, with no cause and no path. MCP hosts then dropped the server silently (DeusData#1828). A detached daemon has no stderr and no channel back to its launcher, so the host now appends one start-failure record per failed start to <cache>/logs/cbm-daemon.start-failures.log -- next to cbm-daemon.log, through the same owner-only private-log open. Deliberately NOT the runtime directory: that is exactly what a full /tmp cannot write, and a new artifact there would entangle the stale-generation cleanup that DeusData#1894 just hardened. The record carries the listener detail from the previous commit (component, stage, errno, path) plus the runtime directory and a wall-clock stamp, and is written while the lifetime reservation is still held, so a client that watches the generation vanish already finds the cause. The client bootstrap loop reads that record (every 50 ms after its own spawn, and unconditionally before any relaunch), accepts only records for its endpoint not older than its spawn instant (2 s skew), and stops at once: error: CBM daemon failed to start: pending_publication failed with ENOSPC (No space left on device) at /tmp/cbm-daemon-1000/cbm-<key>.sock.pending.tmp; see ~/.cache/codebase-memory-mcp/logs/cbm-daemon.log The read is an optional bootstrap op (unit contracts with fake ops are untouched); the production op resolves the cache log directory. Every other wait state -- a live or stopping generation, a mute holder, a concurrent starter -- keeps its existing semantics and diagnostics. Tests (daemon_bootstrap): - daemon_bootstrap_fails_fast_when_daemon_dies_at_publication: the production bootstrap (real cohort, probe, startup lock, handoff) with a seam-only spawn override that forks a REAL daemon host whose record writes fail with ENOSPC. RED before: 34.7 s and "active or starting"; GREEN after: FAILED in well under 5 s naming ENOSPC and the temp path, one child spawned, daemon log carries errno=ENOSPC path=... . - daemon_bootstrap_start_failure_record_round_trip: record/read/format contract, a stale record and another endpoint's record are ignored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…sting_db The route probe opened whatever resolve_db_path returned, which inside the staged run is the run's own stage. On a first index nothing was copied into it, so the probe opened a 0-byte mkstemp placeholder, failed its integrity check, and warned "pipeline.route path=full reason=invalid_existing_db" -- on every first index of every project. Read back after an OOM kill it looked like a corrupted database and a daily "corrupt -> full rebuild -> OOM" loop (DeusData#1864); the loop is "no database -> full -> OOM", and the stale 0-byte stage never influenced the next run at all (each run mints a fresh name). cbm_pipeline_run now records whether the destination existed and was copied into the stage, and the route decision returns full early with reason=no_existing_db (or existing_db_backup_failed, after the backup_failed_full_rebuild warning) without probing a placeholder. "invalid_existing_db" stays reserved for a real copy that fails its integrity check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Every config editor (JSON-like, TOML, YAML, text) opens with O_NOFOLLOW and refuses symlinks, so a link planted at ~/.cursor/mcp.json cannot point a privileged writer at another file. Users keep exactly those files as symlinks into a dotfiles checkout; the blanket refusal made install and uninstall fail on them and, before the previous commit, turned uninstall into data loss (DeusData#1954). Decision C: a symlink the INVOKING user owns, resolving to a regular file the same user owns, is followed — as an explicit opt-in of the agent-configuration writers only. cbm_install_agent_configs and the uninstall activation register the user's configuration roots (HOME, XDG_CONFIG_HOME) for the duration of their work; a link whose own directory is outside every registered root, or any link seen by another editor caller, stays refused, so repository paths never follow. One shared helper (src/cli/config_edit_path.c) runs the race-safe sequence: lstat(link) → owner == invoking uid (a root process follows root-owned links only) → realpath → the target's parent directory opened as a dirfd (O_DIRECTORY|O_NOFOLLOW; owned by the invoking user or root, not writable by others unless sticky) → openat(dirfd, base, O_NOFOLLOW) → fstat: regular file, same owner, link count 1, and the same (dev, ino) as stat(realpath). Each editor reads through that validated descriptor, stages its temp file with openat(dirfd, …, O_CREAT|O_EXCL|O_NOFOLLOW), runs its pre-publish snapshot comparison against the resolved path, and commits with renameat on the pinned dirfd, so the link itself is never replaced. Every other link is refused with an observable reason (surfaced in the uninstall diagnostics): another owner, root over a non-root link, dangling, a directory or special target, a hard-linked or swapped target, a foreign or world-writable parent. Whether the target must also stay under a root is a pending user decision behind CBM_CONFIG_EDIT_TARGET_UNDER_ROOT (default: anywhere the user owns). Windows never follows reparse points. Tests: each editor suite gains a same-owner link edited in place under the opt-in (link preserved and still pointing at the same target, temp file staged beside the target, bytes identical to the same edit on a plain file, no stray temp files) and keeps its refusal cases; the JSON-like suite adds no-opt-in, outside-root, dangling, directory, identity-skew and world-writable-parent refusals. The foreign-owner and root-run refusals move the observer through a test seam (a test cannot create a foreign-owned link without root; a real root run demotes the link first). test_cli.c drives `uninstall -y` end to end through a symlinked ~/.cursor/mcp.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Review of the previous head found the global rule unsound for one caller: cbm_mkdir_p also creates <repo_path>/.codebase-memory inside the checkout being indexed, and git creates symlinks owned by whoever cloned, so a repository shipping `.codebase-memory -> ~/.ssh` would have been followed by the artifact writer. "User-owned" says nothing about "user-intended" once the path root is a checkout. cbm_mkdir_p keeps the strict walk (root-owned links only). The new cbm_mkdir_p_ex(path, mode, policy) with CBM_MKDIR_FOLLOW_OWNED also follows a symlink owned by the invoking user, and only callers whose paths are rooted in the user's own configuration opt in: the installer's agent roots, skills, instruction files and hook directories, the JSON-like config editor's parent creation, the cache root, <cache>/logs, the UI config directory and the DB parent. The artifact directory inside the checkout and the ~/.local/bin creations stay strict; the activation transaction re-walks the latter with its own no-follow policy anyway. What a follow lands on is now checked too: the opened target must be a directory owned by root or the invoking user and not world-writable unless sticky, so a link swapped between the fstatat and the openat cannot steer the walk into a directory an attacker controls or one where anyone can pre-plant entries. This applies to root-owned links as well; macOS /private/tmp is root-owned and sticky and still passes. Refusal restores the first openat's errno. Tests: the plain walk refuses an own link that the opted-in walk follows; targets that are world-writable without the sticky bit are refused; as root, a foreign-owned target and a user-owned link are both refused, each followed by restoring ownership and walking again; the identical `.codebase-memory -> <user-owned dir>` link is refused by the plain walk and followed once opted in; cbm_artifact_export refuses a checked-in artifact-dir link and leaves its target empty; the installer test gains the euid-0 leg with home, link and target owned by a non-root uid. Under euid 0 the tests demote their links to a non-root owner, since root-owned links are trusted by both walks. Refs DeusData#1722 Signed-off-by: TheStranjer <791672+TheStranjer@users.noreply.github.com>
…ollowed Review of the previous head reproduced a check-then-use window in the parent-chain walk: the link was judged by name (fstatat) and then opened by name (openat), with nothing tying the two to the same entry. An LD_PRELOAD shim that swaps the link for one pointing elsewhere the moment the judgement returns made both earlier heads follow the swapped link, including one owned by a foreign uid under root; the post-follow target check only bounds where such a swap can land. The link is now never opened by name after it has been judged. On Linux an O_PATH|O_NOFOLLOW descriptor pins the inode, fstat on that descriptor makes the ownership decision and readlinkat(fd, "") reads the target text from the same inode; on other systems the link is stat'ed by name before and after the readlinkat and both must be the same inode with the same owner. The walk then opens the text from the parent directory, so a relative text resolves against the link's own directory exactly as the kernel resolves it, and the target check runs on what that opens. The readlinkat form is used rather than /proc/self/fd so the walk does not depend on /proc being mounted. What is still done by path is resolving the text itself, which the target check bounds; the comment says so. Test: relative, ../, absolute and chained link texts resolve from the link's directory with the process CWD moved elsewhere. The race itself is exercised out of tree with the harness from the review thread. Refs DeusData#1722 Signed-off-by: TheStranjer <791672+TheStranjer@users.noreply.github.com>
…alar-indicators fix(yaml): scope > | [ ] indicators to the value start so Hermes configs install (DeusData#1924)
…mlinked-config # Conflicts: # tests/test_cli.c
…elf-trigger fix(watcher): exclude the indexer's own artifact dir from the git dirty signature (DeusData#1953)
…ore-depth fix(discover): stack every nested .gitignore on a path, not just the shallowest (DeusData#1973)
…me-sanitizer-timeout-only fix(tests): scale daemon runtime sanitizer timeout
…-return-types fix(extract): a C-family function's return type keeps its pointer and qualifiers
…reate-race fix(daemon): losing the runtime-directory creation race is not a failure on Windows
…attern-boundaries fix(service-patterns): match library ids on identifier boundaries
Sync pkg/scoop/codebase-memory-mcp.json with the Scoop Main bucket manifest (v0.11.0): add arm64, notes, and matching hashes/URLs. Signed-off-by: Ivan Lin <35771002+ivanlinhf@users.noreply.github.com>
…rap-sync-spawn fix(test): the ENOSPC bootstrap test no longer lets a deadline decide its verdict
…ree-tests test(hook-augment): pin worktree payload-cwd resolution
…e-isolation fix(test): isolate smoke harness daemon runtime
Fix/smoke tool inventory
feat(pipeline): link markdown file references into the graph
…covery-fix fix(windows): preserve worker recovery errors
Resolve the RUN_TEST list conflict in tests/test_daemon_application.c by keeping both sides' new tests. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Ivan Lin <35771002+ivanlinhf@users.noreply.github.com>
Signed-off-by: Mike Parsons <212198286+TheDarkniteFalls@users.noreply.github.com>
`codebase-memory-mcp cli <tool> --flag value` builds the tool's JSON arguments from typed flags. Array-typed parameters such as `semantic_query` used to accept only repeated flags; the JSON spelling shown in the MCP schema (`--semantic-query '["a","b"]'`) was stored as a single opaque string element (DeusData#2150). `cli_add_typed` now expands such a literal; this pins the edge cases of that contract so it cannot regress: - a well-formed array of strings contributes its elements - an explicit empty array `[]` contributes nothing (not the keyword "[]") - a malformed literal stays one literal string, without an error - non-string items are copied as typed values, not stringified - literals and repeated flags mix, accumulating in argument order - repeated literals accumulate Signed-off-by: Hubert Tarnacki <hubert.tarnacki@gmail.com>
fix(routes): accept relative JAX-RS @path templates and compose Scala class-level @path (DeusData#2147)
…zed-build-config test: verify sanitizer build configuration
…anch-head-regression test(pipeline): cover changed-file and empty-commit Branch HEAD refresh
test(cli): pin JSON array literal handling for array-typed arguments (DeusData#2150)
…dows-unicode-git-root fix(windows): support Unicode repository paths
…0.11.0 chore(scoop): bump packaged manifest to 0.11.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fast-forward fork main onto upstream/main (722 commits, no fork-only divergence).