rinapi 개발 포트폴리오 EN

RIN API / 콘셉트 영상 00:40

프로젝트 보기

소개

RIN API는 에이전트의 식별 정보와 운영 주체를 연결하기 위해 만든 식별 레지스트리 프로토타입입니다.

출발한 문제

같은 모델의 휴머노이드나 비슷한 이름의 온라인 에이전트라도 운영 주체와 신뢰 수준은 다를 수 있습니다. 겉모습으로 구별하기 어려운 상황에서, 문제가 생겼을 때 운영·책임 주체를 확인할 기준이 필요하다고 생각했습니다.

기획 방향

초기에는 여러 서비스가 에이전트의 등록 관계를 함께 검증하는 블록체인 기반 레지스트리를 구상했습니다. 공유할 수 있는 짧은 ID를 두고, 발급 계정과 공개 식별자, 연결 정보를 기록하는 권한을 나눴습니다.

첫 구현

온체인 등록에 앞서 HTTP API로 ID 발급·공개 조회·토큰 기반 클레임을 구현했습니다. 실제 에이전트가 문서를 읽고 사용할 수 있는지 OpenClaw에 직접 사용을 요청해 확인했습니다.

OpenClawAssistant의 공개 ID 카드

관련 제품·표준과 비교

2026.11.10 공식 문서 기준 · 현재 제품과의 비교

제품·표준주요 역할RIN과의 차이
Microsoft Entra Agent ID [1]기업용 신원관리 제품조직 테넌트의 에이전트 신원과 토큰을 관리하고, 담당자·그룹인 sponsor를 연결합니다.RIN은 독립 HTTP 서비스에서 짧은 ID와 공개 조회 흐름을 구현했습니다. 조직 권한 관리와 sponsor 수명주기는 현재 범위 밖입니다.
ERC-8004 [2]온체인 레지스트리 표준안 · DraftERC-721 기반 신원, 평판 피드백, 독립 검증 결과를 다루는 세 레지스트리를 제안합니다.RIN은 중앙 DB에서 상태를 관리하고 지갑·온체인 등록 없이 호출합니다. 평판과 외부 검증 기능은 포함하지 않습니다.
A2A Agent Card [3]에이전트 통신·발견 표준에이전트의 기능·접속 방식·인증 요건을 기술합니다. 선택적으로 카드에 JWS 서명을 추가할 수 있습니다.RIN은 조회용 ID 발급과 클레임 상태 관리에 집중합니다. A2A 호환과 서명된 카드는 구현하지 않았습니다.

RIN의 초점

짧은 ID를 공유하고, 사람은 공개 카드로·에이전트는 JSON으로 같은 레코드를 조회하는 흐름에 초점을 뒀습니다. 발급 계정의 키와 특정 RIN의 연결 권한을 나눠 발급·조회·클레임을 HTTP API로 구현했습니다.

현재 범위는 중앙 레지스트리 프로토타입입니다. 실제 운영자 신원과 ID를 제시한 에이전트의 제어권 확인은 남은 과제입니다.

비교 근거 · 공식 문서

[1] Microsoft Learn · Agent identities

테넌트 내 신원, blueprint 기반 토큰, sponsor의 역할을 확인했습니다.

https://learn.microsoft.com/en-us/entra/agent-id/agent-identities

[2] EIP-8004 · Trustless Agents

Identity·Reputation·Validation Registry와 Draft 상태를 확인했습니다.

https://eips.ethereum.org/EIPS/eip-8004

[3] A2A specification · Agent Card

Agent Card의 발견·인증 정보와 선택적 JWS 서명을 확인했습니다.

https://a2a-protocol.org/latest/specification/

[4] W3C · Decentralized Identifiers v1.0

DID의 제어권·검증 방법·해석 모델을 참고했습니다. DID는 특정 블록체인 기술을 전제로 하지 않습니다.

https://www.w3.org/TR/2022/REC-did-core-20220719/

[5] PostgreSQL · Read Committed isolation

동시 UPDATE가 행을 기다린 뒤 WHERE 조건을 다시 평가하는 동작을 확인했습니다.

https://www.postgresql.org/docs/18/transaction-iso.html#XACT-READ-COMMITTED

설계

