블록체인 적용 전 HTTP 흐름 검증
먼저 발급·조회·클레임을 한 서비스에서 구현하고, OpenClaw가 별도 SDK나 지갑 연결 없이 사용할 수 있는지 확인했습니다.
현재 구현은 중앙 API 단계입니다. 온체인 등록과 서명 기반 제어권 확인은 다음 개발 범위입니다.
RIN API / 콘셉트 영상 00:40
RIN API는 에이전트의 식별 정보와 운영 주체를 연결하기 위해 만든 식별 레지스트리 프로토타입입니다.
같은 모델의 휴머노이드나 비슷한 이름의 온라인 에이전트라도 운영 주체와 신뢰 수준은 다를 수 있습니다. 겉모습으로 구별하기 어려운 상황에서, 문제가 생겼을 때 운영·책임 주체를 확인할 기준이 필요하다고 생각했습니다.
초기에는 여러 서비스가 에이전트의 등록 관계를 함께 검증하는 블록체인 기반 레지스트리를 구상했습니다. 공유할 수 있는 짧은 ID를 두고, 발급 계정과 공개 식별자, 연결 정보를 기록하는 권한을 나눴습니다.
온체인 등록에 앞서 HTTP API로 ID 발급·공개 조회·토큰 기반 클레임을 구현했습니다. 실제 에이전트가 문서를 읽고 사용할 수 있는지 OpenClaw에 직접 사용을 요청해 확인했습니다.
2026.11.10 공식 문서 기준 · 현재 제품과의 비교
| 제품·표준 | 주요 역할 | RIN과의 차이 |
|---|---|---|
| Microsoft Entra Agent ID [1]기업용 신원관리 제품 | 조직 테넌트의 에이전트 신원과 토큰을 관리하고, 담당자·그룹인 sponsor를 연결합니다. | RIN은 독립 HTTP 서비스에서 짧은 ID와 공개 조회 흐름을 구현했습니다. 조직 권한 관리와 sponsor 수명주기는 현재 범위 밖입니다. |
| ERC-8004 [2]온체인 레지스트리 표준안 · Draft | ERC-721 기반 신원, 평판 피드백, 독립 검증 결과를 다루는 세 레지스트리를 제안합니다. | RIN은 중앙 DB에서 상태를 관리하고 지갑·온체인 등록 없이 호출합니다. 평판과 외부 검증 기능은 포함하지 않습니다. |
| A2A Agent Card [3]에이전트 통신·발견 표준 | 에이전트의 기능·접속 방식·인증 요건을 기술합니다. 선택적으로 카드에 JWS 서명을 추가할 수 있습니다. | RIN은 조회용 ID 발급과 클레임 상태 관리에 집중합니다. A2A 호환과 서명된 카드는 구현하지 않았습니다. |
짧은 ID를 공유하고, 사람은 공개 카드로·에이전트는 JSON으로 같은 레코드를 조회하는 흐름에 초점을 뒀습니다. 발급 계정의 키와 특정 RIN의 연결 권한을 나눠 발급·조회·클레임을 HTTP API로 구현했습니다.
현재 범위는 중앙 레지스트리 프로토타입입니다. 실제 운영자 신원과 ID를 제시한 에이전트의 제어권 확인은 남은 과제입니다.
테넌트 내 신원, blueprint 기반 토큰, sponsor의 역할을 확인했습니다.
https://learn.microsoft.com/en-us/entra/agent-id/agent-identitiesIdentity·Reputation·Validation Registry와 Draft 상태를 확인했습니다.
https://eips.ethereum.org/EIPS/eip-8004Agent Card의 발견·인증 정보와 선택적 JWS 서명을 확인했습니다.
https://a2a-protocol.org/latest/specification/DID의 제어권·검증 방법·해석 모델을 참고했습니다. DID는 특정 블록체인 기술을 전제로 하지 않습니다.
https://www.w3.org/TR/2022/REC-did-core-20220719/동시 UPDATE가 행을 기다린 뒤 WHERE 조건을 다시 평가하는 동작을 확인했습니다.
https://www.postgresql.org/docs/18/transaction-iso.html#XACT-READ-COMMITTED현재 프로토타입은 하나의 중앙 레지스트리에서 ID와 상태를 관리합니다. HTTP 요청을 받은 Fastify가 인증과 상태 변경을 처리하고, PostgreSQL이 식별자와 연결 기록을 저장합니다.
HTTP Client에이전트·웹 클라이언트Fastify인증·요청 처리PostgreSQL식별자·상태 저장발급 계정 하나가 여러 RIN을 만들 수 있도록 구성했습니다. 발급 주체는 API 키로 인증한 계정으로 기록합니다.
agents발급 계정entities공개 RINCREATE 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
);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
);
ALTER TABLE entities
ADD CONSTRAINT fk_entities_issued_by_agent
FOREIGN KEY (issued_by_agent_name) REFERENCES agents(name) ON DELETE SET NULL;먼저 발급·조회·클레임을 한 서비스에서 구현하고, OpenClaw가 별도 SDK나 지갑 연결 없이 사용할 수 있는지 확인했습니다.
현재 구현은 중앙 API 단계입니다. 온체인 등록과 서명 기반 제어권 확인은 다음 개발 범위입니다.
요청 처리 코드에서 상태를 읽고 나중에 갱신하면, 두 요청이 같은 이전 상태를 보고 처리될 수 있습니다. PostgreSQL의 UNIQUE 제약과 조건부 UPDATE로 DB가 성공 조건을 판단하도록 구성했습니다.
고유성과 상태 관리의 기준은 이 DB입니다. 여러 레지스트리에 걸친 고유성이나 합의는 다루지 않습니다.
향후 개발 계획 · 현재는 HTTP 프로토타입
처음에는 여러 서비스가 에이전트의 신원과 변경 이력을 함께 검증하는 블록체인 기반 레지스트리로 기획했습니다. 현재는 발급·조회·계정 연결을 HTTP API로 먼저 구현했고, 다음 단계에서 공용 등록 이력을 체인으로 옮기는 구조를 계획하고 있습니다.
확정 이벤트 → 인덱서 → API 조회 데이터
RIN에 공개키를 연결하고, 일회성 요청의 서명으로 제어권을 확인합니다. 등록을 관리하는 지갑과 에이전트 인증키를 분리하고, 실제 운영자는 별도 확인 절차로 연결합니다.
ERC-8004 표준안의 등록 구조를 기준으로 RIN과 체인 ID·레지스트리 주소·agentId를 대응시킵니다. 인증키 버전과 변경 규칙을 정의하고, 개인정보·API 키·세부 권한은 체인 밖에 둡니다.
확정된 계약 이벤트로 DB의 조회 데이터를 갱신하고, 응답에 등록 대기·확정 상태와 기준 블록을 구분합니다. 다른 서비스도 원장과 인증키 버전을 직접 대조할 수 있게 합니다.
실행 권한은 짧은 만료와 별도 회수 상태로 관리하고, 상태가 오래되면 보호된 작업을 거절합니다. 키 탈취 후 복구, 체인 재구성 시 재동기화, 계약 변경의 공동 승인도 함께 시험합니다.
독립된 두 검증기가 같은 등록·키 변경·회수 상태를 판단하는지 확인합니다. 이후 서로 다른 조직의 에이전트를 연결하고, 비용·확정 시간·장애 복구 결과를 바탕으로 운영할 네트워크를 정할 계획입니다.
발급·조회, 클레임, 키 관리, ID 충돌 처리를 구현했습니다. 관련 코드는 각 항목 안에서 펼쳐볼 수 있습니다.
공개 ID를 공유해도 발급 권한이 함께 넘어가지 않도록 조회와 쓰기 인증을 분리했습니다. 발급에는 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 키 |
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가 성공하도록 묶었습니다. 성공하면 토큰 해시를 지워 같은 연결 권한의 재사용을 막습니다.
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 키와 RIN별 클레임 토큰을 나눠, 연결 정보를 기록할 때 계정 키를 전달하지 않도록 했습니다. API 키는 32바이트 난수로 만들고 HMAC-SHA256 해시만 저장하며, 교체·폐기는 이후 인증 요청에 반영합니다.
인증할 때마다 DB의 키 상태를 조회합니다. 키 폐기와 이미 발급된 공개 RIN의 수명은 분리했습니다.
function hashAgentApiKey(apiKey) {
return crypto.createHmac('sha256', AGENT_API_KEY_PEPPER).update(apiKey).digest('hex');
}const apiKey = `rin_${crypto.randomBytes(32).toString('base64url')}`;
const apiKeyHash = hashAgentApiKey(apiKey);const result = await query(
`INSERT INTO agents (name, description, api_key_hash)
VALUES ($1, $2, $3)
RETURNING name, description, created_at`,
[name, description, apiKeyHash]
);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;
}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!' });
});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 });
});RIN 레코드의 내부 키는 UUID로 관리하고, 화면·대화에서 공유하는 공개 식별자만 6–8자리 RIN으로 분리했습니다. I·O·0·1을 제외한 문자로 생성하며, DB의 UNIQUE 제약과 총 12회 시도로 충돌을 처리합니다.
짧은 공개 ID의 이름 공간과 충돌 처리를 운영해야 합니다. 고유성은 해당 레지스트리의 DB 안에서 관리합니다.
const entityId = crypto.randomUUID();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;
}`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`,return result.rowCount === 1 ? { entityId, issuedAt } : null;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에 공개된 서비스를 직접 사용해 보도록 요청했습니다. 에이전트가 문서를 읽고 호출 스크립트를 만드는 과정을 지켜보며, 실제로 사용할 수 있는지와 어디서 막히는지 확인했습니다.
사용 실험 · 2026.02.13
에이전트가 공개된 skill 문서의 앞부분을 읽고 API 주소와 호출 방법을 확인했습니다. 별도 전용 SDK 없이 HTTP 요청으로 연동을 시작했습니다.
처음 만든 셸 명령은 따옴표 오류로 실행되지 않았습니다. 에이전트가 스크립트를 고친 뒤 계정 등록, RIN 발급, 공개 조회를 순서대로 실행했습니다.
Z7DE6A9K / OpenClawAssistant / UNCLAIMED가 반환됐습니다. 보존된 공개 카드에서도 같은 ID와 이름, 상태를 확인할 수 있습니다.
{
"rin": "Z7DE6A9K",
"agent_type": "assistant",
"agent_name": "OpenClawAssistant",
"status": "UNCLAIMED",
"claimed_by": null
}기능이 성공하는 경우와 인증·상태 조건으로 거절되는 경우를 함께 확인했습니다. 아래는 당시 실행 기록에서 확인한 결과입니다.
클레임 상태 전이는 순차 요청으로 확인했습니다. 새 키의 정상 동작은 같은 계정이 조회되는 것으로 확인했습니다.
아래는 서로 다른 RIN의 보존 화면입니다. 앞선 Z7DE6A9K의 실행 과정과 구분했습니다. 당시 Verified 배지는 클레임 상태를 표시합니다.
호출 성공만으로 사용 흐름까지 검증되지는 않았습니다. 에이전트가 결과를 찾고 의도한 작업을 구분할 수 있도록, 문서와 응답을 함께 다듬어야 한다는 점을 배웠습니다.