# Workspace descriptor

> sibling 저장소 인벤토리, role·capabilities·prohibited, master_seat, workspace-descriptor-check 액션과 해석 순서.

- Repository: local/mogui-ADE-orchestrator

- Human docs: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac
- Complete Markdown: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac/llms-full.txt

## Source Files

- `config/workspace-descriptor.example.json`
- `src/master_runtime/core/workspace_descriptor.py`
- `scripts/workspace-descriptor-check`
- `docs/public/orca-concepts.md`
- `tests/test_workspace_descriptor.py`
- `master-ops/onboarding/02-workspace-facts.md`

---

---
title: "Workspace descriptor"
description: "sibling 저장소 인벤토리, role·capabilities·prohibited, master_seat, workspace-descriptor-check 액션과 해석 순서."
---

Workspace descriptor는 워크스페이스 루트 아래 sibling 저장소의 **선언적 인벤토리**다. 로더는 `src/master_runtime/core/workspace_descriptor.py`에 있고, 공개 가드 CLI는 `scripts/workspace-descriptor-check`다. 템플릿은 `config/workspace-descriptor.example.json`만 배송하며, 인스턴스 파일은 온보딩이 쓰는 `config/workspace-descriptor.json`(커밋하지 않음)이다. 소비자는 하드코딩된 product 경로 목록 대신 이 파일의 `prohibited`를 읽는다.

<Info>
워크스페이스 루트는 **plain folder**다. git parent가 아니며 submodule parent도 아니다. Orca가 해당 폴더에 "not a valid worktree folder"류 라벨을 붙여도 folder project 기준으로 정상이다.
</Info>

## 역할과 경계

| 표면 | 경로·식별자 | 책임 |
| --- | --- | --- |
| 스키마·로더 | `master_runtime.core.workspace_descriptor` | 해석 순서, 검증, 경로 매칭, `is_prohibited` |
| 예제(템플릿) | `config/workspace-descriptor.example.json` | 필드 문서(`_docs`)와 예시 인벤토리 |
| 인스턴스 파일 | `config/workspace-descriptor.json` | 온보딩이 측정·기록한 실측 인벤토리 |
| CLI | `scripts/workspace-descriptor-check` | path×action 금지 여부 질의 |
| 작성 절차 | `master-ops/onboarding/02-workspace-facts.md` | REPO_LIST 확정 후 파일 착지 |
| 운영 규칙 | `master-ops/docs/charter/04-worker-routing-review.md` | 디스패치 전 금지 액션 측정 |

로더는 기본 인벤토리를 **발명하지 않는다**. 파일이 없으면 `source_path=None`, `repositories=()`인 honest unconfigured다.

## 해석 순서

경로 결정은 `resolve_config_path` / `load_workspace_descriptor`와 CLI가 공유한다.

1. **명시 경로** — CLI `--config` 또는 API `path=` 인자 (env보다 우선)
2. **환경 변수** — `WORKSPACE_DESCRIPTOR`, 없으면 `MOGUI_WORKSPACE_DESCRIPTOR` (비어 있지 않은 값만)
3. **기본 파일** — `<repo>/config/workspace-descriptor.json`
4. **unconfigured** — 파일이 없으면 에러 없이 빈 디스크립터 (`source_path is None`)

```text
--config  >  WORKSPACE_DESCRIPTOR  >  MOGUI_WORKSPACE_DESCRIPTOR
         >  config/workspace-descriptor.json  >  unconfigured
```

`action_is_prohibited`와 CLI는 unconfigured일 때 `WorkspaceDescriptorError`를 내고, CLI exit **2**로 판정 불가를 표시한다. 권한을 추정하지 않는다.

## JSON 스키마

루트는 JSON object다. 키가 `_`로 시작하면 로더가 무시한다(`_docs` 등 문서 전용 필드).

### 워크스페이스 레벨

<ParamField body="workspace_root_is_plain_folder" type="boolean" required>
기본값 `true`. `true`가 아니면 파싱 실패. submodule parent·비 plain 루트를 거부한다.
</ParamField>

