
AI 에이전트 개발 환경 「Orca」의 플러그인을 직접 만들어 보았다
요약
AI 에이전트 개발 환경 Orca의 플러그인 제작 과정을 상세히 안내합니다. 빌드 도구나 복잡한 패키지 관리 없이 폴더에 파일 2개만 배치하여 플러그인을 구현할 수 있습니다. 특히 HTML, CSS, JavaScript로 작성하는 사이드바 '패널'을 주제로 하여 최소 구성의 샘플과 절차를 제시했습니다.
핵심 포인트
- Orca 플러그인은 빌드 도구 없이 파일 2개만으로 제작 가능합니다.
- 핵심은 `orca-plugin.json` 매니페스트와 패널 폴더 구조입니다.
- 패널은 HTML, CSS, JavaScript로 작성되며 사이드바에 표시됩니다.
- 플러그인 기능은 현재 '실험적(Experimental)' 단계이므로 변경될 수 있습니다.
지난번, AI 에이전트용 개발 환경 「Orca」에 추가된 플러그인 기능과 마켓플레이스(Marketplace)에 대해 조사한 기사를 작성했습니다.
그때 남았던 의문이 "이거, 나도 직접 만들 수 있을까?"였습니다. 결론부터 말하자면 만들 수 있습니다. 빌드 도구(Build tool)나 패키지 관리(Package management)는 필요 없으며, 폴더에 파일 2개를 두기만 하면 동작합니다.
다만, 만드는 방법을 정리한 자료를 찾을 수 없었습니다. 공식 문서(Official document)에 플러그인 개발 항목은 아직 없으며, 마켓플레이스에 나열된 플러그인들을 살펴보아도 매니페스트(Manifest)에 무엇을 쓸 수 있는지는 알 수 없습니다.
그래서 이 기사에, Orca 플러그인을 제로 베이스에서 만들어 설치하기까지의 절차를 남겨둡니다. 사양은 Orca v1.4.176의 애플리케이션 번들(Application bundle)을 읽어 확인한 것입니다. 플러그인 기능 자체에 대한 설명은 지난 기사에 맡길 테니, 필요하다면 먼저 그쪽을 읽어주세요.
기사 후반부에는 실제로 직접 만든 플러그인 이야기도 조금 적어두었습니다.
Orca의 플러그인 기능은 「실험적(Experimental)」이라고 표시되어 있는 단계의 기능입니다. 이 기사의 내용은 향후 버전에 따라 변경될 가능성이 있습니다.
사이드바(Sidebar)에 표시되는 **패널(Panel)**을 만듭니다. 패널은 Orca의 오른쪽 사이드바에 나오는 작은 화면으로, HTML, CSS, JavaScript로 작성합니다.
패널을 주제로 삼은 이유는 두 가지입니다. 하나는 만든 결과가 눈에 보인다는 것. 또 하나는 패널만 있는 플러그인은 백그라운드(Background)에서 동작하는 프로세스(Process)를 가지지 않기 때문에, 권한(Permission)의 범위가 이해하기 쉽다는 점입니다.
먼저 목표를 보여드리겠습니다. 이 기사의 절차대로 만든 패널이 이것입니다.
내용물은 벽돌 깨기 게임입니다. 주제는 무엇이든 상관없으며, 이 기사에서 설명하는 뼈대는 어떤 패널이든 변하지 않습니다. 본문의 절차는 최소 구성의 샘플로 진행하며, 벽돌 깨기 이야기는 마지막에 정리하겠습니다.
필요한 것은 폴더 하나와 그 안의 파일 2개입니다.
my-plugin/
├── orca-plugin.json # 매니페스트 (Manifest)
└── panel/
...
package.json이나 node_modules, 빌드 결과물(Build artifact)은 필요 없습니다. orca-plugin.json이 있는 폴더를 Orca에 지정하면, 그것이 플러그인으로 취급됩니다.
위치는 어디든 상관없습니다. 이 기사에서는 ~/orca-plugins/my-plugin을 사용합니다.
매니페스트의 전체 모습은 다음과 같습니다.
{
"manifestVersion": 1,
"id": "my-plugin",
...
각 항목의 내용은 다음과 같습니다.
| 항목 | 내용 |
|---|---|
manifestVersion | 현재는 1 고정 |
id | 플러그인의 식별자(Identifier) ※명명 규칙에 제약 있음 (후술) |
publisher | 발행자의 식별자. id와 조합하여 publisher.id가 플러그인의 키(Key)가 됨 |
name | 설정 화면에 표시되는 이름 |
version | 시맨틱 버전 (Semantic version) |
description | 설정 화면과 권한 다이얼로그(Permission dialog)에 표시되는 설명 |
engines.orca | 대응하는 Orca의 버전 |
pluginApi | 플러그인 API의 버전. 현재는 1 |
contributes | 플러그인이 제공하는 것 |
capabilities | 요구하는 권한 |
그중에서 작성하기 쉬운 실수 4가지를 보충합니다.
id가 orca-로 시작하는 경우, 혹은 publisher가 stablyai인 경우, 해당 플러그인은 Orca의 예약 식별자(Reserved identifier)로 취급됩니다. 예약 식별자는 로컬 폴더로부터의 설치가 거부되기 때문에, 식별자를 바꾸지 않는 한 설치할 수 없습니다.
곤란한 점은, 이 설치에 실패했을 때의 에러 메시지가 "플러그인 설치에 실패했습니다. 소스를 확인하고 다시 시도해 주세요."라는 범용적인 문구가 된다는 것입니다. 원인이 식별자에 있다고 표시되지 않습니다.
engines.orca는 정규 표현식(Regular expression) ^>=\d+\.\d+\.\d+$로 검증됩니다.
"engines": { "orca": ">=1.4.0" } // OK
"engines": { "orca": "^1.4.0" } // NG
"engines": { "orca": ">=1.4" } // NG
...
npm의 감각으로 ^1.4.0
이라고 작성하면 거부됩니다.
contributes
스키마는 strict (엄격)하게 정의되어 있기 때문에, 정의되지 않은 키를 작성하면 매니페스트(manifest) 전체가 무효화됩니다. 작성 가능한 키는 다음 7가지입니다.
| 키 | 내용 |
|---|---|
panels | 사이드바 패널 |
commands | 커맨드 팔레트(command palette)에 추가할 커맨드 |
events | 구독할 이벤트 |
keybindings | 키보드 단축키 |
languagePacks | 언어 팩 |
vmRecipes | VM 레시피 |
agents | 에이전트 정의 |
테마나 아이콘, 스킬, MCP 서버의 등록은 할 수 없습니다. 지난 기사에서 마켓플레이스 인덱스에 8건이 등록되어 있는데 왜 3건만 표시되는가라는 의문을 남겼었습니다. 표시되지 않는 5건은 모두 themes나 icons와 같이 현재 스키마에 없는 키를 사용하고 있습니다. 스키마 검증을 통과하지 못하기 때문에 목록에 나오지 않는 것으로 보입니다.
권한 지정은 문자열 배열이 아니라, kind를 가진 객체 배열입니다.
"capabilities": ["workspace:read"] // NG
"capabilities": [{ "kind": "workspace:read" }] // OK
지정할 수 있는 kind는 7종류입니다. 표의 오른쪽 열은 권한 다이얼로그에 실제로 표시되는 문구를 그대로 기재했습니다.
| kind | 권한 다이얼로그 표시 |
|---|---|
workspace:read | 포커스된 워크트리(worktree)의 이름, 브랜치, 터미널 리스트를 읽음 |
terminal:send | 보이는 터미널에 텍스트 입력 (항상 특정 터미널) |
notifications:show | 플러그인 이름이 라벨링된 데스크톱 알림 표시 |
storage | 플러그인 전용 스토리지 폴더에 데이터 저장 |
secrets | 플러그인 전용 암호화 보관소에 시크릿(secret)을 저장 및 읽기 |
events | 워크트리가 생성 또는 삭제되고, 에이전트 상태가 변경될 때 알림을 받음 |
settings:own | 플러그인 전용 설정을 읽고 변경 |
요청한 권한은 활성화 시의 권한 다이얼로그에 그대로 목록으로 표시됩니다. 서두의 블록 깨기( workspace:read와 notifications:show를 요청)의 경우는 다음과 같습니다.
사용하지 않는 권한을 작성하면 이 목록이 길어져 사용자에게 불필요한 불안감을 줄 뿐이므로, 필요한 것만 작성합니다.
이 부분이 처음에 당황스러운 지점입니다. entry에 지정하는 파일에는 완결된 HTML 문서가 아니라, <body> 이후만 작성합니다.
Orca 측에서 다음과 같은 셸(shell)을 구성하고, 그 끝에 지정한 파일의 내용을 그대로 연결합니다.
<!doctype html>
<html class="...">
<head>
...
따라서 entry에 <!doctype html>이나 <head>를 작성하면 중복됩니다.
<body>
<div class="wrap">
<p id="branch">読み込み中…</p>
...
CSS와 JavaScript는 같은 파일 안에 작성합니다. 외부 파일 로드는 할 수 없습니다.
패널은 sandbox="allow-scripts"가 붙은 iframe 안에서 srcDoc으로서 렌더링됩니다. 이로 인해 발생하는 제약 사항이 4가지 있습니다.
1. 외부와의 통신이 불가능함
적용되는 CSP는 다음과 같습니다.
default-src 'none'; connect-src 'none'; script-src 'unsafe-inline';
style-src 'unsafe-inline'; img-src data:; font-src data:;
base-uri 'none'; form-action 'none'
connect-src 'none'이므로, fetch, XMLHttpRequest
、WebSocket 중 어느 것도 통과하지 못합니다. 이미지와 폰트는 data: URI만 가능합니다. CDN에서 라이브러리를 읽어올 수도 없습니다. React를 사용하고 싶다면, 번들링(Bundled)한 코드를 통째로 <script> 안에 작성해야 합니다.
2. 상태를 저장할 수 없음
점수나 설정을 저장하여 다음에 열었을 때 이어가는 것은 불가능합니다. allow-same-origin이 붙어 있지 않기 때문에, 패널의 오리진(Origin)은 불투명(Opaque)해집니다. localStorage에 접근하려고 하면 예외(Exception)가 발생합니다. 게다가 srcDoc은 마운트(Mount)될 때마다 문서를 새로 만들기 때문에, 사이드바를 닫았다가 다시 열면 변수의 내용이 사라집니다.
플러그인 API에는 storage.get / storage.set이 있지만, 이것들은 패널에서 호출할 수 없습니다(후술). 패널만으로 완결되는 플러그인은 현재 상태를 가질 수 없습니다.
3. 음성 파일을 읽어올 수 없음
CSP에 media-src 지정이 없으므로 default-src 'none'으로 떨어져, <audio>를 통한 로드는 차단됩니다. 단, Web Audio API의 AudioContext는 외부 리소스를 가져오지 않기 때문에, 파형(Waveform)을 생성하는 방식이라면 소리를 낼 수 있습니다.
const audio = new AudioContext(); // 사용자 조작 이후에 생성할 것
const osc = audio.createOscillator();
osc.frequency.value = 880;
...
4. 메시지에는 상한이 있음
후술할 호스트 통신에는 메시지 1개당 64KB, 10초당 30건이라는 상한이 있습니다. 게다가 호스트의 핑(Ping)에 응답하지 않는 패널은 정지됩니다. 매 프레임마다 값을 질의하는 방식의 코드는 작성할 수 없습니다.
쉘(Shell)의 :root에는 Orca 본체의 색상 배합이 CSS 커스텀 프로퍼티(Custom Property)로서 20개 주입됩니다.
--background --foreground
--card --card-foreground
--popover --popover-foreground
...
값은 Orca 본체의 계산된 스타일(Computed Style)에서 그대로 복사되므로, 이것들을 사용하여 작성하면 테마 전환에 자동으로 대응합니다.
.card {
background: var(--card);
color: var(--card-foreground);
...
반대로 색상을 직접 작성하면, 테마를 전환했을 때 패널만 색상이 바뀌지 않아 주변과 어울리지 않게 됩니다.
패널에서 Orca의 기능을 사용하려면 부모 창(Parent Window)에 postMessage를 보냅니다.
패널에서 보내는 메시지는 다음과 같은 형태입니다.
{
type: 'orca-panel-action',
requestId: 'req-1', // 임의의 문자열 (128자 이내)
...
결과는 다음과 같은 형태로 돌아옵니다.
{
type: 'orca-panel-action-result',
requestId: 'req-1',
...
실패한 경우에는 ok: false와 errorCode / error가 반환됩니다.
매번 작성하는 것은 번거로우므로, Promise로 감싸는 헬퍼(Helper)를 준비해 두면 편리합니다.
let seq = 0;
const pending = {};
window.addEventListener('message', (event) => {
...
사용법은 다음과 같습니다.
callHost('workspace.readContext').then((res) => {
if (!res.ok) return;
const branch = res.value.branch; // "refs/heads/main"
...
branch는 refs/heads/가 붙은 완전한 ref 명으로 반환되므로, 표시하려면 이를 제거해 둡니다.
Orca의 애플리케이션 번들 내에 있는 API 정의를 읽어보면, 플러그인 API 메서드는 13개가 정의되어 있습니다. 각 메서드는 패널에서 호출할 수 있는지 여부를 나타내는 panel 플래그를 가지고 있으며, 이것이 true인 것은 다음 3개뿐입니다.
| 메서드 | 필요한 capability | 패널에서 |
|---|---|---|
workspace.readContext | workspace:read | ○ |
terminal.sendText | terminal:send | ○ |
notifications.show | notifications:show | ○ |
storage.get / set / delete / keys | storage | × |
secrets.get / set / delete | secrets | × |
settings.get / set | settings:own | × |
저장 관련 기능이 모두 panel: false인 것은 의도적인 설계라고 생각됩니다. 샌드박스 (Sandbox) 내의 코드에 디스크로의 쓰기 경로를 전달하지 않겠다는 의미일 것입니다.
참고로 notifications.show로 알림을 띄우면, 제목의 맨 앞에 자동으로 publisher.id: 가 붙습니다. title에 Done을 전달하면, 실제 알림은 your-name.my-plugin: Done이 됩니다.
파일 2개가 모두 준비되었다면, Orca에 설치합니다.
- 설정 → 플러그인을 엽니다 - 「플러그인 시스템」을 켭니다
- 「플러그인 설치」를 누르고, 「로컬 폴더」에 플러그인 폴더의 전체 경로를 입력한 뒤 「설치」를 누릅니다
- 목록에 추가된 플러그인의 「검토 후 활성화」를 누릅니다
- 권한 다이얼로그의 내용을 확인하고 「플러그인 활성화」를 누릅니다
단계 5에서 나타나는 것이 2장의 마지막에 실었던 권한 다이얼로그입니다. 매니페스트 (Manifest)에 작성한 capabilities가 그대로 일본어로 나열됩니다.
활성화하면 사이드바에 contributes.panels[].title의 이름으로 패널이 나타납니다.
설치 시 플러그인 파일은 Orca의 관리하에 복사됩니다. 따라서 원래 폴더를 수정해도 자동으로 반영되지 않습니다. 수정할 때마다 다시 설치해야 합니다.
잘 작동하지 않을 때는 플러그인 목록의 「...」에서 「로그 표시」를 선택하면 최근 200행을 읽을 수 있습니다. 매니페스트 검증 에러도 여기서 나타납니다.
「플러그인 설치」에는 「로컬 폴더」 외에도 「Git URL」 탭이 있습니다. 리포지토리 (Repository)를 공개해 두면, URL만으로 다른 사람의 Orca에도 설치하게 할 수 있습니다.
이번에 만든 벽돌 깨기도 이 방식으로 공개해 두었습니다. 「Git URL」 탭에 다음 URL을 붙여넣고 「설치」를 누르기만 하면 됩니다.
URL 끝의 #ref는 필수입니다. 설치를 고정하기 위한 지정이므로, 태그 (Tag)나 커밋 해시 (Commit Hash)를 사용합니다. 버전마다 태그를 만들어 두는 것이 가장 깔끔합니다.
설치에는 몇 가지 제한이 있습니다.
- 파일 수 2000개까지
- 총 용량 50MB까지
- 심볼릭 링크 (Symbolic Link)를 포함하면 거부됨
- 루트 직하의
.git은 제외됨 (하위 디렉토리의.git은 제외되지 않음)
node_modules를 포함한 채로 설치하려고 하면, 대부분 파일 수 제한에 걸립니다.
패널이 아니라 이벤트 (Event)에 반응하여 동작하는 처리를 작성하고 싶다면 **워커 (Worker)**를 사용합니다. 이 글의 주제에서는 벗어나므로 요점만 정리하겠습니다.
매니페스트에 main을 추가합니다.
{
"main": "worker.mjs",
"contributes": {
...
엔트리 (Entry)는 ESM이며, activate를 기본 내보내기 (Default Export) 합니다.
export default async function activate(ctx) {
ctx.log(`activated: ${ctx.grantedCapabilities.join(', ')}`);
ctx.events.on('worktree.created', async (payload) => {
...
ctx에서 사용할 수 있는 것은 commands.register / events.on / host.call / grantedCapabilities / log 입니다.
입니다. 구독할 수 있는 이벤트는 worktree.created / worktree.removed / agent.status.changed 의 3종류뿐이며, 전달되는 페이로드(payload)도 필요한 최소한의 필드로 제한되어 있습니다.
워커(Worker)를 사용하는 경우, 권한 다이얼로그에는 다음과 같은 문구가 표시됩니다.
이러한 권한은 플러그인이 Orca API를 사용하는 방법을 제한합니다. 워커는 여전히 컴퓨터에서 일반적인 프로세스로 실행되며, 파일, 네트워크 및 기타 프로세스에 대한 완전한 액세스 권한을 가집니다.
capabilities가 제한하는 것은 Orca API 호출뿐입니다. 워커 자체는 일반적인 Node 프로세스이므로, 권한 선언 없이도 fs, fetch, child_process를 사용할 수 있습니다. 패널(Panel)의 철저한 샌드박스(sandbox)와는 전제 조건이 완전히 다릅니다.
main을 가진 플러그인은 권한 다이얼로그의 배지(badge)도 「워커」가 되며, 위의 경고 문구가 표시됩니다. 배포하는 입장에서는 그곳에 무엇을 작성했는지 설명할 수 있는 상태로 두고 싶을 것입니다.
여기까지가 만드는 방법입니다. 이제부터는 제가 이 절차를 통해 무엇을 만들었는지에 대해 조금 이야기해 보겠습니다.
소재는 서두에 올린 벽돌 깨기 게임입니다. 사이드바 너비에 딱 맞는 사이즈로, 클릭하면 시작되고 마우스로 패들을 움직이며, 블록을 깰 때마다 소리가 납니다. 실용성은 없습니다. 패널을 이용한 플러그인 개발을 간편하게 체험할 수 있다는 이유로 선택했습니다. 만들면서 확인하고 싶었던 것은 다음 3가지입니다.
- Canvas 그리기와
requestAnimationFrame이 iframe 안에서 정상적으로 작동하는가 - 효과음을 재생하는 방법이 있는가
- 호스트(host)의 기능을 패널에서 호출할 수 있는가
마지막 점에 대해서는, 게임 종료 시 workspace.readContext로 브랜치(branch) 이름을 가져와 notifications.show로 데스크톱 알림을 띄우도록 했습니다.
맨 앞의 test3가 당시 열려 있던 워크트리(worktree)의 브랜치 이름입니다. 게임에 필요한 기능은 아니지만, 패널에서 Orca의 API를 호출하는 부분을 실제로 작동시켜 확인하고 싶었기에 넣었습니다.
만든 순서는 대략 다음과 같습니다.
- 매니페스트(manifest)와 거의 빈
index.html만 있는 최소 구성으로 설치하여, 에러 없이 패널이 표시되는지 확인한다 - 패널의 기능을 구현한다 (이번에는 벽돌 깨기)
- 다시 설치하여 기대한 대로 작동하는지 확인한다
처음에 1단계를 거쳐두면, 이후에는 브라우저에서 작동하는 HTML을 작성하는 것과 다를 바 없습니다. 작동하지 않을 때 매니페스트의 문제인지 코드의 문제인지 구분하기 쉬워지므로, 익숙해지기 전까지는 1단계를 거치는 것을 추천합니다.
코드는 아래에 올려두었습니다. 패널 구현 예시로서 관심 있는 분들은 참고해 보시기 바랍니다.
Orca의 플러그인은 폴더와 파일 2개로 만들 수 있습니다. 빌드 환경이나 타입 정의(type definition)는 필요 없으며, HTML을 작성할 수 있다면 패널을 만들 수 있습니다.
반면, 패널의 실행 환경은 상당히 엄격하게 제한되어 있습니다. 통신할 수 없고, 저장할 수 없으며, 외부 파일을 읽을 수 없습니다. 이 3가지를 먼저 파악해 두면 무엇을 만들 수 있고 무엇을 만들 수 없는지 가늠할 수 있습니다.
아직 실험적인 기능이므로, 이 기사의 내용도 다음 버전에서 바뀔 수 있습니다. 그럼에도 불구하고, 만들 수 있다는 사실을 알고 있는 것 자체에는 의미가 있다고 생각합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기