feat(workflow): 다중 운영자 — 레지스트리 + decidedBy 신원 강제 - #57
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
root(서버 토큰)는 관리 전용으로 분리하고, 결정·조회(pending 목록, decide, history, chain)와 WS 전체 스트림은 개별 운영자 토큰으로 수행한다. 엄격 모드에선 decidedBy가 토큰 신원(root/operatorId)으로 강제되어 body 위조가 불가능하다. revoke 시 해당 운영자 소켓 4401 즉시 종료 + 토큰 무효화, sha256 해시 저장·재등록 토큰 회전·영속화는 actor 레지스트리 패턴 그대로. 기존 history 테스트는 신원 강제에 맞게 기대값(root) 갱신. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 24a064dc9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| sendJson(res, error?.code === 'BODY_TOO_LARGE' ? 413 : 400, { error: error?.code || 'Invalid JSON body' }); | ||
| return; | ||
| } | ||
| const registered = registerOperator(data); |
There was a problem hiding this comment.
Close old operator sockets when rotating tokens
When this register endpoint is used as the documented upsert/token-rotation path for an existing operator, the old token is invalidated for future HTTP auth but any WebSocket that already authenticated with that old token stays isAuthorized and continues receiving the full operator stream because only the revoke route calls closeOperatorSockets. In strict mode this means rotating a leaked operator token does not actually remove live access until the stale socket disconnects, so the existing operator's sockets should be closed as part of rotation.
Useful? React with 👍 / 👎.
| const id = sanitizeText(operatorId, 80); | ||
| if (!id) return null; |
There was a problem hiding this comment.
Reject the reserved root operator id
Because root is the server-token identity, allowing an operator to register with operatorId: 'root' makes that operator's decisions audit as decidedBy: 'root' and also collides with server-token WebSocket bookkeeping when /api/operators/root/revoke closes operator sockets by id. In strict mode this breaks the identity boundary and can close root dashboard sockets, so the registry should reserve/reject this id.
Useful? React with 👍 / 👎.
요약
마지막 예약 스펙 구현. 스펙:
docs/superpowers/specs/2026-08-04-workflow-multi-operator-design.mdPOST /api/operators/register(upsert=토큰 회전, sha256 해시 저장),GET /api/operators,POST /api/operators/:id/revoke.decidedBy를 토큰 신원(root/operatorId)으로 강제 — body 위조 무시, 이력에 실제 결정자가 남는다. open 모드는 현행 유지.WORKFLOW_AUTH로 전체 스트림 구독(AUTH_OK에 operatorId), revoke 시 소켓 4401(OPERATOR_REVOKED) 즉시 종료.테스트
신규 4건(신원 강제·위조 무시 / 관리 라우트 root 전용 / WS 전체 스트림+revoke 4401·root 소켓 유지 / 재시작 후 토큰 유효) — 전체 서버 32 + UI 29 통과.
경계
workflow/+ docs만 수정. RBAC/승인선은 후속.🤖 Generated with Claude Code