<ParamField body="workspace_root" type="string | null">
확인된 워크스페이스 루트 **절대 경로**. 절대 path 조회 시 이 루트 아래 relative remainder로만 매칭한다. 절대 조회를 쓰지 않으면 생략/`null` 가능.
</ParamField>

<ParamField body="master_seat" type="string">
마스터 세션 좌석 기록. 보통 multi-repo면 folder workspace of workspace root, 단일 저장소 워크스페이스면 primary worktree. 문자열이며 비어 있어도 파싱은 통과한다(온보딩 Verify는 비어 있지 않은 값을 요구).
</ParamField>

<ParamField body="repositories" type="array">
멤버 저장소 배열. `null`/생략이면 빈 튜플. `require_repositories()` 호출 시 비어 있으면 에러.
</ParamField>

### 저장소 엔트리

| 필드 | 타입 | 필수 | 규칙 |
| --- | --- | --- | --- |
| `name` | string | 예 | 비어 있지 않음. 보통 폴더 basename. 인벤토리 내 중복 불가 |
| `path` | string | 예 | 워크스페이스 루트 상대. 단일 저장소 워크스페이스면 `.`. 인벤토리 내 중복 불가 |
| `remote` | string | 아니오 | 기본 `""`. 측정된 origin URL; 없으면 빈 문자열 |
| `role` | string | 예 | **`product` \| `ops`만** 허용 |
| `capabilities` | string[] | 아니오 | 생략 시 `[]`. open set |
| `prohibited` | string[] | **예** | 누락/`null` 무효. 금지 없음은 명시적 `[]`만 |

**role**

| 값 | 의미 | 온보딩 기본 `prohibited` |
| --- | --- | --- |
| `product` | 워커가 조율하는 코드 저장소 | `["direct-main-commit", "force-push"]` |
| `ops` | 워크스페이스 거버넌스/ops 저장소 | `["force-push"]` |

**capabilities / prohibited (open set, documented floor)**

로더 상수:

- `KNOWN_CAPABILITIES`: `pr`, `dispatch-target`
- `KNOWN_PROHIBITIONS`: `direct-main-commit`, `force-push`

추가 문자열을 넣어도 스키마 범프 없이 허용된다. floor 값 밖 토큰을 거부하지 않는다. 현재 CLI/가드가 실행 시 검사하는 축은 **`prohibited` 멤버십**이다.

### 예제 페이로드

```json
{
  "workspace_root_is_plain_folder": true,
  "workspace_root": "/absolute/path/to/workspace-root",
  "master_seat": "folder-workspace-of-workspace-root",
  "repositories": [
    {
      "name": "example-product",
      "path": "example-product",
      "remote": "https://github.com/example/example-product.git",
      "role": "product",
      "capabilities": ["pr", "dispatch-target"],
      "prohibited": ["direct-main-commit", "force-push"]
    },
    {
      "name": "example-ops",
      "path": "example-ops",
      "remote": "https://github.com/example/example-ops.git",
      "role": "ops",
      "capabilities": ["pr", "dispatch-target"],
      "prohibited": ["force-push"]
    }
  ]
}
```

## 경로 매칭 (`repository_for_path`)

유일 hit가 필요하다. 모호하면 `WorkspaceDescriptorError`.

| 후보 | 매칭 조건 |
| --- | --- |
| 선언 `path`와 완전 일치 | 상대 경로, 포함 `"."` |
| 선언 `name`과 완전 일치 | 후보에 path separator 없음 (`widget` ↔ name `widget`) |
| 절대 경로 | `workspace_root`가 바인딩된 경우에만; 루트 하위 relative remainder가 선언 path와 일치 |
| 실패 | `workspace_root` 없는 절대 경로; 루트 밖 절대 경로; 비정확 multi-segment 상대 (`other/app` ↛ `services/app`) |

basename만으로 다른 parent 아래 동일 이름을 훔치지 않는다.

## 금지 판정 (`is_prohibited`)

```text
action_key = strip(action)   # 빈 문자열이면 에러
repo = repository_for_path(path)
if repo is None:
    return default_when_unknown_repo   # 기본 True = fail closed
return action_key in repo.prohibited
```