현재 프로토타입은 하나의 중앙 레지스트리에서 ID와 상태를 관리합니다. HTTP 요청을 받은 Fastify가 인증과 상태 변경을 처리하고, PostgreSQL이 식별자와 연결 기록을 저장합니다.

  1. HTTP Client에이전트·웹 클라이언트
  2. Fastify인증·요청 처리
  3. PostgreSQL식별자·상태 저장

식별자와 권한 분리

RIN공개
에이전트의 공개 정보를 조회하는 식별자
API 키비공개
발급 계정 인증, RIN 발급과 계정 작업
클레임 토큰비공개
특정 RIN에 연결 정보를 한 번 기록하는 권한

발급 계정 하나가 여러 RIN을 만들 수 있도록 구성했습니다. 발급 주체는 API 키로 인증한 계정으로 기록합니다.

초기 테이블과 발급 관계 코드
migrations/004_agents.sql · 1–8 · 코드 발췌
CREATE TABLE IF NOT EXISTS agents (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL UNIQUE,
  description TEXT,
  api_key_hash TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_seen_at TIMESTAMPTZ
);
migrations/001_init.sql · 7–17 · 코드 발췌
CREATE TABLE IF NOT EXISTS entities (
  entity_id TEXT PRIMARY KEY,
  rin TEXT NOT NULL UNIQUE,
  agent_type TEXT NOT NULL,
  agent_name TEXT,
  status TEXT NOT NULL DEFAULT 'UNCLAIMED',
  claimed_by TEXT,
  claimed_at TIMESTAMPTZ,
  issued_at TIMESTAMPTZ NOT NULL
);
migrations/007_issued_by_agent.sql · 20–22 · 코드 발췌
ALTER TABLE entities
  ADD CONSTRAINT fk_entities_issued_by_agent
  FOREIGN KEY (issued_by_agent_name) REFERENCES agents(name) ON DELETE SET NULL;

왜 이 구조인가

블록체인 적용 전 HTTP 흐름 검증

먼저 발급·조회·클레임을 한 서비스에서 구현하고, OpenClaw가 별도 SDK나 지갑 연결 없이 사용할 수 있는지 확인했습니다.

현재 구현은 중앙 API 단계입니다. 온체인 등록과 서명 기반 제어권 확인은 다음 개발 범위입니다.

데이터 규칙을 DB에서 처리 [5]

요청 처리 코드에서 상태를 읽고 나중에 갱신하면, 두 요청이 같은 이전 상태를 보고 처리될 수 있습니다. PostgreSQL의 UNIQUE 제약과 조건부 UPDATE로 DB가 성공 조건을 판단하도록 구성했습니다.

고유성과 상태 관리의 기준은 이 DB입니다. 여러 레지스트리에 걸친 고유성이나 합의는 다루지 않습니다.

블록체인 확장 설계

향후 개발 계획 · 현재는 HTTP 프로토타입

처음에는 여러 서비스가 에이전트의 신원과 변경 이력을 함께 검증하는 블록체인 기반 레지스트리로 기획했습니다. 현재는 발급·조회·계정 연결을 HTTP API로 먼저 구현했고, 다음 단계에서 공용 등록 이력을 체인으로 옮기는 구조를 계획하고 있습니다.

지금 적용을 미룬 이유
온체인 등록에 앞서 실제 에이전트가 ID를 발급받고 조회하는 흐름부터 확인했습니다. OpenClaw 연동으로 사용 흐름을 검증한 뒤, 키 제어권과 운영자 확인을 보강하고 체인을 연결하는 순서로 개발 범위를 나눴습니다.
현재 구조의 한계
지금은 RIN 서버와 DB가 등록 상태의 기준입니다. 서버 장애와 운영 정책에 의존하고, 다른 서비스가 우리 서버를 거치지 않고 등록·변경 이력을 검증할 수 없습니다.
도입할 조건
서로 다른 조직이 같은 RIN을 검증하고, 한 운영자에게 등록 관리권을 맡기기 어려운 단계에서 온체인 등록을 적용합니다. 도입 전에는 키 연결·회수 규칙과 독립 검증기의 동작을 먼저 확인합니다.

다음 구현 구조

On-chain
공용 레지스트리등록 ID · 인증키 버전 · 관리권 변경 이력
Off-chain
RIN API / PostgreSQL프로필 · 운영자 증명 · 실행 권한

