Skip to content

Add one-command Codex and pi integration with MCP search and startup hooks - #160

Merged
Shengyu Fu (shengyfu) merged 5 commits into
microsoft:mainfrom
fenixc9:feat/agent-integration-installer
Sep 18, 2026
Merged

Shengyu Fu (shengyfu) merged 5 commits into
microsoft:mainfrom
fenixc9:feat/agent-integration-installer

Conversation

@fenixc9

@fenixc9 MR.GOOD (fenixc9) commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Agents currently need manual setup to discover tgrep, keep its server running, and choose it for repository searches. This adds a checkout-local installer that configures MCP search tools, startup prewarming, and usage guidance for Codex and pi in one command.

Changes

  • Add install-agent.sh with interactive selection and project/user scoped install, doctor, repair, and uninstall commands. Preserve existing configuration, track owned sections/files, back up changes, and roll back failed writes. repair --root rebases owned files after a project is moved or copied, and doctor reports integrations that still point at an old root.
  • Provide a Python standard-library stdio MCP adapter exposing search_code and find_files. Reuse the existing CLI, bound output and execution time, reject paths outside the repository, and terminate query subprocesses on cancellation/shutdown. freshness=current uses --no-index to verify recent edits.
  • Return structured repository-relative results in every output mode, including per-file counts parsed from JSON records instead of path:count text.
  • Configure Codex MCP and session-start hooks, preferring inline TOML hooks when both representations exist; provide a pi extension that bridges the same MCP tools, prewarms on session start, and reconnects if the adapter exits. Share services by canonical repository path and index settings, with automatic index construction and startup locking.
  • Reject symlinked components in owned installation paths, and check manifest targets against an exact per-agent destination allowlist before reading or removing them.
  • Document setup and limitations, and run the integration tests in Linux/macOS CI.

Scope and behavior

This version supports Linux/macOS and Python 3.11+. It encourages use of tgrep without overriding built-in tools or rewriting shell commands. Codex hooks still require the host's normal trust review. The integration uses a separate cache rather than adopting manually managed indexes whose settings cannot be verified. Uninstall retains indexes, shared services, binaries, and recovery backups.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --workspace
  • python3 -B -m unittest discover -s scripts/agent -p 'test_*.py' -v — 31 tests covering configuration preservation, rollback, install/repair/uninstall, moved checkouts, symlinked destinations, service startup/recovery, freshness, path boundaries, cancellation, structured output, and real MCP calls through the pi bridge.
  • bash -n install-agent.sh and git diff --cached --check

Local verification was performed on Linux. The pi bridge test exercises the extension contract without a model/API key; it is not an end-to-end interactive agent session test.

Copilot AI balanced review requested due to automatic review settings September 16, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Final review identified one critical and eight moderate issues affecting installer safety, configuration handling, runtime cleanup, and bridge reliability.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a one-command, checkout-local tgrep integration for Codex and pi using MCP search tools, startup hooks, and shared services.

Changes:

  • Adds transactional project/user installation, repair, doctor, and uninstall workflows.
  • Adds MCP runtime, pi bridge, freshness handling, cancellation, and path validation.
  • Adds documentation, integration tests, and Linux/macOS CI coverage.
File summaries
File Review summary
scripts/agent/test_pi.mjs Tests the generated pi bridge contract; no findings.
scripts/agent/test_agent.py Tests installer and runtime integration; no findings.
scripts/agent/runtime.py Moderate: count output is not structured or normalized; malformed service responses can crash requests; process cleanup has a poll()/killpg() race.
scripts/agent/README.md Documents setup and runtime behavior; no findings.
scripts/agent/pi-extension.ts Moderate: a child exit after connection can cause tool calls to wait 40 seconds; check liveness and reject or reconnect.
scripts/agent/install.py Critical: reject symlinked parent components before writes. Moderate: prefer inline hooks, migrate paths after checkout moves, use single-pass substitutions, and honor exclusions for non-Git roots.
README.md Links to the integration installer and guide; no findings.
install-agent.sh Provides the installer entry point; no findings.
.github/workflows/ci.yml Runs Unix integration tests; no findings.
Review details

