[GH-ISSUE #7396] [Bug] export_environment_bundle calls its bundle destination an extraction output root #4402

Closed
opened 2026-10-31 00:49:31 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7396

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

The public export_environment_bundle MCP tool returns Extraction output root already exists when a valid capture Evidence file is supplied with an occupied output_directory. Both the top-level error message and the field issue repeat that extraction terminology, although the requested operation constructs an agent environment bundle. An existing regular file receives the same wording. These requests correctly return invalid_request / invalid_input; the defect is the message's reference to another operation.

Reproduction

  1. Start in the baseline checkout with its dependencies and built runtime available, and put its pinned Node 24.22.1 on PATH. Run the following Python 3 script. It allocates an empty scratch directory, creates the HAR inline, obtains capture Evidence through the public CLI, redirects HOME/XDG/TMPDIR, and starts and stops its own localhost MCP HTTP server. Fixture paths are relative to that fresh directory; absolute MCP arguments are computed from those paths. Port 18923 must be unused.
import json, os, pathlib, subprocess, tempfile, time, urllib.request
repo=pathlib.Path.cwd()
root=pathlib.Path(tempfile.mkdtemp(prefix='rea-bundle-')).resolve()
os.chdir(root)
env=dict(os.environ, REA_LOG_LEVEL='silent', NODE_DISABLE_COMPILE_CACHE='1')
for key in ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']:
    p=root/key.lower(); p.mkdir(exist_ok=True); env[key]=str(p)
har={'log':{'version':'1.2','creator':{'name':'fixture','version':'1'},'entries':[{'startedDateTime':'2024-01-01T00:00:00.000Z','time':1,'request':{'method':'GET','url':'https://example.test/api/items','httpVersion':'HTTP/1.1','headers':[],'queryString':[],'cookies':[],'headersSize':-1,'bodySize':0},'response':{'status':200,'statusText':'OK','httpVersion':'HTTP/1.1','headers':[],'cookies':[],'content':{'size':2,'mimeType':'application/json','text':'{}'},'redirectURL':'','headersSize':-1,'bodySize':2},'cache':{},'timings':{'send':0,'wait':1,'receive':0}}]}}
(root/'capture.har').write_text(json.dumps(har))
p=subprocess.run(['node',str(repo/'scripts/rea.mjs'),'inspect-web-network-capture','capture.har','har','--format','json'],env=env,capture_output=True,text=True,timeout=30)
(root/'capture.json').write_text(p.stdout)
print('capture exit:',p.returncode)
assert p.returncode==0
(root/'occupied').mkdir(exist_ok=True); (root/'occupied/keep.txt').write_text('keep\n'); (root/'regular-file').write_text('keep\n')
headers={'Content-Type':'application/json','Accept':'application/json, text/event-stream'}
def request(body):
    req=urllib.request.Request('http://127.0.0.1:18923/mcp',data=json.dumps(body).encode(),headers=headers)
    with urllib.request.urlopen(req,timeout=30) as r:
        text=r.read().decode(); h=r.headers
    data=next((line[6:] for line in text.splitlines() if line.startswith('data: ')),text)
    return json.loads(data) if data else {},h
with open(root/'server.stdout','w') as out, open(root/'server.stderr','w') as err:
    server=subprocess.Popen(['node',str(repo/'scripts/rea.mjs'),'mcp','--http','--port','18923'],env=env,stdout=out,stderr=err)
    try:
        for i in range(100):
            try:
                init,h=request({'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'probe','version':'1'}}}); break
            except OSError:
                if server.poll() is not None: raise RuntimeError((root/'server.stderr').read_text())
                time.sleep(.1)
        else: raise TimeoutError('server start')
        headers['mcp-session-id']=h['mcp-session-id']
        request({'jsonrpc':'2.0','method':'notifications/initialized'})
        results=[]
        for i,dest in enumerate(['occupied','regular-file','occupied','regular-file','missing-parent/child','fresh-output']):
            response,_=request({'jsonrpc':'2.0','id':i+2,'method':'tools/call','params':{'name':'export_environment_bundle','arguments':{'source':{'kind':'evidence-file','path':str(root/'capture.json')},'output_directory':str(root/dest),'dry_run':True}}})
            (root/f'result-{i+1}.json').write_text(json.dumps(response,indent=2))
            results.append({'destination':dest,'response':response})
            r=response['result']; content=json.loads(r['content'][0]['text']); error=content.get('error')
            print(dest, json.dumps({'code':error['code'],'category':error['category'],'message':error['message'],'issues':error['details']['issues']}) if error else json.dumps({'status':r.get('structuredContent',{}).get('status')}))
        (root/'results.json').write_text(json.dumps(results,indent=2))
        assert (root/'occupied/keep.txt').read_text()=='keep\n'
        assert sorted(p.name for p in (root/'occupied').iterdir())==['keep.txt']
        assert (root/'regular-file').read_text()=='keep\n'
        assert not (root/'missing-parent').exists() and not (root/'fresh-output').exists()
        print('destinations preserved; control writes nothing')
    finally:
        server.terminate()
        try:server.wait(timeout=10)
        except subprocess.TimeoutExpired:server.kill();server.wait()
  1. Observe the following output, trimmed to the first occurrence of each destination case. The script makes two calls each for occupied and regular-file and one call each for the missing-parent case and fresh control.
capture exit: 0
occupied {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output root already exists", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output root already exists"}]}
regular-file {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output root already exists", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output root already exists"}]}
missing-parent/child {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output parent is unavailable", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output parent is unavailable"}]}
fresh-output {"status": "planned"}
destinations preserved; control writes nothing
  1. The complete sequence was executed twice independently: the wording defect reproduced in 4/4 occupied-directory calls and 4/4 regular-file calls. The missing-parent wording reproduced in 2/2 calls; the absent-destination dry-run control returned planned in 2/2 calls. The marker contents and occupied directory's listing remained unchanged.

