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). # 그러면 api_base = http://agent-backend:8000 (container_name, 내부 포트 8000).
# #
# 실행 흐름 (SKILL.md §0.6 경로 A): # 실행 흐름 (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 하고 실패 처리한다. # stage_error 발생 시 세션을 cancel 하고 실패 처리한다.
# 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다. # 전체 이벤트 로그 / final_output / summary 는 아티팩트로 저장된다.
# ※ workspace_name(작업실 이름) 또는 workspace_id(UUID) 중 하나는 반드시 지정.
# #
# 실행 시간 상한 (주의): # 실행 시간 상한 (주의):
# Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이 # Gitea 의 [actions] ENDLESS_TASK_TIMEOUT 과 act_runner 의 runner.timeout 기본값이
@@ -48,10 +50,15 @@ on:
required: false required: false
default: 'jsahn' default: 'jsahn'
type: string type: string
workspace_id: workspace_name:
description: 'AgentBackend workspace_id' description: '작업실 이름 (예: 팬아웃 테스트). GET /workspaces/lookup 으로 UUID 자동 해석'
required: false required: false
default: 'e1d0baed-cec9-44af-9989-b7aab09bcfd1' default: ''
type: string
workspace_id:
description: '작업실 UUID 직접 지정(override). 비우면 workspace_name 으로 해석'
required: false
default: ''
type: string type: string
user_input: user_input:
description: 'Agent 에 전달할 user_input' description: 'Agent 에 전달할 user_input'
@@ -95,16 +102,18 @@ jobs:
YAML_PATH: ${{ inputs.yaml_path }} YAML_PATH: ${{ inputs.yaml_path }}
API_BASE: ${{ inputs.api_base }} API_BASE: ${{ inputs.api_base }}
AGENT_USER_ID: ${{ inputs.user_id }} AGENT_USER_ID: ${{ inputs.user_id }}
AGENT_WORKSPACE_NAME: ${{ inputs.workspace_name }}
AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }} AGENT_WORKSPACE_ID: ${{ inputs.workspace_id }}
AGENT_USER_INPUT: ${{ inputs.user_input }} AGENT_USER_INPUT: ${{ inputs.user_input }}
START_STAGE_INDEX: ${{ inputs.start_stage_index }} START_STAGE_INDEX: ${{ inputs.start_stage_index }}
MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }} MAX_RUNTIME_SECONDS: ${{ inputs.max_runtime_seconds }}
# '=' 형식 필수: user_input 이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식 # '=' 형식 필수: 값이 '-' 로 시작하는 자유 텍스트여도 argparse 가 값으로 인식
run: | run: |
python scripts/run_agent_api.py \ python scripts/run_agent_api.py \
--yaml-path="$YAML_PATH" \ --yaml-path="$YAML_PATH" \
--api-base="$API_BASE" \ --api-base="$API_BASE" \
--user-id="$AGENT_USER_ID" \ --user-id="$AGENT_USER_ID" \
--workspace-name="$AGENT_WORKSPACE_NAME" \
--workspace-id="$AGENT_WORKSPACE_ID" \ --workspace-id="$AGENT_WORKSPACE_ID" \
--user-input="$AGENT_USER_INPUT" \ --user-input="$AGENT_USER_INPUT" \
--start-stage-index="$START_STAGE_INDEX" \ --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. Upload an Agent YAML to AgentBackend and execute it via SSE.
Flow (SKILL.md §0.6 — 경로 A): 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 등록) 1. POST {api_base}/upload-agent?user_id=... (multipart YAML 등록)
2. POST {api_base}/sse/agent/{name}/start (SSE 실행) 2. POST {api_base}/sse/agent/{name}/start (SSE 실행)
3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함) 3. stage_complete 이벤트 수신 시 자동 confirm (CI 무인 실행, 재시도 포함)
@@ -25,6 +26,7 @@ import os
import signal import signal
import sys import sys
import time import time
import unicodedata
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
@@ -58,6 +60,7 @@ class AgentRunner:
self.yaml_path = Path(args.yaml_path) self.yaml_path = Path(args.yaml_path)
self.user_id = args.user_id self.user_id = args.user_id
self.workspace_id = args.workspace_id self.workspace_id = args.workspace_id
self.workspace_name = args.workspace_name
self.user_input = args.user_input self.user_input = args.user_input
self.start_stage_index = args.start_stage_index self.start_stage_index = args.start_stage_index
self.read_timeout = args.read_timeout self.read_timeout = args.read_timeout
@@ -84,6 +87,74 @@ class AgentRunner:
def _remaining(self) -> float: def _remaining(self) -> float:
return self.deadline - time.monotonic() 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 # ------------------------------------------------------------------ upload
def upload(self) -> None: def upload(self) -> None:
@@ -269,6 +340,7 @@ class AgentRunner:
def run(self) -> int: def run(self) -> int:
try: try:
self.resolve_workspace()
self.upload() self.upload()
mode = "start" mode = "start"
@@ -349,7 +421,8 @@ class AgentRunner:
f"| Agent | `{self.agent_name or '-'}` |", f"| Agent | `{self.agent_name or '-'}` |",
f"| YAML | `{self.yaml_path}` |", f"| YAML | `{self.yaml_path}` |",
f"| Session | `{self.session_id or '-'}` |", 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"| 최종 상태 | {status_icon} `{self.final_status}` |",
f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |", f"| 소요 시간 | {elapsed // 60}m {elapsed % 60}s (상한 {int(self.max_runtime) // 60}m) |",
f"| 확인(confirm)한 stage 수 | {self.stages_confirmed} |", 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("--yaml-path", required=True, help="실행할 Agent YAML 경로 (repo 상대/절대)")
p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api") p.add_argument("--api-base", default="https://legalpoc.eroomai.com/api")
p.add_argument("--user-id", default="jsahn") 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("--user-input", default="")
p.add_argument("--start-stage-index", type=int, default=0) p.add_argument("--start-stage-index", type=int, default=0)
p.add_argument("--output-dir", default="agent_run_output") p.add_argument("--output-dir", default="agent_run_output")