@underdogai/mesh-event-schemas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/apppacks.d.ts +15652 -0
- package/dist/apppacks.d.ts.map +1 -0
- package/dist/apppacks.js +71 -0
- package/dist/apppacks.js.map +1 -0
- package/dist/blocks.d.ts +2320 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +472 -0
- package/dist/blocks.js.map +1 -0
- package/dist/capabilities.d.ts +35 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +37 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/events.d.ts +4557 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +571 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/namespace.d.ts +56 -0
- package/dist/namespace.d.ts.map +1 -0
- package/dist/namespace.js +60 -0
- package/dist/namespace.js.map +1 -0
- package/dist/plugins.d.ts +131 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +66 -0
- package/dist/plugins.js.map +1 -0
- package/dist/registry.d.ts +67 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +212 -0
- package/dist/registry.js.map +1 -0
- package/dist/surfaces.d.ts +54323 -0
- package/dist/surfaces.d.ts.map +1 -0
- package/dist/surfaces.js +523 -0
- package/dist/surfaces.js.map +1 -0
- package/dist/templates.d.ts +390 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +493 -0
- package/dist/templates.js.map +1 -0
- package/package.json +50 -0
- package/src/apppacks.ts +90 -0
- package/src/blocks.ts +544 -0
- package/src/capabilities.ts +58 -0
- package/src/events.ts +758 -0
- package/src/index.ts +15 -0
- package/src/namespace.ts +64 -0
- package/src/plugins.ts +118 -0
- package/src/registry.ts +261 -0
- package/src/surfaces.ts +610 -0
- package/src/templates.ts +736 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @underdogai/mesh-event-schemas
|
|
3
|
+
*
|
|
4
|
+
* org.corp.* Matrix 커스텀 이벤트 + UI Block 데이터의 단일 진실 공급원.
|
|
5
|
+
* appservice-agent, erp-bridge, client-web, mcp-erp 가 공통으로 import 한다.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./namespace.js";
|
|
8
|
+
export * from "./blocks.js";
|
|
9
|
+
export * from "./events.js";
|
|
10
|
+
export * from "./registry.js";
|
|
11
|
+
export * from "./capabilities.js";
|
|
12
|
+
export * from "./templates.js";
|
|
13
|
+
export * from "./surfaces.js";
|
|
14
|
+
export * from "./apppacks.js";
|
|
15
|
+
export * from "./plugins.js";
|
package/src/namespace.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 커스텀 이벤트 네임스페이스.
|
|
3
|
+
*
|
|
4
|
+
* RFC §5: 모든 도메인 이벤트는 `org.corp.<domain>.<event>` 형태로 네임스페이스 + 버전을 가진다.
|
|
5
|
+
* 회사/배포별로 바꿀 수 있도록 한 곳(NS)에서만 정의한다.
|
|
6
|
+
*/
|
|
7
|
+
export const NS = "org.corp" as const;
|
|
8
|
+
|
|
9
|
+
/** UI 카드를 일반 `m.room.message` 안에 끼워 넣을 때 쓰는 content 확장 키.
|
|
10
|
+
* (지원하지 않는 클라이언트는 body 텍스트로 graceful degrade, 우리 클라는 이 키를 보고 렌더) */
|
|
11
|
+
export const UI_CONTENT_KEY = `${NS}.ui` as const;
|
|
12
|
+
|
|
13
|
+
/** 표준 이벤트 타입 상수. */
|
|
14
|
+
export const EventType = {
|
|
15
|
+
/** 사용자/에이전트가 보내는 구조화된 명령 (선택적 — 보통은 m.room.message 텍스트를 에이전트가 파싱) */
|
|
16
|
+
Command: `${NS}.command`,
|
|
17
|
+
/** 에이전트 → 사용자: 인라인 렌더되는 UI 카드 (전용 이벤트 타입 형태) */
|
|
18
|
+
UiCard: `${NS}.ui.card`,
|
|
19
|
+
/** 사용자 → 에이전트: UI 카드 상호작용 결과 */
|
|
20
|
+
UiAction: `${NS}.ui.action`,
|
|
21
|
+
/** 상태 이벤트: 에이전트 역량 프로필 (state_key = 에이전트 MXID) */
|
|
22
|
+
AgentCapability: `${NS}.agent.capability`,
|
|
23
|
+
/** 상태 이벤트: 엔티티(방)의 현재 워크플로 상태 (last-writer-wins) */
|
|
24
|
+
WorkflowState: `${NS}.workflow.state`,
|
|
25
|
+
/** 상태 이벤트: 룸↔도메인 바인딩 — 룸이 자기 app/엔티티를 선언 (state_key = "") */
|
|
26
|
+
RoomBinding: `${NS}.room.binding`,
|
|
27
|
+
/** 상태 이벤트: 엔티티(방)의 표시용 필드 스냅샷 — 팩 서피스 {{binding}} 해석 소스 (state_key = "") */
|
|
28
|
+
EntitySnapshot: `${NS}.entity.snapshot`,
|
|
29
|
+
/** HITL 승인 라이프사이클: 승인 요청 (여러 승인자 대상, 메시지 이벤트) */
|
|
30
|
+
ApprovalRequest: `${NS}.approval.request`,
|
|
31
|
+
/** HITL 승인 라이프사이클: 승인자의 결정 (승인/반려, 요청 이벤트를 m.reference) */
|
|
32
|
+
ApprovalDecision: `${NS}.approval.decision`,
|
|
33
|
+
/** 에이전트 런 계획: 에이전트가 어떤 단계 계획으로 일하는지 (런 타임라인의 토대, 메시지 이벤트) */
|
|
34
|
+
AgentPlan: `${NS}.agent.plan`,
|
|
35
|
+
/** 에이전트 런 단계 갱신: 한 단계의 상태/공정률 진행 (plan 을 m.reference, 메시지 이벤트) */
|
|
36
|
+
AgentStep: `${NS}.agent.step`,
|
|
37
|
+
/** 작업항목 생성: 사람↔에이전트 공유 작업 라이프사이클의 루트 (메시지 이벤트) */
|
|
38
|
+
TaskCreate: `${NS}.task.create`,
|
|
39
|
+
/** 작업항목 갱신: 상태/배정/진행 변경 (create 를 m.reference, 메시지 이벤트) */
|
|
40
|
+
TaskUpdate: `${NS}.task.update`,
|
|
41
|
+
/** 에이전트 → 에이전트: 작업 위임 요청 (A2A) */
|
|
42
|
+
A2aRequest: `${NS}.a2a.request`,
|
|
43
|
+
/** 에이전트 → 에이전트: 위임 결과 회신 (A2A) */
|
|
44
|
+
A2aResult: `${NS}.a2a.result`,
|
|
45
|
+
/** 상태 이벤트: 앱(App Service)이 데이터로 실어 보내는 셸 서피스(패널/헤더) — "AS만 추가해도" 회사별 UI 등록 (state_key = "") */
|
|
46
|
+
AppSurface: `${NS}.app.surface`,
|
|
47
|
+
/** 상태 이벤트: Space(컨테이너)가 추가·관리하는 App Service(앱) 목록 — Space=앱 추가 단위 (state_key = "") */
|
|
48
|
+
SpaceApps: `${NS}.space.apps`,
|
|
49
|
+
/** 상태 이벤트: 룸(채널)이 직접 선언한 App Service(앱) 키 목록 — space.apps 의 per-room 짝, 탭으로 노출 (state_key = "") */
|
|
50
|
+
RoomApps: `${NS}.room.apps`,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/** ERP 인바운드 이벤트 타입 접두사. 실제 타입은 `org.corp.erp.<entity>.<verb>` 로 만든다. */
|
|
54
|
+
export const ERP_EVENT_PREFIX = `${NS}.erp.` as const;
|
|
55
|
+
|
|
56
|
+
/** `org.corp.erp.invoice.updated` 같은 ERP 이벤트 타입 문자열을 생성한다. */
|
|
57
|
+
export function erpEventType(entity: string, verb: string): string {
|
|
58
|
+
return `${ERP_EVENT_PREFIX}${entity}.${verb}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 주어진 이벤트 타입이 ERP 인바운드 이벤트인지 판별. */
|
|
62
|
+
export function isErpEventType(type: string): boolean {
|
|
63
|
+
return type.startsWith(ERP_EVENT_PREFIX);
|
|
64
|
+
}
|
package/src/plugins.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 플러그인 레지스트리 — 활성화된 도메인 플러그인의 "신분증"(manifest)을 모으고,
|
|
3
|
+
* 부팅 시 각 plugin.register() 를 호출해 App Pack·이벤트·템플릿을 레지스트리에 wiring 한다.
|
|
4
|
+
*
|
|
5
|
+
* 한 플러그인 = 한 도메인(`packages/plugin-<domain>`). 클라이언트는 활성(enabled) 목록을
|
|
6
|
+
* loadPlugins() 한 번으로 부팅 시 등록한다 → 새 도메인 추가 = enabled 배열에 한 줄.
|
|
7
|
+
* (docs/dev-plan.md §1.4 / docs/extensibility.md §10 / TASK.yml P0-3)
|
|
8
|
+
*
|
|
9
|
+
* App Pack(getAppPack) 처럼 클라/UI 평면의 봉합선이며, 데이터/AS 평면(events·templates)은
|
|
10
|
+
* 각 plugin.register() 안에서 함께 등록한다. 임의 코드 실행 없이 "선언된 것만" 꽂는 구조.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 플러그인이 선언하는 capability 표면(서드파티 로딩 1차 슬라이스 = 신뢰 경계의 *명시적 선언*).
|
|
17
|
+
* 호스트가 "이 앱은 어떤 scope/백엔드 도구를 쓸 수 있는가"를 부팅 전에 감사할 수 있게 한다.
|
|
18
|
+
* App(defineApp/withActions)은 이 값을 *자기 액션에서 자동 도출*하므로 코드와 선언이 어긋나지 않는다(drift 0).
|
|
19
|
+
* 런타임 강제는 이미 존재한다: scope=서버 게이트(evaluateAction), backendTools=액션별 화이트리스트(dispatchAction).
|
|
20
|
+
* 즉 이 manifest 는 그 둘의 *감사 가능한 상한 선언*이다(샌드박스/서명/별프로세스는 여기 범위 밖 = G5/SEC-2 이후).
|
|
21
|
+
*/
|
|
22
|
+
export interface PluginCapabilities {
|
|
23
|
+
/** 이 앱의 액션이 요구하는 capability scope 합집합(서버 게이트가 검사하는 것). */
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
/** 이 앱의 핸들러가 부를 수 있는 MCP 백엔드 도구 합집합(액션별 화이트리스트의 합). */
|
|
26
|
+
backendTools?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 플러그인 매니페스트 — 플러그인의 신분증(런타임 등록 없이 메타데이터만). */
|
|
30
|
+
export interface PluginManifest {
|
|
31
|
+
/** 전역 고유 ID(패키지 1개 = 1개). */
|
|
32
|
+
id: string;
|
|
33
|
+
/** 사람이 읽는 이름. */
|
|
34
|
+
displayName?: string;
|
|
35
|
+
/** 룸 바인딩(org.corp.room.binding)의 app 키 — App Pack 의 app 과 일치. */
|
|
36
|
+
app: string;
|
|
37
|
+
/** 이 플러그인이 다루는 엔티티 타입(예: ["lead"], ["repo","pr","issue"]). */
|
|
38
|
+
entityTypes: string[];
|
|
39
|
+
/** 매니페스트 스키마 버전(전방호환 — 미래 필드 추가 시 협상 기준). 없으면 1. */
|
|
40
|
+
manifestVersion?: number;
|
|
41
|
+
/**
|
|
42
|
+
* 이 플러그인이 register() 에서 레지스트리에 등록하는 것의 선언(문서/검수용 메타).
|
|
43
|
+
* 코어가 아니라 플러그인이 소유하는 도메인 이벤트/템플릿/서피스를 명시한다 — "도메인=플러그인"의 신분증.
|
|
44
|
+
*/
|
|
45
|
+
provides?: {
|
|
46
|
+
/** registerEventEntry 로 등록하는 이벤트 타입(예: ["org.corp.forgejo.pr.review"]). */
|
|
47
|
+
events?: string[];
|
|
48
|
+
/** registerTemplate 로 등록하는 템플릿 이름(예: ["scmRef","prReview","ciRun"]). */
|
|
49
|
+
templates?: string[];
|
|
50
|
+
/** registerAppPack 으로 기여하는 App Pack app 키(예: ["forgejo"]). */
|
|
51
|
+
surfaces?: string[];
|
|
52
|
+
};
|
|
53
|
+
/** 선언된 capability 상한(scope/backendTools) — App 은 자기 액션에서 자동 도출(드리프트 0). */
|
|
54
|
+
capabilities?: PluginCapabilities;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 매니페스트 검증 스키마 — 부팅(loadPlugins) 시 모든 플러그인 신분증을 검증한다(서드파티 로딩 신뢰 경계). */
|
|
58
|
+
export const PluginManifestSchema = z.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
displayName: z.string().optional(),
|
|
61
|
+
app: z.string().min(1),
|
|
62
|
+
entityTypes: z.string().array(),
|
|
63
|
+
manifestVersion: z.number().int().min(1).optional(),
|
|
64
|
+
provides: z
|
|
65
|
+
.object({
|
|
66
|
+
events: z.string().array().optional(),
|
|
67
|
+
templates: z.string().array().optional(),
|
|
68
|
+
surfaces: z.string().array().optional(),
|
|
69
|
+
})
|
|
70
|
+
.optional(),
|
|
71
|
+
capabilities: z
|
|
72
|
+
.object({
|
|
73
|
+
scopes: z.string().array().optional(),
|
|
74
|
+
backendTools: z.string().array().optional(),
|
|
75
|
+
})
|
|
76
|
+
.optional(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** 플러그인 = manifest + 부팅 시 1회 호출되는 register(). 클라 enabled 목록의 원소. */
|
|
80
|
+
export interface Plugin extends PluginManifest {
|
|
81
|
+
/** App Pack·이벤트·템플릿을 레지스트리에 등록한다(부팅 시 1회). */
|
|
82
|
+
register: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const registry = new Map<string, Plugin>();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 단일 플러그인을 등록하고 register() 를 호출한다.
|
|
89
|
+
* 중복 id 는 graceful 하게 무시한다(먼저 등록된 것 유지 → 멱등). 이 패키지는 서버/클라/MCP
|
|
90
|
+
* 공용 스키마라 환경(console 등)에 의존하지 않는다 — 충돌 가시화는 호출측(로더)의 몫.
|
|
91
|
+
*/
|
|
92
|
+
export function registerPlugin(plugin: Plugin): void {
|
|
93
|
+
if (registry.has(plugin.id)) return;
|
|
94
|
+
// 신뢰 경계(서드파티 로딩 1차): register() 가 레지스트리를 건드리기 *전에* 매니페스트(신분증)를 검증한다.
|
|
95
|
+
// 잘못된 신분증은 부팅을 멈춘다(fail-fast) — 잘못 등록된 채 절반만 동작하는 것보다 즉시 가시화가 안전.
|
|
96
|
+
const valid = PluginManifestSchema.safeParse(plugin);
|
|
97
|
+
if (!valid.success) {
|
|
98
|
+
const detail = valid.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
99
|
+
throw new Error(`[plugins] 매니페스트 검증 실패 (id=${(plugin as { id?: string }).id ?? "?"}): ${detail}`);
|
|
100
|
+
}
|
|
101
|
+
registry.set(plugin.id, plugin);
|
|
102
|
+
plugin.register();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 활성(enabled) 플러그인 목록을 순서대로 등록한다. */
|
|
106
|
+
export function loadPlugins(plugins: Plugin[]): void {
|
|
107
|
+
for (const plugin of plugins) registerPlugin(plugin);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 등록된 플러그인 목록. */
|
|
111
|
+
export function listPlugins(): Plugin[] {
|
|
112
|
+
return [...registry.values()];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** id 로 등록된 플러그인을 조회한다. */
|
|
116
|
+
export function getPlugin(id: string): Plugin | undefined {
|
|
117
|
+
return registry.get(id);
|
|
118
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이벤트 스키마 레지스트리 (RFC §5).
|
|
3
|
+
*
|
|
4
|
+
* "어떤 타입이 존재하고, 스키마가 무엇이며, 누가 발행할 수 있는지"를 한 곳에서 통제한다.
|
|
5
|
+
* 이게 없으면 규모가 커질 때 "이벤트 수프"가 된다. appservice는 들어온 이벤트를 여기로
|
|
6
|
+
* 검증(validateEvent)하고, 클라이언트는 알 수 없는 타입을 안전하게 무시한다.
|
|
7
|
+
*/
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import {
|
|
10
|
+
A2aRequestContentSchema,
|
|
11
|
+
A2aResultContentSchema,
|
|
12
|
+
AgentCapabilityContentSchema,
|
|
13
|
+
AgentPlanContentSchema,
|
|
14
|
+
AgentStepContentSchema,
|
|
15
|
+
ApprovalDecisionContentSchema,
|
|
16
|
+
ApprovalRequestContentSchema,
|
|
17
|
+
CommandContentSchema,
|
|
18
|
+
EntitySnapshotContentSchema,
|
|
19
|
+
ErpEventContentSchema,
|
|
20
|
+
RoomAppsContentSchema,
|
|
21
|
+
RoomBindingContentSchema,
|
|
22
|
+
TaskCreateContentSchema,
|
|
23
|
+
TaskUpdateContentSchema,
|
|
24
|
+
UiActionContentSchema,
|
|
25
|
+
UiCardContentSchema,
|
|
26
|
+
WorkflowStateContentSchema,
|
|
27
|
+
} from "./events.js";
|
|
28
|
+
import { ERP_EVENT_PREFIX, EventType } from "./namespace.js";
|
|
29
|
+
import { AppSurfaceContentSchema } from "./surfaces.js";
|
|
30
|
+
|
|
31
|
+
/** 누가 이 이벤트를 발행할 자격이 있는가 (서버측 검증의 1차 힌트). */
|
|
32
|
+
export type Publisher = "human" | "agent" | "bridge";
|
|
33
|
+
|
|
34
|
+
/** 메시지 이벤트(타임라인)인지 상태 이벤트(state, last-writer-wins)인지. */
|
|
35
|
+
export type EventKind = "message" | "state";
|
|
36
|
+
|
|
37
|
+
export interface RegistryEntry {
|
|
38
|
+
/** 정확한 타입 매칭. prefix가 설정된 엔트리는 type 대신 prefix로 매칭한다. */
|
|
39
|
+
type?: string;
|
|
40
|
+
/** `org.corp.erp.` 처럼 접두사 매칭이 필요한 경우. */
|
|
41
|
+
prefix?: string;
|
|
42
|
+
kind: EventKind;
|
|
43
|
+
schema: z.ZodTypeAny;
|
|
44
|
+
publishableBy: Publisher[];
|
|
45
|
+
description: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 코어(빌트인) 이벤트 엔트리 — 항상 조회 우선이며 플러그인이 덮어쓸 수 없다(코어 격리).
|
|
50
|
+
* 플러그인은 자기 `org.corp.<domain>.*` 엔트리를 registerEventEntry 로 런타임 추가한다.
|
|
51
|
+
*/
|
|
52
|
+
export const EVENT_REGISTRY: RegistryEntry[] = [
|
|
53
|
+
{
|
|
54
|
+
type: EventType.Command,
|
|
55
|
+
kind: "message",
|
|
56
|
+
schema: CommandContentSchema,
|
|
57
|
+
publishableBy: ["human", "agent"],
|
|
58
|
+
description: "구조화된 명령 (선택적; 보통은 m.room.message 텍스트를 에이전트가 파싱)",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: EventType.UiCard,
|
|
62
|
+
kind: "message",
|
|
63
|
+
schema: UiCardContentSchema,
|
|
64
|
+
publishableBy: ["agent"],
|
|
65
|
+
description: "에이전트 → 사용자: 인라인 렌더되는 UI 카드 (전용 이벤트 타입 형태)",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: EventType.UiAction,
|
|
69
|
+
kind: "message",
|
|
70
|
+
schema: UiActionContentSchema,
|
|
71
|
+
publishableBy: ["human", "agent"],
|
|
72
|
+
description: "사용자 → 에이전트: UI 카드 상호작용 결과",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
prefix: ERP_EVENT_PREFIX,
|
|
76
|
+
kind: "message",
|
|
77
|
+
schema: ErpEventContentSchema,
|
|
78
|
+
publishableBy: ["bridge"],
|
|
79
|
+
description: "ERP 인바운드 이벤트 (org.corp.erp.<entity>.<verb>)",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: EventType.AgentCapability,
|
|
83
|
+
kind: "state",
|
|
84
|
+
schema: AgentCapabilityContentSchema,
|
|
85
|
+
publishableBy: ["agent"],
|
|
86
|
+
description: "에이전트 역량 프로필 (상태 이벤트, state_key = 에이전트 MXID)",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: EventType.WorkflowState,
|
|
90
|
+
kind: "state",
|
|
91
|
+
schema: WorkflowStateContentSchema,
|
|
92
|
+
publishableBy: ["agent", "bridge"],
|
|
93
|
+
description: "엔티티(방)의 현재 워크플로 상태 (상태 이벤트, LWW)",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: EventType.RoomBinding,
|
|
97
|
+
kind: "state",
|
|
98
|
+
schema: RoomBindingContentSchema,
|
|
99
|
+
publishableBy: ["human", "agent", "bridge"],
|
|
100
|
+
description: "룸↔도메인 바인딩: 룸이 자기 app/엔티티를 선언 (상태 이벤트, state_key = '')",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: EventType.RoomApps,
|
|
104
|
+
kind: "state",
|
|
105
|
+
schema: RoomAppsContentSchema,
|
|
106
|
+
publishableBy: ["human", "agent"],
|
|
107
|
+
description: "룸이 탭으로 노출하는 App Service 키 목록 — space.apps 의 per-room 짝. 사람(채널설정)·에이전트(프로비저너)만 선언, 브리지 금지. 거버넌스=부모 space.apps 부분집합 (상태 이벤트, state_key = '')",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: EventType.EntitySnapshot,
|
|
111
|
+
kind: "state",
|
|
112
|
+
schema: EntitySnapshotContentSchema,
|
|
113
|
+
publishableBy: ["human", "agent", "bridge"],
|
|
114
|
+
description: "엔티티 표시 필드 스냅샷 (상태 이벤트, LWW) — 팩 서피스 {{binding}} 해석 소스",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: EventType.AppSurface,
|
|
118
|
+
kind: "state",
|
|
119
|
+
schema: AppSurfaceContentSchema,
|
|
120
|
+
publishableBy: ["human", "agent", "bridge"],
|
|
121
|
+
description: "앱이 데이터로 실어 보내는 셸 서피스(패널/헤더) — AS만 추가해도 회사별 UI 등록 (상태 이벤트, state_key='')",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: EventType.ApprovalRequest,
|
|
125
|
+
kind: "message",
|
|
126
|
+
schema: ApprovalRequestContentSchema,
|
|
127
|
+
publishableBy: ["human", "agent"],
|
|
128
|
+
description: "HITL 승인 요청 (다단계 승인자 대상) — 사람/에이전트가 발행",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: EventType.ApprovalDecision,
|
|
132
|
+
kind: "message",
|
|
133
|
+
schema: ApprovalDecisionContentSchema,
|
|
134
|
+
publishableBy: ["human", "agent"],
|
|
135
|
+
description: "HITL 승인 결정 (승인/반려) — 승인자(사람/에이전트)가 발행, 요청을 m.reference",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: EventType.AgentPlan,
|
|
139
|
+
kind: "message",
|
|
140
|
+
schema: AgentPlanContentSchema,
|
|
141
|
+
publishableBy: ["agent"],
|
|
142
|
+
description: "에이전트 런 계획 (단계 골격 + 공정률) — 에이전트가 발행, 런 타임라인의 토대",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: EventType.AgentStep,
|
|
146
|
+
kind: "message",
|
|
147
|
+
schema: AgentStepContentSchema,
|
|
148
|
+
publishableBy: ["agent"],
|
|
149
|
+
description: "에이전트 런 단계 갱신 (상태/공정률) — 에이전트가 발행, plan 을 m.reference",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: EventType.TaskCreate,
|
|
153
|
+
kind: "message",
|
|
154
|
+
schema: TaskCreateContentSchema,
|
|
155
|
+
publishableBy: ["human", "agent"],
|
|
156
|
+
description: "작업항목 생성 (사람↔에이전트 공유 작업 라이프사이클의 루트) — 사람/에이전트가 발행",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: EventType.TaskUpdate,
|
|
160
|
+
kind: "message",
|
|
161
|
+
schema: TaskUpdateContentSchema,
|
|
162
|
+
publishableBy: ["human", "agent"],
|
|
163
|
+
description: "작업항목 갱신 (상태/배정/진행) — 사람/에이전트가 발행, create 를 m.reference",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: EventType.A2aRequest,
|
|
167
|
+
kind: "message",
|
|
168
|
+
schema: A2aRequestContentSchema,
|
|
169
|
+
publishableBy: ["agent"],
|
|
170
|
+
description: "에이전트 → 에이전트 작업 위임 요청 (A2A)",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: EventType.A2aResult,
|
|
174
|
+
kind: "message",
|
|
175
|
+
schema: A2aResultContentSchema,
|
|
176
|
+
publishableBy: ["agent"],
|
|
177
|
+
description: "에이전트 → 에이전트 위임 결과 회신 (A2A)",
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/** 플러그인이 런타임에 추가한 엔트리(org.corp.<domain>.*). 코어 뒤에 둔다 = 코어 우선. */
|
|
182
|
+
const runtimeEntries: RegistryEntry[] = [];
|
|
183
|
+
|
|
184
|
+
/** 엔트리의 식별 키 — 정확 타입 또는 접두사. */
|
|
185
|
+
function entryKey(entry: RegistryEntry): string | undefined {
|
|
186
|
+
return entry.type ?? entry.prefix;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 코어 + 런타임 엔트리 전체 (코어가 앞 = 조회/매칭 우선). */
|
|
190
|
+
export function listEventEntries(): RegistryEntry[] {
|
|
191
|
+
return [...EVENT_REGISTRY, ...runtimeEntries];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 플러그인 이벤트 엔트리를 런타임 등록한다 (부팅 시 plugin.register() 안에서 호출).
|
|
196
|
+
* - type/prefix 중 하나는 있어야 한다(없으면 무시).
|
|
197
|
+
* - 코어/기존 엔트리와 키가 겹치면 무시(코어 격리·멱등).
|
|
198
|
+
* 등록되면 true, 무시되면 false.
|
|
199
|
+
*/
|
|
200
|
+
export function registerEventEntry(entry: RegistryEntry): boolean {
|
|
201
|
+
const key = entryKey(entry);
|
|
202
|
+
if (!key) return false;
|
|
203
|
+
const existing = listEventEntries();
|
|
204
|
+
if (existing.some((e) => entryKey(e) === key)) return false;
|
|
205
|
+
// 정확-타입 엔트리가 기존 prefix 엔트리 아래로 떨어지면 lookupEntry 가 prefix 를 먼저 매칭해 영원히 가려진다
|
|
206
|
+
// (등록은 됐다고 true 를 주지만 schema/publishableBy 가 절대 안 쓰임). 그런 조용한 섀도잉을 거부한다.
|
|
207
|
+
const t = entry.type;
|
|
208
|
+
if (t && existing.some((e) => e.prefix && t.startsWith(e.prefix))) return false;
|
|
209
|
+
runtimeEntries.push(entry);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 주어진 이벤트 타입에 해당하는 레지스트리 엔트리를 찾는다 (정확/접두사 매칭, 코어 우선). */
|
|
214
|
+
export function lookupEntry(type: string): RegistryEntry | undefined {
|
|
215
|
+
return listEventEntries().find((e) => (e.type ? e.type === type : e.prefix ? type.startsWith(e.prefix) : false));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type ValidationResult =
|
|
219
|
+
| { ok: true; type: string; kind: EventKind; data: unknown }
|
|
220
|
+
| { ok: false; type: string; reason: "unknown_type" | "schema_mismatch"; issues?: string[] };
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 이벤트 content를 레지스트리로 검증한다.
|
|
224
|
+
* 알 수 없는 타입 → unknown_type, 스키마 불일치 → schema_mismatch.
|
|
225
|
+
*/
|
|
226
|
+
export function validateEvent(type: string, content: unknown): ValidationResult {
|
|
227
|
+
const entry = lookupEntry(type);
|
|
228
|
+
if (!entry) return { ok: false, type, reason: "unknown_type" };
|
|
229
|
+
const parsed = entry.schema.safeParse(content);
|
|
230
|
+
if (!parsed.success) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
type,
|
|
234
|
+
reason: "schema_mismatch",
|
|
235
|
+
issues: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { ok: true, type, kind: entry.kind, data: parsed.data };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 발행자가 해당 타입을 보낼 자격이 있는지 (서버측 권한 검증의 보조). */
|
|
242
|
+
export function canPublish(type: string, publisher: Publisher): boolean {
|
|
243
|
+
const entry = lookupEntry(type);
|
|
244
|
+
return entry ? entry.publishableBy.includes(publisher) : false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 발신자 MXID 를 발행자 분류(human/agent/bridge)로 매핑한다 (docs/tickets/SEC-3).
|
|
249
|
+
*
|
|
250
|
+
* 서버가 인바운드 이벤트를 받을 때 `canPublish(type, classifySender(sender))` 로 "이 발신자가 이 타입을
|
|
251
|
+
* 보낼 자격이 있는가"를 강제하는 데 쓴다. 분류는 AS 독점 네임스페이스 관례를 따른다
|
|
252
|
+
* (infra/registration: `@agent_*` = 에이전트, `@erp_*` = 브리지, 그 외 = 사람).
|
|
253
|
+
*
|
|
254
|
+
* ⚠️ 반드시 홈서버가 서명·귀속한 event.sender 로 호출할 것 — 본문(content)의 from_* 필드는 위조 가능하다.
|
|
255
|
+
*/
|
|
256
|
+
export function classifySender(senderId: string): Publisher {
|
|
257
|
+
const localpart = senderId.split(":")[0] ?? "";
|
|
258
|
+
if (localpart.startsWith("@agent_")) return "agent";
|
|
259
|
+
if (localpart.startsWith("@erp_")) return "bridge";
|
|
260
|
+
return "human";
|
|
261
|
+
}
|