feat: select workspace by name via /workspaces/lookup

Add workspace_name input; the runner resolves it to a workspace_id through
GET {api_base}/workspaces/lookup?user_id=... before uploading the agent.
workspace_id remains as an explicit override. Name matching is NFC-normalized
(handles Korean NFD/NFC) with a case-insensitive fallback, and a no-match
error lists the available workspace names. Requires one of name/id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:12:04 +09:00
co-authored by Claude Opus 4.8
parent 1f34c9186f
commit 6b8cff4556
2 changed files with 92 additions and 7 deletions
+14 -5
View File
@@ -21,9 +21,11 @@
# 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000).
#
# 실행 흐름 (SKILL.md §0.6 경로 A):
# upload-agent 등록 → SSE start → stage_complete 자동 confirm → execution_complete
# workspaces/lookup(이름→UUID) → upload-agent 등록 → SSE start →
# stage_complete 자동 confirm → execution_complete
# stage_error 발생 시 세션을 cancel 하고 실패 처리한다.
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
# ※ workspace_name(작업실 이름) 또는 workspace_id(UUID) 중 하나는 반드시 지정.
#
# 실행 시간 상한 (주의):
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
@@ -48,10 +50,15 @@ on:
required: false
default: 'jsahn'
type: string
workspace_id:
description: 'AgentBackend workspace_id'
workspace_name:
description: '작업실 이름 (예: 팬아웃 테스트). GET /workspaces/lookup 으로 UUID 자동 해석'
required: false
default: 'e1d0baed-cec9-44af-9989-b7aab09bcfd1'
default: ''
type: string
workspace_id:
description: '작업실 UUID 직접 지정(override). 비우면 workspace_name 으로 해석'
required: false
default: ''
type: string
user_input:
description: 'Agent 에 전달할 user_input'
@@ -95,16 +102,18 @@ jobs:
YAML_PATH: ${{ inputs.yaml_path }}
API_BASE: ${{ inputs.api_base }}
AGENT_USER_ID: ${{ inputs.user_id }}
AGENT_WORKSPACE_NAME: ${{ inputs.workspace_name }}
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
AGENT_USER_INPUT: ${{ inputs.user_input }}
START_STAGE_INDEX: ${{ inputs.start_stage_index }}
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
# '=' 형식 필수: user_input 이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
# '=' 형식 필수: 값이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
run: |
python scripts/run_agent_api.py \
--yaml-path="$YAML_PATH" \
--api-base="$API_BASE" \
--user-id="$AGENT_USER_ID" \
--workspace-name="$AGENT_WORKSPACE_NAME" \
--workspace-id="$AGENT_WORKSPACE_ID" \
--user-input="$AGENT_USER_INPUT" \
--start-stage-index="$START_STAGE_INDEX" \
+78 -2
View File
@@ -3,6 +3,7 @@
Upload an Agent YAML to AgentBackend and execute it via SSE.
Flow (SKILL.md §0.6 — 경로 A):
0. GET {api_base}/workspaces/lookup?user_id=... (workspace_name → UUID 해석)
1. POST {api_base}/upload-agent?user_id=... (multipart YAML 등록)
2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
@@ -25,6 +26,7 @@ import os
import signal
import sys
import time
import unicodedata
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import quote
@@ -58,6 +60,7 @@ class AgentRunner:
self.yaml_path = Path(args.yaml_path)
self.user_id = args.user_id
self.workspace_id = args.workspace_id
self.workspace_name = args.workspace_name
self.user_input = args.user_input
self.start_stage_index = args.start_stage_index
self.read_timeout = args.read_timeout
@@ -84,6 +87,74 @@ class AgentRunner:
def _remaining(self) -> float:
return self.deadline - time.monotonic()
# ------------------------------------------------------------- workspace
def resolve_workspace(self) -> None:
"""workspace_id 를 확정한다.
- workspace_id 가 주어지면(override) 그대로 사용.
- 아니면 workspace_name 을 GET /workspaces/lookup 으로 UUID 해석.
둘 다 없으면 SystemExit.
"""
if self.workspace_id:
log(f"Using explicit workspace_id: {self.workspace_id}")
return
if not self.workspace_name:
raise SystemExit("ERROR: workspace_name 또는 workspace_id 중 하나를 지정해야 합니다.")
log(f"Resolving workspace by name: {self.workspace_name!r} (user_id={self.user_id})")
try:
resp = self.api_client.get(
f"{self.api_base}/workspaces/lookup",
params={"user_id": self.user_id},
)
except httpx.HTTPError as exc:
raise SystemExit(f"ERROR: workspaces/lookup 요청 실패: {exc}")
if resp.status_code != 200:
raise SystemExit(f"ERROR: workspaces/lookup HTTP {resp.status_code}: {resp.text[:500]}")
try:
items = resp.json().get("workspaces", [])
except json.JSONDecodeError:
raise SystemExit(f"ERROR: workspaces/lookup 응답이 JSON 이 아님: {resp.text[:300]}")
def norm(s: object) -> str:
return unicodedata.normalize("NFC", (s if isinstance(s, str) else "").strip())
def field(w: dict, *keys: str) -> str:
for k in keys:
v = w.get(k)
if v:
return v
return ""
target = norm(self.workspace_name)
names, exact, ci = [], [], []
for w in items:
name = field(w, "name", "workspace_name", "title")
wid = field(w, "workspace_id", "id")
names.append(name)
if not wid:
continue
if norm(name) == target:
exact.append(wid)
elif norm(name).lower() == target.lower():
ci.append(wid)
chosen = exact if exact else ci
if len(chosen) == 1:
self.workspace_id = chosen[0]
log(f"Resolved workspace {self.workspace_name!r} -> {self.workspace_id}")
elif len(chosen) > 1:
raise SystemExit(
f"ERROR: 워크스페이스 이름 {self.workspace_name!r} 이 여러 개 매칭됩니다: {chosen}"
)
else:
available = ", ".join(repr(n) for n in names) or "(없음)"
raise SystemExit(
f"ERROR: {self.workspace_name!r} 에 해당하는 워크스페이스를 찾지 못했습니다. "
f"사용 가능: {available}"
)
# ------------------------------------------------------------------ upload
def upload(self) -> None:
@@ -269,6 +340,7 @@ class AgentRunner:
def run(self) -> int:
try:
self.resolve_workspace()
self.upload()
mode = "start"
@@ -349,7 +421,8 @@ class AgentRunner:
f"| Agent | `{self.agent_name or '-'}` |",
f"| YAML | `{self.yaml_path}` |",
f"| Session | `{self.session_id or '-'}` |",
f"| user_id / workspace_id | `{self.user_id}` / `{self.workspace_id}` |",
f"| workspace | `{self.workspace_name or '-'}` (`{self.workspace_id or '-'}`) |",
f"| user_id | `{self.user_id}` |",
f"| 최종 상태 | {status_icon} `{self.final_status}` |",
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
f"| 확인(confirm)한 stage 수 | {self.stages_confirmed} |",
@@ -374,7 +447,10 @@ def parse_args() -> argparse.Namespace:
p.add_argument("--yaml-path", required=True, help="실행할 Agent YAML 경로 (repo 상대/절대)")
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
p.add_argument("--user-id", default="jsahn")
p.add_argument("--workspace-id", default="e1d0baed-cec9-44af-9989-b7aab09bcfd1")
p.add_argument("--workspace-name", default="",
help="작업실 이름. GET /workspaces/lookup 으로 UUID 해석 (workspace-id 미지정 시)")
p.add_argument("--workspace-id", default="",
help="작업실 UUID 직접 지정(override). 지정 시 workspace-name 무시")
p.add_argument("--user-input", default="")
p.add_argument("--start-stage-index", type=int, default=0)
p.add_argument("--output-dir", default="agent_run_output")