| 상황 | 기본 동작 |
| --- | --- |
| 매칭된 repo, action ∈ `prohibited` | prohibited |
| 매칭된 repo, action ∉ `prohibited` | allowed |
| 미매칭 경로 | fail closed (`default_when_unknown_repo=True`) |
| CLI `--allow-unknown-repo` | 미매칭 시 allowed (`default_when_unknown_repo=False`) |
| descriptor unconfigured | 판정 거부 (에러 / CLI exit 2) |

## CLI: `workspace-descriptor-check`

```console
$ scripts/workspace-descriptor-check \
    --path <repository-path> \
    --action <action-name> \
    [--config <descriptor.json>] \
    [--allow-unknown-repo] \
    [--json]
```

### 플래그

| 플래그 | 필수 | 설명 |
| --- | --- | --- |
| `--path` | 예 | 조회 대상 저장소 경로(절대 또는 루트 상대) |
| `--action` | 예 | 검사할 액션 (예: `direct-main-commit`, `force-push`) |
| `--config` | 아니오 | 명시 descriptor 경로 (env·기본 경로 무시) |
| `--allow-unknown-repo` | 아니오 | 미매칭 경로를 allowed로 처리 (기본은 fail closed) |
| `--json` | 아니오 | 기계 판독 decision object를 stdout에 출력 |

### Exit 코드

| 코드 | 의미 |
| --- | --- |
| `0` | ALLOWED — 매칭 repo의 `prohibited`에 action 없음 |
| `1` | PROHIBITED — action이 금지 목록에 있음 (또는 기본 fail-closed 미매칭) |
| `2` | undecidable — unconfigured, invalid JSON/UTF-8, 잘못된 스키마, 빈 action 등 |

### 출력

**텍스트 (기본)**

```text
PROHIBITED: action=direct-main-commit repo=app path=app
ALLOWED: action=pr repo=app path=app
```

**JSON (`--json`)**

성공 시:

```json
{
  "allow": false,
  "decidable": true,
  "path": "app",
  "action": "direct-main-commit",
  "prohibited": true,
  "repository": {
    "name": "app",
    "path": "app",
    "role": "product",
    "prohibited": ["direct-main-commit"]
  },
  "source_path": "/abs/path/to/workspace-descriptor.json"
}
```

판정 불가 시:

```json
{
  "allow": false,
  "decidable": false,
  "path": "app",
  "action": "direct-main-commit",
  "reason": "unconfigured_or_invalid",
  "message": "workspace descriptor is unconfigured"
}
```

### 운영 호출 예

워커 라우팅 전 product main 직접 커밋·force-push 측정:

```bash
"{{RUNTIME_ROOT}}/scripts/workspace-descriptor-check" \
  --path <repository-path> \
  --action direct-main-commit
```

exit `1` → 금지. exit `0` → 해당 repo 금지 목록에 없음. exit `2` → fail closed, 권한 발명 금지. 동일하게 `--action force-push`를 사용한다.

## 온보딩에서 쓰는 방법

Step 1 (`02-workspace-facts.md`)에서 `{{REPO_LIST}}` 확정 후:

<Steps>
  <Step title="예제 복사 (없을 때만)">
    `config/workspace-descriptor.json`이 없으면 example을 복사한다. filled 파일은 커밋하지 않는다.

```console
$ cd "{{RUNTIME_ROOT}}"
$ test -f config/workspace-descriptor.json \
  || cp config/workspace-descriptor.example.json config/workspace-descriptor.json
```
  </Step>
  <Step title="멤버별 측정·기록">
    각 확정 멤버에 `name`, 루트 상대 `path`, `git remote get-url origin`으로 측정한 `remote`, `role`, `capabilities`, `prohibited`를 쓴다. remote는 `{{WORKSPACE_ROOT}}/<path>` 기준이다.
  </Step>
  <Step title="워크스페이스 필드">
    `workspace_root_is_plain_folder: true`, `workspace_root` = 확인된 절대 `{{WORKSPACE_ROOT}}`, `master_seat` = 좌석 단계에서 쓸 seat form.
  </Step>
  <Step title="소유자 확인">
    초안을 읽어 확인한 뒤에만 final로 취급한다. 측정되지 않은 저장소를 발명하지 않는다.
  </Step>
