에이전트에게 도구 설명을 필요할 때 보여 주는 원리를 독립 Python 모듈로 다시 만들었다. 실제 계정이나 브라우저 없이 묶음별 발견과 정확한 이름 선택, 비활성 도구 제외와 모르는 필드 거절을 살펴볼 수 있다. 실행 엔진은 포함하지 않는다.

정의를 읽는 기능부터 분리했다

catalog 항목에는 name·family·effect·enabled·fields만 둔다. 각 입력 필드는 string·integer·boolean 중 하나와 필수 여부, 문자열 최대 길이 선언을 가진다. 정의 전체를 검증한 뒤 요청한 묶음에 있는 활성 항목만 돌려준다. 이름이 중복되거나 모르는 정의 필드가 있으면 목록 전체를 오류로 처리한다.

신뢰할 수 없는 카탈로그에서 이상한 한 항목만 건너뛰고 실행 가능한 나머지를 찾는 방식으로 두지 않았다. 어떤 도구가 있다는 사실을 입력 데이터가 선언하는 구조이므로 실제 연결기에서 이 정의의 출처를 관리해야 한다. 이 예제에는 외부 파일 로더와 도구 설치 기능을 넣지 않았다.

all에서도 비활성 도구는 나오지 않는다

discover는 family와 선택적인 정확한 name을 받는다. reading을 요청하면 다른 묶음의 입력 도구가 섞이지 않는다. all은 묶음 필터를 풀 뿐 enabled 필터를 풀지 않는다. 비활성 이름을 직접 지정해도 TOOL_NOT_AVAILABLE_IN_FAMILY로 멈춘다.

이름이 비슷하다는 이유로 다른 도구로 바꾸거나 첫 항목을 대체 반환하지 않는다. 요청한 묶음 자체를 모르면 UNKNOWN_FAMILY다. 발견 실패와 실제 실행 실패도 다르다. 여기서 오류가 났다면 실행은 한 번도 시작하지 않았고 현재 정의를 다시 읽어야 한다.

설명과 검사는 같은 작은 문법을 쓴다

반환하는 inputSchema는 fields에서 만든다. validate_arguments도 같은 설명에서 허용 키와 필수 키, 값의 자료형과 문자열 길이를 확인한다. 두 군데에 따로 적은 계약이 시간이 지나 서로 달라지는 문제를 줄이려는 구조다.

