kisenon
Agent-Safe Change Control

흔한 함정

스케일 투 제로와 에이전트 안전을 조용히 무력화하는 안티패턴 — 그리고 피하는 방법.

이 패턴들은 오류를 내지 않습니다. CI를 통과하고, diff에서 합리적으로 보이며, 당신이 비용을 지불하고 있는 보장 — 스케일 투 제로 절감이나 에이전트 안전 — 을 조용히 취소합니다. 아래 각 항목: 무슨 일이 일어나는지, 왜 해로운지, 그리고 해결책.

킵얼라이브 코드가 스케일 투 제로를 무력화합니다

에이전트가 "도움이 되려고" 타이머로 데이터베이스를 건드리는 코드를 추가합니다. 모든 주기적 쿼리가 활동으로 계산되어 일시 중지 타이머를 리셋하므로, 0으로 스케일하도록 설계한 엔드포인트가 결코 잠들지 않습니다 — 그리고 유휴가 더는 무료가 아니게 됩니다.

여러 형태로 나타납니다. 프런트엔드나 백엔드 하트비트:

// runs forever, holds the compute awake forever
setInterval(async () => {
  await pool.query("SELECT 1");   // "health check"
}, 30_000);

핸들러가 우연히 DB를 쿼리하는 엔드포인트를 가리키는 외부 업타임 모니터 — 그래서 모든 프로브가 컴퓨트를 깨웁니다:

app.get("/health", async (_req, res) => {
  await pool.query("SELECT 1");   // monitored every 60s → never idle
  res.send("ok");
});

또는 검증 쿼리로 라이브 연결을 유지하는 커넥션 풀 설정 — 0보다 큰 min, 또는 서버리스 "워머":

const pool = new Pool({
  min: 2,                         // keeps ≥2 connections open + validated
  // a validation/keep-alive query on each idle connection resets the timer
});

해결책: 에이전트가 작성한 diff에서 주기적 데이터베이스 접근을 감사하세요. 라이브니스 검사는 HTTP 계층에 두고 — DB가 아니라 앱을 프로브하고 — 풀이 0으로 빠지게 두세요(min: 0). 콘솔 컴퓨트 뷰는 엔드포인트가 0으로 스케일하지 않는지 보여주며, 이는 떠도는 하트비트를 빠르게 드러냅니다.

에이전트에게 main 자격 증명을 넘기면 ASCC가 단락됩니다

에이전트에게 main 브랜치 연결 문자열을 주거나 — 또는 당신 본인 사용자로 로그인한 채 keon을 실행하게 두는 것 — 은 ASCC가 집행하는 모든 것을 우회합니다: 샌드박스 격리, LEASH 예산, LINT 및 BLAST 검사, 프로모트 게이트, 귀속, undo 앵커. 이제 에이전트는 프로덕션에 직접 쓰고 있으며, 에이전트 변경을 안전하게 만드는 루프는 전혀 실행되지 않습니다.

에이전트는 main에 쓸 수 있는 자격 증명을 결코 보유해서는 안 됩니다. 프로모트는 설계상 서버 측입니다 — cp가 프로덕션에 쓰는 유일한 주체입니다.

해결책: Settings → API keys에서 에이전트 범위 API 키(capability = agent, scope = a project, 짧은 만료)를 발급하고, 에이전트에게 샌드박스 연결 문자열만 넘기며, 그 환경에서 더 강력한 자격 증명을 제거하세요(완전한 DATABASE_URL 없음, ~/.pgpass 없음, 같은 셸에 로그인된 keon 세션 없음). 에이전트에게 범위가 제한된 자격 증명 넘기기Guardrails의 SCOPE 섹션을 참조하세요.

웨이크업 주변의 재시도 폭풍