Suppressed comments (7)

scripts/agent/install.py:215

  • When both an inline TOML hooks table and a hooks.json file exist, this condition always chooses the JSON representation, even though the documented behavior is to use the inline TOML hooks whenever they are present. The newly added hook can therefore be written to a file Codex does not load while the inline configuration remains unchanged. Prefer the inline branch whenever "hooks" in parsed and only fall back to hooks.json when it is absent.
        if hooks_raw is not None or "hooks" not in parsed:
            hooks = json.loads(hooks_raw or b"{}")

scripts/agent/install.py:235

  • These replacements are applied sequentially to the template and also scan values inserted by earlier replacements. A valid checkout or configuration path containing a token such as __TOOLS__ or __CONFIG__ will therefore be rewritten by a later iteration, producing invalid TypeScript or incorrect absolute paths. Use a single-pass placeholder substitution or collision-proof markers.
        for key, value in substitutions.items():
            template = template.replace(key, json.dumps(value))

scripts/agent/install.py:184

  • For a supported project root outside Git, tgrep intentionally ignores .gitignore unless --no-require-git is supplied (see AGENTS.md:209-214). The default flags here remain empty even though the installer adds /.tgrep-agent/ to .gitignore, so the service indexes its own managed runtime/configuration and does not honor that exclusion. Detect non-Git project roots and include the matching index flag, or exclude the managed directory another way.
    flags = []
    if args.no_require_git:
        flags.append("--no-require-git")

scripts/agent/pi-extension.ts:43

  • If the MCP child exits after connect() resolves but before this request is sent, child is undefined and the optional write becomes a no-op. The returned promise then waits the full 40-second timer instead of reporting the disconnect or reconnecting, so a crashed adapter can make every next tool call hang. Check that the child/stdin is still alive before writing and reject or reconnect when it is not.
      child?.stdin?.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");

scripts/agent/runtime.py:238

  • output_mode="count" takes the -c --with-filename branch, but this fallback stores the whole CLI line as text. Unlike the content/files modes, callers therefore receive an absolute path:count string (the command searches an absolute root), not a normalized path and count field. Parse the final count and normalize the filename before adding the result so the advertised count mode is structured and does not expose the checkout path.
            else:
                item = {"text": text}

scripts/agent/runtime.py:121

  • A valid JSON scalar or array can be returned by a stale/unrelated process at the port in serve.json; reply.get then raises AttributeError, which is not caught here. Indexed requests can consequently crash instead of treating the service as unavailable and falling back to a scan. Validate the response shape or include AttributeError in the handled failures.
                    reply = json.loads(stream.readline(65536))
            return reply.get("result")
        except (OSError, ValueError, KeyError, TypeError):

scripts/agent/runtime.py:280

  • The poll() check and killpg() are racy: the query can exit after poll() returns None, causing os.killpg to raise ProcessLookupError and mask the actual search result during normal cleanup/cancellation. Treat an already-gone process group as successfully terminated before waiting and closing the pipes.
            if proc.poll() is None:
                os.killpg(proc.pid, signal.SIGKILL)
            proc.wait()
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/agent/install.py
Comment thread scripts/agent/install.py
@fenixc9

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

- Reject symlinked components in owned installation paths before any read,
  write, backup or lock creation, and stop resolving project-owned bases
  through symlinks.
- Rebase manifest records when a project is moved or copied: `repair --root`
  migrates owned files to the new location, `doctor` reports integrations that
  still point at the old root, and the old project is never read or changed.
- Prefer inline TOML hooks over hooks.json when both exist, leaving the JSON
  file untouched.