확정 이벤트 → 인덱서 → API 조회 데이터

  1. 에이전트와 인증키 연결

    RIN에 공개키를 연결하고, 일회성 요청의 서명으로 제어권을 확인합니다. 등록을 관리하는 지갑과 에이전트 인증키를 분리하고, 실제 운영자는 별도 확인 절차로 연결합니다.

  2. 테스트넷에 등록 기능 구현

    ERC-8004 표준안의 등록 구조를 기준으로 RIN과 체인 ID·레지스트리 주소·agentId를 대응시킵니다. 인증키 버전과 변경 규칙을 정의하고, 개인정보·API 키·세부 권한은 체인 밖에 둡니다.

  3. API를 조회·연동 계층으로 전환

    확정된 계약 이벤트로 DB의 조회 데이터를 갱신하고, 응답에 등록 대기·확정 상태와 기준 블록을 구분합니다. 다른 서비스도 원장과 인증키 버전을 직접 대조할 수 있게 합니다.

  4. 회수·복구와 운영 권한 설계

    실행 권한은 짧은 만료와 별도 회수 상태로 관리하고, 상태가 오래되면 보호된 작업을 거절합니다. 키 탈취 후 복구, 체인 재구성 시 재동기화, 계약 변경의 공동 승인도 함께 시험합니다.

확장 단계의 완료 기준

독립된 두 검증기가 같은 등록·키 변경·회수 상태를 판단하는지 확인합니다. 이후 서로 다른 조직의 에이전트를 연결하고, 비용·확정 시간·장애 복구 결과를 바탕으로 운영할 네트워크를 정할 계획입니다.

구현

발급·조회, 클레임, 키 관리, ID 충돌 처리를 구현했습니다. 관련 코드는 각 항목 안에서 펼쳐볼 수 있습니다.

발급과 공개 조회

공개 ID를 공유해도 발급 권한이 함께 넘어가지 않도록 조회와 쓰기 인증을 분리했습니다. 발급에는 API 키가 필요하고, 공개 응답은 허용한 식별·프로필 필드만 골라 구성합니다.

조회 응답은 등록된 정보와 상태를 보여줍니다. 그 정보를 제시한 실행 주체의 진위까지 증명하는 구조는 아닙니다.

주요 API와 필요한 권한
엔드포인트권한
POST /api/v1/agents/register발급 계정 등록인증 없음
GET /api/v1/agents/me인증된 계정 조회API 키
POST /api/registerRIN 발급API 키
GET /api/id/:rin공개 정보 조회인증 없음
POST /api/claim연결 정보 기록클레임 토큰
POST /api/v1/agents/rotate-key키 교체API 키
POST /api/v1/agents/revoke키 폐기API 키
공개 조회 응답 코드
src/server.js · 540–555 · 코드 발췌
const response = {
  rin: row.rin,
  agent_type: row.agent_type,
  agent_name: row.agent_name || undefined,
  status: row.status,
};

if (row.status === 'CLAIMED') {
  response.claimed_by = row.claimed_by || undefined;
}

const profile = {};
if (row.bio) profile.bio = row.bio;
if (row.avatar_url) profile.avatar_url = row.avatar_url;
if (row.links && Object.keys(row.links).length > 0) profile.links = row.links;
if (Object.keys(profile).length > 0) response.profile = profile;

중복 클레임 방지

상태를 조회한 뒤 별도로 갱신하는 두 단계 대신, 상태와 토큰이 모두 맞을 때만 하나의 UPDATE가 성공하도록 묶었습니다. 성공하면 토큰 해시를 지워 같은 연결 권한의 재사용을 막습니다.

server.js · SQL · 코드 발췌
UPDATE entities
SET status = 'CLAIMED',
    claimed_by = $1,
    claimed_at = $2,
    claim_token_hash = NULL,
    claim_token_issued_at = NULL
WHERE rin = $3
  AND status = 'UNCLAIMED'
  AND claim_token_hash = $4
RETURNING rin, status, claimed_by, claimed_at

클레임은 제출한 이름을 기록하는 단계입니다. 성공 응답을 잃고 재요청하면 409가 되므로, 응답 유실 후 복구 규칙은 별도로 필요합니다.

API 키 교체와 폐기

발급 계정의 API 키와 RIN별 클레임 토큰을 나눠, 연결 정보를 기록할 때 계정 키를 전달하지 않도록 했습니다. API 키는 32바이트 난수로 만들고 HMAC-SHA256 해시만 저장하며, 교체·폐기는 이후 인증 요청에 반영합니다.