일시 중지 후 첫 연결은 컴퓨트를 깨워야 하므로, 웜 연결보다 느립니다. 한 번의 느린 연결을 본 에이전트가 그것을 "고칠" 수 있습니다 — 공격적인 클라이언트 타임아웃과 엔드포인트를 두들기는 재시도 루프로, 또는 킵얼라이브(함정 #1)를 추가하여 아예 깨울 필요가 없게 만들어서. 둘 다 일회성 비용을 덧칠하려고 절감을 내다 버립니다.

해결책: 클라이언트 connect_timeout을 10초 정도로 설정하고 첫 연결에서는 인내하세요. 웜 경로가 작동하면 콜드 스타트는 동일 리전에서 1초 미만입니다 — 약간 느린 한 번의 연결은 예상된 것이지, 우회 설계할 결함이 아닙니다.

postgresql://…?connect_timeout=10

코드에 커밋된 자격 증명

에이전트가 연결 문자열과 API 키를 소스와 .env 파일에 하드코딩하고, 그것이 git에 도달합니다. 유출된 키는 공개(또는 나중에 공개되는) 저장소에 도달한 지 몇 시간 안에 수집됩니다 — 창은 며칠이 아니라 시간 단위로 측정됩니다.

해결책: 비밀은 환경을 통해 주입하고, 결코 커밋하지 마세요. .env 파일을 git 밖에 두세요. 유출이 있으면 Settings → API keys에서 즉시 로테이션하세요 — 노출된 키를 폐기하고 새 것을 발급하세요.

main에 대한 직접 DDL은 프로모트 루프를 우회합니다

main에 곧바로 마이그레이션을 실행하면 변경을 되돌릴 수 있고 감사 가능하게 만드는 캡처를 건너뜁니다. 방출된 up/down 마이그레이션(EMIT), 서명된 원장 항목(LEDGER), undo 앵커(UNDO)를 잃습니다. 스키마는 변경되었지만, 플랫폼이 재생하거나 롤백할 수 있는 기록이 없습니다.

해결책: "사소한" 스키마 변경이라도 샌드박스 → 프로모트하세요. 프로모트 루프가 마이그레이션, 원장 항목, 앵커를 생성하는 것입니다. 직접 ALTER는 그중 어느 것도 생성하지 않습니다.

롤백된 트랜잭션 안의 쓰기도 여전히 착지합니다

캡처는 에이전트가 실행하는 문장을 기록합니다 — 트랜잭션 경계 인식이 없습니다. BEGIN … ROLLBACK 안에서 실행된 문장도 여전히 캡처되며, 그 샌드박스가 프로모트되면 재생되어 적용됩니다. 샌드박스의 ROLLBACK은 그것들을 캡처에서 지우지 않습니다.

BEGIN;
UPDATE users SET plan = 'pro';   -- captured
ROLLBACK;                        -- does NOT remove it from the capture

해결책: 탐색적 작업을 "취소"하려고 샌드박스 안의 ROLLBACK에 의존하지 마세요. 작업이 사라지길 원하면 샌드박스를 버리세요 — 그것이 캡처가 존중하는 폐기 경로입니다.

예산이나 TTL을 넘겨 샌드박스에 작업을 방치하기

샌드박스는 임시적입니다. 샌드박스가 LEASH 예산을 소진하면 자동으로 버려지며(status=discarded), 그 안의 프로모트되지 않은 작업은 모두 사라집니다. 샌드박스를 "나중에 돌아올" 장수 브랜치처럼 취급하는 것이 하루치 변경이 사라지는 방식입니다.

해결책: 프로모트하거나, 새 샌드박스에서 작업을 다시 캡처하세요. 샌드박스는 기본적으로 일회용으로 취급하세요 — 견고한 아티팩트는 샌드박스가 아니라 프로모트입니다.

보장을 잘못 읽기

검사가 실제로 무엇을 약속하는지에 관한 세 가지 구체적인 함정:

  • 폭발 반경 드라이런은 권고입니다. 프로모트가 무엇을 할지를 측정합니다 — 실제 락, 지속 시간, 행 수 — 하지만 결코 프로모트를 게이팅하지 않습니다. 깨끗한 드라이런은 정보이지 청신호가 아닙니다. 그리고 프로모트 전에 드라이런이 필수인지 여부는 드라이런이 아니라 POLICY의 결정입니다.

  • Approve는 정책 block을 재정의할 수 없습니다. 사람의 Approve는 정책 block이나 레인 밖 문장을 결코 통과시킬 수 없습니다. block을 지나는 유일한 방법은 감사되는 정책 변경입니다 — 설계상 수동 재정의 스위치는 없습니다.

  • 마스킹된 포크는 값을 마스킹하지, 형태를 마스킹하지 않습니다. 마스킹된 포크에서는 이 마스킹되므로, 에이전트가 그 마스킹된 값에 대해 작성한 데이터 의존적 DML은 재생 시 부모의 실제 행과 매칭되지 않습니다. 스키마 레인 작업은 값에 독립적이며 충실하게 재생됩니다. 마스킹된 데이터에 대해 개발된 값 의존적 DML은 그렇지 않습니다.