<a id="asset-lifecycle"></a>
# 자산·scope·coverage 흐름

ARTEX는 전역 자산 graph, 회사 귀속 범위, 태스크별 자산 연결, 태스크 scope, 탐색 node의 asset anchor를 분리한다. 같은 domain이 한 번만 upsert돼도 여러 task가 이를 참조할 수 있고, “자산이 존재함”, “이 task에서 볼 수 있음”, “테스트를 허용함”, “coverage 분모에 포함함”은 서로 다른 상태다. [Asset store](evidence:asset-store)

```mermaid
flowchart LR
  Input[UI · Agent · ScopeSentry] --> Normalize[유형별 정규화]
  Normalize --> Gate[Global block + Task block/allow]
  Gate -->|거부| Reject[오류·삽입/intent 중단]
  Gate -->|허용| Assets[(assets)]
  Assets --> Link[(task_asset_links / task_ids)]
  Assets --> Company[(companies + company_scope)]
  Assets --> Anchor[(exploration_anchors)]
  Link --> Scope[(task_scope)]
  Scope --> Coverage[coverage denominator]
  Anchor --> Intent[worker intent]
  Dormant[조립됐지만 호출되지 않는\nDNS/HTTP enrichment] -.-> Assets

  click Gate "../security-boundaries.md#asset-gate" "Asset gate"
  click Assets "../contracts/db/task-exploration.md#table-assets" "assets table"
  click Scope "../contracts/db/task-exploration.md#table-task-scope" "task_scope"
  click Coverage "#coverage" "Coverage 계산"
  click Dormant "#enrichment" "연결되지 않은 보강 경로"
```

<a id="asset-types"></a>
## 자산 유형과 정체성

| 유형 | 주요 식별 입력 | dedup/연결 의미 |
|---|---|---|
| `root_domain` | 정규화 domain | domain unique partial index |
| `subdomain` | domain + optional record type/value | domain/record type 조합으로 합쳐지고 DNS 값은 병합될 수 있음 |
| `ip` | 유효 IPv4/IPv6 | IP unique; bound domains/open ports는 자산 속성 |
| `app` | bundle id 또는 app name | bundle id 우선, 없으면 name 기준 partial unique |
| `service` HTTP | 정규화 URL | URL unique; status/title/technology/auth와 host/IP 연결 |
| `service` other | domain/IP + port + service/protocol | HTTP와 다른 composite identity |
| `endpoint` | URL + method | parameter 관찰을 합치며 서비스/host와 논리 연결 |

