---
url: https://docs.ogmabox.com/ko/plugins/README.md
description: >-
  백엔드 로직, 프런트엔드 패널, 명령, 권한 및 마켓플레이스 메타데이터를 갖춘 Ogma 플러그인을 만들고 패키징, 설치, 활성화 및
  배포합니다.
---

# Ogma 플러그인 시스템 {#ogma-plugin-system}

Ogma 플러그인은 사용자 지정 백엔드 로직, 프런트엔드 UI 패널 및 워크플로 단계를 통해 도구를 확장합니다. 플러그인은 디스크의 디렉터리에서 로컬로 설치되며, 프로젝트별로 활성화되고 샌드박스 환경에서 실행됩니다.

이 문서는 플러그인 개발자를 위한 기본 참조입니다.

가장 빠르게 시작하려면 [플러그인 빠른 시작](/ko/plugins/quickstart)을 확인하세요.

***

## 빠른 시작 {#quick-start}

### 최소한의 백엔드 플러그인 {#minimal-backend-plugin}

```
my-plugin/
  manifest.json
  backend/script.js
```

`manifest.json`:

```json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "plugins": [
    {
      "kind": "backend",
      "id": "my-plugin-backend",
      "entrypoint": "backend/script.js"
    }
  ]
}
```

`backend/script.js`(ES2020, 번들러를 사용하는 경우 import 사용 가능):

```js
async function init(sdk) {
  sdk.console.log("my-plugin started");

  sdk.events.onInterceptResponse(function(req, res) {
    if (res.getCode() === 403) {
      sdk.console.warn("403 on " + req.getUrl());
    }
  });
}
```

이것만으로 백엔드 전용 플러그인을 실행할 수 있습니다. 코드는 `backend/script.js` 하나의 파일로 유지하고 `manifest.json`에서 직접 지정하세요.

### 일 분 만에 빌드하기(TypeScript 소스) {#one-minute-build-flow-typescript-source}

TypeScript로 작성하는 경우 다음 구조를 사용합니다.

```text
my-plugin/
  manifest.json
  backend/
    src/index.ts
```

빌드:

```bash
pnpm add -D @ogmabox/ogma-sdk esbuild typescript
pnpm exec esbuild backend/src/index.ts --bundle --format=iife --platform=neutral --external:@ogma/sdk --external:@ogmabox/ogma-sdk --outfile=backend/script.js
```

설치: **플러그인 > 설치**에서 `my-plugin/` 디렉터리를 선택한 다음 활성화합니다.

***

## 매니페스트 참조 {#manifest-reference}

`manifest.json`은 패키지 루트 디렉터리에 있습니다. 모든 필드는 대소문자를 구분합니다.

### 최상위 필드 {#top-level-fields}