이 문법은 일부러 작다. 중첩 객체와 날짜·URL·정수의 부호와 크기를 검증하지 않으며 완전한 JSON Schema 검사기가 아니다. max_length는 공통 필드 선언에 있지만 실제 길이 제약은 문자열에만 쓴다. 실제 업무에 붙이려면 필요한 의미 조건과 수치 범위를 별도로 추가하고 설명과 검사를 함께 바꿔야 한다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Discover and validate a deliberately small scalar contract. No executor."""
import copy
import re

class CatalogError(ValueError):pass

def _keys(value,fields):
    if not isinstance(value,dict) or set(value)!=set(fields):raise CatalogError('INVALID_FIELDS')

def _label(value):
    if not isinstance(value,str) or not re.fullmatch(r'[a-z][a-z0-9._-]{0,79}',value):raise CatalogError('INVALID_LABEL')

def _catalog(items):
    if not isinstance(items,list) or len(items)>1000:raise CatalogError('INVALID_CATALOG')
    names=set()
    for item in items:
        _keys(item,['name','family','effect','enabled','fields'])
        _label(item['name']);_label(item['family'])
        if item['name'] in names:raise CatalogError('DUPLICATE_TOOL')
        names.add(item['name'])
        if item['effect'] not in ['read','change'] or type(item['enabled']) is not bool:raise CatalogError('INVALID_METADATA')
        if not isinstance(item['fields'],dict) or len(item['fields'])>30:raise CatalogError('INVALID_FIELDS')
        for name,spec in item['fields'].items():
            _label(name);_keys(spec,['type','required','max_length'])
            if spec['type'] not in ['string','integer','boolean'] or type(spec['required']) is not bool:raise CatalogError('INVALID_SPEC')
            if type(spec['max_length']) is not int or not 1<=spec['max_length']<=20000:raise CatalogError('INVALID_SPEC')
    return items

def discover(items,family,name=None):
    _catalog(items);_label(family)
    if name is not None:_label(name)
    families={x['family'] for x in items}
    if family!='all' and family not in families:raise CatalogError('UNKNOWN_FAMILY')
    chosen=[x for x in items if x['enabled'] and (family=='all' or x['family']==family) and (name is None or x['name']==name)]
    if name is not None and not chosen:raise CatalogError('TOOL_NOT_AVAILABLE_IN_FAMILY')
    out=[]
    for item in chosen:
        properties={}
        for key,spec in item['fields'].items():
            properties[key]={'type':spec['type']}
            if spec['type']=='string':properties[key]['maxLength']=spec['max_length']
        out.append({'name':item['name'],'family':item['family'],'effect':item['effect'],'inputSchema':{'type':'object','properties':properties,'required':[k for k,v in item['fields'].items() if v['required']],'additionalProperties':False},'executes':False})
    return copy.deepcopy(out)

def validate_arguments(items,name,arguments):
    """Checks data only. It grants no permission and starts no operation."""
    descriptions=discover(items,'all',name)
    schema=descriptions[0]['inputSchema'];fields=schema['properties']
    if not isinstance(arguments,dict) or set(arguments)-set(fields) or set(schema['required'])-set(arguments):raise CatalogError('INVALID_ARGUMENT_KEYS')
    for key,value in arguments.items():
        spec=fields[key];kind=spec['type']
        if kind=='string':
            if not isinstance(value,str) or len(value)>spec['maxLength']:raise CatalogError('INVALID_ARGUMENT_VALUE')
        elif kind=='integer':
            if type(value) is not int:raise CatalogError('INVALID_ARGUMENT_VALUE')
        elif type(value) is not bool:raise CatalogError('INVALID_ARGUMENT_VALUE')
    return {'name':name,'arguments':copy.deepcopy(arguments),'shape_valid':True,'authorized':False,'executed':False}

분류와 권한을 분리하는 시험을 넣었다

reading 예제에는 effect가 change인 탭 선택도 넣었다. 묶음 이름이 읽기라고 모든 동작이 상태를 바꾸지 않는다는 뜻은 아님을 코드에 드러낸다. 검증 반환값의 authorized와 executed는 항상 거짓이다. 형식이 맞다고 계정이나 작업을 조작할 권한이 생기지 않는다.

21개 시험은 묶음 제외와 정확한 이름, 비활성·미지 도구, 정의 오염과 중복 이름을 다룬다. 참값을 정수로 쓰거나 정수 1을 참값으로 쓰는 경우도 거절한다. 문자열은 한도까지 보존하고 초과하면 잘라 쓰지 않고 오류로 끝낸다. 반환 설명을 고쳐도 원래 정의가 바뀌지 않는지도 확인했다.

보이는 기록과 실제 시험의 범위를 나눈다

다섯 합성 기록은 묶음 조회, 도구 하나 조회, 형식만 통과한 입력, 비활성 도구 거절, 모르는 입력 키 거절이다. 로컬 Python으로 기록을 만들고 아래 선택기가 그 결과를 읽게 했다. 방문자의 실제 프로그램 목록을 찾아보거나 도구를 실행하는 화면은 아니다.

원본 폴더의 21개 시험 뒤 ZIP 바이트와 각 항목 해시를 대조하고 새 폴더에 추출해 같은 시험을 실행한다. 두 언어의 실제 인라인 선택 코드는 DOM 대역에서 기록과 맞는지 본다. 이 범위를 실제 브라우저 렌더링·키보드 검수나 원래 북극성의 전체 회귀 시험이라고 표현하지 않는다.

합성 로컬 기록의 읽기 뷰어

실제 연결기가 이어 받아야 할 책임

ZIP에는 구현·합성 정의·시험·기록 생성기와 README가 있다. 표준 라이브러리만 쓰며 수정·재사용할 수 있다. 핵심 모듈에는 파일 읽기나 네트워크 호출, 계정 값과 실행 함수가 없다. 기록 생성기만 같은 폴더에 합성 JSON을 쓴다.

업무용으로 연결할 때는 신뢰한 정의 출처, 도구별 의미 검증, 실제 계정과 프로젝트의 권한, 현재 상태와 실행 뒤 결과를 확인하는 계층이 필요하다. 발견한 스키마가 어떤 버전인지도 관리해야 한다. 나는 이 모듈을 실행 자동화 전체로 소개하지 않고 필요한 설명을 정확히 고르는 원리를 시험할 수 있는 작은 부품으로 공유한다.

코드·시험·실행 기록 ZIP 받기
Python 표준 라이브러리 · 합성 자료 · 수정과 재사용 가능