</Steps>

**기본 값 (온보딩 권장, 소유자 수정 가능)**

| role | capabilities 기본 | prohibited 기본 |
| --- | --- | --- |
| `product` | `["pr", "dispatch-target"]` | `["direct-main-commit", "force-push"]` |
| `ops` | `["pr", "dispatch-target"]` | `["force-push"]` |

## 검증 실패 모드

| 조건 | 결과 |
| --- | --- |
| 파일 없음 | unconfigured (`source_path=None`) |
| 비 UTF-8 / 비 JSON / root ≠ object | `WorkspaceDescriptorError` |
| `workspace_root_is_plain_folder` ≠ true | 거부 |
| `role` ∉ {`product`,`ops`} | 거부 |
| `prohibited` 누락/`null` | 거부 (실수로 기본 금지가 사라지는 것 방지) |
| 중복 `path` 또는 중복 `name` | 거부 |
| path 다중 매칭 | 거부 |
| unconfigured에서 `action_is_prohibited` / CLI | 에러 / exit 2 |

## 아키텍처 위치

```mermaid
flowchart TB
  subgraph Onboarding["온보딩 Step 1"]
    RL["{{REPO_LIST}} 측정"]
    WD["config/workspace-descriptor.json"]
    RL --> WD
  end

  subgraph Resolve["해석 순서"]
    ENV["WORKSPACE_DESCRIPTOR / MOGUI_WORKSPACE_DESCRIPTOR"]
    FILE["config/workspace-descriptor.json"]
    UNC["unconfigured"]
    ENV --> FILE --> UNC
  end

  subgraph Consumers["소비자"]
    LDR["workspace_descriptor.py"]
    CLI["workspace-descriptor-check"]
    RT["worker routing / charter §4"]
  end

  WD --> Resolve
  Resolve --> LDR
  LDR --> CLI
  CLI --> RT
```

마스터 좌석 규칙(folder workspace vs primary worktree)과 selector 형식은 descriptor 스키마 밖이며 Orca 객체 모델 문서에 속한다. descriptor의 `master_seat`는 그 선택을 **기록**하는 필드다.

## 관련 모듈 API

| 심볼 | 용도 |
| --- | --- |
| `load_workspace_descriptor(path=None, *, repo_root=None, environ=None)` | 디스크 로드; 없으면 unconfigured |
| `resolve_config_path(explicit=None, *, repo_root=None, environ=None)` | 경로만 결정 |
| `action_is_prohibited(path, action, *, config_path=None, …)` | 로드 + 금지 검사; unconfigured면 raise |
| `WorkspaceDescriptor.repository_for_path` | 인벤토리 조회 |
| `WorkspaceDescriptor.is_prohibited` | 금지 멤버십 |
| `WorkspaceDescriptor.require_repositories` | 비어 있으면 raise |
| `WorkspaceDescriptorError` | 설정·스키마·매칭 실패 |

## Related pages

<CardGroup>
  <Card title="Orca 객체 모델" href="/orca-object-model">
    folder workspace, master 좌석, selector 형식, plain folder 라벨.
  </Card>
  <Card title="프로그레시브 온보딩" href="/onboarding">
    Step 1 workspace facts와 descriptor 착지 절차.
  </Card>
  <Card title="Supervised dispatch" href="/supervised-dispatch">
    디스패치 전 금지 액션 가드와 워커 라우팅 맥락.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    WORKSPACE_DESCRIPTOR·MOGUI_* 및 다른 인스턴스 설정 해석 순서.
  </Card>
  <Card title="CLI 레퍼런스" href="/cli-reference">
    공개 scripts/ 명령 표와 workspace-descriptor-check 요약.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    unconfigured·exit 2·placement 관련 복구 신호.
  </Card>
</CardGroup>