| 필드 | 필수 | 타입 | 설명 |
|-------|----------|------|-------|
| `id` | 예 | string | 소문자, 숫자, 하이픈만 허용합니다. 최대 64자이며 설치된 플러그인 간에 고유해야 합니다. |
| `version` | 예 | string | Semver: `MAJOR.MINOR.PATCH` |
| `name` | 아니요 | string | UI에 표시되는 이름입니다. 기본값은 `id`입니다. |
| `description` | 아니요 | string | 한 줄 요약입니다. |
| `author` | 아니요 | object | `{ "name": "...", "email": "...", "url": "..." }` |
| `homepage` | 아니요 | string | 소스 저장소 또는 문서의 URL입니다. |
| `plugins` | 예 | array | 하나 이상의 플러그인 구성 요소 항목입니다(아래 참조). |
| `permissions` | 아니요 | array | 필요한 권한 이름 목록입니다([권한](#permissions) 참조). |

### 플러그인 구성 요소 항목 {#plugin-component-entry}

`plugins` 배열의 각 객체는 하나의 구성 요소를 설명합니다.

**백엔드 구성 요소:**

```json
{
  "kind": "backend",
  "id": "my-plugin-backend",
  "entrypoint": "backend/script.js",
  "runtime": "javascript",
  "assets": "backend/assets"
}
```

**프런트엔드 구성 요소:**

```json
{
  "kind": "frontend",
  "id": "my-plugin-frontend",
  "entrypoint": "frontend/script.js",
  "style": "frontend/style.css",
  "assets": "frontend/assets",
  "backend": { "id": "my-plugin-backend" }
}
```

| 필드 | 필수 | 설명 |
|-------|----------|-------|
| `kind` | 예 | `"backend"` 또는 `"frontend"` |
| `id` | 예 | 매니페스트 내에서 고유해야 합니다. 소문자와 하이픈을 사용합니다. |
| `entrypoint` | 예 | JS 진입점 파일의 상대 경로입니다. |
| `style` | 아니요 | 플러그인 iframe에서 로드되는 CSS 파일입니다. |
| `assets` | 아니요 | `/plugins/{id}/assets/` 아래에서 제공되는 정적 에셋 디렉터리입니다. |
| `backend.id` | 아니요 | `sdk.backend.*` RPC를 위해 프런트엔드 구성 요소를 백엔드 구성 요소에 연결합니다. |
| `runtime` | 아니요(백엔드 전용) | `"javascript"`(기본값이며 유일하게 지원되는 값). |

***

## 백엔드 플러그인 API (sdk) {#backend-plugin-api-sdk}

백엔드 `sdk` 객체는 `init(sdk)` 함수에 전달됩니다. `async`로 표시된 메서드 외에는 모두 동기식입니다.

### `sdk.console` {#sdk-console}

```js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")
```

플러그인 로그 버퍼에 기록합니다(로그 탭에서 확인 가능). 최대 500개 항목을 보관하며 각 메시지는 1 KB에서 잘립니다.

### `sdk.meta` {#sdk-meta}

```js
sdk.meta.id()          // > string: plugin id (e.g. "my-plugin")
sdk.meta.packageId()   // > string: same as id
sdk.meta.version()     // > string: semver (e.g. "1.0.0")
sdk.meta.path()        // > string: writable data directory for this plugin
```

`sdk.meta.path()`는 플러그인에서 쓰기 가능한 전용 데이터 디렉터리(예: `~/.local/share/ogma/plugins/my-plugin/data`)를 가리킵니다. 디렉터리는 자동으로 생성되며 재시작 후에도 플러그인 데이터를 유지하는 데 사용할 수 있습니다.

상태 및 파일 도우미로 `sdk.storage`, `sdk.path`, `sdk.fs`도 사용할 수 있습니다.

### `sdk.storage` {#sdk-storage}

```js
sdk.storage.get("key")      // > string | null
sdk.storage.set("key", "value")
sdk.storage.delete("key")
sdk.storage.clear()
sdk.storage.keys()          // > string[]
```

`sdk.storage`는 플러그인 id별로 분리되며 플러그인을 재시작해도 유지됩니다.

### `sdk.fs` {#sdk-fs}

```js
sdk.fs.readFile("relative/file.txt")      // > string
sdk.fs.writeFile("relative/file.txt", "text")
sdk.fs.appendFile("relative/file.txt", "more")
sdk.fs.exists("relative/file.txt")        // > boolean
sdk.fs.existsSync("relative/file.txt")    // > boolean
sdk.fs.list("relative/dir")              // > string[]
sdk.fs.mkdir("relative/dir")
```

`sdk.fs`는 `sdk.meta.path()` 아래의 파일로 제한됩니다.

`read`와 `write`는 호환성을 위한 `readFile`과 `writeFile`의 별칭으로 유지됩니다. 덮어쓰고 싶지 않은 파일을 만들기 전에 `exists` 또는 `existsSync`를 사용하세요. 이 API는 플러그인 런타임에서 동기식으로 동작하며 전체 Node.js `fs` 모듈이 아닙니다. 플러그인의 파일 접근에는 `plugin_storage` 권한이 필요하며 플러그인 전용 데이터 디렉터리 안으로 제한됩니다. 워크플로 JavaScript의 파일 시스템 컨텍스트는 다릅니다. [워크플로 파일 접근](../app/workflows.md#javascript-and-files)을 확인하세요.

### `sdk.path` {#sdk-path}

```js
sdk.path.join("a", "b", "c")
sdk.path.basename("/tmp/file.txt")
sdk.path.dirname("/tmp/file.txt")
sdk.path.extname("file.txt")
sdk.path.resolve("/a", "b")
sdk.path.isAbsolute("/tmp/file.txt")
sdk.path.sep
```

### `sdk.events` {#sdk-events}

Ogma 이벤트의 콜백을 등록합니다. 모든 콜백은 QuickJS 샌드박스 안에서 동기식으로 호출됩니다.

```js
sdk.events.onInterceptRequest(function(req) {
  // req: RequestSpecRaw
  // Return a modified RequestSpecRaw to mutate the request.
  // Return null/undefined to pass through unchanged.
});

sdk.events.onInterceptResponse(function(req, res) {
  // req: Request (read-only), res: Response (read-only)
  // Return value is ignored.
});

sdk.events.onProjectChange(function() {
  // no callback args
});

sdk.events.onFindingCreated(function(finding) {
  // finding: { id, title, reporter }
});
```

### `sdk.requests` {#sdk-requests}

```js
// Get a single HTTP entry by id
var entry = sdk.requests.get("entry-id");
// entry: { id, method, host, path, query, tls, ... } or null

// Search HTTP history
var results = sdk.requests.search({ limit: 20, offset: 0 });
// results: { entries: [...], total: N }

// Send an HTTP request (requires send_requests permission)
var response = await sdk.requests.send(spec);
// spec: RequestSpecRaw (see below)
// response: Response
```

`sdk.requests.send`를 사용하려면 매니페스트에 `send_requests` 권한을 선언하고 사용자가 해당 권한을 부여해야 합니다. [권한](#permissions)을 확인하세요.

### `sdk.findings` {#sdk-findings}

```js
// Create a finding (requires write_findings permission)
sdk.findings.create({
  title: "SSRF via redirect",
  reporter: "my-plugin",
  dedupeKey: "ssrf-" + request.getId(),
  request: { id: request.getId() }
});

// Check if a finding already exists (dedup check)
var exists = sdk.findings.exists({ dedupeKey: "ssrf-abc" });

// List findings
var page = sdk.findings.list({ limit: 20, offset: 0 });

// Get a single finding
var finding = sdk.findings.get("finding-id");
```

`sdk.findings.create` 호출 제한: 분당 10회, 플러그인 세션당 500회, 이벤트 콜백당 3회.

### `sdk.api` {#sdk-api}

프런트엔드에서 `sdk.backend.*`를 통해 호출할 수 있는 백엔드 RPC 함수를 등록합니다.

```js
// In backend init:
sdk.api.register("getScans", function(scanId) {
  return { scans: [] };
});

// Emit an event to connected frontends:
sdk.api.send("scan:complete", { scanId: 1, status: "ok" });
```

핸들러는 프런트엔드에서 전달한 인수를 받습니다(추가 `sdk` 인수는 주입되지 않음). 반환값은 JSON으로 직렬화되어 호출자에게 전달됩니다.

프런트엔드는 `sdk.backend.getScans(scanId)`를 통해 호출합니다. [프런트엔드 플러그인 API](#frontend-plugin-api-sdk)를 확인하세요.

`sdk.api.send`는 플러그인별 대기열(최대 200개 항목)에 이벤트를 넣습니다. 프런트엔드는 `sdk.backend.onEvent`로 이 대기열을 폴링합니다.

### `sdk.replay`, `sdk.projects`, `sdk.scope`, `sdk.workflows`, `sdk.matchReplace` {#sdk-replay-sdk-projects-sdk-scope-sdk-workflows-sdk-matchreplace}

이들은 읽기 전용 조회 네임스페이스입니다. 전체 메서드 시그니처는 [백엔드 SDK 참조](./backend-sdk.md)를 확인하세요.

### 요청 클래스 {#request-classes}

**`RequestSpecRaw`** - 가로챈 요청을 나타냅니다. `onInterceptRequest`에서 받습니다.

```js
spec.getMethod()          // > string
spec.setMethod("POST")
spec.getHost()            // > string
spec.setHost("example.com")
spec.getPort()            // > number
spec.getPath()            // > string
spec.setPath("/new/path")
spec.getQuery()           // > string
spec.getTls()             // > boolean
spec.getHeaders()         // > Record<string, string[]>
spec.setHeader("X-Foo", "bar")
spec.getBody()            // > Body | null
spec.setBody("new body")
spec.getRaw()             // > Uint8Array (raw bytes) or []
spec.setRaw(bytes)        // set raw bytes

// Create a new spec from a URL string:
var spec = new RequestSpecRaw("https://example.com/path?q=1");
```

**`Request`** - 캡처된 요청을 나타내는 읽기 전용 객체입니다(`sdk.requests.get`에서 가져옴).

```js
req.getId()
req.getMethod()
req.getHost()
req.getPort()
req.getTls()
req.getPath()
req.getQuery()
req.getUrl()           // > full URL string
req.getHeaders()       // > Record<string, string>
req.getHeader("name")
req.getBody()          // > Body | null
req.getCreatedAt()     // > Date
req.toSpec()           // > RequestSpec (mutable copy)
```

**`Response`** - 캡처된 응답을 나타내는 읽기 전용 객체입니다.

```js
res.getCode()            // > number (HTTP status)
res.getHeaders()         // > Record<string, string>
res.getHeader("name")
res.getBody()            // > Body | null
res.getRoundtripTime()   // > number (ms)
res.getCreatedAt()       // > Date
```

**`Body`**:

```js
body.toText()    // > string
body.toJson()    // > parsed object or null
body.toRaw()     // > Uint8Array
body.length      // > number (original size, may differ from toText() if truncated)
```

***

## 프런트엔드 플러그인 API (sdk) {#frontend-plugin-api-sdk}

프런트엔드 플러그인 코드는 `/plugins/{id}/ui`에서 로드되는 샌드박스 iframe에서 실행됩니다. iframe은 `postMessage`로 Ogma 호스트와 통신하며, 호스트는 호출을 백엔드로 중계합니다.

SDK는 `window.ogmaSDK`를 통해 사용할 수 있습니다. 호스트 브리지가 연결된 후 실제 SDK를 받으려면 `ogmaSDK.ready(cb)`를 호출하세요.

```js
ogmaSDK.ready(function(sdk) {
  // sdk is the live SDK - safe to call any method here
  sdk.log.info("frontend ready");
});
```

모든 SDK 메서드는 Promise를 반환합니다.

### `sdk.log` {#sdk-log}

```js
sdk.log.info("message")
sdk.log.warn("message")
sdk.log.error("message")
```

### `sdk.meta` {#sdk-meta-1}

```js
var meta = await sdk.meta.get();
// { pluginId, packageId, name, version, ogmaVersion }
```

### `sdk.requests` {#sdk-requests-1}

```js
var entry = await sdk.requests.get({ id: "entry-id" });
var result = await sdk.requests.search({ limit: 20, offset: 0, query: "host:example.com" });
```

`read_http_history` 권한이 필요합니다(자동 부여, 사용자 승인 불필요).

### `sdk.findings` {#sdk-findings-1}

```js
var page = await sdk.findings.list({ limit: 20, offset: 0 });
```

`read_findings` 권한이 필요합니다(자동 부여).

### `sdk.scope` {#sdk-scope}

```js
var scope = await sdk.scope.getActive();
```

### `sdk.projects` {#sdk-projects}

```js
var project = await sdk.projects.getCurrent();
```

### `sdk.backend` - 백엔드 RPC {#sdk-backend-backend-rpc}

백엔드에서 `sdk.api.register`로 등록한 함수를 호출합니다.

```js
// Call a named backend function
var result = await sdk.backend.call("getScans", [scanId]);

// Poll for backend-emitted events (sdk.api.send on the backend side)
var { events, next_since } = await sdk.backend.poll(since);
// events: [{ event: "scan:complete", args: [...] }]

// Register an event listener (uses polling internally)
sdk.backend.onEvent("scan:complete", function(data) {
  console.log("scan done", data);
});
```

`sdk.backend.onEvent`는 내부적으로 2초 간격의 폴링 루프를 사용합니다. 반환된 구독 해제 함수를 호출하면 수신을 중지합니다.

```js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();
```

### `sdk.navigation` {#sdk-navigation}

```js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });
```

탐색 페이지를 등록합니다. 현재 호스트는 수신 확인만 합니다. 전체 라우터 통합은 개발 중입니다.

### `sdk.sidebar` {#sdk-sidebar}

```js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });
```

사이드바 항목을 등록합니다. 현재 플러그인 UI 패널 내부에만 적용되며, 전역 사이드바 슬롯 연결은 개발 중입니다.

### `sdk.commands` {#sdk-commands}

```js
await sdk.commands.register("my-plugin:scan", {
  name: "Scan with My Plugin",
  handler: function(context) { /* ... */ }
});
```

호스트는 수신 확인을 합니다. 명령 팔레트 통합은 개발 중입니다.

### `sdk.menu` {#sdk-menu}

```js
await sdk.menu.registerItem({
  type: "Request",
  commandId: "my-plugin:scan",
  leadingIcon: "shield"
});
```

호스트는 수신 확인을 합니다. 컨텍스트 메뉴 삽입은 개발 중입니다.

### `sdk.window` {#sdk-window}

```js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });
```

플러그인 패널에 토스트 알림을 표시합니다. 유형: `info`, `success`, `warning`, `error`.

### `sdk.ui` {#sdk-ui}

```js
sdk.ui.resize(600);                              // request iframe height change
sdk.ui.sidebar.registerItem("name", "/path");    // alias for sdk.sidebar.registerItem
```

***

## 권한 {#permissions}

`manifest.json`에 권한을 선언합니다.

```json
{
  "permissions": ["send_requests", "write_findings"]
}
```

### 자동 부여 권한(사용자 승인 불필요) {#auto-granted-permissions-no-user-approval-needed}

설치된 모든 플러그인에 항상 부여됩니다.

| 권한 | 허용되는 기능 |
|------------|----------------|
| `read_http_history` | `sdk.requests.get`, `sdk.requests.search` |
| `read_findings` | `sdk.findings.get`, `sdk.findings.list` |
| `read_scope` | `sdk.scope.getActive` |
| `read_projects` | `sdk.projects.getCurrent`, `sdk.projects.list` |
| `plugin_storage` | `sdk.storage`, `sdk.fs`, `sdk.path` |

### 보호된 권한(사용자 승인 필요) {#protected-permissions-require-user-approval}

매니페스트에 선언해야 하며 사용자가 권한 탭에서 명시적으로 부여해야 합니다.

| 권한 | 허용되는 기능 |
|------------|----------------|
| `send_requests` | `sdk.requests.send` - 외부로 HTTP 요청 전송 |
| `write_findings` | `sdk.findings.create`, `sdk.findings.update` |

보호된 권한을 선언한 플러그인을 활성화하면 사용자에게 확인 메시지가 표시됩니다. 권한 탭에서 언제든지 권한을 부여하거나 취소할 수도 있습니다.

***

## 플러그인 패키지 구조 {#plugin-package-structure}

플러그인 패키지는 로컬 디렉터리에서 설치할 수 있으며, 브라우저 환경에서는 내보낸 `.zip` 파일로도 설치할 수 있습니다.

```
my-plugin/
  manifest.json           - required
  backend/
    script.js             - bundled backend JS (ES2020)
  frontend/
    script.js             - bundled frontend JS
    style.css             - optional CSS
    assets/               - static assets (images, fonts, etc.)
```

### 백엔드 스크립트 요구 사항 {#backend-script-requirements}

* 하나의 독립적인 JS 파일이어야 합니다.
* `require()`와 동적 `import()`는 지원되지 않습니다.
* `init(sdk)` 함수를 내보내거나 전역 함수로 정의해야 합니다.
* QuickJS에서 지원하는 ES2020 하위 집합: `async/await`, `Promise`, `Map`, `Set`, `Symbol`, `Proxy`, `Date`, `RegExp`, `JSON`. `fetch`와 `Buffer`는 사용할 수 없습니다.
* 지원되는 정적 import는 플러그인 전처리로 해석됩니다: `@ogma/sdk`, `crypto`, `fs`, `path`.
* 최대 파일 크기: 256 KB.

### 프런트엔드 스크립트 요구 사항 {#frontend-script-requirements}

* 샌드박스 iframe 안에서 실행됩니다. `connect-src: 'self'`가 허용되므로 플러그인은 `/plugins/{id}/api/*`에 POST 요청을 보내고 `/plugins/{id}/events/poll`을 폴링할 수 있습니다.
* CSP: `default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'`.
* iframe 샌드박스에는 `allow-same-origin`이 없으므로 플러그인은 Ogma의 부모 DOM이나 쿠키에 접근할 수 없습니다.
* SDK 접근에는 `ogmaSDK.ready(cb)`를 사용하세요. 콜백이 실행되기 전에 SDK 메서드를 호출하지 마세요.

***

## Ogma용 플러그인 빌드 {#building-a-plugin-for-ogma}

백엔드는 하나의 번들 JS 파일이어야 하므로 설치 전에 TypeScript/ES 모듈 소스를 번들링해야 합니다.

권장 도구 구성:

```bash
# Install dependencies
pnpm install

# Bundle backend (outputs a single CJS/IIFE file):
esbuild packages/backend/src/index.ts \
  --bundle \
  --platform=neutral \
  --format=iife \
  --global-name=_plugin \
  --outfile=dist/backend/script.js \
  --external:@ogma/sdk --external:@ogmabox/ogma-sdk

# Bundle frontend:
vite build packages/frontend --outDir ../../dist/frontend
```

Caido 개발 도구(`@caido-community/dev`)를 사용하는 경우 `caido-dev build`를 실행한 다음, 루트에 `manifest.json`이 있는 Ogma 호환 구조로 결과물을 복사하세요.

***

## 플러그인 설치 {#installing-a-plugin}

1. 왼쪽 사이드바에서 **플러그인**을 엽니다.
2. **설치**를 클릭합니다(설치됨 탭 상단).
3. 데스크톱 앱에서는 **찾아보기**를 클릭해 운영체제의 폴더 선택 창을 엽니다. 브라우저에서는 서버 측 플러그인 디렉터리의 전체 경로를 입력합니다.
4. **유효성 검사**를 클릭해 매니페스트와 파일 목록을 확인합니다.
5. 검사를 통과하면 **설치**를 클릭합니다.
6. 목록에서 플러그인을 선택하고 **활성화**를 클릭합니다.
7. 플러그인이 보호된 권한을 선언한 경우 활성화하기 전에 **권한** 탭에서 검토하고 부여합니다.

***

## 문제 해결 {#troubleshooting}

**플러그인 초기화가 오류 표시 없이 실패함:** **로그** 탭을 확인하세요. 주요 원인은 다음과 같습니다.

* `sdk.meta.path()`를 호출했지만 데이터 디렉터리를 만들지 못함.
* `init()`에서 처리되지 않은 예외 발생.
* `sdk.*` 호출이 누락되었거나 철자가 잘못됨.

**프런트엔드가 빈 화면으로 표시됨:** 브라우저 콘솔에서 CSP 위반을 확인하세요. 프런트엔드 스크립트가 SDK 메서드에 접근하기 전에 `ogmaSDK.ready(cb)`를 호출하는지 확인하세요.

**`sdk.requests.send`에서 `Permission denied` 발생:** 매니페스트에 `send_requests` 권한을 선언하고 사용자가 권한 탭에서 부여해야 합니다.

**프런트엔드에서 `sdk.api.register`로 등록한 함수를 호출할 수 없음:** 백엔드는 설치만 된 상태가 아니라 활성화되어 있어야 합니다. 함수 이름은 프런트엔드가 `sdk.backend.call`에 전달하는 이름과 대소문자까지 정확히 일치해야 합니다.

**호환성 경고가 오류로 표시됨:** 실행을 막지는 않지만 API 기능에 부족한 부분이 있음을 나타냅니다. API 지원 범위는 위 SDK 매핑 표를 확인하세요.