- Substitute pi extension placeholders in a single pass so a path containing a
  token such as __TOOLS__ stays literal.
- Honor .gitignore exclusions in non-Git session roots by adding
  --no-require-git to service and query commands at runtime.
- Return structured repository-relative count results parsed from JSON end
  records instead of raw `path:count` CLI lines.
- Treat malformed service status replies as unavailable instead of raising, and
  ignore an already-exited process group during query cleanup.
- Reject pi bridge tool calls when the MCP child is gone instead of waiting for
  the 40-second request timer, so the next call reconnects.
Copilot AI review requested due to automatic review settings September 16, 2026 15:41
@fenixc9

Copy link
Copy Markdown
Contributor Author

All findings from the review are addressed in fe7d85a, with a regression test for each:

  • Symlinked installation components: reject_symlinks preflights owned paths and their existing parents before any read, write, backup or lock creation, and project-owned bases are no longer resolved through symlinks. Tests cover .codex, .pi, .pi/extensions, .tgrep-agent, .tgrep-agent/codex and .tgrep-agent/backups, plus atomic() and manifest-target cases.
  • Moved checkout: manifest records are rebased to the selected root for repair/reinstall/uninstall, and doctor fails with an explicit "run repair with --root pointing to the new project" message. The test copies a project, repairs one agent, uninstalls the other, and verifies the original location stays byte-identical.
  • Inline hooks: the inline TOML representation now wins whenever hooks exists in config.toml, leaving hooks.json untouched.
  • Template substitution: placeholders are replaced in a single pass with escaped keys, so a path containing __TOOLS__/__CONFIG__ stays literal.
  • Non-Git roots: the runtime adds --no-require-git when the session root is not a Git repository, for the service launcher and queries alike, so the managed /.tgrep-agent/ exclusion applies. Covered in both freshness modes.
  • Count output: parsed from JSON end records into {path, count} with repository-relative paths, so filenames containing colons or newlines stay unambiguous.
  • Malformed status replies: the response is shape-checked (jsonrpc/id/num_files) and treated as unavailable instead of raising AttributeError.
  • poll()/killpg() race: an already-reaped process group is treated as terminated, so cleanup cannot mask a truncated result.
  • pi bridge: a call made after the child exits rejects immediately with a reconnect hint instead of waiting on the 40-second timer, and the next call reconnects.

python3 -B -m unittest discover -s scripts/agent -p 'test_*.py' -v reports 25 tests, all passing on Linux (Node 22+ also runs the generated pi bridge through real MCP calls).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings affect reinstall behavior, path safety, result paths, and MCP lifecycle handling.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

Previously missed (1) — in code that hasn't changed since the last review.

scripts/agent/pi-extension.ts:97

  • Tool cancellation is ignored during connection setup: execute waits for connect() before passing the signal to the actual call, while connect() initializes the MCP child with request() without that signal. If startup hangs (or the signal is already aborted), the tool can wait the full 40-second initialization timeout and leave an unnecessary adapter process instead of rejecting promptly. Thread the abort signal through connection/initialization and check it before spawning.