인증할 때마다 DB의 키 상태를 조회합니다. 키 폐기와 이미 발급된 공개 RIN의 수명은 분리했습니다.

키 생성·해시 저장 코드
src/server.js · 92–94 · 코드 발췌
function hashAgentApiKey(apiKey) {
  return crypto.createHmac('sha256', AGENT_API_KEY_PEPPER).update(apiKey).digest('hex');
}
src/server.js · 208–209 · 코드 발췌
const apiKey = `rin_${crypto.randomBytes(32).toString('base64url')}`;
const apiKeyHash = hashAgentApiKey(apiKey);
src/server.js · 245–250 · 코드 발췌
const result = await query(
  `INSERT INTO agents (name, description, api_key_hash)
   VALUES ($1, $2, $3)
   RETURNING name, description, created_at`,
  [name, description, apiKeyHash]
);
인증 코드
src/server.js · 96–120 · 코드 발췌
async function requireAgentAuth(req, reply) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }

  const apiKey = auth.slice('Bearer '.length).trim();
  if (!apiKey || !apiKey.startsWith('rin_')) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }

  const apiKeyHash = hashAgentApiKey(apiKey);
  const result = await query(
    `SELECT name, description, created_at, last_seen_at, revoked_at
     FROM agents
     WHERE api_key_hash = $1 AND revoked_at IS NULL`,
    [apiKeyHash]
  );

  if (result.rowCount === 0) {
    reply.code(401).send({ error: 'Unauthorized' });
    return null;
  }
키 교체 코드
src/server.js · 277–293 · 코드 발췌
fastify.post('/api/v1/agents/rotate-key', async (req, reply) => {
  const agent = await requireAgentAuth(req, reply);
  if (!agent) return;

  const apiKey = `rin_${crypto.randomBytes(32).toString('base64url')}`;
  const apiKeyHash = hashAgentApiKey(apiKey);

  await query(
    `UPDATE agents
     SET api_key_hash = $1,
         revoked_at = NULL
     WHERE name = $2`,
    [apiKeyHash, agent.name]
  );

  return reply.send({ api_key: apiKey, rotated: true, important: 'SAVE YOUR API KEY!' });
});
키 폐기 코드
src/server.js · 295–310 · 코드 발췌
fastify.post('/api/v1/agents/revoke', async (req, reply) => {
  const agent = await requireAgentAuth(req, reply);
  if (!agent) return;

  await query(
    `UPDATE agents
     SET revoked_at = NOW(),
         bio = NULL,
         avatar_url = NULL,
         links = NULL
     WHERE name = $1`,
    [agent.name]
  );

  return reply.send({ revoked: true });
});

짧은 ID의 충돌 처리

RIN 레코드의 내부 키는 UUID로 관리하고, 화면·대화에서 공유하는 공개 식별자만 6–8자리 RIN으로 분리했습니다. I·O·0·1을 제외한 문자로 생성하며, DB의 UNIQUE 제약과 총 12회 시도로 충돌을 처리합니다.

짧은 공개 ID의 이름 공간과 충돌 처리를 운영해야 합니다. 고유성은 해당 레지스트리의 DB 안에서 관리합니다.

ID 생성 코드
src/server.js · 136 · 코드 발췌
const entityId = crypto.randomUUID();
src/rin.js · 3–17 · 코드 발췌
const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

function randomChar() {
  const idx = crypto.randomInt(0, ALPHABET.length);
  return ALPHABET[idx];
}

export function generateRin() {
  const length = crypto.randomInt(6, 9);
  let out = '';
  for (let i = 0; i < length; i += 1) {
    out += randomChar();
  }
  return out;
}
충돌 처리 코드
src/server.js · 139–144 · 코드 발췌
`INSERT INTO entities (
  entity_id, rin, agent_type, agent_name, status, issued_at,
  claim_token_hash, claim_token_issued_at, issued_by_agent_name
 )
 VALUES ($1, $2, $3, $4, 'UNCLAIMED', $5, $6, $7, $8)
 ON CONFLICT (rin) DO NOTHING`,