Expected behavior

Describe the rejected destination as the environment bundle's output directory, or use neutral output-destination terminology, while preserving the refusal, field attribution and existing contents. Exact CLI wording is not required: the MCP tool has its own documented requirement that the directory be absent.

The contract in docs/agent-environments.md, under “Build a bundle in one step” (lines 218-233), says: “Over MCP, export_environment_bundle builds the same bundle and returns the same report.” It also states that output_directory “must be an absolute path that does not exist yet, whose parent does” and that dry_run still checks the destination. The repository's AGENTS.md, under “Architecture and Cleanup”, requires meaningful failure reasons. These support an environment bundle or output-destination diagnostic rather than extraction vocabulary.

Impact and scope

This was a wording defect in all eight tested occupied-destination calls. The error code, category and field attribution were correct, and both marker files retained their contents. An absent destination produced a valid dry-run plan with the same capture Evidence. No failed bundle construction, data corruption or security boundary crossing was observed. All calls used dry_run: true; non-dry-run requests, retained Evidence, other destination variants and other operating systems were not executed.

REA version

6.8.0 (source commit a905a71e31)

Runtime and platform

Node 24.22.1; macOS 27.0.1 arm64; Python 3 driver; no external analysis provider involved.

Target and artifact context

Synthetic one-request HAR for https://example.test/api/items, converted into capture Evidence by the public CLI. Agent environment bundle output selections consisting of an occupied directory, an existing regular file, a missing parent and an absent destination. No recorded URL was fetched.

Interface

MCP

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed through the built HTTP MCP endpoint after initialize and notifications/initialized: the error's code was invalid_request, its category was invalid_input, and both message and details.issues[0].message were Extraction output root already exists. The issue path was ["output_directory"], and details.operation and recovery.operation correctly named export_environment_bundle. The script retains raw responses as result-1.json through result-6.json in its generated scratch directory.

Source-only (not executed separately): the public registration in src/server/registerApplicationTools/environmentBundle.ts parses the tool contract before calling the handler. src/application/environment/environmentBundleTool.ts:57 checks the destination before resolving capture Evidence. src/artifacts/SafeOutputTree.ts:149 performs that early admission, and its existing-destination branch at line 167 supplies the extraction sentence. src/artifacts/SafeOutputTreeAdmission.ts:39 projects the same refusal text into the top-level message and field issue. These source facts explain the observed messages; the public runtime calls establish reachability.

Typed outcome or recovery expected

Keep invalid_request, invalid_input, the output_directory issue and input-contract recovery naming export_environment_bundle. Replace the extraction terms with neutral destination or environment bundle wording for the occupied-output and missing-parent refusals.

Analysis or suspected cause

Source-derived: the environment bundle handler reuses shared output-tree admission and passes its error through unchanged. The shared producer's extraction wording therefore becomes public environment bundle terminology. Correcting the vocabulary at the responsible boundary should preserve the documented absent-directory policy, including its application during dry runs.

Additional context

Related (closed):
https://github.com/morluto/rea/issues/5457 covered the CLI's inaccurate identity-change diagnosis for an existing file; its closure confirms the CLI correction, while this baseline's separately executed MCP preflight still returns the extraction-root message.

