<a id="security-overview"></a>
# 보안·승인 경계

ARTEX에는 task scope data, prompt constraint, asset block/allow gate, tool-input intercept, optional LLM judge, human approval, JWT API auth가 있다. 이들은 한 개의 일관된 “scope firewall”이 아니라 서로 다른 단계에 적용되는 경계다.

<a id="enforcement-map"></a>
## 강제 지점 지도

```mermaid
flowchart LR
  INPUT[Goal / description / operator rules] --> DEC[Goal decomposer]
  DEC --> SCOPE[(task_scope)]
  DEC --> CONS[(task_constraints)]
  CONS -->|prompt injection| PLAN[Planner]
  CONS -->|prompt injection| WORK[Worker]
  CHAT[ChatAgent] --> HOOK[Guard PreToolUse]
  WORK --> HOOK
  DEC --> NOHOOK[ARTEX Guard 미배선]
  PLAN --> NOHOOK
  MAIN[Task MainAgent] --> NOHOOK
  HOOK -->|intercept-enabled tool| IR[Intercept rules]
  HOOK -->|그 밖의 tool| DISPATCH[Tool handler]
  NOHOOK --> DISPATCH
  IR -->|no match| J{LLM judge enabled?}
  IR -->|matched action| DECISION{allow / ask / deny}
  J -->|off: allow| DISPATCH
  J -->|verdict / fail action| DECISION
  DECISION -->|allow| DISPATCH
  DECISION -->|ask| HUMAN{human / timeout decision}
  DECISION -->|deny| BLOCK[Blocked]
  HUMAN -->|allow| DISPATCH
  HUMAN -->|deny / cancel| BLOCK
  DISPATCH -->|add_intent| AIG{Anchor asset gate}
  DISPATCH -->|insert_assets| ASG{Insert asset gate\nloaded rules only}
  DISPATCH -->|other tool| EXEC[Execution]
  AIG -->|allow| GRAPH[(Exploration graph)]
  AIG -->|deny / rule read error| BLOCK
  ASG -->|no loaded-rule denial| GRAPH
  ASG -->|loaded rule deny| BLOCK
  EXEC --> AUDIT[(intercept/activity audit)]
  BLOCK --> AUDIT

  click SCOPE "#scope-gap" "task scope"
  click CONS "#scope-gap" "prompt constraints"
  click AIG "#asset-gate" "intent asset gate"
  click ASG "#asset-gate" "insert asset gate"
  click IR "#tool-boundary" "tool intercept"
  click J "#judge" "LLM judge"
  click AUDIT "#auditability" "audit"
```

| 판단 | deterministic인가 | 적용 범위 | 기본/실패 동작 |
|---|---|---|---|
| API JWT 검증 | 예 | `/api/auth/*`, `/api/health`를 제외한 API | HMAC 서명과 `jwt.Parse` 기본 claim 검증에 의존; `exp/sub/iss/aud` 존재·기대값은 별도 요구하지 않음. change-password는 면제 prefix 아래라 handler가 다시 JWT를 검사 |
| anchored intent의 asset match | 예 | `add_intent`의 asset ids | rule 조회 오류도 거부(fail-closed) |
| 새 asset의 block/allow match | 예 | `insert_assets`의 입력 candidate | 읽힌 rule에는 block/allow 적용; rule 조회 오류는 무시 |
| task constraint 준수 | 아니오 | planner/worker prompt | 모델 자기검사 |
| tool intercept rule match | 예 | Guard hook가 배선된 Worker/ChatAgent의 intercept-enabled tool | first priority match의 allow/deny/ask; config/rule lazy-load 오류는 차단으로 승격되지 않음 |
| unmatched tool 판단 | 선택적 AI | judge가 enabled+wired일 때 | 기본 judge off → allow |
| judge 오류/파싱 실패 | 설정 | judge path | 기본 fail action `allow` |
| ask 승인 | 사람/timeout | matched call | rule별 timeout, 없으면 취소/결정까지 대기 |
| finding semantic validity | 아니오 | `report_finding` 결과 | worker 주장으로 writeback |