`assets` row는 JSON/array 속성과 typed columns를 함께 쓴다. DB partial unique index는 물리 중복의 일부를 막지만 URL/domain normalization과 merge 정책은 `AssetStore` 코드가 소유한다. 같은 root cause나 동일 의미 endpoint의 semantic dedup을 보장하지 않는다. [자산 schema](../contracts/db/task-exploration.md#table-assets)

<a id="asset-sources"></a>
## 자산이 들어오는 경로

| 경로 | 입력→결과 | 실패·완료 경계 |
|---|---|---|
| Agent `insert_assets` | 혼합 배열을 유형별 validate/upsert, task·intent provenance 연결 | item별 partial success; `results`와 `errors`를 함께 반환 |
| UI/API `/api/assets` | 사용자 입력을 server handler가 upsert | 응답은 DB mutation 기준 |
| Task asset attach/scope | 기존 asset 연결 또는 domain/IP scope에서 asset 생성·연결 | 한 요청의 validation/transaction 규칙은 `db/task_assets.go`가 소유 |
| ScopeSentry sync | remote project/task asset을 가져와 ARTEX type으로 변환·upsert | remote API 성공과 local insert 결과를 구분 |
| Company scope | domain/IP/CIDR/ICP/keyword 규칙을 저장하고 기존 asset 귀속 재계산 | keyword는 agent 힌트이며 모든 종류가 자동 귀속 key는 아님 |
| Enrichment engine | manager가 engine을 만들고 worker tool set에 주입 | 현재 production 코드에는 `ResolveDomain`/`ProbeSite` 호출자가 없어 자산 write 뒤 자동 실행되지 않음 |

ScopeSentry credential과 datasource는 settings에 저장되고 sync handler가 외부 API를 호출한다. 이 문서 작성에서는 원격 service를 실행하지 않았다. [ScopeSentry](evidence:scopesentry-sync)

<a id="asset-write"></a>
## Agent write와 provenance

`insert_assets`는 model이 `task_id`를 정하지 못하게 하고 runtime의 `ToolSet.taskID`를 사용한다. 각 성공 item은 asset을 intent owner에 anchor하고 `task_asset_links` source/source summary를 갱신한다. 최상위로 명시 삽입된 item만 그 자산 유형의 보수적 `task_scope`를 자동 추가하며, 내부 파생 asset이 scope를 연쇄 확장하지 않게 한다. [Asset tool](evidence:asset-tools)

처리 순서는 item별 **gate → validate/upsert → provenance/anchor → auto scope**다. 배열 전체 transaction은 아니므로 앞 item 성공 뒤 뒤 item이 실패할 수 있다. caller는 전체 성공을 가정하지 말고 index별 결과를 확인해야 한다.

<a id="scope-model"></a>
## Company scope와 task scope

| scope | 지원 kind | 역할 | 강제 범위 |
|---|---|---|---|
| `company_scope` | domain, ip, cidr, icp, keyword | asset의 조직 귀속과 agent 검색 문맥 | task outbound authorization 자체가 아님 |
| `task_scope` | company, root_domain, subdomain, ip, cidr, icp, keyword | task의 범위 edge와 coverage denominator | 일부 asset query/tool gate의 입력; 모든 egress 중앙 강제는 아님 |
| task intercept rule | exact/fuzzy domain·IP·URL, CIDR + block/allow | intent anchor/agent insert의 사전 gate | 해당 호출 지점에 한정 |
| global asset intercept | 같은 match kind의 block | 모든 task의 금지 자산 | 해당 호출 지점에 한정 |

CIDR parser는 IPv4 `/16`보다 넓고 IPv6 `/32`보다 넓은 입력을 거부하며, bare public suffix도 scope로 허용하지 않는다. bare IP는 host CIDR(`/32`, `/128`)로 정규화한다. Task scope upsert는 unique 규칙으로 idempotent하다. [Task scope](evidence:task-scope) [Company scope](evidence:company-scope)

<a id="asset-gate-order"></a>
## Block/allow 판정 순서와 장애 정책

1. enabled global block과 task block 중 하나라도 domain/IP/URL/CIDR 후보에 맞으면 거부한다.
2. block에 맞지 않고 enabled task allow rule이 하나 이상 있으면, allow 중 어느 것도 맞지 않는 자산을 거부한다.
3. enabled allow rule이 없으면 whitelist 단계는 비활성이다.

같은 evaluator를 쓰더라도 caller의 조회 실패 정책이 다르다.

| 호출 지점 | rule 조회 실패 | 결과 |
|---|---|---|
| Planner/Main `add_intent` anchor | 오류를 반환 | intent 생성 중단, fail-closed에 가까움 |
| Agent `insert_assets` | 오류를 무시하고 읽힌 rule 집합으로 진행 | asset insert가 진행될 수 있음 |
| 이미 저장된 asset check | 오류 반환 | caller가 중단 여부 결정 |

따라서 “규칙이 존재한다”만으로 모든 네트워크 동작이 막힌다고 결론내릴 수 없다. 정확한 우회면은 [scope와 egress gap](../security-boundaries.md#scope-gap)을 본다. [Gate 구현](evidence:asset-gate)

<a id="coverage"></a>
## Coverage 분모와 tested 신호

coverage가 켜진 task는 `task_scope`, task asset link, intent anchor와 graph 상태를 이용해 전체/테스트됨/미테스트 자산을 계산하고 graph API에 노출한다. `coverage_enabled=false`는 coverage tool/일부 분모 누적을 제한하지만 모든 asset 저장을 끄는 설정은 아니다. 특히 agent의 최상위 `insert_assets`는 주석상 task scope를 coverage toggle과 독립적으로 누적하는 반면 worker 시작 시 anchor 자산 auto scope는 toggle을 확인한다. 두 경로를 같은 규칙으로 가정하면 안 된다. [Task assets](evidence:task-assets) [Worker](evidence:worker)

“tested”는 intent/anchor/상태를 바탕으로 한 시스템 coverage 표지다. 특정 취약점 class의 충분한 방법론, 인증 역할 조합, 시간 변화까지 소진했다는 증거는 아니다.

<a id="enrichment"></a>
## 조립됐지만 실행 진입점이 없는 비 AI enrichment

Manager는 enrichment engine을 만들고 worker의 `ToolSet.SetEnrich`까지 호출한다. 그러나 이 snapshot의 production 코드에서 `ResolveDomain`과 `ProbeSite`를 호출하는 곳은 interface 선언과 method 정의 밖에 없다. 따라서 자산이 들어온 뒤 DNS/HTTP 보강이 **자동 실행된다고 볼 수 없다**. 현재 상태는 실행 능력이 조립됐지만 trigger가 연결되지 않은 dormant 경로다. [Enrichment](evidence:asset-enrich) [Tool 조립](evidence:assembly-agent)

호출자가 연결될 경우 engine 자체는 bounded queue와 worker pool을 갖고 `(kind, asset id)`를 5분 cooldown으로 dedup한다. queue가 가득 차면 job을 로그 후 버린다. DNS는 `dnsx`로 A/AAAA/CNAME을 조회해 IP/subdomain을 upsert한다. HTTP probe는 최대 12초, redirect를 따라가지 않고, body를 1 MiB까지 읽어 status/length/title을 갱신한다. capture proxy 주소는 요청마다 해석되며 비활성일 때 direct로 갈 수 있다. 이는 구현된 capability 설명이며 현재 실행 흐름 설명은 아니다.

코드 주석은 DNS를 ungated라 명시한다. HTTP probe에 전달되기 전 caller가 어떤 gate를 적용했는지와 direct fallback을 함께 검토해야 한다. queue/cooldown은 영속 상태가 아니므로 process restart 후 다시 실행될 수 있다.

<a id="asset-failures"></a>
## 오해하기 쉬운 경계

- asset의 `company_id`는 ownership 해석이지 bug-bounty authorization의 독립 증거가 아니다.
- `task_scope`가 있다고 모든 Bash/WebFetch/MCP/custom tool destination이 그 범위로 제한되는 것은 아니다.
- endpoint URL unique는 method까지 포함하지만 request body·role·tenant 조합 coverage를 표현하지 않는다.
- enrichment는 현재 자동 호출되지 않는다. 장래 trigger가 연결돼도 best-effort queue이며 실패/drop이 coverage gap으로 자동 승격되는 코드는 없다.
- ScopeSentry import 성공은 upstream 데이터의 최신성·정확성·허가를 검증하지 않는다.
- task inheritance/source 관계로 보이는 asset과 직접 task asset을 UI/API가 구별하는 경로가 있으므로 provenance 없이 flat list로 합치지 않는다.