The latest published rea-agents version returned by npm view rea-agents version was 6.8.0. No known-good MCP version or regression range was established. No source changes or mocks were used, and nothing was published.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7396 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area MCP or agent integration ### Observed behavior The public `export_environment_bundle` MCP tool returns `Extraction output root already exists` when a valid capture Evidence file is supplied with an occupied `output_directory`. Both the top-level error message and the field issue repeat that extraction terminology, although the requested operation constructs an agent environment bundle. An existing regular file receives the same wording. These requests correctly return `invalid_request` / `invalid_input`; the defect is the message's reference to another operation. ### Reproduction 1. Start in the baseline checkout with its dependencies and built runtime available, and put its pinned Node 24.22.1 on PATH. Run the following Python 3 script. It allocates an empty scratch directory, creates the HAR inline, obtains capture Evidence through the public CLI, redirects HOME/XDG/TMPDIR, and starts and stops its own localhost MCP HTTP server. Fixture paths are relative to that fresh directory; absolute MCP arguments are computed from those paths. Port 18923 must be unused. ```python import json, os, pathlib, subprocess, tempfile, time, urllib.request repo=pathlib.Path.cwd() root=pathlib.Path(tempfile.mkdtemp(prefix='rea-bundle-')).resolve() os.chdir(root) env=dict(os.environ, REA_LOG_LEVEL='silent', NODE_DISABLE_COMPILE_CACHE='1') for key in ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']: p=root/key.lower(); p.mkdir(exist_ok=True); env[key]=str(p) har={'log':{'version':'1.2','creator':{'name':'fixture','version':'1'},'entries':[{'startedDateTime':'2024-01-01T00:00:00.000Z','time':1,'request':{'method':'GET','url':'https://example.test/api/items','httpVersion':'HTTP/1.1','headers':[],'queryString':[],'cookies':[],'headersSize':-1,'bodySize':0},'response':{'status':200,'statusText':'OK','httpVersion':'HTTP/1.1','headers':[],'cookies':[],'content':{'size':2,'mimeType':'application/json','text':'{}'},'redirectURL':'','headersSize':-1,'bodySize':2},'cache':{},'timings':{'send':0,'wait':1,'receive':0}}]}} (root/'capture.har').write_text(json.dumps(har)) p=subprocess.run(['node',str(repo/'scripts/rea.mjs'),'inspect-web-network-capture','capture.har','har','--format','json'],env=env,capture_output=True,text=True,timeout=30) (root/'capture.json').write_text(p.stdout) print('capture exit:',p.returncode) assert p.returncode==0 (root/'occupied').mkdir(exist_ok=True); (root/'occupied/keep.txt').write_text('keep\n'); (root/'regular-file').write_text('keep\n') headers={'Content-Type':'application/json','Accept':'application/json, text/event-stream'} def request(body): req=urllib.request.Request('http://127.0.0.1:18923/mcp',data=json.dumps(body).encode(),headers=headers) with urllib.request.urlopen(req,timeout=30) as r: text=r.read().decode(); h=r.headers data=next((line[6:] for line in text.splitlines() if line.startswith('data: ')),text) return json.loads(data) if data else {},h with open(root/'server.stdout','w') as out, open(root/'server.stderr','w') as err: server=subprocess.Popen(['node',str(repo/'scripts/rea.mjs'),'mcp','--http','--port','18923'],env=env,stdout=out,stderr=err) try: for i in range(100): try: init,h=request({'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'probe','version':'1'}}}); break except OSError: if server.poll() is not None: raise RuntimeError((root/'server.stderr').read_text()) time.sleep(.1) else: raise TimeoutError('server start') headers['mcp-session-id']=h['mcp-session-id'] request({'jsonrpc':'2.0','method':'notifications/initialized'}) results=[] for i,dest in enumerate(['occupied','regular-file','occupied','regular-file','missing-parent/child','fresh-output']): response,_=request({'jsonrpc':'2.0','id':i+2,'method':'tools/call','params':{'name':'export_environment_bundle','arguments':{'source':{'kind':'evidence-file','path':str(root/'capture.json')},'output_directory':str(root/dest),'dry_run':True}}}) (root/f'result-{i+1}.json').write_text(json.dumps(response,indent=2)) results.append({'destination':dest,'response':response}) r=response['result']; content=json.loads(r['content'][0]['text']); error=content.get('error') print(dest, json.dumps({'code':error['code'],'category':error['category'],'message':error['message'],'issues':error['details']['issues']}) if error else json.dumps({'status':r.get('structuredContent',{}).get('status')})) (root/'results.json').write_text(json.dumps(results,indent=2)) assert (root/'occupied/keep.txt').read_text()=='keep\n' assert sorted(p.name for p in (root/'occupied').iterdir())==['keep.txt'] assert (root/'regular-file').read_text()=='keep\n' assert not (root/'missing-parent').exists() and not (root/'fresh-output').exists() print('destinations preserved; control writes nothing') finally: server.terminate() try:server.wait(timeout=10) except subprocess.TimeoutExpired:server.kill();server.wait() ``` 2. Observe the following output, trimmed to the first occurrence of each destination case. The script makes two calls each for `occupied` and `regular-file` and one call each for the missing-parent case and fresh control. ```text capture exit: 0 occupied {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output root already exists", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output root already exists"}]} regular-file {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output root already exists", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output root already exists"}]} missing-parent/child {"code": "invalid_request", "category": "invalid_input", "message": "Extraction output parent is unavailable", "issues": [{"path": ["output_directory"], "reason": "invalid_value", "message": "Extraction output parent is unavailable"}]} fresh-output {"status": "planned"} destinations preserved; control writes nothing ``` 3. The complete sequence was executed twice independently: the wording defect reproduced in 4/4 occupied-directory calls and 4/4 regular-file calls. The missing-parent wording reproduced in 2/2 calls; the absent-destination dry-run control returned `planned` in 2/2 calls. The marker contents and occupied directory's listing remained unchanged. ### Expected behavior Describe the rejected destination as the environment bundle's output directory, or use neutral output-destination terminology, while preserving the refusal, field attribution and existing contents. Exact CLI wording is not required: the MCP tool has its own documented requirement that the directory be absent. The contract in `docs/agent-environments.md`, under “Build a bundle in one step” (lines 218-233), says: “Over MCP, `export_environment_bundle` builds the same bundle and returns the same report.” It also states that `output_directory` “must be an absolute path that does not exist yet, whose parent does” and that `dry_run` still checks the destination. The repository's `AGENTS.md`, under “Architecture and Cleanup”, requires meaningful failure reasons. These support an environment bundle or output-destination diagnostic rather than extraction vocabulary. ### Impact and scope This was a wording defect in all eight tested occupied-destination calls. The error code, category and field attribution were correct, and both marker files retained their contents. An absent destination produced a valid dry-run plan with the same capture Evidence. No failed bundle construction, data corruption or security boundary crossing was observed. All calls used `dry_run: true`; non-dry-run requests, retained Evidence, other destination variants and other operating systems were not executed. ### REA version 6.8.0 (source commit a905a71e31f71457cb21fd056b234f872126974e) ### Runtime and platform Node 24.22.1; macOS 27.0.1 arm64; Python 3 driver; no external analysis provider involved. ### Target and artifact context Synthetic one-request HAR for `https://example.test/api/items`, converted into capture Evidence by the public CLI. Agent environment bundle output selections consisting of an occupied directory, an existing regular file, a missing parent and an absent destination. No recorded URL was fetched. ### Interface MCP ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed through the built HTTP MCP endpoint after `initialize` and `notifications/initialized`: the error's `code` was `invalid_request`, its `category` was `invalid_input`, and both `message` and `details.issues[0].message` were `Extraction output root already exists`. The issue path was `["output_directory"]`, and `details.operation` and `recovery.operation` correctly named `export_environment_bundle`. The script retains raw responses as `result-1.json` through `result-6.json` in its generated scratch directory. Source-only (not executed separately): the public registration in `src/server/registerApplicationTools/environmentBundle.ts` parses the tool contract before calling the handler. `src/application/environment/environmentBundleTool.ts:57` checks the destination before resolving capture Evidence. `src/artifacts/SafeOutputTree.ts:149` performs that early admission, and its existing-destination branch at line 167 supplies the extraction sentence. `src/artifacts/SafeOutputTreeAdmission.ts:39` projects the same refusal text into the top-level message and field issue. These source facts explain the observed messages; the public runtime calls establish reachability. ### Typed outcome or recovery expected Keep `invalid_request`, `invalid_input`, the `output_directory` issue and input-contract recovery naming `export_environment_bundle`. Replace the extraction terms with neutral destination or environment bundle wording for the occupied-output and missing-parent refusals. ### Analysis or suspected cause Source-derived: the environment bundle handler reuses shared output-tree admission and passes its error through unchanged. The shared producer's extraction wording therefore becomes public environment bundle terminology. Correcting the vocabulary at the responsible boundary should preserve the documented absent-directory policy, including its application during dry runs. ### Additional context Related (closed): https://github.com/morluto/rea/issues/5457 covered the CLI's inaccurate identity-change diagnosis for an existing file; its closure confirms the CLI correction, while this baseline's separately executed MCP preflight still returns the extraction-root message. The latest published `rea-agents` version returned by `npm view rea-agents version` was 6.8.0. No known-good MCP version or regression range was established. No source changes or mocks were used, and nothing was published.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4402
No description provided.