@things-factory/board-import 10.1.24 → 10.1.26

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.
@@ -1,4 +1,18 @@
1
1
  import { type ToolCategory } from '@things-factory/ai-client-base';
2
+ export declare const IMPORT_TOOL_PRIVILEGE: {
3
+ readonly importBoardAsync: {
4
+ readonly category: "board-import";
5
+ readonly privilege: "mutation";
6
+ };
7
+ readonly getImportSession: {
8
+ readonly category: "board-import";
9
+ readonly privilege: "query";
10
+ };
11
+ readonly materializeImportSession: {
12
+ readonly category: "board-import";
13
+ readonly privilege: "mutation";
14
+ };
15
+ };
2
16
  /**
3
17
  * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가
4
18
  * 부담스러울 때 import 후 명시 호출 가능.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BOARD_IMPORT_TOOL_CATEGORY = void 0;
3
+ exports.BOARD_IMPORT_TOOL_CATEGORY = exports.IMPORT_TOOL_PRIVILEGE = void 0;
4
4
  exports.registerBoardImportTools = registerBoardImportTools;
5
5
  /**
6
6
  * board-import 의 LLM tool 카테고리 등록.
@@ -44,7 +44,10 @@ function getDeps() {
44
44
  const actionsMod = require('./import-session/import-actions');
45
45
  // eslint-disable-next-line @typescript-eslint/no-var-requires
46
46
  const materializeMod = require('./import-session/materialize-from-session');
47
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
48
+ const authMod = require('@things-factory/auth-base');
47
49
  _depsCache = {
50
+ checkPermission: authMod.checkPermission,
48
51
  ImportSession: isMod.ImportSession,
49
52
  Board: bsMod.Board,
50
53
  Group: bsMod.Group,
@@ -67,6 +70,34 @@ function requireDomainContext(state) {
67
70
  }
68
71
  return { domain: state.domain, user: state.user };
69
72
  }
73
+ /*
74
+ * A tool is a second entrance to a door that already exists, so it asks for what that door asks for.
75
+ *
76
+ * `materializeImportSession` and `importBoardAsync` sit behind `board-import:mutation` as GraphQL
77
+ * mutations, and `importSession` behind `board-import:query` (import-session-resolver.ts). The tools
78
+ * called the same service functions with no check at all, so whoever could open a conversation —
79
+ * `ai-assistant:mutation` — could create a Board through it.
80
+ *
81
+ * The check lives in the builder rather than in whichever chat door dispatches the tool: there is
82
+ * more than one such door, and the effect has to be guarded wherever it is reached from. The same
83
+ * object is also declared on the spec as `privilege`, which is what a door reads to decide whether
84
+ * to offer the tool at all; declaring and enforcing from one constant keeps the two from drifting.
85
+ */
86
+ exports.IMPORT_TOOL_PRIVILEGE = {
87
+ importBoardAsync: { category: 'board-import', privilege: 'mutation' },
88
+ getImportSession: { category: 'board-import', privilege: 'query' },
89
+ materializeImportSession: { category: 'board-import', privilege: 'mutation' }
90
+ };
91
+ async function requireImportPrivilege(tool, state) {
92
+ const { domain, user } = requireDomainContext(state);
93
+ const required = exports.IMPORT_TOOL_PRIVILEGE[tool];
94
+ const allowed = await getDeps().checkPermission(required, user, domain, state.unsafeIP, state.prohibitedPrivileges);
95
+ if (!allowed) {
96
+ throw new Error(`[board-import] '${tool}' needs ${required.category}:${required.privilege}. ` +
97
+ 'Being allowed to talk to the assistant does not open it.');
98
+ }
99
+ return { domain, user };
100
+ }
70
101
  const importToolsCategory = {
71
102
  name: 'board-import',
72
103
  description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',
@@ -106,6 +137,7 @@ const importToolsCategory = {
106
137
  {
107
138
  kind: 'external',
108
139
  name: 'importBoardAsync',
140
+ privilege: exports.IMPORT_TOOL_PRIVILEGE.importBoardAsync,
109
141
  description: 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +
110
142
  '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +
111
143
  'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',
@@ -134,7 +166,7 @@ const importToolsCategory = {
134
166
  required: ['attachmentId']
135
167
  },
136
168
  builder: async (args, ctx) => {
137
- const { domain, user } = requireDomainContext(ctx?.state);
169
+ const { domain, user } = await requireImportPrivilege('importBoardAsync', ctx?.state);
138
170
  const session = await getDeps().startImportJobAsync({
139
171
  attachmentId: args.attachmentId,
140
172
  chatSessionId: args.chatSessionId,
@@ -152,6 +184,7 @@ const importToolsCategory = {
152
184
  {
153
185
  kind: 'read',
154
186
  name: 'getImportSession',
187
+ privilege: exports.IMPORT_TOOL_PRIVILEGE.getImportSession,
155
188
  description: 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +
156
189
  '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +
157
190
  'message 에 에러 사유.',
@@ -163,7 +196,7 @@ const importToolsCategory = {
163
196
  required: ['sessionId']
164
197
  },
165
198
  builder: async (args, ctx) => {
166
- const { domain, user } = requireDomainContext(ctx?.state);
199
+ const { domain, user } = await requireImportPrivilege('getImportSession', ctx?.state);
167
200
  const session = await getDeps().fetchImportSession(args.sessionId, { domain, user }, (0, shell_1.getRepository)(getDeps().ImportSession));
168
201
  if (!session)
169
202
  return { found: false };
@@ -183,6 +216,7 @@ const importToolsCategory = {
183
216
  {
184
217
  kind: 'external',
185
218
  name: 'materializeImportSession',
219
+ privilege: exports.IMPORT_TOOL_PRIVILEGE.materializeImportSession,
186
220
  description: '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +
187
221
  'completed 인 세션만 처리. 결과 Board 는 state="draft" 로 생성되므로 사용자가 검수 후 ' +
188
222
  '발행해야 한다.',
@@ -204,7 +238,7 @@ const importToolsCategory = {
204
238
  required: ['sessionId', 'name']
205
239
  },
206
240
  builder: async (args, ctx) => {
207
- const { domain, user } = requireDomainContext(ctx?.state);
241
+ const { domain, user } = await requireImportPrivilege('materializeImportSession', ctx?.state);
208
242
  const session = await getDeps().fetchImportSession(args.sessionId, { domain, user }, (0, shell_1.getRepository)(getDeps().ImportSession));
209
243
  const { Board, Group, materializeFromSession } = getDeps();
210
244
  const board = await materializeFromSession(session, {
@@ -1 +1 @@
1
- {"version":3,"file":"import-tools.js","sourceRoot":"","sources":["../../server/service/import-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-import 의 LLM tool 카테고리 등록.\n *\n * board-ai 의 chat 흐름 안에서 사용자가 \"이 도면을 보드로 만들어줘\" / \"import 결과 검수\n * 후 이 이름으로 저장\" 같은 자연어를 발화하면 LLM 이 본 카테고리의 tool 들을 호출한다.\n *\n * 노출되는 tool:\n * - listImportAdapters (read) : 등록된 FormatAdapter 능력 조회\n * - importBoardAsync (external) : Attachment → ImportSession 비동기 시작\n * - getImportSession (read) : ImportSession 진행/결과 조회\n * - materializeImportSession (external) : ImportSession.result → 새 Board entity\n *\n * dispatch wiring:\n * board-ai 의 chat 메서드가 ToolCallContext (resolver state — domain/user/tx) 를 closure 로\n * 주입한다. builder 는 ctx.state 에서 typeorm getRepository / domain / user 를 추출해\n * service-level 함수 (import-actions, materialize-from-session) 를 호출한다.\n *\n * 이 layer 가 없으면 builder 가 typeorm 에 직접 의존해야 했고, board-ai 가 typeorm 까지\n * 알게 되는 leakage 가 생겼을 것 — 의도적으로 builder 가 ctx.state.typeorm 같은 식으로\n * 접근하지 않고 things-factory 표준 getRepository 를 호출.\n */\nimport { getRepository } from '@things-factory/shell'\nimport {\n registerToolCategory,\n type ToolCategory,\n type ToolSpec\n} from '@things-factory/ai-client-base'\n\n// 모든 server-side 의존성 (entity / service 함수 / 외부 패키지) 은 module-level import\n// 없이 lazy require — module load 만으로 typeorm decorator / 외부 controllers chain\n// (예: board-service 의 puppeteer pool) 이 평가되어 jest 환경에서 깨지는 것을 회피.\n// 운영에서는 builder 첫 호출 시 cache 채워져 이후 호출은 직접 dispatch.\n//\n// 이 패턴은 LLM tool layer 의 일반 정책으로 가져갈 수 있다 — registerToolCategory 가\n// 평가되는 시점은 server bootstrap 의 매우 이른 단계라, 그때 typeorm DataSource init 이\n// 아직 끝나지 않았을 가능성이 있다.\nlet _depsCache: any | undefined\nfunction getDeps(): any {\n if (!_depsCache) {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const isMod = require('./import-session/import-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const bsMod = require('@things-factory/board-service')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const actionsMod = require('./import-session/import-actions')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const materializeMod = require('./import-session/materialize-from-session')\n _depsCache = {\n ImportSession: isMod.ImportSession,\n Board: bsMod.Board,\n Group: bsMod.Group,\n startImportJobAsync: actionsMod.startImportJobAsync,\n fetchImportSession: actionsMod.fetchImportSession,\n describeAdapter: actionsMod.describeAdapter,\n materializeFromSession: materializeMod.materializeFromSession\n }\n }\n return _depsCache\n}\n\n/**\n * tool builder 의 ctx.state 가 도메인 식별을 갖는지 검증. 미설정이면 명확한 에러로 실패 —\n * board-ai 의 toolCallContext wiring 누락 / 비인증 호출 등을 빠르게 찾기 위해.\n */\nfunction requireDomainContext(state: any): { domain: any; user: any } {\n if (!state || !state.domain) {\n throw new Error(\n '[board-import] tool builder 의 ctx.state.domain 미설정. ' +\n 'board-ai 의 ChatOptions.toolCallContext 에 ResolverContext.state 가 전달됐는지 확인.'\n )\n }\n return { domain: state.domain, user: state.user }\n}\n\nconst importToolsCategory: ToolCategory = {\n name: 'board-import',\n description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',\n /*\n * ⚠ 이 지침은 **아키텍트가 이 파일의 도구 설명만 보고 쓴 최소본**이다(2026-09-09).\n *\n * `registerToolCategory` 가 지침 없는 카테고리를 거절하게 되면서, 이 카테고리가 걸리는 유일한\n * 자리였다. 도구를 등록한 쪽이 규율을 소유하므로(§`ToolCategory.guidance`), **이 도구를 아는\n * 사람이 도메인 규율로 바꿔 주십시오.** 아래는 위 `description` 들에서 직접 읽어낸 것만 적었고\n * 도메인 지식을 지어내지 않았다.\n *\n * 두 줄이 특히 중요하다 — 이 카테고리에는 **즉시 돌아오지만 끝나지 않은** 도구와 **확인을 받아야\n * 하는** 도구가 있다. 그 둘이 지침이 필요한 전형이다.\n */\n guidance: [\n '- 어떤 형식을 변환할 수 있는지 물으면 추측하지 말고 `listImportAdapters` 를 호출한다. 등록된 어댑터만이 답이다.',\n '- `importBoardAsync` 는 **즉시 돌아오지만 변환은 끝나지 않았다.** 세션을 돌려줄 뿐이다 — `getImportSession` 으로 `completed` 를 확인하기 전에 \"변환했습니다\"라고 답하면 사용자는 없는 결과를 찾는다.',\n '- `getImportSession` 이 `failed` 면 그 `message` 를 그대로 옮긴다. 원인을 지어내지 않는다.',\n '- `materializeImportSession` 은 **사용자가 명시적으로 확인한 뒤에만** 호출한다. 미리보기를 보여 주고 저장 여부를 물은 다음이다. 만들어지는 Board 는 `draft` 이므로 발행은 사용자가 한다.',\n '- attachment id 를 지어내지 않는다. 미리 업로드된 것만 있고, 없으면 업로드가 먼저라고 말한다.'\n ].join('\\n'),\n specs: [\n {\n kind: 'read',\n name: 'listImportAdapters',\n description:\n '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +\n '사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',\n schema: { type: 'object', properties: {} },\n builder: async () => {\n // 동적 import — pipeline 모듈의 DEFAULT_ADAPTERS 를 inspect.\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const pipelineModule = require('./pipeline/index')\n const adapters: any[] = pipelineModule.DEFAULT_ADAPTERS ?? []\n return adapters.map(getDeps().describeAdapter)\n }\n },\n {\n kind: 'external',\n name: 'importBoardAsync',\n description:\n 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +\n '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +\n 'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',\n schema: {\n type: 'object',\n properties: {\n attachmentId: {\n type: 'string',\n description: 'Attachment id (attachment-base 에 미리 업로드된 도면 파일).'\n },\n chatSessionId: {\n type: 'string',\n description: 'board-ai chat session id (선택). 진행 메시지가 chat 흐름에 합류.'\n },\n scopes: {\n type: 'array',\n items: { type: 'string' },\n description: 'ImportRule 의 scope (예: [\"fmsim\"]). 도메인별 매핑 규칙 활성화.'\n },\n parseOptions: {\n type: 'object',\n description: 'Adapter 파싱 옵션 (excludeLayers / maxEntities / context 등).',\n additionalProperties: true\n }\n },\n required: ['attachmentId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = requireDomainContext(ctx?.state)\n const session = await getDeps().startImportJobAsync(\n {\n attachmentId: args.attachmentId,\n chatSessionId: args.chatSessionId,\n scopes: args.scopes,\n parseOptions: args.parseOptions\n },\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n return {\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message\n }\n }\n },\n {\n kind: 'read',\n name: 'getImportSession',\n description:\n 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +\n '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +\n 'message 에 에러 사유.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string', description: 'ImportSession id (importBoardAsync 가 반환).' }\n },\n required: ['sessionId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = requireDomainContext(ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n if (!session) return { found: false }\n return {\n found: true,\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message,\n // result 는 큰 boardModel 을 포함할 수 있어 status 가 completed 일 때만.\n result: session.status === 'completed' ? session.result : undefined,\n totalEntities: session.totalEntities,\n completedAt: session.completedAt\n }\n }\n },\n {\n kind: 'external',\n name: 'materializeImportSession',\n description:\n '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +\n 'completed 인 세션만 처리. 결과 Board 는 state=\"draft\" 로 생성되므로 사용자가 검수 후 ' +\n '발행해야 한다.',\n themeNote:\n '⚠ 사용자의 명시적 confirm 후에만 호출. import 결과 미리보기를 보여주고 \"이 이름으로 ' +\n '저장하시겠어요?\" 같은 확인 받은 다음 발동.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n name: { type: 'string', description: '새 Board 이름 — 같은 도메인 내 unique.' },\n description: { type: 'string' },\n groupId: { type: 'string' },\n type: { type: 'string', enum: ['main', 'sub', 'popup'] },\n thumbnail: {\n type: 'string',\n description: 'Base64 thumbnail. 미지정 시 빈 placeholder 사용 (별도 thumbnail 생성 호출).'\n }\n },\n required: ['sessionId', 'name']\n },\n builder: async (args, ctx) => {\n const { domain, user } = requireDomainContext(ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n const { Board, Group, materializeFromSession } = getDeps()\n const board = await materializeFromSession(\n session as any,\n {\n sessionId: args.sessionId,\n name: args.name,\n description: args.description,\n groupId: args.groupId,\n type: args.type,\n thumbnail: args.thumbnail\n },\n { domain, user },\n {\n boardRepo: getRepository(Board) as any,\n groupRepo: getRepository(Group) as any\n }\n )\n // 큰 model 본문은 LLM 회신에서 제외 — id 와 메타만.\n return {\n boardId: (board as any).id,\n name: (board as any).name,\n state: (board as any).state,\n sourceImportSessionId: (board as any).sourceImportSessionId\n }\n }\n }\n ] as ToolSpec[]\n}\n\n// 등록 — module 로드 시 side-effect.\nregisterToolCategory(importToolsCategory)\n\n/**\n * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가\n * 부담스러울 때 import 후 명시 호출 가능.\n */\nexport function registerBoardImportTools(): void {\n registerToolCategory(importToolsCategory)\n}\n\n/**\n * 테스트용 — registerToolCategory 의 입력으로 사용된 카테고리 객체. 외부에서 inspect 가능.\n */\nexport const BOARD_IMPORT_TOOL_CATEGORY = importToolsCategory\n"]}
1
+ {"version":3,"file":"import-tools.js","sourceRoot":"","sources":["../../server/service/import-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-import 의 LLM tool 카테고리 등록.\n *\n * board-ai 의 chat 흐름 안에서 사용자가 \"이 도면을 보드로 만들어줘\" / \"import 결과 검수\n * 후 이 이름으로 저장\" 같은 자연어를 발화하면 LLM 이 본 카테고리의 tool 들을 호출한다.\n *\n * 노출되는 tool:\n * - listImportAdapters (read) : 등록된 FormatAdapter 능력 조회\n * - importBoardAsync (external) : Attachment → ImportSession 비동기 시작\n * - getImportSession (read) : ImportSession 진행/결과 조회\n * - materializeImportSession (external) : ImportSession.result → 새 Board entity\n *\n * dispatch wiring:\n * board-ai 의 chat 메서드가 ToolCallContext (resolver state — domain/user/tx) 를 closure 로\n * 주입한다. builder 는 ctx.state 에서 typeorm getRepository / domain / user 를 추출해\n * service-level 함수 (import-actions, materialize-from-session) 를 호출한다.\n *\n * 이 layer 가 없으면 builder 가 typeorm 에 직접 의존해야 했고, board-ai 가 typeorm 까지\n * 알게 되는 leakage 가 생겼을 것 — 의도적으로 builder 가 ctx.state.typeorm 같은 식으로\n * 접근하지 않고 things-factory 표준 getRepository 를 호출.\n */\nimport { getRepository } from '@things-factory/shell'\nimport {\n registerToolCategory,\n type ToolCategory,\n type ToolSpec\n} from '@things-factory/ai-client-base'\n\n// 모든 server-side 의존성 (entity / service 함수 / 외부 패키지) 은 module-level import\n// 없이 lazy require — module load 만으로 typeorm decorator / 외부 controllers chain\n// (예: board-service 의 puppeteer pool) 이 평가되어 jest 환경에서 깨지는 것을 회피.\n// 운영에서는 builder 첫 호출 시 cache 채워져 이후 호출은 직접 dispatch.\n//\n// 이 패턴은 LLM tool layer 의 일반 정책으로 가져갈 수 있다 — registerToolCategory 가\n// 평가되는 시점은 server bootstrap 의 매우 이른 단계라, 그때 typeorm DataSource init 이\n// 아직 끝나지 않았을 가능성이 있다.\nlet _depsCache: any | undefined\nfunction getDeps(): any {\n if (!_depsCache) {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const isMod = require('./import-session/import-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const bsMod = require('@things-factory/board-service')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const actionsMod = require('./import-session/import-actions')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const materializeMod = require('./import-session/materialize-from-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const authMod = require('@things-factory/auth-base')\n _depsCache = {\n checkPermission: authMod.checkPermission,\n ImportSession: isMod.ImportSession,\n Board: bsMod.Board,\n Group: bsMod.Group,\n startImportJobAsync: actionsMod.startImportJobAsync,\n fetchImportSession: actionsMod.fetchImportSession,\n describeAdapter: actionsMod.describeAdapter,\n materializeFromSession: materializeMod.materializeFromSession\n }\n }\n return _depsCache\n}\n\n/**\n * tool builder 의 ctx.state 가 도메인 식별을 갖는지 검증. 미설정이면 명확한 에러로 실패 —\n * board-ai 의 toolCallContext wiring 누락 / 비인증 호출 등을 빠르게 찾기 위해.\n */\nfunction requireDomainContext(state: any): { domain: any; user: any } {\n if (!state || !state.domain) {\n throw new Error(\n '[board-import] tool builder 의 ctx.state.domain 미설정. ' +\n 'board-ai 의 ChatOptions.toolCallContext 에 ResolverContext.state 가 전달됐는지 확인.'\n )\n }\n return { domain: state.domain, user: state.user }\n}\n\n/*\n * A tool is a second entrance to a door that already exists, so it asks for what that door asks for.\n *\n * `materializeImportSession` and `importBoardAsync` sit behind `board-import:mutation` as GraphQL\n * mutations, and `importSession` behind `board-import:query` (import-session-resolver.ts). The tools\n * called the same service functions with no check at all, so whoever could open a conversation —\n * `ai-assistant:mutation` — could create a Board through it.\n *\n * The check lives in the builder rather than in whichever chat door dispatches the tool: there is\n * more than one such door, and the effect has to be guarded wherever it is reached from. The same\n * object is also declared on the spec as `privilege`, which is what a door reads to decide whether\n * to offer the tool at all; declaring and enforcing from one constant keeps the two from drifting.\n */\nexport const IMPORT_TOOL_PRIVILEGE = {\n importBoardAsync: { category: 'board-import', privilege: 'mutation' },\n getImportSession: { category: 'board-import', privilege: 'query' },\n materializeImportSession: { category: 'board-import', privilege: 'mutation' }\n} as const\n\ntype GuardedImportTool = keyof typeof IMPORT_TOOL_PRIVILEGE\n\nasync function requireImportPrivilege(tool: GuardedImportTool, state: any): Promise<{ domain: any; user: any }> {\n const { domain, user } = requireDomainContext(state)\n const required = IMPORT_TOOL_PRIVILEGE[tool]\n const allowed = await getDeps().checkPermission(required, user, domain, state.unsafeIP, state.prohibitedPrivileges)\n if (!allowed) {\n throw new Error(\n `[board-import] '${tool}' needs ${required.category}:${required.privilege}. ` +\n 'Being allowed to talk to the assistant does not open it.'\n )\n }\n return { domain, user }\n}\n\nconst importToolsCategory: ToolCategory = {\n name: 'board-import',\n description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',\n /*\n * ⚠ 이 지침은 **아키텍트가 이 파일의 도구 설명만 보고 쓴 최소본**이다(2026-09-09).\n *\n * `registerToolCategory` 가 지침 없는 카테고리를 거절하게 되면서, 이 카테고리가 걸리는 유일한\n * 자리였다. 도구를 등록한 쪽이 규율을 소유하므로(§`ToolCategory.guidance`), **이 도구를 아는\n * 사람이 도메인 규율로 바꿔 주십시오.** 아래는 위 `description` 들에서 직접 읽어낸 것만 적었고\n * 도메인 지식을 지어내지 않았다.\n *\n * 두 줄이 특히 중요하다 — 이 카테고리에는 **즉시 돌아오지만 끝나지 않은** 도구와 **확인을 받아야\n * 하는** 도구가 있다. 그 둘이 지침이 필요한 전형이다.\n */\n guidance: [\n '- 어떤 형식을 변환할 수 있는지 물으면 추측하지 말고 `listImportAdapters` 를 호출한다. 등록된 어댑터만이 답이다.',\n '- `importBoardAsync` 는 **즉시 돌아오지만 변환은 끝나지 않았다.** 세션을 돌려줄 뿐이다 — `getImportSession` 으로 `completed` 를 확인하기 전에 \"변환했습니다\"라고 답하면 사용자는 없는 결과를 찾는다.',\n '- `getImportSession` 이 `failed` 면 그 `message` 를 그대로 옮긴다. 원인을 지어내지 않는다.',\n '- `materializeImportSession` 은 **사용자가 명시적으로 확인한 뒤에만** 호출한다. 미리보기를 보여 주고 저장 여부를 물은 다음이다. 만들어지는 Board 는 `draft` 이므로 발행은 사용자가 한다.',\n '- attachment id 를 지어내지 않는다. 미리 업로드된 것만 있고, 없으면 업로드가 먼저라고 말한다.'\n ].join('\\n'),\n specs: [\n {\n kind: 'read',\n name: 'listImportAdapters',\n description:\n '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +\n '사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',\n schema: { type: 'object', properties: {} },\n builder: async () => {\n // 동적 import — pipeline 모듈의 DEFAULT_ADAPTERS 를 inspect.\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const pipelineModule = require('./pipeline/index')\n const adapters: any[] = pipelineModule.DEFAULT_ADAPTERS ?? []\n return adapters.map(getDeps().describeAdapter)\n }\n },\n {\n kind: 'external',\n name: 'importBoardAsync',\n privilege: IMPORT_TOOL_PRIVILEGE.importBoardAsync,\n description:\n 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +\n '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +\n 'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',\n schema: {\n type: 'object',\n properties: {\n attachmentId: {\n type: 'string',\n description: 'Attachment id (attachment-base 에 미리 업로드된 도면 파일).'\n },\n chatSessionId: {\n type: 'string',\n description: 'board-ai chat session id (선택). 진행 메시지가 chat 흐름에 합류.'\n },\n scopes: {\n type: 'array',\n items: { type: 'string' },\n description: 'ImportRule 의 scope (예: [\"fmsim\"]). 도메인별 매핑 규칙 활성화.'\n },\n parseOptions: {\n type: 'object',\n description: 'Adapter 파싱 옵션 (excludeLayers / maxEntities / context 등).',\n additionalProperties: true\n }\n },\n required: ['attachmentId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('importBoardAsync', ctx?.state)\n const session = await getDeps().startImportJobAsync(\n {\n attachmentId: args.attachmentId,\n chatSessionId: args.chatSessionId,\n scopes: args.scopes,\n parseOptions: args.parseOptions\n },\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n return {\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message\n }\n }\n },\n {\n kind: 'read',\n name: 'getImportSession',\n privilege: IMPORT_TOOL_PRIVILEGE.getImportSession,\n description:\n 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +\n '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +\n 'message 에 에러 사유.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string', description: 'ImportSession id (importBoardAsync 가 반환).' }\n },\n required: ['sessionId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('getImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n if (!session) return { found: false }\n return {\n found: true,\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message,\n // result 는 큰 boardModel 을 포함할 수 있어 status 가 completed 일 때만.\n result: session.status === 'completed' ? session.result : undefined,\n totalEntities: session.totalEntities,\n completedAt: session.completedAt\n }\n }\n },\n {\n kind: 'external',\n name: 'materializeImportSession',\n privilege: IMPORT_TOOL_PRIVILEGE.materializeImportSession,\n description:\n '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +\n 'completed 인 세션만 처리. 결과 Board 는 state=\"draft\" 로 생성되므로 사용자가 검수 후 ' +\n '발행해야 한다.',\n themeNote:\n '⚠ 사용자의 명시적 confirm 후에만 호출. import 결과 미리보기를 보여주고 \"이 이름으로 ' +\n '저장하시겠어요?\" 같은 확인 받은 다음 발동.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n name: { type: 'string', description: '새 Board 이름 — 같은 도메인 내 unique.' },\n description: { type: 'string' },\n groupId: { type: 'string' },\n type: { type: 'string', enum: ['main', 'sub', 'popup'] },\n thumbnail: {\n type: 'string',\n description: 'Base64 thumbnail. 미지정 시 빈 placeholder 사용 (별도 thumbnail 생성 호출).'\n }\n },\n required: ['sessionId', 'name']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('materializeImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n const { Board, Group, materializeFromSession } = getDeps()\n const board = await materializeFromSession(\n session as any,\n {\n sessionId: args.sessionId,\n name: args.name,\n description: args.description,\n groupId: args.groupId,\n type: args.type,\n thumbnail: args.thumbnail\n },\n { domain, user },\n {\n boardRepo: getRepository(Board) as any,\n groupRepo: getRepository(Group) as any\n }\n )\n // 큰 model 본문은 LLM 회신에서 제외 — id 와 메타만.\n return {\n boardId: (board as any).id,\n name: (board as any).name,\n state: (board as any).state,\n sourceImportSessionId: (board as any).sourceImportSessionId\n }\n }\n }\n ] as ToolSpec[]\n}\n\n// 등록 — module 로드 시 side-effect.\nregisterToolCategory(importToolsCategory)\n\n/**\n * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가\n * 부담스러울 때 import 후 명시 호출 가능.\n */\nexport function registerBoardImportTools(): void {\n registerToolCategory(importToolsCategory)\n}\n\n/**\n * 테스트용 — registerToolCategory 의 입력으로 사용된 카테고리 객체. 외부에서 inspect 가능.\n */\nexport const BOARD_IMPORT_TOOL_CATEGORY = importToolsCategory\n"]}