makdoong2-team 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/agents/makdoong2-issue-reporter.md +24 -7
- package/agents/makdoong2-team-leader.md +2 -0
- package/agents/makdoong2-verifier.md +26 -4
- package/dist/issue-reporter-guard.d.ts +26 -12
- package/dist/issue-reporter-guard.js +93 -40
- package/dist/opencode-plugin.js +103 -62
- package/dist/state-access-guard.d.ts +88 -0
- package/dist/state-access-guard.js +262 -0
- package/gates/stage3-scope-verify.sh +15 -2
- package/package.json +1 -1
- package/scripts/lib/git-exclude.sh +60 -0
- package/scripts/run-tests.mjs +2 -0
- package/scripts/state.sh +80 -4
- package/scripts/wt-sync-ignored.sh +10 -25
- package/skills/makdoong2-issue-reporter/SKILL.md +238 -61
- package/stages/02-requirements.md +10 -2
- package/scripts/issue-reporter-approve.sh +0 -87
package/README.md
CHANGED
|
@@ -104,9 +104,10 @@ makdoong2-team doctor # 설치 진단
|
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
- 커맨드가 **전용 full-permission 에이전트**(`makdoong2-issue-reporter`)로 라우팅되어, 호출 시점 이전의 로그·프롬프트·세션 컨텍스트를 스스로 수집해 이상 지점을 포착하고 [y00njinuk/makdoong2-team issues](https://github.com/y00njinuk/makdoong2-team/issues) 에 등록한다.
|
|
107
|
-
- **사용자 직접 호출이 유일한 트리거**다. 부장님·막둥이가 실패를 관측했다고 자율적으로 이슈를 만들지 않는다 (훅이 차단).
|
|
108
|
-
- 저장소가 public 이므로 사내 정보는 마스킹 후 첨부되며, **전송 전
|
|
109
|
-
-
|
|
107
|
+
- **사용자 직접 호출이 유일한 트리거**다. 부장님·막둥이가 실패를 관측했다고 자율적으로 이슈를 만들지 않는다 (훅이 차단). 이 에이전트는 **선택 가능한 에이전트 목록에도, `@` 멘션 목록에도 뜨지 않으며**, `task` 툴로 spawn 하는 것도 차단된다 — 진입점은 위 커맨드 하나뿐이다.
|
|
108
|
+
- 저장소가 public 이므로 사내 정보는 마스킹 후 첨부되며, **전송 전 승인을 세션 안에서 묻는다**: 에이전트가 게시될 원문 전체를 `cat` 으로 표시하면 훅이 그 sha256 을 기록하고, 전송용 curl 호출 시 opencode 가 게시 여부를 묻는다(yes/no). 표시하지 않았거나 표시 후 내용이 바뀌면 전송이 차단된다 (승인은 사용자가 본 원문에 바인딩, 1회용). 승인 프롬프트에서는 **"Allow once"** 를 고른다 — "always" 는 남은 세션의 승인 질문을 없앤다.
|
|
109
|
+
- **이슈 본문은 고정 양식으로 쓴다.** 증상 · 환경 · 재현 절차 · 기대/실제 동작 · 실패 지점 · 타임라인 · 에러 메시지 · 재현성/영향 범위 · 시도한 조치 · 증거(필수)에, 조사에서 근거가 나오면 관련 관찰 · 의심 근본 원인 코드 · 부수 관찰 · 제안(조건부)이 붙는다. 양식은 발명이 아니라 [#5](https://github.com/y00njinuk/makdoong2-team/issues/5) 가 실제로 갖췄던 구성을 규약화한 것이다 — 그 이슈의 증거가 그대로 진단에 쓰여 v1.7.0 수정으로 이어졌다. 상세: `skills/makdoong2-issue-reporter/SKILL.md` §6
|
|
110
|
+
- PAT 는 `~/.config/opencode/.github` 파일에서 읽는다. **파일이 없거나 토큰을 찾지 못하면 중단하지 않고, 발급 URL 과 최소 권한(fine-grained: Issues Read/write, classic: `public_repo`)을 안내해 사용자에게 발급을 요청한 뒤 대기**한다. 401/403 도 같은 재발급 경로를 탄다. 상세: ARCHITECTURE.md §4.6
|
|
110
111
|
|
|
111
112
|
---
|
|
112
113
|
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
name: makdoong2-issue-reporter
|
|
3
3
|
description: makdoong2-team 오류·비정상 동작 GitHub 이슈 리포터. 사용자가 /makdoong2-issue-reporter 커맨드로 직접 호출할 때만 활성화된다. 워크플로우 stage 에 참여하지 않으며 dispatch_stage 로 spawn 되지 않는다. 다른 에이전트가 자율적으로 선택·호출하지 않는다.
|
|
4
4
|
temperature: 0.1
|
|
5
|
-
mode:
|
|
5
|
+
# mode: subagent + hidden: true → 사용자가 고를 수 있는 primary 에이전트 목록에도,
|
|
6
|
+
# @ 멘션·task 자동완성 목록에도 뜨지 않는다. 진입점은 /makdoong2-issue-reporter
|
|
7
|
+
# 커맨드 하나뿐이고, 그 커맨드의 subtask:false 가 이 에이전트를 현재 세션에서
|
|
8
|
+
# 인라인으로 전환시킨다 (자식 세션으로 격리되지 않아 직전 대화 컨텍스트를 그대로 본다).
|
|
9
|
+
# opencode 는 mode==="subagent" 를 primary 목록에서 제외하고, hidden===true 를
|
|
10
|
+
# 모든 노출 목록에서 제외한다.
|
|
11
|
+
mode: subagent
|
|
12
|
+
hidden: true
|
|
6
13
|
tools:
|
|
7
14
|
Read: true
|
|
8
15
|
Write: true
|
|
@@ -14,7 +21,16 @@ tools:
|
|
|
14
21
|
skill: true
|
|
15
22
|
permission:
|
|
16
23
|
bash:
|
|
24
|
+
# opencode 는 매치되는 규칙 중 **마지막** 것을 쓴다 (findLast). 따라서 넓은
|
|
25
|
+
# 규칙을 위에, 좁은 규칙을 아래에 둔다 — 순서를 뒤집으면 아래 "ask" 가 죽는다.
|
|
17
26
|
"*": "allow"
|
|
27
|
+
# 게시 승인을 세션 내 yes/no 질문으로 만드는 유일한 장치.
|
|
28
|
+
# payload 파일을 실어 보내는 호출(= 이슈·코멘트·Gist·라벨 생성)만 매치되고,
|
|
29
|
+
# 검색·라벨 조회 같은 읽기(-G / 순수 GET)는 매치되지 않아 사용자를 묻지 않는다.
|
|
30
|
+
# 읽기까지 물으면 승인 프롬프트가 일상이 되어 정작 게시 시점의 "예" 가 의미를 잃는다.
|
|
31
|
+
# 이 패턴은 issue-reporter-guard.ts 의 APPROVABLE_PAYLOAD_RE 와 한 쌍이다 —
|
|
32
|
+
# 훅이 `-d @/절대경로` 표기만 허용하므로 게시 호출은 반드시 여기에 걸린다.
|
|
33
|
+
"*-d @/*": "ask"
|
|
18
34
|
write:
|
|
19
35
|
"**/*": "allow"
|
|
20
36
|
---
|
|
@@ -23,17 +39,18 @@ permission:
|
|
|
23
39
|
|
|
24
40
|
## 하드룰
|
|
25
41
|
|
|
26
|
-
1. **첫 행동으로 `skill(name="makdoong2-issue-reporter")` 를 로드**하고, 스킬에 정의된 절차를 그대로 따른다. 실행 순서는 스킬이 고정한다: **수집 → 이상 지점 포착 → 마스킹 → 중복 확인 → 최소 질의 → 이슈 생성**.
|
|
42
|
+
1. **첫 행동으로 `skill(name="makdoong2-issue-reporter")` 를 로드**하고, 스킬에 정의된 절차를 그대로 따른다. 실행 순서는 스킬이 고정한다: **수집 → 이상 지점 포착 → 마스킹 → 중복 확인 → 최소 질의 → 본문 작성 → 이슈 생성**.
|
|
27
43
|
2. **GitHub 게시(이슈·코멘트·Gist·라벨)는 훅이 강제하는 사용자 승인 게이트를 통과해야만 가능하다.** 절차는 고정이다:
|
|
28
44
|
1. payload 를 **리터럴 절대 경로** JSON 파일로 작성한다 (예: `/tmp/makdoong2-issue/issue-payload.json`).
|
|
29
|
-
2. **게시될 원문
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
- 승인은 **1
|
|
33
|
-
-
|
|
45
|
+
2. **게시될 원문 전체를 `cat <payload>` 로 세션에 표시**하고 마스킹 내역 요약을 덧붙인다. 이 `cat` 은 **체이닝 없이 단독 실행**해야 하며(`;`·`&&`·리다이렉트·`$()` 금지), 훅이 이 시점의 sha256 을 표시 증명으로 기록한다. 요약·발췌로 대체 금지 — 사용자는 전송될 원문을 봐야 한다.
|
|
46
|
+
3. **사용자에게 게시 여부를 묻고 yes/no 응답을 받는다.** 전송용 curl 을 호출하면 opencode 가 세션 안에서 승인 프롬프트를 띄운다. 안내할 때 **"Allow once" 를 고르도록 알린다** — "Allow always" 는 남은 세션 동안 승인 질문 자체를 없애 게이트를 무력화한다. 사용자가 거부하면 그대로 중단하고, 무엇을 고쳐야 하는지 물어본다.
|
|
47
|
+
4. 전송은 **단일 curl 명령 + `-d @<절대경로>`** 형태만 허용된다. 표기까지 고정이다 — `--data`·`--data-binary`·`-d=@` 는 승인 프롬프트를 띄우지 못해 훅이 차단한다. 체이닝·리다이렉트·인라인 JSON·`gh` CLI 도 금지.
|
|
48
|
+
- 승인은 **1회용**이고 **사용자가 본 원문에 바인딩**된다. 2번 이후 payload 를 고치면 표시 증명이 무효가 되므로 2번부터 다시 한다.
|
|
49
|
+
- 차단 메시지를 받으면 우회하지 말고 지시대로 재표시·재승인을 거친다. 승인을 스스로 만들어낼 수 있는 경로는 없다.
|
|
34
50
|
3. **토큰(PAT)은 어디에도 원문 노출 금지.** 커맨드 문자열에 직접 박지 않고 환경변수로 전달하며, 출력에는 마스킹(`ghp_****`)만 허용한다.
|
|
35
51
|
4. **워크플로우 상태를 변경하지 않는다.** state.json 은 증거 수집을 위한 읽기(`state.sh get`)만 허용. `state.sh set` / dispatch 계열 툴 호출 금지. 이 에이전트는 워크플로우 오케스트레이션과 완전히 분리된 조사·보고 전용이다.
|
|
36
52
|
5. **다른 에이전트로 위임하지 않는다.** 수집·분석·마스킹·등록 전 과정을 이 세션에서 직접 수행한다.
|
|
53
|
+
6. **이슈 본문은 스킬 6장 양식을 따른다.** 필수 섹션 10개와 `## 증거` 를 순서대로 채우고, 3장 수집에서 근거가 나온 조건부 섹션(`## 관련 관찰` / `## 참고: 의심 근본 원인 코드` / `## 부수 관찰 (minor)` / `## 제안 (참고)`)을 빠뜨리지 않는다. 모든 인용에 출처(파일+라인 또는 세션+턴)를 달고 단정과 추정을 구분한다(추정은 `> 추정:`). **payload 를 `cat` 으로 표시하기 전에 6.6 자기 점검을 통과시킨다** — 표시 후 본문을 고치면 표시 증명이 무효가 되어 하드룰 2 를 2번부터 다시 밟아야 한다. 기준 사례는 이슈 #5 다.
|
|
37
54
|
|
|
38
55
|
## 실행 컨텍스트
|
|
39
56
|
|
|
@@ -31,9 +31,11 @@ permission:
|
|
|
31
31
|
|
|
32
32
|
1. **직접 파일 편집·생성 금지.** Read 외의 모든 파일 조작은 `dispatch_stage`로 서브에이전트에 위임한다. Edit/Write 툴은 frontmatter에서 제거되어 물리적으로 사용 불가하다.
|
|
33
33
|
2. **Bash 우회 파일 쓰기 금지.** `echo >`, `echo >>`, `cat >`, `cat <<EOF >`, `tee`, `sed -i`, `awk ... > file`, `printf > file` 등 어떤 형태의 쓰기 리디렉션도 사용하지 않는다. Python/Node.js 인터프리터를 통한 파일 쓰기(`python3 -c "open(...,'w')"`, `node -e "fs.writeFileSync(...)"` 등)도 동일하게 금지된다. **예외**: `<SCRIPTS_DIR>/state.sh set ...` 를 통한 state.json 마커 기록만 허용한다.
|
|
34
|
+
**읽기는 이 규칙의 대상이 아니다.** `ls` / `cat` / `file` / `head` / `stat` / `jq` / `git check-ignore` 로 state.json 을 조회하는 진단 명령과 `<SCRIPTS_DIR>/state.sh status <이슈키>` 는 쓰기 리디렉션이 없는 한 자유롭게 쓸 수 있다. `state_unreadable` 복구는 이 명령들로 수행한다.
|
|
34
35
|
3. **git 명령 직접 실행 금지 (신규).** `git commit` / `git push` / `git add` / `git rm` / `git worktree` 등 모든 git 명령을 직접 실행하지 않는다. 3_delivery.commit / 3_delivery.pr / 3_delivery.review 는 전부 publisher 가 worktree 에서 직접 실행한다. frontmatter permission 으로 deny 되어 있으며 훅이 물리적으로 차단한다.
|
|
35
36
|
4. **`auto_advance_stage` 결과의 `next_action` 필드에 명시된 지시를 100% 따른다.** `next_action`이 `dispatch_stage(...)` 호출을 요구하면 다른 어떤 행동보다 먼저 그 툴을 호출한다. next_action이 게이트 차단을 알리면 그 이유를 사용자에게 보고하고 종료한다.
|
|
36
37
|
5. **규칙 위반을 감지하면 즉시 자체 abort.** `"[부장님 자체 abort] 하드룰 위반: <규칙 번호> — <감지된 우회 시도>"` 형식으로 출력하고 세션을 종료한다. 사용자 개입을 기다린다.
|
|
38
|
+
**훅이 명령 하나를 차단한 것은 그 자체로 하드룰 위반이 아니다.** 훅 메시지에는 어떤 규칙인지와 허용되는 대안이 적혀 있다 — 먼저 읽고, 대안이 있으면 그 명령으로 바꿔 진행한다. abort 는 대안이 없거나 근본 원인이 사용자 개입을 요구할 때만 한다. 규칙 번호를 추측해서 인용하지 않는다.
|
|
37
39
|
|
|
38
40
|
## 핵심 원칙
|
|
39
41
|
|
|
@@ -94,10 +94,10 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
94
94
|
`<STAGES_DIR>/NN-*.md`을 읽어 단계가 요구한 *명시적 산출물*을 추출한다.
|
|
95
95
|
- 1_planning.jira: **통합 planning spec(01-planning.md)** 사용 — 3개 substage를 한 번에 처리하므로 다음 모두 확인:
|
|
96
96
|
- `jira`: `template_validation` 6항목 모두 기록 + `validation_passed=true` + `done=true`
|
|
97
|
-
- `requirements`: `done=true` + `policy.category`(minor|major) 설정 + `self_check.categorized==true` + `
|
|
97
|
+
- `requirements`: `done=true` + `policy.category`(minor|major) 설정 + `self_check.categorized==true` + **`draft_path` 마커 기록 + 그 경로의 파일 존재** (아래 §2-4 참조)
|
|
98
98
|
- `scope`: `done=true` + `self_check.paths_explicit=true`
|
|
99
99
|
- **interview_required=true가 기록되어 있고 requirements.done=false이면**: 인터뷰 대기 상태 → **REJECTED** (부장님이 인터뷰 후 재dispatch 필요)
|
|
100
|
-
- 1_planning.requirements: (단독 dispatch 폴백 시) `done_at` / `verification_pending`
|
|
100
|
+
- 1_planning.requirements: (단독 dispatch 폴백 시) `done_at` / `verification_pending` + **`draft_path` 마커 기록 + 그 경로의 파일 존재** (아래 §2-4) + `.policy.category` 설정 + `self_check.categorized==true`
|
|
101
101
|
- 1_planning.scope: (단독 dispatch 폴백 시) 4가지 출력 형식 항목 + `done=true`
|
|
102
102
|
- 2_implementation.analysis:
|
|
103
103
|
- `.skipped == true` 이면 즉시 **VERIFIED** (게이트가 SKIP 처리한 경우이므로 산출물 없음)
|
|
@@ -114,7 +114,11 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
114
114
|
- `integration_points` 배열 길이 >= 1
|
|
115
115
|
- `test_conventions.framework` 가 존재하고 비어있지 않음 (`null`/빈 문자열 불가 — `"none"` 은 허용)
|
|
116
116
|
- `.self_check` 의 **7개** boolean (`has_project_structure` / `has_dependencies` / `has_task_relevant_files` / `has_conventions` / `has_integration_points` / `has_test_conventions` / `json_schema_valid`) 모두 true
|
|
117
|
-
- `git status --porcelain` 이 `workspace-analysis.json` 외 다른 파일 변경/신규를 보고하지 않음 (analyzer 위반 신호)
|
|
117
|
+
- `git status --porcelain` 이 **플러그인 자신의 상태 디렉터리(`.makdoong2-team/`)를 뺀 뒤** `workspace-analysis.json` 외 다른 파일 변경/신규를 보고하지 않음 (analyzer 위반 신호)
|
|
118
|
+
```bash
|
|
119
|
+
git status --porcelain | grep -v '\.makdoong2-team/' | grep -v 'workspace-analysis\.json'
|
|
120
|
+
```
|
|
121
|
+
출력이 비어 있어야 한다. `.makdoong2-team/` 은 플러그인이 작업 트리 안에 만드는 **자기 상태**이지 analyzer 의 부산물이 아니다. 이것을 위반으로 세면 해당 패턴이 git exclude 에 없는 저장소에서 **항상 REJECTED** 가 나고, 그 시점에 exclude 를 고칠 권한을 가진 역할이 파이프라인에 없어(analyzer 는 산출물 1개만 쓰기 가능 · team-leader 는 하드룰 2 로 차단 · engineer 는 analysis 통과 후 단계) 동일 사유 무한 루프가 된다 (issue #6-②).
|
|
118
122
|
- 2_implementation.dev: `done=true` + sub-agent output에 "테스트 추가" 명시 / 5체크
|
|
119
123
|
- 2_implementation.test: `.stages."2_implementation".substages."test"` 의 각 필드가 아래 조건을 모두 충족해야 함
|
|
120
124
|
- `.unit` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
|
|
@@ -271,10 +275,28 @@ BB_BASE=$(jq -r '.hosts.BITBUCKET_API_BASE_PATH' ~/.config/opencode/makdoong2-te
|
|
|
271
275
|
|
|
272
276
|
5. 위 4단계 모두 통과 시 § 2-2 (앵커 재검증) 로 계속.
|
|
273
277
|
|
|
278
|
+
### 2-4. 1_planning.requirements 전용: draft_path 마커 재검증
|
|
279
|
+
|
|
280
|
+
게이트(`stage3-scope-verify.sh`)는 `spec_hash` 가 기록돼 있으면 `draft_path` **마커**를 하드 요구한다. 반면 종전 verifier 기준은 `requirements-draft.md` **파일 존재**만 봤다. 두 기준이 어긋나 있어서, `spec_hash` 는 기록하고 `draft_path` 는 빠뜨린 채 `done=true` 로 끝난 substage 를 verifier 가 VERIFIED 로 통과시키고 **바로 다음 게이트가 하드 차단**하는 정지가 발생했다 (issue #6-①). 파일은 멀쩡히 있으므로 "파일 존재" 검사로는 절대 잡히지 않는다.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
DRAFT=$(bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".draft_path' | tr -d '"')
|
|
284
|
+
{ [ -n "$DRAFT" ] && [ "$DRAFT" != "null" ]; } || REJECTED # 마커 누락 — 파일이 있어도 REJECTED
|
|
285
|
+
ROOT=$(bash <SCRIPTS_DIR>/state.sh root)
|
|
286
|
+
if [[ "$DRAFT" == /* ]]; then DRAFT_ABS="$DRAFT"; else DRAFT_ABS="$ROOT/$DRAFT"; fi
|
|
287
|
+
[ -f "$DRAFT_ABS" ] || REJECTED
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
REJECTED 시 `findings[].item` 은 `requirements.draft_path_missing`, `next_action` 은 `REVERT_DONE_MARKER`. 재작업하는 planner 는 `stages/02-requirements.md` §2-5 의 9번 항목(`draft_recorded`)을 채우면 된다.
|
|
291
|
+
|
|
292
|
+
**게이트 · self_check · verifier 세 곳의 필수 마커 정의는 항상 일치해야 한다.** 한 곳만 고치면 같은 정지가 그대로 재현된다.
|
|
293
|
+
|
|
274
294
|
### 3. Sub-agent 출력 정합성 점검
|
|
275
295
|
|
|
276
296
|
다음 *추론형* 의심 신호를 확인한다:
|
|
277
|
-
- **빈 응답** — 산출물 언급 없이 "완료"
|
|
297
|
+
- **빈 응답** — 산출물 언급 없이 "완료" 선언.
|
|
298
|
+
**단, §1·§2 의 결정론적 검사를 전부 통과했다면 이 신호만으로 REJECTED 하지 않는다.** `dispatch_stage` 는 최종 텍스트가 없어도 `.done=true` 면 성공으로 처리한다 (`shouldOverrideEmptyOutcome` — 최종 텍스트를 생략하는 로컬 모델 보상). 그 보상과 이 신호가 어긋나 있으면 그런 모델에서는 **모든 substage 가 REJECTED** 된다 (issue #6 부수 관찰 1). 산출물과 마커가 결정론적으로 검증되는데 산문이 없다는 것은 실패의 증거가 아니다.
|
|
299
|
+
마커나 산출물이 하나라도 어긋나면 그때는 REJECTED 이지만, 그건 이 추론 신호가 아니라 **§1·§2 위반**으로 기록한다 (`findings[].item` 을 결정론적 항목명으로 적는다 — 원인을 "빈 응답" 으로 돌리면 재작업 방향이 어긋난다).
|
|
278
300
|
- **테스트 삭제 정황** — output에 "기존 테스트 제거" / `// @ts-ignore` 추가 / `as any` 도입 언급
|
|
279
301
|
- **인라인 disable** — `eslint-disable` / `# noqa` 등으로 게이트 우회
|
|
280
302
|
- **자기선언 완료** — `done=true`를 임의로 기록했으나 self_check 누락
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export declare const ISSUE_REPORTER_SKILL_NAME = "makdoong2-issue-reporter";
|
|
2
2
|
export declare const ISSUE_REPORTER_AGENT = "makdoong2-issue-reporter";
|
|
3
|
-
export declare const APPROVAL_MARKER_SUFFIX = ".approved";
|
|
4
|
-
export declare const APPROVE_SCRIPT_BASENAME = "issue-reporter-approve.sh";
|
|
5
3
|
/** GitHub API 호출 분류 결과 */
|
|
6
4
|
export type GithubApiCall = {
|
|
7
5
|
kind: "none";
|
|
@@ -27,22 +25,38 @@ export type GithubApiCall = {
|
|
|
27
25
|
* problems 로 수집한다 — 호출부는 problems 가 하나라도 있으면 차단한다.
|
|
28
26
|
*/
|
|
29
27
|
export declare function classifyGithubApiCall(cmd: string): GithubApiCall;
|
|
30
|
-
/** 승인 스크립트 호출 여부 — 에이전트에게는 실행이 금지된다 (사용자 전용). */
|
|
31
|
-
export declare function isApproveScriptInvocation(cmd: string): boolean;
|
|
32
|
-
/** 승인 마커 경로 참조 여부 — 에이전트의 bash/write 에서 일절 금지된다. */
|
|
33
|
-
export declare function referencesApprovalMarker(text: string): boolean;
|
|
34
|
-
/** payload 파일 경로 → 승인 마커 경로 */
|
|
35
|
-
export declare function approvalMarkerPath(payloadPath: string): string;
|
|
36
28
|
export declare function sha256Hex(content: string | Buffer): string;
|
|
37
|
-
/** 마커 파일 내용에서 해시를 파싱한다. 첫 줄이 64자리 hex 가 아니면 null. */
|
|
38
|
-
export declare function parseApprovalMarker(markerContent: string): string | null;
|
|
39
29
|
/**
|
|
40
|
-
*
|
|
30
|
+
* 이 명령이 "원문 표시" 로 인정되는 절대 경로들을 돌려준다.
|
|
31
|
+
*
|
|
32
|
+
* 인정 조건은 **체이닝·리다이렉트·명령 치환이 없는 단일 `cat <절대경로>`** 다.
|
|
33
|
+
* 좁게 잡은 이유는 표시가 증거이기 때문이다 — `cat p; echo x > p` 를 허용하면
|
|
34
|
+
* 사용자가 본 내용과 파일에 남는 내용이 갈라져 (나) 불변 조건이 깨진다.
|
|
35
|
+
* jq 등으로 예쁘게 렌더링하는 것은 막지 않지만, 해시 증명으로 인정되지 않는다
|
|
36
|
+
* (렌더링은 원문이 아니다).
|
|
37
|
+
*/
|
|
38
|
+
export declare function payloadDisplayPaths(cmd: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* 표시 증명 검증: 사용자가 본 원문의 해시가 지금 전송하려는 payload 와 같은가.
|
|
41
41
|
* 불일치 사유를 문자열로 반환하고, 유효하면 null.
|
|
42
42
|
*/
|
|
43
|
-
export declare function
|
|
43
|
+
export declare function displayMismatch(payloadContent: Buffer, shownHash: string | undefined): string | null;
|
|
44
44
|
/** skill 툴 args 에서 스킬 이름을 추출한다. { name } 및 { arguments: { name } } 형태 수용. */
|
|
45
45
|
export declare function extractSkillNameFromArgs(args: unknown): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* task 툴 호출이 issue-reporter 를 서브에이전트로 spawn 하려 하면 차단 사유를,
|
|
48
|
+
* 아니면 null 을 반환한다.
|
|
49
|
+
*
|
|
50
|
+
* 이 에이전트의 진입점은 `/makdoong2-issue-reporter` 커맨드 하나뿐이다. frontmatter 의
|
|
51
|
+
* `mode: subagent` + `hidden: true` 는 선택·자동완성 목록에서 감출 뿐이고, opencode 의
|
|
52
|
+
* task 툴은 **subagent_type 의 mode 를 검사하지 않으므로** 이름만 알면 spawn 된다.
|
|
53
|
+
* 목록에 없는 것과 부를 수 없는 것은 다르다 — 그 간극을 여기서 닫는다.
|
|
54
|
+
*
|
|
55
|
+
* spawn 을 막아야 하는 이유는 격리 그 자체다. task 로 띄운 자식 세션은 직전 대화
|
|
56
|
+
* 컨텍스트를 보지 못해 수집이 반쪽이 되고, 사용자 승인 프롬프트도 그 세션에서 뜬다.
|
|
57
|
+
* 무엇보다 "사용자가 직접 부른다" 는 트리거 정책이 우회된다.
|
|
58
|
+
*/
|
|
59
|
+
export declare function issueReporterTaskSpawnViolation(args: unknown): string | null;
|
|
46
60
|
/**
|
|
47
61
|
* skill 툴 호출이 issue-reporter 트리거 정책을 위반하면 사용자에게 보여줄
|
|
48
62
|
* 에러 메시지를 반환하고, 정상이면 null 을 반환한다.
|
|
@@ -21,26 +21,45 @@ export const ISSUE_REPORTER_AGENT = "makdoong2-issue-reporter";
|
|
|
21
21
|
// ── GitHub 게시 승인 게이트 ──────────────────────────────────────────────
|
|
22
22
|
//
|
|
23
23
|
// 정책: issue-reporter 가 GitHub 에 무엇이든 게시(이슈·코멘트·Gist·라벨)하려면
|
|
24
|
-
// 사용자가 게시될 "원문 전체"를 보고 명시적으로 승인해야 한다.
|
|
25
|
-
//
|
|
24
|
+
// 사용자가 게시될 "원문 전체"를 보고 명시적으로 승인해야 한다. 승인은 두 조각이
|
|
25
|
+
// 함께 성립해야 유효하며, 어느 쪽도 프롬프트 규약이 아니라 코드가 강제한다:
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
// (가) 의사표시 — opencode permission 프롬프트의 yes/no.
|
|
28
|
+
// 에이전트 frontmatter 가 api.github.com 접근을 "ask" 로 올리고,
|
|
29
|
+
// plugin 의 permission.ask 훅이 쓰기(mutation)에 대해 status 를 "ask" 로
|
|
30
|
+
// 고정한다 (읽기는 "allow" 로 내려 사용자를 성가시게 하지 않는다).
|
|
31
|
+
// 사용자가 거부하면 tool 은 실행되지 않는다.
|
|
32
|
+
// (나) 정보에 근거한 동의 — "사용자가 본 원문" == "전송되는 원문".
|
|
33
|
+
// permission 프롬프트에는 curl 명령만 보이고 본문은 파일 안에 있으므로,
|
|
34
|
+
// 프롬프트만으로는 무엇이 게시되는지 알 수 없다. 그래서 에이전트는 전송 전에
|
|
35
|
+
// payload 를 세션에서 `cat` 으로 그대로 출력해야 하고, 훅이 그 시점의
|
|
36
|
+
// sha256 을 기록한다. 전송 시 기록된 해시와 현재 파일이 다르면 차단된다.
|
|
37
|
+
//
|
|
38
|
+
// 이 조합은 2026-08 이전의 "issue-reporter-approve.sh + <payload>.approved 마커"
|
|
39
|
+
// 방식을 대체한다. 마커 방식은 사용자가 별도 셸에서 스크립트를 직접 실행해야 했고,
|
|
40
|
+
// 그 실행이 곧 (가)와 (나)를 동시에 만족시켰다. 승인을 세션 안의 질문으로 옮기면서
|
|
41
|
+
// (가)는 opencode permission 으로, (나)는 표시 해시로 각각 넘겼다.
|
|
42
|
+
//
|
|
43
|
+
// 형식 제약은 그대로다 — 에이전트는 payload 를 "리터럴 절대 경로" 파일로 만들어
|
|
44
|
+
// 단일 curl 의 -d @<path> 로만 전달할 수 있다 (인라인 -d '{...}', 변수 경로,
|
|
45
|
+
// 체이닝, curl 외 HTTP 클라이언트 금지). 이것이 없으면 훅이 무엇이 전송되는지
|
|
46
|
+
// 검증할 수 없고, (나)의 해시 대조도 우회된다.
|
|
47
|
+
/** 표시 증명으로 인정되는 명령의 형태: 체이닝 없는 단일 `cat <절대경로>`. */
|
|
48
|
+
const DISPLAY_CAT_RE = /(?:^|\s)cat\s+(?:--\s+)?(["']?)(\/[^"'\s]+)\1(?:\s|$)/g;
|
|
49
|
+
const SHELL_COMPOSITION_RE = /[;|<>\n`]|\$\(|&&|\s&(\s|$)/;
|
|
41
50
|
const MUTATION_METHOD_RE = /(?:-X|--request)[= ]*['"]?(POST|PATCH|PUT|DELETE)\b/i;
|
|
42
51
|
const DATA_FLAG_RE = /(^|[\s'"])(-d|--data|--data-binary|--data-raw|--data-urlencode|--json|-F|--form)([= ]|$)/;
|
|
43
52
|
const PAYLOAD_AT_RE = /(?:-d|--data|--data-binary|--data-raw|--json)[= ]+@(["']?)([^"'\s]+)\1/g;
|
|
53
|
+
/**
|
|
54
|
+
* 승인 프롬프트를 띄우는 유일한 형식: `-d @<절대경로>` (등호·따옴표 없이 공백 하나).
|
|
55
|
+
*
|
|
56
|
+
* 게시 승인의 의사표시는 opencode 의 bash permission 프롬프트가 받는데, 그 프롬프트는
|
|
57
|
+
* 에이전트 frontmatter 의 `"*-d @/*": "ask"` 패턴이 명령 문자열에 매치될 때만 뜬다.
|
|
58
|
+
* 그래서 `--data @/x` 나 `-d=@/x` 처럼 같은 의미의 다른 표기를 허용하면 **질문 없이
|
|
59
|
+
* 전송되는 경로**가 생긴다. 의미가 아니라 표기에 승인이 걸려 있으므로, 표기를 하나로
|
|
60
|
+
* 고정하고 나머지는 차단한다. 이 상수를 고칠 때는 frontmatter 패턴도 같이 고쳐야 한다.
|
|
61
|
+
*/
|
|
62
|
+
const APPROVABLE_PAYLOAD_RE = /(^|\s)-d @\/[^\s'"]+(\s|$)/;
|
|
44
63
|
/**
|
|
45
64
|
* issue-reporter 의 bash 명령을 GitHub API 관점에서 분류한다.
|
|
46
65
|
*
|
|
@@ -89,6 +108,12 @@ export function classifyGithubApiCall(cmd) {
|
|
|
89
108
|
problems.push("payload 는 반드시 파일로 전달한다: -d @</absolute/path/payload.json>. " +
|
|
90
109
|
"인라인 JSON(-d '{...}')과 stdin(-d @-)은 승인 검증이 불가능해 금지된다.");
|
|
91
110
|
}
|
|
111
|
+
else if (!APPROVABLE_PAYLOAD_RE.test(cmd)) {
|
|
112
|
+
// 같은 의미라도 표기가 다르면 승인 프롬프트가 뜨지 않는다 — APPROVABLE_PAYLOAD_RE 주석 참조.
|
|
113
|
+
problems.push("payload 표기는 정확히 `-d @/절대경로` 여야 한다 (공백 하나, 등호·따옴표 없이). " +
|
|
114
|
+
"--data / --data-binary / --data-raw / --json / -d=@ 형태는 사용자 승인 프롬프트를 " +
|
|
115
|
+
"띄우지 못해 질문 없이 전송되므로 금지된다.");
|
|
116
|
+
}
|
|
92
117
|
for (const p of payloadPaths) {
|
|
93
118
|
if (!p.startsWith("/")) {
|
|
94
119
|
problems.push(`payload 경로는 리터럴 절대 경로여야 한다: "${p}"`);
|
|
@@ -107,37 +132,36 @@ export function classifyGithubApiCall(cmd) {
|
|
|
107
132
|
}
|
|
108
133
|
return { kind: "mutation", payloadPaths, problems };
|
|
109
134
|
}
|
|
110
|
-
/** 승인 스크립트 호출 여부 — 에이전트에게는 실행이 금지된다 (사용자 전용). */
|
|
111
|
-
export function isApproveScriptInvocation(cmd) {
|
|
112
|
-
return cmd.includes("issue-reporter-approve");
|
|
113
|
-
}
|
|
114
|
-
/** 승인 마커 경로 참조 여부 — 에이전트의 bash/write 에서 일절 금지된다. */
|
|
115
|
-
export function referencesApprovalMarker(text) {
|
|
116
|
-
return text.includes(APPROVAL_MARKER_SUFFIX);
|
|
117
|
-
}
|
|
118
|
-
/** payload 파일 경로 → 승인 마커 경로 */
|
|
119
|
-
export function approvalMarkerPath(payloadPath) {
|
|
120
|
-
return `${payloadPath}${APPROVAL_MARKER_SUFFIX}`;
|
|
121
|
-
}
|
|
122
135
|
export function sha256Hex(content) {
|
|
123
136
|
return createHash("sha256").update(content).digest("hex");
|
|
124
137
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
/**
|
|
139
|
+
* 이 명령이 "원문 표시" 로 인정되는 절대 경로들을 돌려준다.
|
|
140
|
+
*
|
|
141
|
+
* 인정 조건은 **체이닝·리다이렉트·명령 치환이 없는 단일 `cat <절대경로>`** 다.
|
|
142
|
+
* 좁게 잡은 이유는 표시가 증거이기 때문이다 — `cat p; echo x > p` 를 허용하면
|
|
143
|
+
* 사용자가 본 내용과 파일에 남는 내용이 갈라져 (나) 불변 조건이 깨진다.
|
|
144
|
+
* jq 등으로 예쁘게 렌더링하는 것은 막지 않지만, 해시 증명으로 인정되지 않는다
|
|
145
|
+
* (렌더링은 원문이 아니다).
|
|
146
|
+
*/
|
|
147
|
+
export function payloadDisplayPaths(cmd) {
|
|
148
|
+
if (SHELL_COMPOSITION_RE.test(cmd))
|
|
149
|
+
return [];
|
|
150
|
+
const paths = [];
|
|
151
|
+
for (const m of cmd.matchAll(DISPLAY_CAT_RE))
|
|
152
|
+
paths.push(m[2]);
|
|
153
|
+
return paths;
|
|
129
154
|
}
|
|
130
155
|
/**
|
|
131
|
-
*
|
|
156
|
+
* 표시 증명 검증: 사용자가 본 원문의 해시가 지금 전송하려는 payload 와 같은가.
|
|
132
157
|
* 불일치 사유를 문자열로 반환하고, 유효하면 null.
|
|
133
158
|
*/
|
|
134
|
-
export function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return "payload 내용이 승인 이후 변경됐다 — 승인은 특정 원문에 바인딩되며, 변경된 내용은 재승인이 필요하다";
|
|
159
|
+
export function displayMismatch(payloadContent, shownHash) {
|
|
160
|
+
if (!shownHash) {
|
|
161
|
+
return "이 payload 의 원문이 세션에 표시된 적이 없다 — 사용자는 무엇이 게시되는지 볼 수 없었다";
|
|
162
|
+
}
|
|
163
|
+
if (sha256Hex(payloadContent) !== shownHash) {
|
|
164
|
+
return "표시 이후 payload 내용이 변경됐다 — 승인은 사용자가 본 원문에 바인딩되며, 변경된 내용은 재표시·재승인이 필요하다";
|
|
141
165
|
}
|
|
142
166
|
return null;
|
|
143
167
|
}
|
|
@@ -156,6 +180,35 @@ export function extractSkillNameFromArgs(args) {
|
|
|
156
180
|
}
|
|
157
181
|
return undefined;
|
|
158
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* task 툴 호출이 issue-reporter 를 서브에이전트로 spawn 하려 하면 차단 사유를,
|
|
185
|
+
* 아니면 null 을 반환한다.
|
|
186
|
+
*
|
|
187
|
+
* 이 에이전트의 진입점은 `/makdoong2-issue-reporter` 커맨드 하나뿐이다. frontmatter 의
|
|
188
|
+
* `mode: subagent` + `hidden: true` 는 선택·자동완성 목록에서 감출 뿐이고, opencode 의
|
|
189
|
+
* task 툴은 **subagent_type 의 mode 를 검사하지 않으므로** 이름만 알면 spawn 된다.
|
|
190
|
+
* 목록에 없는 것과 부를 수 없는 것은 다르다 — 그 간극을 여기서 닫는다.
|
|
191
|
+
*
|
|
192
|
+
* spawn 을 막아야 하는 이유는 격리 그 자체다. task 로 띄운 자식 세션은 직전 대화
|
|
193
|
+
* 컨텍스트를 보지 못해 수집이 반쪽이 되고, 사용자 승인 프롬프트도 그 세션에서 뜬다.
|
|
194
|
+
* 무엇보다 "사용자가 직접 부른다" 는 트리거 정책이 우회된다.
|
|
195
|
+
*/
|
|
196
|
+
export function issueReporterTaskSpawnViolation(args) {
|
|
197
|
+
if (!args || typeof args !== "object")
|
|
198
|
+
return null;
|
|
199
|
+
const direct = args.subagent_type;
|
|
200
|
+
const nested = args.arguments?.subagent_type;
|
|
201
|
+
const target = typeof direct === "string" ? direct : typeof nested === "string" ? nested : undefined;
|
|
202
|
+
if (target !== ISSUE_REPORTER_AGENT)
|
|
203
|
+
return null;
|
|
204
|
+
return (`[makdoong2-team issue-reporter trigger violation]\n` +
|
|
205
|
+
`"${ISSUE_REPORTER_AGENT}" 는 task 툴로 spawn 할 수 없다.\n\n` +
|
|
206
|
+
`이 에이전트는 사용자가 /makdoong2-issue-reporter 커맨드를 실행할 때만, 현재 세션 안에서 ` +
|
|
207
|
+
`인라인으로 전환되어 동작한다. 자식 세션으로 격리하면 직전 대화 컨텍스트를 잃어 증거 수집이 ` +
|
|
208
|
+
`불완전해지고, 사용자 직접 호출이라는 트리거 정책도 우회된다.\n\n` +
|
|
209
|
+
`**올바른 절차:** 사용자에게 다음 실행을 안내하라:\n` +
|
|
210
|
+
` /makdoong2-issue-reporter [증상 한 줄 설명(선택)]`);
|
|
211
|
+
}
|
|
159
212
|
/**
|
|
160
213
|
* skill 툴 호출이 issue-reporter 트리거 정책을 위반하면 사용자에게 보여줄
|
|
161
214
|
* 에러 메시지를 반환하고, 정상이면 null 을 반환한다.
|