AGENTS.md · n8n 입력·출력 검증과 보안 작업 규칙 (73일차 실습 폴더)
이 파일은 CLI 코딩 에이전트(Codex, Claude Code, Antigravity 등)가 이 폴더에서 n8n 워크플로우를 만들 때 따르는 규칙입니다. 사람도 읽을 수 있게 썼습니다. 72일차 규칙과 같고, 3절이 오늘 만드는 보안 IT 문의 Agent의 계약, 5절에 입력 검증 · 규칙 · 판별 LLM · 출력 필터 노드가 더해졌습니다.
1. 작업 순서 (항상 이 순서)
- 요청을 읽고, 만들 워크플로우의 입력 · 노드 순서 · 결과 모양을 3~5줄로 먼저 정리해 보여 준다. 보안 요청이면 어느 관문(입력 검증 · 규칙 · 판별 LLM · 데이터 API · 출력 필터)에서 무엇을 막는지도 함께 보여 준다.
- 워크플로우 JSON을
my-work/<이름>.json에 쓴다. 이미 있으면 그 파일을 고친다 (새 파일을 만들지 않는다).
uv run tools/flow.py check my-work/<이름>.json → 오류(✘)가 없을 때까지 고친다.
- 끝나면 한국어로 짧게 보고한다: 바꾼 노드, check 결과, 그리고 사람이 실행할 명령 두 줄.
- push와 test는 사람이 실행해 결과를 확인한다. 사람이 "네가 push·test까지 해"라고 하면 직접 실행해도 된다 (push는 약 10초, n8n이 잠깐 재시작됨).
- 사람이 실패 보고서(
runs/*.json)를 알려 주면 실패한 케이스의 요청·응답을 읽고 원인을 설명한 뒤 고친다. 같은 곳을 3번 고쳐도 실패하면 멈추고 다른 방법을 제안한다.
- 문서(
my-work/*.md)를 써 달라는 요청이면 워크플로우를 만들지 않고 문서만 쓴다.
2. 환경 (바꾸지 말 것)
| 항목 |
값 |
| n8n |
2.42.6, 65일차 공식 설치 ~/day65-n8n (Docker 프로젝트 day65-n8n, 컨테이너 보통 day65-n8n-n8n-1), 브라우저 http://localhost:5678 |
| n8n 켜고 끄기 |
~/aio-01d4-stu/Last3Mon/65/n8n에서 docker compose start/stop (사람이 함, Ollama 컨테이너는 없음) |
| 게시된 웹훅 주소 |
밖에서 http://localhost:5678/webhook/<path> |
| OpenAI Credential |
id openAiCourse0001, name OpenAI (.env), 모델 gpt-4.1-mini |
| 다른 Credential |
쓰지 않음 (Ollama 등 없음, LLM은 모두 위 OpenAI Credential) |
| 데이터 API |
n8n 안에서 http://data-api:8800, 터미널에서 http://localhost:8800 (73일차 판 Last3Mon/73/data-api: 72일차 + 직원 목록 + 접수 건 연락처) |
| 공격 대상 (비교 기준) |
reference/open-it-agent.json (웹훅 open-it-agent, 고치지 않는다) |
데이터 API (73일차 판)
| 요청 |
응답 |
GET /faq?q=<질문> |
{"query", "results": [{"id", "title", "answer", "score"}]} |
GET /tickets/<id>?requester=<이름> |
접수 건 {"ticket_id", "title", "requester", "status", "assignee", "note", "contact": {"phone", "email"}} · 없으면 404 · 남의 건이면 403 {"error": "not your ticket"} |
GET /employees/<이름> |
{"name", "dept"} · 없는 직원이면 HTTP 404 {"error": "unknown employee"} |
GET /employees |
{"employees": [...]} (이준호 · 박서연 · 최유진 · 한지훈 · 김민지 · 정우성) |
POST /audit 본문 {"user", "route", "blocked_by", "reason", "masked", ...} |
{"stored": true, "count"} · 보안 감사 로그 한 줄 (Lab 03 과제 2, 메모리에 최근 1000줄) |
GET /audit · POST /audit-clear |
{"events": [{"t", ...}]} · 비우기 {"cleared"} |
POST /cache/<key> · GET /cache/<key> · POST /cache-clear |
72일차와 같음 |
3. 오늘 만드는 것의 계약 (요청에 다른 말이 없으면 이대로)
| 무엇 |
파일 · 웹훅 |
입력 (본문) |
결과 |
| 보안 IT 문의 Agent |
my-work/secure-agent.json · POST /webhook/my-secure (처음은 reference/open-it-agent.json을 복사) |
question(필수, 500자 이하), user(직원 이름) |
{"route", "answer", "tools_used", "reason"} → Lab 03부터 reason 대신 security |
| 보안 요약 (Lab 03 과제 2) |
my-work/security-report.json · POST /webhook/my-security-report |
없음 |
{"total", "answered", "blocked": {"rule", "guard", "output_filter"}, "masked", "pii_in_log", "blocked_users"} |
- 입력이 잘못되면 HTTP 400
{"error": "invalid request", "codes": [...]}. codes: question_required · too_long(500자 넘음) · unknown_user(직원 목록에 없음). LLM을 부르지 않는다.
route: 자동 답변 · 차단. 차단할 때 answer는 정해진 안내문, tools_used: [], reason(맞은 규칙 · 판별 LLM의 이유 · guard_down · output_leak).
- 규칙은 글자를 정리한 뒤에 본다 (Lab 01 과제 2): NFKC(전각 → 반각), 보이지 않는 글자(
\u200B 등) 지우기, 그리고 띄어쓰기 · 점 · 하이픈을 뺀 글자에도 한 번 더.
- 판별 LLM이 멈추면 막는다 (Lab 02 과제 2, fail-closed):
Injection guard의 오류 출력 → Guard down(route: 차단, reason: guard_down, "잠시 뒤 다시"). 규칙 · 입력 검증은 LLM 없이 그대로.
security(Lab 03): {"blocked_by": "" | "rule" | "guard" | "output_filter", "reason": "...", "masked": <가린 개수>}.
- 감사 로그(Lab 03 과제 2): 200으로 끝나는 모든 결과(차단 · 답)마다
POST /audit에 {user, route, blocked_by, reason, masked, tools_used}. 질문 · 답 글자와 연락처는 남기지 않는다 (pii_in_log 0). 400(입력 검증)은 남기지 않는다.
- 질문한 사람(
requester)은 body.user에서 가져온다. LLM이 정하게 하지 않는다 ($fromAI 금지). 남의 건인지는 데이터 API가 판단한다(403).
- 지시문(systemMessage)에 비밀(코드 · 번호 · 내부 메모)을 넣지 않는다. 직원의 질문은
<question> 태그 안의 데이터로 넘긴다.
- 답에 전화번호 · 이메일 · 주민등록번호가 있으면 가린다 (예:
010-****-2135, y***@example.com).
4. 워크플로우 파일 규칙
- 파일 하나에 워크플로우 하나. 노드
name은 영어로, 겹치지 않게. 노드마다 type, typeVersion, position, parameters.
- API 키를 JSON에 쓰지 않는다. LLM 노드는 위 Credential id만 쓴다.
- 웹훅은
POST, responseMode: "responseNode", 결과는 Respond to Webhook 노드로. 갈래마다 Respond 노드를 따로 두어도 된다.
- 요청 본문은
$json.body.question처럼 body 아래. 다른 노드 뒤에서는 $('Question webhook').first().json.body.
5. 검증된 노드 (n8n 2.42.6에서 실행 확인)
입력 검증: 직원 확인 HTTP → Code → If
규칙 검사 (Code) · If
- If 출력 0번이 true(막음), 1번이 false(통과).
판별 LLM (Agent, Tool 없음) + Code로 읽기
- 판별 LLM의 답을 읽지 못하면 공격으로 본다 (69일차 fail-closed).
IT Agent + Tool (67일차와 같음, 질문한 사람은 웹훅에서)
Search FAQ(httpRequestTool, /faq?q=$fromAI(...))와 OpenAI chat model은 67일차와 같다. Tool은 ai_tool, 모델은 ai_languageModel로 Agent에 연결.
- Tool 호출 기록:
$json.intermediateSteps[].action.tool → tools_used.
출력 필터 (Code)
글자 정리 뒤 규칙 (Lab 01 과제 2)
compact(띄어쓰기를 뺀 글자)에도 보면 정상 질문이 더 걸릴 수 있다. 정상 질문 테스트를 같이 돌린다.
판별 LLM이 멈추면 (Lab 02 과제 2)
Injection guard에 "onError": "continueErrorOutput", Guard model options에 "timeout": 8000, "maxRetries": 0 (멈췄을 때 오래 기다리지 않게).
- 연습: 테스트 파일의
before: [{"llm": "down"}], after: [{"llm": "up"}] (OpenAI Credential을 일부러 망가뜨렸다 되돌림, 72일차와 같음).
감사 로그 (HTTP) · 보안 요약 (Lab 03 과제 2)
- 기록이 실패해도 사용자 답은 나간다 (
continueRegularOutput). 보안 요약 워크플로우는 GET http://data-api:8800/audit → Code에서 세기 → Respond.
Edit Fields (Set) — 차단 안내
- 객체 필드:
"type": "object", "value": "={{ { blocked_by: 'rule', reason: $('Injection rules').first().json.rule, masked: 0 } }}".
6. 연결 (connections)
- Agent마다 Chat model을 따로 둔다 (판별 LLM의
Guard model, IT Agent의 OpenAI chat model).
- 두 관문(규칙 · 판별 LLM)이 같은
Blocked 노드로 들어와도 된다. 누가 막았는지는 $('노드').isExecuted로 구분.
- 한 노드에서 두 노드로 나가도 된다:
"Blocked": {"main": [[{"node": "Send blocked", ...}, {"node": "Log event", ...}]]} (답을 보내고 기록도 남김).
onError: continueErrorOutput인 노드의 main은 [ [성공 쪽], [실패 쪽] ]: "Injection guard": {"main": [[Parse guard], [Guard down]]}.
7. 표현식 주의
- 표현식은
=로 시작한다. {{ }} 안에서 객체를 만들 때 }}가 붙지 않게 띄어 쓴다: "={{ { blocked_by: 'rule' } }}"
- 정규식을 JSON 안에 쓸 때
\는 두 번(\\s). Code 노드 안에는 URL 클래스가 없다.
8. 테스트 파일 (cases/*.json)
--webhook open-it-agent로 같은 테스트를 공격 대상에 보낼 수 있다. 케이스마다 "webhook": "my-security-report"처럼 다른 웹훅을 부를 수도 있다.
uv run tools/flow.py test <파일> -v: -v(--verbose)는 케이스마다 보낸 요청 JSON과 받은 응답 JSON을 화면에 보여 준다 (보고서 runs/*.json에도 같은 것이 남음). 모든 명령이 -v를 받는다: call -v 요청 · 응답, bench -v 응답마다 · 정렬한 시간, check -v · push -v 노드 · 연결 목록과 실행한 docker 명령, doctor -v 본 것 (API 키는 sk-***로 가림).
equals · contains · not_contains · status · max_seconds, before · after는 72일차와 같다.
- 테스트를 통과시키려고 테스트 파일의 기대 결과를 바꾸지 않는다. 기대 결과가 틀렸다고 생각되면 사람에게 묻는다.
9. 하지 말 것
tools/, data-api/, requests/, reference/, 이 파일(AGENTS.md)을 고치지 않는다 (요청받은 경우만).
- 지시문 · 워크플로우 JSON에 비밀(코드 · 출입 번호 · 키 · 내부 메모)을 쓰지 않는다.
- 막으려고 정상 질문까지 막지 않는다 (정상 질문 테스트도 함께 통과해야 한다).
- n8n 컨테이너를 지우거나
docker compose down을 하지 않는다. ~/day65-n8n(n8n 설치·비밀값)을 읽거나 고치지 않는다. Credential을 새로 만들지 않는다.
.env나 API 키를 읽어서 출력하지 않는다.