<a id="scope-gap"></a>
## Task scope와 실제 outbound의 차이

goal decomposer가 `task_scope`와 `task_constraints`를 만들 수 있고, constraints는 planner/worker system prompt 최상위 block으로 들어간다. prompt는 out-of-scope 발견을 fact로만 남기고 새 intent/action을 만들지 말라고 지시한다. [constraint 근거](evidence:constraints) [goal 근거](evidence:goals-agent)

그러나 guard package 주석은 과거 RoE authorization-scope mechanism이 제거됐고 대체가 나중에 추가될 수 있다고 명시한다. 모든 Bash/WebFetch/browser/MCP request의 실제 목적지를 파싱해 `task_scope`와 대조하는 중앙 gateway는 이 소스에서 확인되지 않았다. [guard 근거](evidence:guard)

더 세부적으로:

- `add_intent`는 asset id가 있을 때 asset gate를 검사한다. unanchored intent는 같은 검사를 통과하지 않으며, rule 조회가 실패하면 intent 생성을 거부한다.
- `insert_assets`도 입력 candidate에 asset rules를 적용하지만 global/task rule 조회 오류를 무시한다. 읽기에 실패한 rule set은 적용되지 않은 채 insert가 진행될 수 있다.
- 이 검사는 **그래프에 넣을 asset/intent**를 막는 것이지, 임의 Bash script나 WebFetch URL의 모든 network destination을 승인하는 것은 아니다.
- worker에는 task Guard를 감싼 steer hook가 배선되고 일반·custom 대화의 `ChatAgent`에는 chat Guard가 배선된다. 현재 정적 호출 경로상 goals decomposer, planner, task `MainAgent`의 session options에는 ARTEX Guard hook가 없다. 이 경로들도 tools와 `PermissionModeBypass`를 사용할 수 있다. Norma 내부 보완은 이 저장소 범위 밖이다.
- proxy는 capture/forward 계층이며 authorization gateway로 구현되지 않았다. MITM 실패 host를 transparent tunnel로 fail-open하는 경로도 있다.

따라서 `task_scope row가 있다 = 모든 network I/O가 강제 제한된다`는 주장은 할 수 없다. 승인된 테스트 시스템으로 사용하려면 실제 egress gateway 또는 모든 네트워크 tool adapter가 공통 destination policy를 호출하는지 별도 검증·보완해야 한다.

<a id="asset-gate"></a>
## Asset block/allow gate

global asset rules와 task-level block rules를 먼저 적용해 하나라도 맞으면 거부한다. 그 뒤 task allow rule이 하나라도 활성화돼 있으면 그중 어느 것도 맞지 않는 candidate를 “허용 범위 밖”으로 거부한다. domain/IP/URL exact/fuzzy와 CIDR match가 있다. 다만 `add_intent`는 rule 조회 오류를 거부하는 반면 `insert_assets`는 global/task rule 조회 오류를 무시하므로, 같은 정책도 호출 지점에 따라 실패 동작이 다르다. [asset gate 근거](evidence:asset-gate) [intent 근거](evidence:agent-tools)

기본 seed에는 `.gov`, `.gov.cn`, `.edu`, `.edu.cn` fuzzy block이 있지만 settings flag로 한 번만 넣고 이후 사용자가 disable/delete한 것을 되살리지 않는다. 이 기본 목록은 완전한 authorization policy가 아니며 국가·프로그램별 scope를 표현하지 않는다. [DB seed 근거](evidence:db-open)

<a id="tool-boundary"></a>
## Tool intercept와 승인

Guard가 배선된 Worker/ChatAgent 경로에서 `PreToolUse`는 intercept 대상 tool이면 priority 순 DB rule을 먼저 평가한다. 기본 대상은 `Bash`, `WebFetch`, `web_search`, `shell_open`, `shell_send`, `Write`, `Edit`, `MultiEdit`다. 규칙은 tool name 또는 raw JSON input에 substring/regex match하며 첫 match의 `allow`, `deny`, `ask`를 따른다. Goals/Planner/MainAgent에는 현재 이 hook 배선이 없다. [guard 근거](evidence:guard) [intercept 근거](evidence:intercept) [worker 근거](evidence:worker)

