From 6b8cff4556d392d6fbfaf32a8a002dc79fd1a249 Mon Sep 17 00:00:00 2001 From: jsahn Date: Tue, 7 Jul 2026 16:12:04 +0900 Subject: [PATCH] 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) --- .gitea/workflows/run-agent.yml | 19 +++++--- scripts/run_agent_api.py | 80 +++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/run-agent.yml b/.gitea/workflows/run-agent.yml index 3845b71b..d4b62be1 100644 --- a/.gitea/workflows/run-agent.yml +++ b/.gitea/workflows/run-agent.yml @@ -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" \ diff --git a/scripts/run_agent_api.py b/scripts/run_agent_api.py index 670542ed..71a23bf4 100644 --- a/scripts/run_agent_api.py +++ b/scripts/run_agent_api.py @@ -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")