Skip to content
Source
Sign inSign up

[Bug]: ACP Registry rejects contained executable names starting with two dots #35831

Send to my agent
Opencoygeek(GitHub profile, opens in a new tab) opened this issue 0 comments

Summary

An ACP Registry agent cannot be prepared when its executable name begins with two literal dots, even though the executable remains inside its installation directory. T3 reports that the command resolves outside the directory instead of accepting the contained file. A deterministic local fixture reproduced this with an ordinary executable name as the passing control; no third-party agent was launched.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Use a checkout of the tested commit with its frozen-lockfile dependencies already installed.
  2. Save the script below as repro-contained-name.sh outside the checkout and run sh repro-contained-name.sh CHECKOUT, replacing CHECKOUT with the checkout directory.
  3. Observe that the agent control prepares successfully and the ..agent fixture returns archive_invalid.

One run reproduced the failure deterministically. The script requires POSIX sh, Git, Python 3, the repository's Node runtime, and the already-installed repository dependencies. It creates a detached disposable worktree and fake Registry/download responses under a temporary directory, sets disposable HOME, TMPDIR, and T3 state, runs one focused fixture test with one worker, and removes the worktree and temporary inputs on exit. It never contacts the Registry CDN or executes a fixture binary. Exit 0 and RESULT: expected behavior mean both contained names prepare; exit 1 and RESULT: defect reproduced mean the control passes and the contained double-dot name is rejected; exit 2 and RESULT: inconclusive mean setup, control, or an unrelated assertion failed.

Reproduction script: repro-contained-name.sh
#!/bin/sh
set -u
if [ "$#" -ne 1 ]; then
  echo 'Usage: sh repro-contained-name.sh CHECKOUT' >&2
  echo 'RESULT: inconclusive'
  exit 2
fi
repo=$(cd "$1" && pwd -P) || exit 2
git --version
node --version
python3 --version
scratch=$(mktemp -d) || exit 2
cleanup() {
  git -C "$repo" worktree remove --force "$scratch/wt" >/dev/null 2>&1 || true
  rm -rf "$scratch"
}
trap cleanup EXIT HUP INT TERM
mkdir -p "$scratch/home" "$scratch/tmp" "$scratch/state"
export HOME="$scratch/home" TMPDIR="$scratch/tmp" T3CODE_HOME="$scratch/state"
export G12_RESULT="$scratch/result.json"
if ! git -C "$repo" worktree add --quiet --detach "$scratch/wt" HEAD; then
  echo 'RESULT: inconclusive'
  exit 2
fi
python3 - "$repo" "$scratch/wt" <<'PY'
import pathlib,sys
source,target=map(pathlib.Path,sys.argv[1:])
parents=[pathlib.Path('.'),pathlib.Path('scripts'),pathlib.Path('oxlint-plugin-t3code')]
for base in ('apps','packages','infra'):
 parents += [p.parent.relative_to(source) for p in (source/base).glob('*/package.json')]
for rel in parents:
 dep=source/rel/'node_modules'; dest=target/rel/'node_modules'
 if dep.exists() and dest.parent.exists() and not dest.exists(): dest.symlink_to(dep,target_is_directory=True)
PY
if [ ! -x "$scratch/wt/node_modules/.bin/vp" ]; then
  echo 'Dependencies must already be installed in CHECKOUT.' >&2
  echo 'RESULT: inconclusive'
  exit 2
fi
"$scratch/wt/node_modules/.bin/vp" --version
cat > "$scratch/wt/packages/provider-acp-registry/src/server/ContainedNameRepro.test.ts" <<'TS'
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientResponse from "effect/http/HttpClientResponse";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import * as Support from "./AcpRegistrySupport.ts";
import { writeFileSync } from "node:fs";
const registryUrl="https://registry.test/registry.json";
const bytes=new TextEncoder().encode("fixture bytes; never executed\n");
it.effect("prepares a contained command whose filename starts with two dots",()=>Effect.gen(function*(){
 const fs=yield* FileSystem.FileSystem;
 const root=yield* fs.makeTempDirectoryScoped({prefix:"contained-name-"});
 const agents=["agent","..agent"].map((cmd,index)=>({id:`fixture-${index}`,name:`Fixture ${index}`,version:"1.0.0",description:"Local fixture",license_url:"https://example.test/LICENSE",distribution:{binary:{"linux-x86_64":{archive:`https://registry.test/fixture-${index}.bin`,cmd}}}}));
 const resolver=yield* Support.makeAcpRegistryCatalog({cacheDir:root,toolsDir:`${root}/tools`,registryUrl}).pipe(Effect.provideService(HttpClient.HttpClient,HttpClient.make(request=>Effect.succeed(HttpClientResponse.fromWeb(request,new Response(request.url===registryUrl?JSON.stringify({version:"1.0.0",agents}):bytes))))));
 expect((yield* resolver.prepare({agentId:"fixture-0"})).prepared).toBe(true);
 expect(yield* fs.readFileString(`${root}/tools/fixture-0/1.0.0/linux-x86_64/agent`)).toBe(new TextDecoder().decode(bytes));
 const outcome=yield* resolver.prepare({agentId:"fixture-1"}).pipe(Effect.match({onSuccess:value=>({ok:true,value}),onFailure:error=>({ok:false,error})}));
 if(outcome.ok){
  expect(yield* fs.readFileString(`${root}/tools/fixture-1/1.0.0/linux-x86_64/..agent`)).toBe(new TextDecoder().decode(bytes));
  writeFileSync(process.env.G12_RESULT!,JSON.stringify({control:"prepared",candidate:"prepared",result:"expected"}));
 }else{
  expect(outcome.error.reason).toBe("archive_invalid");
  expect(outcome.error.detail).toBe("ACP Registry archive command resolves outside its installation directory.");
  writeFileSync(process.env.G12_RESULT!,JSON.stringify({control:"prepared",candidate:outcome.error.reason,detail:outcome.error.detail,result:"defect"}));
 }
}).pipe(Effect.scoped,Effect.provide(Layer.mergeAll(NodeServices.layer,NodeCrypto.layer,TestProviderHost.layer().pipe(Layer.provide(Layer.mergeAll(NodeServices.layer,NodeCrypto.layer))))),Effect.provideService(HostProcess.Platform,"linux"),Effect.provideService(HostProcess.Architecture,"x64"),Effect.provideService(HostProcess.Environment,{PATH:"/usr/bin:/bin"})));
TS
if ! (cd "$scratch/wt/packages/provider-acp-registry" && "$scratch/wt/node_modules/.bin/vp" test run --config ../../vite.config.ts src/server/ContainedNameRepro.test.ts --maxWorkers 1) >"$scratch/runner.log" 2>&1; then
  cat "$scratch/runner.log"
  echo 'RESULT: inconclusive'
  exit 2