enabled-tool config의 lazy load가 실패하면 tool이 disabled처럼 보여 Guard가 바로 allow할 수 있다. enabled 확인 뒤 `Match`의 rule load가 실패한 경로는 no-match로 처리돼 judge가 활성·배선됐으면 judge로, 아니면 allow로 간다. 따라서 intercept store 장애는 독립적인 fail-closed 경계가 아니다. [intercept 근거](evidence:intercept)

seeded destructive/exfil patterns는 일반 DB row다. 사용자가 disable/delete할 수 있고, data-exfil pipe rule 일부는 오탐 때문에 기본 disabled라고 코드가 설명한다. 따라서 built-in rule은 변경 불가능한 safety floor가 아니다. bad regex는 load 중 skip된다. [DB seed 근거](evidence:db-open)

`ask`는 pending row와 activity card를 만들고 사람이 결정할 때까지 worker를 block한다. timeout이 없으면 결정/취소까지 무기한이며, timeout이 있으면 rule의 allow/deny 정책을 따른다. cancellation은 pending을 denied/not-executed로 정리한다. [intercept 근거](evidence:intercept)

`PermissionModeBypass`가 각 역할의 session option에 사용되므로 “Norma가 알아서 매 호출 승인한다”는 가정은 맞지 않는다. Worker와 ChatAgent에서는 ARTEX Guard와 intercept-enabled tool 목록이 통제 경계이고, Goals/Planner/MainAgent에는 같은 Guard가 정적 경로상 배선되지 않는다. [worker 근거](evidence:worker)

<a id="judge"></a>
## Optional LLM judge

아무 deterministic rule도 맞지 않을 때 judge가 켜져 있고 reviewer가 배선돼 있으면 full tool input과 context를 모델에 준다. verdict는 allow/ask/deny다. context build 실패는 사람 ask로 보내지만, model error 또는 parse 실패는 설정된 fail action을 쓴다. 기본은 judge 비활성, model timeout 15초, fail action `allow`, ask timeout 300초, ask timeout action `deny`다. [judge 근거](evidence:intercept)

LLM judge는 규칙을 보완하는 분류기이며 authorization source of truth로 보기 어렵다. 모델 profile/prompt/config digest와 input digest를 audit에 남기는 것은 사후 설명성을 높이지만, prompt injection과 분류 오류를 deterministic하게 제거하지 않는다.

<a id="auth-boundary"></a>
## API 인증과 secret 위치

공통 middleware는 `/api/auth/*`와 `/api/health`를 면제한다. `/api/auth/change-password`도 prefix상 면제되지만 handler 안에서 JWT와 기존 암호를 다시 검증한다. `extractToken`은 Bearer header, cookie, query `token`을 route 구분 없이 순서대로 읽으므로 middleware 보호 256개 operation과 change-password 1개, 합계 257개에서 query token이 유효하다. SSE 전용 제한은 구현돼 있지 않다. [auth 근거](evidence:auth)

발급 token은 subject `ARTEX`, 7일 expiry, HS256을 사용하고 password hash는 settings에 bcrypt로 저장한다. 그러나 `verifyJWT`는 signing method가 `SigningMethodHMAC`인지 검사하므로 같은 key를 쓴 HS384/HS512도 수락하며, `exp`, `sub`, `iss`, `aud`의 존재나 기대 값을 별도로 요구하지 않는다. JWT signing key는 `BaseDir/jwt.key`에 mode 0600으로 만들며, 과거 `data/jwt.key`가 있으면 browsable workspace 밖으로 옮긴다.