src/server.js · 156–156 · 코드 발췌
return result.rowCount === 1 ? { entityId, issuedAt } : null;
src/server.js · 159–178 · 코드 발췌
async function createUniqueRin(agentType, agentName, issuedByAgentName) {
  for (let i = 0; i < 12; i += 1) {
    const rin = generateRin();
    const claimToken = generateClaimToken();
    const claimTokenHash = hashClaimToken(claimToken, CLAIM_TOKEN_PEPPER);
    const claimTokenIssuedAt = new Date().toISOString();
    const inserted = await insertEntity({
      rin,
      agentType,
      agentName,
      claimTokenHash,
      claimTokenIssuedAt,
      issuedByAgentName,
    });
    if (inserted) {
      return { rin, issued_at: inserted.issuedAt, claim_token: claimToken };
    }
  }
  throw new Error('Failed to generate unique RIN');
}

OpenClaw 연동

OpenClaw에 공개된 서비스를 직접 사용해 보도록 요청했습니다. 에이전트가 문서를 읽고 호출 스크립트를 만드는 과정을 지켜보며, 실제로 사용할 수 있는지와 어디서 막히는지 확인했습니다.

사용 실험 · 2026.02.13

  1. 문서 확인

    에이전트가 공개된 skill 문서의 앞부분을 읽고 API 주소와 호출 방법을 확인했습니다. 별도 전용 SDK 없이 HTTP 요청으로 연동을 시작했습니다.

  2. 실행 실패와 수정

    처음 만든 셸 명령은 따옴표 오류로 실행되지 않았습니다. 에이전트가 스크립트를 고친 뒤 계정 등록, RIN 발급, 공개 조회를 순서대로 실행했습니다.

    당시 실행 오류 펼치기
    OpenClaw에 남은 셸 따옴표 오류
  3. 발급과 결과 확인

    Z7DE6A9K / OpenClawAssistant / UNCLAIMED가 반환됐습니다. 보존된 공개 카드에서도 같은 ID와 이름, 상태를 확인할 수 있습니다.

원본 실행 기록 · 2026.02.13
도구 응답과 공개 조회 기록
OpenClaw 도구 응답 발췌
{
  "rin": "Z7DE6A9K",
  "agent_type": "assistant",
  "agent_name": "OpenClawAssistant",
  "status": "UNCLAIMED",
  "claimed_by": null
}
보존된 도구 응답 화면
보존된 터미널 화면 · 2CRQ4VSY와 Z7DE6A9K의 공개 조회

검증

기능이 성공하는 경우와 인증·상태 조건으로 거절되는 경우를 함께 확인했습니다. 아래는 당시 실행 기록에서 확인한 결과입니다.

인증 없는 RIN 발급

2026.02.10
POST /api/register · API 키 없음
401

클레임

YJC2US8N · 2026.02.10
잘못된 토큰
403
정상 연결
200
같은 RIN 재요청
409

키 교체·폐기

K5NUH36 · 2026.02.12
교체 전 키로 인증
401
새 키로 같은 계정 조회
성공
키 폐기 후 인증
401

클레임 상태 전이는 순차 요청으로 확인했습니다. 새 키의 정상 동작은 같은 계정이 조회되는 것으로 확인했습니다.

추가 공개 카드 기록

아래는 서로 다른 RIN의 보존 화면입니다. 앞선 Z7DE6A9K의 실행 과정과 구분했습니다. 당시 Verified 배지는 클레임 상태를 표시합니다.

ZU4L2V9 · openclaw-main-session · CLAIMED
RPMVQ9 · openclaw-tester-abc · UNCLAIMED
2CRQ4VSY · openclaw-main-session-profile · UNCLAIMED

회고

호출 성공만으로 사용 흐름까지 검증되지는 않았습니다. 에이전트가 결과를 찾고 의도한 작업을 구분할 수 있도록, 문서와 응답을 함께 다듬어야 한다는 점을 배웠습니다.

남은 작업

  1. 응답 계약 정리

    등록·키 교체의 응답 구조를 맞추고, 클라이언트의 파싱과 오류 처리를 검증할 계획입니다.

  2. 읽기·쓰기 명령 분리

    현재 CLI의 status 명령은 RIN을 발급합니다. 계정 조회는 GET /api/v1/agents/me로 바꿔 부수적인 발급을 없애는 작업이 남아 있습니다.

  3. 제어권 확인

    공개 RIN을 제시한 에이전트가 해당 인증정보를 실제로 제어하는지 확인하는 서명 기반 검증을 다음 실험으로 남겼습니다.