fi
python3 - "$G12_RESULT" <<'PY'
import json,sys
try: value=json.load(open(sys.argv[1]))
except Exception:
 print('RESULT: inconclusive');sys.exit(2)
print('control filename agent:',value['control'])
print('candidate filename ..agent:',value['candidate'])
if 'detail' in value: print(value['detail'])
if value['result']=='expected':print('RESULT: expected behavior');sys.exit(0)
print('RESULT: defect reproduced');sys.exit(1)
PY
status=$?
exit "$status"

Expected behavior

A relative executable filename such as ..agent should be accepted when its resolved file is inside the installation directory. The pinned Registry agent.schema.json defines binaryTarget.cmd as the command to execute after extraction, with no prohibition on this filename; the pinned FORMAT.md includes raw binary distributions. T3's own command-path validation accepts ordinary relative segments and rejects a segment equal to ..; the fixture contains no parent-directory segment. Commands whose resolved files actually escape the install root should continue to be rejected.

Actual behavior

Calling the exported makeAcpRegistryCatalog(...).prepare({ agentId }) with a local fake Registry and raw-binary download succeeds for cmd: "agent" but fails for cmd: "..agent". Both fixtures write the same bytes directly inside their separate managed installation roots. The second call returns archive_invalid with ACP Registry archive command resolves outside its installation directory. The result was observed in a disposable checkout without launching either downloaded fixture.

Interface: Package or setup: ACP Registry catalog preparation, which serves provider installation.

Earliest failure stage: Setup or configuration.

Impact

Minor bug or occasional failure.

Impact: P2 - A valid contained binary command cannot be prepared, preventing use of that Registry distribution; the observed failure is bounded to specific command names.

The failure is established for the synthetic cmd: "..agent" raw-binary fixture. No published Registry agent was observed using this filename, and no claim is made that a current catalog entry is affected. Native Linux and Windows execution and any manual-install/executable-override workaround were not tested.

Version or commit

T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93.

Environment

macOS 27.2 arm64; Node v26.12.1; Python 3.15.2; Git 2.56.0; Vite+ CLI 1.4.0; repository-pinned Effect 4.1.3. The fixture injects the catalog's linux/x64 HostProcess references while using the real local filesystem. This is a catalog-level reproduction, not a native Linux agent run.

Logs or stack traces

Evidence

The exact reproduction script printed:

git version 2.56.0
v26.12.1
Python 3.15.2
vp v1.4.0

Local vite-plus:
  vite-plus  Not found

control filename agent: prepared
candidate filename ..agent: archive_invalid
ACP Registry archive command resolves outside its installation directory.
RESULT: defect reproduced

The CLI version query ran before the script changed into the disposable package directory; the focused runner subsequently completed, and the script emitted its result only after that runner succeeded. The guard at AcpRegistrySupport.ts#L1356-L1368 tests whether the relative path starts with ... For the contained fixture file, that relative path is ..agent. The supported preparation path invokes this guard at AcpRegistrySupport.ts#L1596-L1606.

Workaround

No workaround was tested. The report is limited to the synthetic contained-name fixture.

Restoration check

Run the supplied script against the implementation being assessed: both contained names must prepare successfully, preserve the fixture bytes, and produce RESULT: expected behavior with exit 0. Existing tests rejecting executable paths that actually resolve outside the install root must continue to pass.

References

  • https://github.com/pingdotgg/t3code/pull/34531 — changes when executable mode is applied relative to containment validation; this report concerns rejection of a contained filename by the remaining guard.
  1. tanstack-source added labelsbug

Sign in to join this conversation.