scripts/agent/install.py:312

  • This health check launches the installed runtime with the interpreter running doctor, but both Codex's MCP command and the generated pi extension embed sys.executable from installation time. If that Python path is later removed or changed (a documented repair case), doctor can report OK while the agent cannot start the integration. Record and probe the configured interpreter, or otherwise validate the embedded command before reporting success.
            process = subprocess.Popen([sys.executable, runtime_record["path"], "mcp", "--config", config_record["path"]],
                                       cwd=root, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

scripts/agent/install.py:396

  • These project paths are preflighted even for --scope user, although a user-scoped installation never owns or writes them. Consequently, an unrelated symlinked AGENTS.md or .gitignore in the current repository makes user-scope install, repair, or uninstall fail before touching the user destination. Restrict these two checks to project scope.
    for path in (manifest_path, base / "install.lock", base / "backups", root / "AGENTS.md", root / ".gitignore"):
        reject_symlinks(path)

scripts/agent/install.py:216

  • This always appends a [mcp_servers.tgrep] table. A valid existing TOML configuration such as mcp_servers = { other = { command = "other" } } cannot be followed by a table header because the inline table is already closed, so the validation immediately after this block fails and installation cannot preserve that configuration. Merge inline-table forms (or explicitly report/support the representation) before appending the managed block.
        block = "\n".join([
            "[mcp_servers.tgrep]", f"command = {json.dumps(sys.executable)}",
            "args = " + json.dumps([str(runtime), "mcp", "--config", str(configuration)]),
            "startup_timeout_sec = 10", "tool_timeout_sec = 40",

scripts/agent/runtime.py:128

  • The discovery file contains a PID and port, but this probe accepts any local process that returns a plausible status object and never verifies the recorded server identity or repository. After a crash, another tgrep server can reuse the saved port; ensure_server will then skip startup and indexed queries can be answered by the wrong repository. Validate the server/process identity before treating this status as live.
            info = json.loads((self.index / "serve.json").read_text())
            with socket.create_connection(("127.0.0.1", info["port"]), timeout=0.3) as sock:
                sock.sendall(b'{"jsonrpc":"2.0","id":1,"method":"status"}\n')
                with sock.makefile("rb") as stream:
                    reply = json.loads(stream.readline(65536))
            if not isinstance(reply, dict) or reply.get("jsonrpc") != "2.0" or reply.get("id") != 1:

scripts/agent/runtime.py:97

  • The scalar-string limit is not applied to array members. An MCP caller can send up to 100 arbitrarily large file_types or glob strings, causing oversized subprocess argument lists or excessive memory use before tgrep runs. Apply the same per-item length bound (and ideally a total argument budget) when validating lists.
        if isinstance(value, list) and (len(value) > 100 or any(type(v) is not str or "\0" in v for v in value)):
            raise ValueError(f"Invalid list for {key}")
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Comment thread scripts/agent/install.py
path = Path(path)
if path not in self.before:
self.before[path] = read(path)
return self.after.get(path, self.before[path])
Comment thread scripts/agent/runtime.py
Comment thread scripts/agent/runtime.py
Comment on lines +256 to +258
p = Path(item["path"])
if p.is_absolute():
item["path"] = str(p.relative_to(self.root))
- Refuse inline or dotted mcp_servers/hooks definitions with an actionable
  error instead of appending a block TOML cannot parse, and type-check
  mcp_servers before merging.
- Record the interpreter at installation time and probe it in doctor, so a
  removed or replaced Python fails verification instead of reporting success.
- Preflight project-owned AGENTS.md/.gitignore only for project scope, so
  unrelated symlinks in the current repository no longer block user scope.
- Reject a symlinked tgrep-agent cache directory at install time and symlinked
  per-repository state/index directories at runtime.
- Bound file_types/glob entries by length and total size, not just count.
- Require the recorded server pid to be alive before trusting serve.json, and
  shape-check pid/port before connecting.
- Thread the abort signal through pi bridge startup, check it before spawning,
  and reject calls that abort while initialization is still in flight.
- Document the TOML table requirement, interpreter probe, service state symlink
  rule and list limits, and cover each change with a regression test.
Copilot AI review requested due to automatic review settings September 16, 2026 16:18
@fenixc9

Copy link
Copy Markdown
Contributor Author

Second pass addressed in a4bbe75. Seven findings applied, two I could not reproduce — details and evidence below.

Applied

  • pi-extension.ts — cancellation during startup: connect() now takes the signal, checks it before spawning, races an in-flight shared startup against the caller's abort, and passes the signal into the initialize request. A call that aborts while another call is still initializing rejects immediately instead of waiting out startup, and the first caller's startup still completes. Covered in test_pi.mjs (already-aborted call with a nonexistent working directory must reject as Cancelled rather than surface a spawn error, plus the mid-startup race).
  • install.py — doctor used the runner's interpreter: the interpreter is recorded in the manifest config at installation time, doctor probes it (--version) and uses it for the MCP subprocess, so a removed or replaced Python now fails verification. Covered by a test that installs with a bogus interpreter and asserts doctor reports it.
  • install.py — user scope preflighted project paths: AGENTS.md/.gitignore preflight is now project scope only, so unrelated symlinks in the current repository no longer block user-scope install/repair/uninstall. Covered by a test.
  • install.py — inline mcp_servers: confirmed against tomllib; mcp_servers = { … } cannot be extended by [mcp_servers.tgrep] (same for a dotted/inline hooks). The installer now rejects those representations with an explicit "convert it to [table] headers" message before writing, and the final merge validation converts any other unmergeable form into the same actionable error. Three cases covered. mcp_servers is also type-checked before merging.
  • runtime.py — server identity: serve.json (pid, port) is now shape-checked and the recorded pid must still be alive before its status is trusted, so a stale record whose port was reused is treated as unavailable and the query falls back to a scan/restart. ServerInfo only stores pid and port and the status reply carries no repository identifier, so this is the strongest signal available without changing tgrep's wire protocol; say the word if you'd prefer a handshake added.
  • runtime.py — list argument bounds: file_types/glob entries now have the same 16 KB per-item limit as scalar strings plus a 64 KB total budget. Covered by a test.
  • runtime.py — symlinked service state: the installer rejects a symlinked tgrep-agent cache directory and the runtime rejects symlinked per-repository state/index directories before opening start.lock, serve.log or the index. Covered by a test that asserts nothing is written to the link target.

Not reproduced

  • install.py — "reinstalling sees the old runtime as present": self.after.get(path, self.before[path]) returns a recorded None deletion, because dict.get only falls back when the key is absent ({'a': None}.get('a', 'x') is None, not 'x'). Verified end to end as well: install → uninstall → install succeeds and re-creates the owned runtime, and test_install_reinstall_partial_uninstall_preserves_user_configuration covers repeated installs plus uninstall.
  • runtime.py — scoped results reported relative to the search directory: command() always passes the resolved absolute subdirectory, and tgrep (like ripgrep) prints absolute paths when the search root is absolute, so path="src" already yields src/main.rs after rebasing. Confirmed directly with --json, --files --null and -c against an absolute subdirectory, and test_current_search_filters_and_scoped_paths asserts src/main.rs for both content and files output.

python3 -B -m unittest discover -s scripts/agent -p 'test_*.py' -v now reports 31 tests, all passing on Linux.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in installer validation, runtime path safety, and pi reconnection handling.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

scripts/agent/install.py:138

  • When both agents are installed into a previously absent shared AGENTS.md or .gitignore, the second block sees the first managed block via self.get(path) and records created=False. Removing both blocks then leaves an empty file instead of deleting the file created by the integration. Base the flag on the original value in self.before[path], which is shared across all blocks for that path.
        records.append({"kind": "block", "path": str(path), "text": value, "created": original is None})

scripts/agent/install.py:336

  • doctor only verifies that each managed block/file can be removed; it never parses the surrounding Codex config.toml. A user edit outside the owned block can leave that file syntactically invalid, while the direct MCP probe below still succeeds and doctor reports OK even though Codex cannot load the integration. Parse the host configuration (and the selected hook representation) before reporting success.
            for record in entry["records"]:
                if not Path(record["path"]).exists():
                    raise ValueError(f"Missing managed configuration: {record['path']}")
                check.remove_record(record)

scripts/agent/install.py:458

  • The previous-binary reuse path bypasses the symlink check: a retained private base/bin/tgrep can be replaced with a symlink, is_file() follows it, and binary_path() then resolves and executes the external target for --version/--help and future hooks. Validate the manifest's private binary destination with the same owned-path check before assigning it (while keeping explicitly user-supplied binaries separate).
            if not args.binary and len(previous_binaries) == 1:
                previous_binary = previous_binaries.pop()
                if Path(previous_binary).is_file():
                    args.binary = previous_binary

scripts/agent/runtime.py:155

  • A malformed but readable serve.json with a large integer PID or a port outside the socket range passes the current type checks; os.kill()/socket.create_connection() can then raise OverflowError, which escapes status() instead of treating the record as unavailable and falling back to a scan. Catch OverflowError here (or validate the ranges before using these values).
        except (OSError, ValueError, KeyError, TypeError):
  • Files reviewed: 9/9 changed files
  • Comments generated: 4
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Comment thread scripts/agent/install.py
Comment on lines +343 to +345
interpreter = config.get("python") or sys.executable
subprocess.run([interpreter, "--version"], capture_output=True, check=True, timeout=5)
subprocess.run([config["binary"], "--version"], capture_output=True, check=True, timeout=5)
Comment thread scripts/agent/pi-extension.ts Outdated
Comment thread scripts/agent/runtime.py
Comment on lines +130 to +131
reject_symlinks(self.state)
reject_symlinks(self.index)
Comment thread scripts/agent/runtime.py
Refactor error handling during initialization to ensure proper stopping of the process.

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Signed-off-by: MR.GOOD <[email protected]>
Copilot AI review requested due to automatic review settings September 16, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical installer path-safety issues and moderate configuration and runtime issues block approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (7)

scripts/agent/install.py:468

  • When a reinstall/repair supplies any one index option, this condition skips preservation of the entire previous option set. For example, changing only --max-filesize silently drops an existing --no-require-git setting (and vice versa), so unrelated indexing behavior changes unexpectedly. Preserve each old flag unless that specific option was overridden.
                if old and not (args.max_filesize or args.no_max_filesize or args.no_require_git):

scripts/agent/install.py:174

  • Path.absolute() does not normalize ... If a user sets CODEX_HOME or PI_CODING_AGENT_DIR to a path containing .., installation records that spelling, but prepare_records() later rejects any .. component (line 312), so the installation cannot be repaired, doctored, or uninstalled. Normalize these paths lexically without resolving symlinks before recording them.
        return Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))).expanduser().absolute()
    return Path(os.environ.get("PI_CODING_AGENT_DIR", str(Path.home() / ".pi/agent"))).expanduser().absolute()

