chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합

- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

@@ -0,0 +1,81 @@
$ErrorActionPreference = 'Stop'
$admin = 'http://localhost:2019'
$id = 'drawing-model-box-sample-deployment'
$public = 'http://10.14.12.22/box-sample/'
$created = $false
function Test-StaticFallback($routes) {
if (@($routes).Count -eq 0) { return $false }
foreach ($r in $routes) {
if (@($r.handle).Count -eq 0) { return $false }
foreach ($h in $r.handle) {
if ($h.handler -eq 'static_response') { continue }
if ($h.handler -eq 'subroute' -and (Test-StaticFallback $h.routes)) { continue }
return $false
}
}
return $true
}
try {
$response = Invoke-WebRequest -UseBasicParsing "$admin/config/"
$config = $response.Content | ConvertFrom-Json
if (-not $response.Headers['ETag']) { throw 'Caddy configuration ETag is required.' }
$servers = @($config.apps.http.servers.PSObject.Properties | Where-Object {
@($_.Value.listen | Where-Object { $_ -match '(^|:)80$' }).Count -gt 0
})
if ($servers.Count -ne 1) { throw 'Cannot uniquely identify the existing HTTP port 80 server.' }
$server = $servers[0]
$routes = @($server.Value.routes)
$own = @($routes | Where-Object { $_.'@id' -eq $id })
$route = @{
'@id' = $id
match = @(@{host=@('10.14.12.22'); path=@('/box-sample','/box-sample/*')})
handle = @(@{handler='reverse_proxy'; upstreams=@(@{dial='127.0.0.1:5092'})})
terminal = $true
}
if ($own.Count -gt 1) { throw 'Duplicate deployment route.' }
if ($own.Count -eq 1) {
if (@($own[0].handle).Count -ne 1 -or $own[0].handle[0].handler -ne 'reverse_proxy' -or
@($own[0].handle[0].upstreams).Count -ne 1 -or $own[0].handle[0].upstreams[0].dial -ne '127.0.0.1:5092' -or
(@($own[0].match[0].path) -join ',') -ne '/box-sample,/box-sample/*' -or
(@($own[0].match[0].host) -join ',') -ne '10.14.12.22' -or -not $own[0].terminal) {
throw 'Existing deployment route differs; refusing to overwrite it.'
}
} else {
# Preserve every middleware, maintenance and service route in its original
# order. Insert only before a proven static-only final fallback.
$insertIndex = $routes.Count - 1
if ($insertIndex -lt 0 -or $routes[$insertIndex].match -or
-not (Test-StaticFallback @($routes[$insertIndex]))) {
throw 'Expected a final unmatched static-only fallback; no settings changed.'
}
foreach ($existing in $routes) {
$json = $existing | ConvertTo-Json -Depth 100 -Compress
# Do not bypass a shared authentication policy or shadow an existing mount.
if ($json -match 'authentication|http_basic|forward_auth|/box-sample') {
throw 'Existing route/authentication needs administrator review; nothing was replaced.'
}
}
$backup = Join-Path $PSScriptRoot ('caddy-before-box-sample-' + (Get-Date -Format 'yyyyMMdd-HHmmss-fff') + '.json')
[IO.File]::WriteAllText($backup, $response.Content)
$name = [Uri]::EscapeDataString($server.Name)
if ($null -eq $server.Value.routes) { throw 'HTTP routes array missing; no automatic replacement allowed.' }
$body = $route | ConvertTo-Json -Depth 30 -Compress
Invoke-WebRequest -UseBasicParsing -Method Put -Uri "$admin/config/apps/http/servers/$name/routes/$insertIndex" -Headers @{'If-Match'=$response.Headers['ETag']} -ContentType 'application/json' -Body $body | Out-Null
$created = $true
}
foreach ($path in @('', 'api/status', 'src/app.js')) {
$check = Invoke-WebRequest -UseBasicParsing -Uri ($public+$path) -TimeoutSec 30
if ($check.StatusCode -ne 200 -or $check.Headers['X-DMS-Deployment'] -ne 'box-sample') { throw "Public route validation failed: $path" }
if ($path -eq 'api/status' -and -not ($check.Content | ConvertFrom-Json).ok) { throw 'API status failed.' }
if ($path -eq 'src/app.js' -and $check.Content -notmatch '/box-sample/api/status') { throw 'Frontend API prefix missing.' }
}
Write-Host "VERIFIED: $public (page, API, JavaScript)."
Write-Host 'Caddy API changes are autosaved; reboot persistence requires Caddy to resume that configuration.'
} catch {
if ($created) {
# Remove only this newly inserted route; never restore over unrelated changes.
try { Invoke-WebRequest -UseBasicParsing -Method Delete -Uri "$admin/id/$id" | Out-Null } catch { Write-Warning 'Could not roll back this deployment route.' }
}
Write-Error $_
exit 1
}
@@ -0,0 +1,13 @@
@echo off
setlocal
echo Connecting /box-sample without rebuilding or deleting model data.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure-caddy.ps1"
if errorlevel 1 goto failed
start "" "http://10.14.12.22/box-sample/"
echo SUCCESS: service address verified.
pause
exit /b 0
:failed
echo FAILED: Please send the error shown above. No model data was deleted.
pause
exit /b 1
@@ -0,0 +1,60 @@
"""Import an owner-authorized ChatGPT login over stdin into a private volume.
Never print credentials or replace a refreshed server login with an old copy.
This script has no HTTP endpoint and is invoked by the server operator only.
"""
import json
import os
from pathlib import Path
import sys
import tempfile
LIMIT=1024*1024
def parse_auth(payload):
if not payload or len(payload)>LIMIT: raise ValueError('Invalid AI authentication file size')
try: value=json.loads(payload)
except (ValueError,UnicodeError): raise ValueError('Invalid AI authentication JSON') from None
if not isinstance(value,dict) or value.get('auth_mode') not in (None,'chatgpt'):
raise ValueError('ChatGPT authentication is required')
tokens=value.get('tokens')
if not isinstance(tokens,dict) or any(not isinstance(tokens.get(k),str) or not tokens[k].strip()
for k in ('access_token','refresh_token','id_token','account_id')):
raise ValueError('Incomplete ChatGPT authentication')
return value
def install(payload, directory):
incoming=parse_auth(payload); directory=Path(directory)
if directory.is_symlink(): raise ValueError('AI authentication directory must not be a symlink')
directory.mkdir(mode=0o700,parents=True,exist_ok=True);directory.chmod(0o700)
target=directory/'auth.json'
if target.is_symlink(): raise ValueError('AI authentication file must not be a symlink')
if target.exists():
existing=parse_auth(target.read_bytes())
if existing['tokens']['account_id']!=incoming['tokens']['account_id']:
raise ValueError('A different server account exists; it was not overwritten')
target.chmod(0o600)
return 'Existing server login preserved (including refreshed credentials)'
descriptor,temporary=tempfile.mkstemp(prefix='.auth-import-',dir=directory)
try:
with os.fdopen(descriptor,'wb') as stream:
stream.write(json.dumps(incoming).encode());stream.flush();os.fsync(stream.fileno())
try: os.link(temporary,target)
except FileExistsError: return install(payload,directory)
finally: Path(temporary).unlink(missing_ok=True)
return 'ChatGPT login installed in the private server volume'
def main():
try:
print(install(sys.stdin.buffer.read(LIMIT+1),Path.home()/'.codex'))
except (ValueError,OSError) as error:
# Messages are fixed strings, never JSON, token values or stdin.
print(str(error) if isinstance(error,ValueError) else 'Cannot install AI authentication',file=sys.stderr)
return 1
return 0
if __name__=='__main__':sys.exit(main())
@@ -0,0 +1,46 @@
"""Install only the same pinned DWG decoder used on the verified local host.
Build-time only. No drawings, credentials or model generation are involved.
"""
import hashlib
from pathlib import Path
import platform
import shutil
import tarfile
import tempfile
import urllib.request
URL = ('https://conda.anaconda.org/conda-forge/linux-64/'
'libredwg-0.11.3876-py38pl5321hf1fe3a4_2.tar.bz2')
SHA256 = '23e82c10d5e09b1444ca099c3710893f363d67a98417b0b69afd7155b9759f21'
def install(destination=Path('/opt/libredwg')):
if platform.machine() not in ('x86_64', 'AMD64'):
raise RuntimeError('This pinned converter requires a linux/amd64 Docker host')
members = {'bin/dwg2dxf': 'bin/dwg2dxf',
'lib/libredwg.so.0.0.11': 'lib/libredwg.so.0.0.11',
'info/licenses/COPYING': 'licenses/COPYING',
'info/recipe/meta.yaml': 'licenses/package-source.yaml'}
with tempfile.TemporaryDirectory(prefix='dwg-runtime-') as tmp:
archive = Path(tmp) / 'runtime.tar.bz2'
with urllib.request.urlopen(URL, timeout=120) as response, archive.open('wb') as out:
shutil.copyfileobj(response, out)
with archive.open('rb') as stream:
if hashlib.file_digest(stream, 'sha256').hexdigest() != SHA256:
raise ValueError('DWG runtime checksum mismatch')
with tarfile.open(archive) as bundle:
for source, relative in members.items():
member = bundle.getmember(source)
if not member.isfile():
raise ValueError('Expected regular DWG runtime file')
target = destination / relative
target.parent.mkdir(parents=True, exist_ok=True)
with bundle.extractfile(member) as inp, target.open('xb') as out:
shutil.copyfileobj(inp, out)
target.chmod(0o755 if relative.startswith(('bin/', 'lib/')) else 0o644)
(destination / 'lib/libredwg.so.0').symlink_to('libredwg.so.0.0.11')
if __name__ == '__main__':
install()
@@ -0,0 +1,84 @@
"""Deployment-only HTTP prefix adapter; modeling code remains unchanged."""
import http.client
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import re
import subprocess
import sys
from urllib.parse import urlsplit
PREFIX = '/box-sample'
UPSTREAM_PORT = 8097
HOP = {'connection', 'transfer-encoding', 'keep-alive', 'upgrade',
'proxy-authenticate', 'proxy-authorization', 'te', 'trailer'}
ROOT_URL = re.compile(rb'''(["'`])/(api/|src/|dxf_rasterizer\.html|model-report\.html)''')
def rewrite(payload):
return ROOT_URL.sub(lambda m: m[1] + PREFIX.encode() + b'/' + m[2], payload)
class Gateway(BaseHTTPRequestHandler):
def forward(self):
parsed = urlsplit(self.path)
if parsed.path == PREFIX:
self.send_response(308)
self.send_header('Location', PREFIX + '/' + ('?' + parsed.query if parsed.query else ''))
self.send_header('Content-Length', '0')
self.end_headers()
return
if not parsed.path.startswith(PREFIX + '/'):
self.send_error(404)
return
if self.headers.get('Transfer-Encoding'):
self.send_error(411, 'Content-Length required')
return
target = self.path[len(PREFIX):]
connection = http.client.HTTPConnection('127.0.0.1', UPSTREAM_PORT, timeout=600)
try:
size = int(self.headers.get('Content-Length', '0'))
if size < 0 or size > 512 * 1024 * 1024:
self.send_error(413)
return
headers = {k: v for k, v in self.headers.items()
if k.lower() not in HOP | {'accept-encoding', 'host'}}
headers['Host'] = '127.0.0.1:' + str(UPSTREAM_PORT)
connection.request(self.command, target, self.rfile.read(size) if size else None, headers)
response = connection.getresponse()
content_type = response.getheader('Content-Type', '').lower()
text = any(t in content_type for t in ('json', 'javascript', 'text/html', 'text/css'))
body = rewrite(response.read()) if text and self.command != 'HEAD' else None
self.send_response(response.status)
for key, value in response.getheaders():
if key.lower() in HOP | {'content-length', 'etag', 'content-md5'}:
continue
if key.lower() == 'location' and value.startswith('/') and not value.startswith('//'):
value = PREFIX + value
self.send_header(key, value)
self.send_header('X-DMS-Deployment', 'box-sample')
self.send_header('Cache-Control', 'no-store')
if body is not None:
self.send_header('Content-Length', str(len(body)))
elif response.getheader('Content-Length') is not None:
self.send_header('Content-Length', response.getheader('Content-Length'))
self.end_headers()
if self.command != 'HEAD':
if body is not None:
self.wfile.write(body)
else:
while chunk := response.read(65536):
self.wfile.write(chunk)
except (ValueError, OSError, http.client.HTTPException):
self.send_error(502, 'Application not ready')
finally:
connection.close()
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = forward
if __name__ == '__main__':
child = subprocess.Popen([sys.executable, 'src/backend/serve.py', '--host', '127.0.0.1', '--port', str(UPSTREAM_PORT)])
try:
ThreadingHTTPServer(('0.0.0.0', 8080), Gateway).serve_forever()
finally:
child.terminate()
child.wait(timeout=15)
@@ -0,0 +1,16 @@
$ErrorActionPreference = 'Stop'
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Join-Path $PSScriptRoot 'configure-caddy.ps1'), [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw ($errors | Out-String) }
$fn = $ast.Find({param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true)
Invoke-Expression $fn.Extent.Text
$good = '{"handle":[{"handler":"subroute","routes":[{"match":[{"path":["/"]}],"handle":[{"handler":"static_response"}]},{"handle":[{"handler":"static_response"}]}]}]}' | ConvertFrom-Json
if (-not (Test-StaticFallback @($good))) { throw 'Static fallback rejected' }
$good.handle[0].routes[1].handle[0].handler = 'authentication'
if (Test-StaticFallback @($good)) { throw 'Authentication fallback accepted' }
$good.handle[0].routes[1].handle[0].handler = 'reverse_proxy'
if (Test-StaticFallback @($good)) { throw 'Proxy fallback accepted' }
if (Test-StaticFallback @()) { throw 'Empty fallback accepted' }
Write-Output 'PASS: syntax, nested static fallback, authentication/proxy/empty guards'
@@ -0,0 +1,43 @@
import json
from pathlib import Path
import tempfile
import unittest
from install_ai_auth import install,parse_auth
def sample(account='owner',refresh='fake-refresh'):
return json.dumps({'auth_mode':'chatgpt','tokens':{'account_id':account,
'access_token':'fake-access','id_token':'fake-id','refresh_token':refresh}}).encode()
class ImportTests(unittest.TestCase):
def test_install_private_file_without_printing_credentials(self):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)/'login';message=install(sample(),root)
self.assertNotIn('fake',message)
self.assertEqual((root/'auth.json').stat().st_mode & 0o777,0o600)
self.assertEqual(root.stat().st_mode & 0o777,0o700)
self.assertEqual(parse_auth((root/'auth.json').read_bytes()),parse_auth(sample()))
def test_refresh_preserved_and_different_account_not_overwritten(self):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)/'login';install(sample(refresh='newer-refresh'),root)
before=(root/'auth.json').read_bytes();install(sample(),root)
self.assertEqual((root/'auth.json').read_bytes(),before)
with self.assertRaises(ValueError):install(sample('different'),root)
self.assertEqual((root/'auth.json').read_bytes(),before)
def test_invalid_inputs_do_not_create_auth_directory(self):
for payload in (b'',b'not json',b'[]',b'{}',b'x'*(1024*1024+1)):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)/'login'
with self.assertRaises(ValueError):install(payload,root)
self.assertFalse(root.exists())
def test_symlink_is_rejected(self):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary);(root/'link').symlink_to(root/'elsewhere')
with self.assertRaises(ValueError):install(sample(),root/'link')
if __name__=='__main__':unittest.main()
@@ -0,0 +1,21 @@
$ErrorActionPreference='Stop'
$launcher=Get-Content (Join-Path $PSScriptRoot '../deploy-server.bat')
$line=@($launcher | Where-Object { $_ -like 'powershell *' -and $_ -like '*$lines=@(& git*' })
if ($line.Count -ne 1) { throw 'Origin verification command missing' }
$command=$line[0].Substring($line[0].IndexOf('-Command "')+10).TrimEnd('"')
$env:DMS_REPOSITORY='https://gitea.hmac.kr/is-div/design-model-box-sample_2.git'
$env:DMS_DEPLOY_SOURCE='C:\folder with spaces\deploy-source'
$cases=@(
@{url=$env:DMS_REPOSITORY;code=0;expected=0},
@{url=($env:DMS_REPOSITORY+"`r`n");code=0;expected=0},
@{url='https://example.invalid/other.git';code=0;expected=4},
@{url='';code=128;expected=2}
)
foreach ($case in $cases) {
$env:TEST_ORIGIN=$case.url
$env:TEST_GIT_CODE=[string]$case.code
$mock='function git { $global:LASTEXITCODE=[int]$env:TEST_GIT_CODE; if ($env:TEST_ORIGIN) { $env:TEST_ORIGIN } }; '
& (Join-Path $PSHOME 'pwsh') -NoProfile -Command ($mock+$command)
if ($LASTEXITCODE -ne $case.expected) { throw "Unexpected exit: $LASTEXITCODE" }
}
Write-Host 'PASS: exact origin, line endings, different repository, Git error'
@@ -0,0 +1,50 @@
"""Offline container smoke check: seed cache, filename normalization and DWG decode.
Run with a read-only existing DWG mounted at /test-input.dwg. No AI calls.
"""
import hashlib
import json
from pathlib import Path
import re
import sys
import tempfile
sys.path.insert(0, '/app/src/backend')
from shared_cache_seed import install_bundle
from model_snapshot_lookup import job_drawings, lookup
from dwg_conversion import convert_dwg
def main():
source = Path('/test-input.dwg')
raw_hash = hashlib.sha256(source.read_bytes()).hexdigest()
with tempfile.TemporaryDirectory(prefix='packaged-dwg-test-') as tmp:
data = Path(tmp)
install_bundle(Path('/app/shared-cache/selected-models.zip'), data)
jobs = data / '8097-jobs'
matches = []
for file in jobs.glob('*.json'):
job = json.loads(file.read_text())
drawings = job_drawings(job, data / 'uploads')
hit = lookup(jobs, drawings, data / 'uploads')
assert hit and hit['jobId'] == file.stem
renamed = [dict(row, name=re.sub(r'_(\d\d)_', r'(\1)', row['name'])) for row in drawings]
assert lookup(jobs, renamed, data / 'uploads')['jobId'] == file.stem
if len(drawings) > 1:
assert lookup(jobs, renamed[:-1], data / 'uploads') is None
changed = [dict(row) for row in drawings]
changed[0]['sha256'] = 'f' * 64
assert lookup(jobs, changed, data / 'uploads') is None
matches.extend(row for row in drawings if row['sha256'] == raw_hash)
assert matches, 'Input DWG does not correspond to a retained model'
audit = convert_dwg(source, data / 'decoded.dxf')
expected = matches[0]['conversionAudit']
assert audit['source_sha256'] == raw_hash
assert audit['entity_count'] == expected['entity_count'], (audit['entity_count'], expected['entity_count'])
print(json.dumps({'cached_models_checked': len(list(jobs.glob('*.json'))),
'dwg_entity_count': audit['entity_count'],
'dxf_bytes_equal_to_saved_conversion': audit['output_sha256'] == expected['output_sha256'],
'ai_calls': 0}))
if __name__ == '__main__':
main()
@@ -0,0 +1,66 @@
import http.client
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import threading
import unittest
import subpath_gateway as gateway
class Backend(BaseHTTPRequestHandler):
def do_GET(self):
if self.path=='/api/status':
data=b'{"ok":true,"asset":"/api/uploads/file","unchanged":"/tmp/model.json"}'
mime='application/json'
elif self.path=='/src/app.js':
data=b"fetch('/api/status'); frame.src=`/dxf_rasterizer.html?file=x`;"
mime='application/javascript'
elif self.path=='/api/uploads/file':data=b'\x00\xff\x01';mime='application/octet-stream'
else:data=b'<link href="styles.css">';mime='text/html'
self.send_response(200);self.send_header('Content-Type',mime)
self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
def do_POST(self):
body=self.rfile.read(int(self.headers['Content-Length']))
self.send_response(200);self.send_header('Content-Length',str(len(body)))
self.end_headers();self.wfile.write(body)
def log_message(self,*args):pass
class GatewayTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.backend=ThreadingHTTPServer(('127.0.0.1',0),Backend)
gateway.UPSTREAM_PORT=cls.backend.server_port
cls.proxy=ThreadingHTTPServer(('127.0.0.1',0),gateway.Gateway)
for s in (cls.backend,cls.proxy):threading.Thread(target=s.serve_forever,daemon=True).start()
@classmethod
def tearDownClass(cls):
for s in (cls.proxy,cls.backend):s.shutdown();s.server_close()
def request(self,path,method='GET',body=None):
c=http.client.HTTPConnection('127.0.0.1',self.proxy.server_port)
c.request(method,path,body);r=c.getresponse();result=(r.status,dict(r.getheaders()),r.read());c.close();return result
def test_mount_redirect_preserves_job(self):
code,headers,_=self.request('/box-sample?job=abc')
self.assertEqual(code,308);self.assertEqual(headers['Location'],'/box-sample/?job=abc')
def test_api_and_javascript_prefix_without_model_path_change(self):
_,headers,body=self.request('/box-sample/api/status')
self.assertIn(b'/box-sample/api/uploads/file',body);self.assertIn(b'/tmp/model.json',body)
self.assertEqual(int(headers['Content-Length']),len(body))
_,_,script=self.request('/box-sample/src/app.js')
self.assertIn(b"fetch('/box-sample/api/status')",script)
self.assertIn(b'/box-sample/dxf_rasterizer.html',script)
def test_binary_and_post_untouched(self):
self.assertEqual(self.request('/box-sample/api/uploads/file')[2],b'\x00\xff\x01')
self.assertEqual(self.request('/box-sample/api/analyze','POST',b'{"drawing":"/api/original"}')[2],b'{"drawing":"/api/original"}')
def test_other_service_paths_rejected(self):
for path in ('/api/status','/box-sample-other/api/status'):
self.assertEqual(self.request(path)[0],404)
if __name__=='__main__':unittest.main()