Docker compose는 `/app/data`와 `/app/skills`만 bind mount하고 `/app/jwt.key`를 명시적으로 mount하지 않는다. container replacement에서 key 지속성이 어떻게 되는지는 이미지/volume 실제 동작을 실행하지 않아 미확인이다. 코드와 manifest 조합상 session invalidation 가능성을 운영 검증해야 한다. [compose 근거](evidence:compose)

LLM API keys, notification secrets, MCP headers, web-search keys 등은 DB/settings/config/env에 걸쳐 있다. UI/API가 redacted view를 주는지 각 handler별 확인이 필요하며, task archive와 전체 DB backup의 secret 포함 범위를 별도로 정해야 한다.

frontend는 token을 localStorage와 JavaScript가 쓰는 `SameSite=Lax` cookie에 함께 저장한다. 이 cookie에는 `HttpOnly`와 `Secure`가 없으므로 browser script에서 읽을 수 있고 HTTP에서도 전송될 수 있다. [frontend auth 근거](evidence:web-auth)

<a id="auditability"></a>
## Provenance와 audit

intercept trace는 tool name과 input digest로 decision, tool call, execution result를 연결한다. 같은 concurrent request가 모호하면 추측 연결을 하지 않도록 설계됐다. explicit allow/deny와 model verdict도 history row로 남고, ask는 pending→decision→execution 상태를 갖는다. [trace 근거](evidence:intercept-trace)

agent activity는 tool_use/tool_result/usage/result를 PostgreSQL에 남기고 finding evidence는 traffic snapshot hash와 ordered binding을 보유한다. 이 조합으로 “어떤 모델/세션이 어떤 호출을 하고 어떤 bytes를 근거로 무엇을 기록했는가”를 상당 부분 재구성할 수 있다. 다만 activity append drop, proxy bypass/unrecorded tunnel, 외부 MCP 자체 로그, DB 밖 tool side effect는 완전성의 빈틈이다.

<a id="finding-trust"></a>
## Finding 신뢰 경계

`report_finding` 성공 시 graph node는 즉시 `confirmed`다. traffic evidence는 optional이고 evidence store는 bytes 무결성만 검증한다. Reporter는 trace/traffic을 문장으로 정리하며 기본적으로 exploit을 다시 수행하지 않는다. Retest는 사람이 시작하는 별도 경로다. [finding 흐름](features/findings-evidence.md#evidence-semantics)

따라서 automation이 finding을 외부 제출하기 전에 별도 validation policy가 필요하다. 최소한 재현 가능성, identity/tenant 대조, 영향 범위, scope, duplicate, evidence completeness를 검사하고 불충분하면 `candidate` 성격으로 취급해야 한다.

<a id="security-failure-modes"></a>
## 실패 조건 요약

| 실패 | 현재 코드가 주는 신호 | 필요한 검증/보완 |
|---|---|---|
| out-of-scope direct request | prompt/asset rule로 일부 예방 | central egress policy와 destination parser |
| Guard 미배선 역할의 tool call | Goals/Planner/MainAgent에는 hook 없음 | 모든 실행 역할에 공통 pre-tool policy 배선 |
| asset rule 조회 장애 | intent는 거부, insert는 해당 rule 없이 진행 가능 | 동일한 fail-closed 정책과 오류 관측 |
| rule 미매치 또는 DB rule 조회 장애 | judge off/unwired면 조용히 allow | 조회 오류를 분리 관측하고 필요한 action class는 fail-closed 적용 |
| judge 장애 | 기본 allow | fail-closed가 필요한 action class 정의 |
| tool 우회 | intercept-enabled 목록 밖 tool/MCP | capability inventory와 공통 hook 적용 확인 |
| unrecorded traffic | proxy fail-open/pass/direct path | egress와 evidence completeness marker |
| false positive finding | 즉시 confirmed | independent validator/retest gate |
| audit gap | append/drop/외부 tool | trace completeness metric과 reconciliation |
| token 노출 | localStorage, JS-readable/non-Secure cookie, 257개 operation의 query token fallback | XSS 완화, HTTPS 강제, access-log redaction, 저장·전달 방식 재설계 |