scripts/agent/install.py:459

  • When reinstalling/repairing both agents, this only reuses a previous binary if all selected agents share one path. If Codex and pi were installed with different valid --binary values, a later combined operation silently replaces both with the PATH/private binary even though the documented behavior says reinstall preserves each previous binary unless a replacement is supplied. Resolve the binary per agent (or reject the ambiguous combined operation) instead of collapsing the set.
            previous_binaries = {manifest["agents"][a]["config"]["binary"] for a in agents if a in manifest["agents"]}
            if not args.binary and len(previous_binaries) == 1:
                previous_binary = previous_binaries.pop()
                if Path(previous_binary).is_file():
                    args.binary = previous_binary
            binary = binary_path(args, base)

scripts/agent/runtime.py:361

  • The adapter accepts up to 16 active calls but runs only four workers. Calls five through 16 can remain queued behind long-running searches; cancellation only sets their event, which is not observed until a worker starts the task, so a cancelled request can wait for the earlier 30-second timeouts instead of rejecting promptly. Track the submitted futures or otherwise remove/cancel queued work when notifications/cancelled arrives.
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:

scripts/agent/runtime.py:173

  • The symlink check only covers the state/index directories, but this open follows a pre-existing state/serve.log symlink. That lets startup diagnostics be written outside the managed cache despite the service-state path checks (and the same audit is needed for other state files opened by the launcher). Reject leaf symlinks before opening these paths.
            with (self.state / "serve.log").open("ab") as log:

scripts/agent/runtime.py:208

  • find_files invokes tgrep --files without passing the requested pattern, then filters only after every indexed/filesystem path has been emitted. A query such as *.rs or a no-match pattern therefore scans and transfers the entire repository before returning, which can dominate the 30-second budget on large trees; apply a bounded/index-side filename filter or add a dedicated filename query instead.
        if name == "find_files":
            cmd += ["--files", "--null", str(path)]
        else:

scripts/agent/runtime.py:225

  • Count mode still requests JSON match records for every matching line and record() discards those records, keeping only the later end statistics. A heavily matching file can therefore produce and parse an unbounded stream despite the response byte/record caps, causing unnecessary work or a timeout; the CLI needs a count-only JSON/summary path that emits per-file stats without match events.
                # CLI count output cannot escape arbitrary filenames. JSON end
                # records provide matched-line counts with unambiguous paths.
                cmd += ["--json", "-C", "0"]
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Comment thread scripts/agent/install.py Outdated
Comment thread scripts/agent/install.py
Comment on lines +420 to +422
data_home = Path(os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local/share")))
base = (root / ".tgrep-agent" if args.scope == "project" else data_home.resolve() / "tgrep-agent")
reject_symlinks(base)
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Signed-off-by: MR.GOOD <[email protected]>
Copilot AI review requested due to automatic review settings September 17, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Four moderate findings remain in path normalization, validation, and symlink handling.

Review details

Suppressed comments (4)

scripts/agent/install.py:421

  • Calling data_home.resolve() removes any symlink components before reject_symlinks(base) runs. With a symlinked XDG_DATA_HOME, user-scope installation therefore writes through the symlink target instead of rejecting it, contrary to the advertised protection for owned installation paths and the equivalent checks used for project state and agent destinations. Keep the absolute, non-resolved path for the preflight check.
    base = (root / ".tgrep-agent" if args.scope == "project" else data_home.resolve() / "tgrep-agent")

scripts/agent/runtime.py:260

  • When the requested path is a subdirectory, tgrep emits paths relative to that search directory (for example main.rs for a query rooted at repo/src), but this converts them directly relative to self.root and returns main.rs instead of src/main.rs. The same missing prefix affects JSON content/count records below and makes repository-relative results and repository-relative find_files globs incorrect for scoped searches; normalize the CLI path relative to the requested directory before applying the repository-root conversion in every output branch.
                resolved = Path(text)
                if not resolved.is_absolute():
                    resolved = self.root / resolved
                if not resolved.resolve().is_relative_to(self.root):
                    return
                text = str(resolved.relative_to(self.root))
                pattern = args.get("pattern", "*")
                if name == "find_files" and not (fnmatch.fnmatchcase(text, pattern) or fnmatch.fnmatchcase(Path(text).name, pattern)):

scripts/agent/runtime.py:155

  • An out-of-range integer in a stale or corrupted serve.json (for example, a port above 65535) passes these type checks and can make socket.create_connection raise OverflowError. That exception is not caught below, so status() aborts ensure_server instead of treating the server as unavailable and falling back to a fresh scan; catch OverflowError or validate port/PID bounds with the malformed-record checks.
        except (OSError, ValueError, KeyError, TypeError):

scripts/agent/runtime.py:131

  • These checks only reject symlinked directories, not symlinked files within them. A pre-created state/start.lock or state/serve.log is followed by open(), and the child similarly follows index/serve.lock/serve.json, so a local redirect can make the integration lock or overwrite files outside the cache despite the documented symlink protection. Validate every service-owned leaf before launch (or use no-follow/atomic creation) as well.
        reject_symlinks(self.state)
        reject_symlinks(self.index)
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

@shengyfu
Shengyu Fu (shengyfu) merged commit 634c48e into microsoft:main Sep 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants