@mandujs/core 0.9.45 → 0.10.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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/brain/doctor/config-analyzer.ts +498 -0
  3. package/src/brain/doctor/index.ts +10 -0
  4. package/src/change/snapshot.ts +46 -1
  5. package/src/change/types.ts +13 -0
  6. package/src/config/index.ts +8 -2
  7. package/src/config/mcp-ref.ts +348 -0
  8. package/src/config/mcp-status.ts +348 -0
  9. package/src/config/metadata.test.ts +308 -0
  10. package/src/config/metadata.ts +293 -0
  11. package/src/config/symbols.ts +144 -0
  12. package/src/contract/index.ts +26 -25
  13. package/src/contract/protection.ts +364 -0
  14. package/src/error/domains.ts +265 -0
  15. package/src/error/index.ts +25 -13
  16. package/src/filling/filling.ts +88 -6
  17. package/src/guard/analyzer.ts +7 -2
  18. package/src/guard/config-guard.ts +281 -0
  19. package/src/guard/decision-memory.test.ts +293 -0
  20. package/src/guard/decision-memory.ts +532 -0
  21. package/src/guard/healing.test.ts +259 -0
  22. package/src/guard/healing.ts +874 -0
  23. package/src/guard/index.ts +119 -0
  24. package/src/guard/negotiation.test.ts +282 -0
  25. package/src/guard/negotiation.ts +975 -0
  26. package/src/guard/semantic-slots.test.ts +379 -0
  27. package/src/guard/semantic-slots.ts +796 -0
  28. package/src/index.ts +2 -0
  29. package/src/lockfile/generate.ts +259 -0
  30. package/src/lockfile/index.ts +186 -0
  31. package/src/lockfile/lockfile.test.ts +410 -0
  32. package/src/lockfile/types.ts +184 -0
  33. package/src/lockfile/validate.ts +308 -0
  34. package/src/runtime/security.ts +155 -0
  35. package/src/runtime/server.ts +320 -258
  36. package/src/utils/differ.test.ts +342 -0
  37. package/src/utils/differ.ts +482 -0
  38. package/src/utils/hasher.test.ts +326 -0
  39. package/src/utils/hasher.ts +319 -0
  40. package/src/utils/index.ts +29 -0
  41. package/src/utils/safe-io.ts +188 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Mandu Symbol 상수 정의 🔣
3
+ *
4
+ * ont-run의 Symbol 기반 메타데이터 패턴 참고
5
+ * @see DNA/ont-run/src/config/categorical.ts
6
+ * @see docs/plans/08_ont-run_adoption_plan.md - 섹션 3.2
7
+ *
8
+ * Symbol.for()를 사용하여 전역 심볼 레지스트리에 등록
9
+ * → 모듈 간에도 동일한 심볼 공유 가능
10
+ */
11
+
12
+ // ============================================
13
+ // 메타데이터 심볼
14
+ // ============================================
15
+
16
+ /**
17
+ * MCP 서버 상태 메타데이터
18
+ * - connected, disconnected, error 등
19
+ */
20
+ export const MCP_SERVER_STATUS = Symbol.for("mandu:mcpServerStatus");
21
+
22
+ /**
23
+ * 검증 컨텍스트 메타데이터
24
+ * - 커스텀 검증 규칙, 에러 메시지 등
25
+ */
26
+ export const VALIDATION_CONTEXT = Symbol.for("mandu:validationContext");
27
+
28
+ /**
29
+ * 스키마 참조 메타데이터
30
+ * - 다른 스키마/함수 참조
31
+ */
32
+ export const SCHEMA_REFERENCE = Symbol.for("mandu:schemaReference");
33
+
34
+ /**
35
+ * 필드 소스 메타데이터
36
+ * - 필드 값의 출처 (env, config, default 등)
37
+ */
38
+ export const FIELD_SOURCE = Symbol.for("mandu:fieldSource");
39
+
40
+ /**
41
+ * 민감 정보 마커
42
+ * - 로깅/출력 시 마스킹 필요
43
+ */
44
+ export const SENSITIVE_FIELD = Symbol.for("mandu:sensitiveField");
45
+
46
+ /**
47
+ * 보호된 필드 마커
48
+ * - AI 에이전트 수정 불가
49
+ */
50
+ export const PROTECTED_FIELD = Symbol.for("mandu:protectedField");
51
+
52
+ /**
53
+ * 기본값 출처 메타데이터
54
+ * - 기본값이 어디서 왔는지 추적
55
+ */
56
+ export const DEFAULT_SOURCE = Symbol.for("mandu:defaultSource");
57
+
58
+ /**
59
+ * 런타임 주입 메타데이터
60
+ * - 런타임에 주입되는 값 표시
61
+ */
62
+ export const RUNTIME_INJECTED = Symbol.for("mandu:runtimeInjected");
63
+
64
+ // ============================================
65
+ // 타입 정의
66
+ // ============================================
67
+
68
+ export interface McpServerStatusMetadata {
69
+ status: "connected" | "disconnected" | "error" | "unknown";
70
+ lastChecked?: string;
71
+ error?: string;
72
+ }
73
+
74
+ export interface ValidationContextMetadata {
75
+ customMessage?: string;
76
+ severity?: "error" | "warning" | "info";
77
+ autoFix?: boolean;
78
+ }
79
+
80
+ export interface SchemaReferenceMetadata {
81
+ type: "mcpServer" | "function" | "schema" | "env";
82
+ name: string;
83
+ optional?: boolean;
84
+ }
85
+
86
+ export interface FieldSourceMetadata {
87
+ source: "env" | "config" | "default" | "computed" | "injected";
88
+ key?: string;
89
+ fallback?: unknown;
90
+ }
91
+
92
+ export interface SensitiveFieldMetadata {
93
+ redactIn: ("log" | "diff" | "snapshot")[];
94
+ mask?: string;
95
+ }
96
+
97
+ export interface ProtectedFieldMetadata {
98
+ reason: string;
99
+ allowedModifiers?: string[];
100
+ }
101
+
102
+ // ============================================
103
+ // 심볼-타입 매핑
104
+ // ============================================
105
+
106
+ export type SymbolMetadataMap = {
107
+ [MCP_SERVER_STATUS]: McpServerStatusMetadata;
108
+ [VALIDATION_CONTEXT]: ValidationContextMetadata;
109
+ [SCHEMA_REFERENCE]: SchemaReferenceMetadata;
110
+ [FIELD_SOURCE]: FieldSourceMetadata;
111
+ [SENSITIVE_FIELD]: SensitiveFieldMetadata;
112
+ [PROTECTED_FIELD]: ProtectedFieldMetadata;
113
+ [DEFAULT_SOURCE]: string;
114
+ [RUNTIME_INJECTED]: boolean;
115
+ };
116
+
117
+ // ============================================
118
+ // 심볼 목록 (순회용)
119
+ // ============================================
120
+
121
+ export const ALL_METADATA_SYMBOLS = [
122
+ MCP_SERVER_STATUS,
123
+ VALIDATION_CONTEXT,
124
+ SCHEMA_REFERENCE,
125
+ FIELD_SOURCE,
126
+ SENSITIVE_FIELD,
127
+ PROTECTED_FIELD,
128
+ DEFAULT_SOURCE,
129
+ RUNTIME_INJECTED,
130
+ ] as const;
131
+
132
+ /**
133
+ * 심볼이 mandu 메타데이터 심볼인지 확인
134
+ */
135
+ export function isManduMetadataSymbol(sym: symbol): boolean {
136
+ return ALL_METADATA_SYMBOLS.includes(sym as any);
137
+ }
138
+
139
+ /**
140
+ * 심볼 이름 가져오기 (디버깅용)
141
+ */
142
+ export function getSymbolName(sym: symbol): string | undefined {
143
+ return sym.description?.replace("mandu:", "");
144
+ }
@@ -10,18 +10,19 @@
10
10
 
11
11
  export * from "./schema";
12
12
  export * from "./types";
13
- export * from "./validator";
14
- export * from "./handler";
15
- export * from "./client";
16
- export * from "./normalize";
17
- export * from "./registry";
18
- export * from "./client-safe";
19
-
20
- import type { ContractDefinition, ContractInstance, ContractSchema } from "./schema";
21
- import type { ContractHandlers, RouteDefinition } from "./handler";
22
- import { defineHandler, defineRoute } from "./handler";
23
- import { createClient, contractFetch, type ClientOptions } from "./client";
24
- import { createClientContract } from "./client-safe";
13
+ export * from "./validator";
14
+ export * from "./handler";
15
+ export * from "./client";
16
+ export * from "./normalize";
17
+ export * from "./registry";
18
+ export * from "./client-safe";
19
+ export * from "./protection";
20
+
21
+ import type { ContractDefinition, ContractInstance, ContractSchema } from "./schema";
22
+ import type { ContractHandlers, RouteDefinition } from "./handler";
23
+ import { defineHandler, defineRoute } from "./handler";
24
+ import { createClient, contractFetch, type ClientOptions } from "./client";
25
+ import { createClientContract } from "./client-safe";
25
26
 
26
27
  /**
27
28
  * Create a Mandu API Contract
@@ -123,7 +124,7 @@ export function createContract<T extends ContractDefinition>(definition: T): T &
123
124
  * Contract-specific Mandu functions
124
125
  * Note: Use `ManduContract` to avoid conflict with other Mandu exports
125
126
  */
126
- export const ManduContract = {
127
+ export const ManduContract = {
127
128
  /**
128
129
  * Create a typed Contract
129
130
  * Contract 스키마 정의 및 타입 추론
@@ -165,9 +166,9 @@ export const ManduContract = {
165
166
  */
166
167
  route: defineRoute,
167
168
 
168
- /**
169
- * Create a type-safe API client from contract
170
- * Contract 기반 타입 안전 클라이언트 생성
169
+ /**
170
+ * Create a type-safe API client from contract
171
+ * Contract 기반 타입 안전 클라이언트 생성
171
172
  *
172
173
  * @example
173
174
  * ```typescript
@@ -180,13 +181,13 @@ export const ManduContract = {
180
181
  * const newUser = await client.POST({ body: { name: "Alice" } });
181
182
  * ```
182
183
  */
183
- client: createClient,
184
-
185
- /**
186
- * Create a client-safe contract
187
- * Client에서 노출할 스키마만 선택
188
- */
189
- clientContract: createClientContract,
184
+ client: createClient,
185
+
186
+ /**
187
+ * Create a client-safe contract
188
+ * Client에서 노출할 스키마만 선택
189
+ */
190
+ clientContract: createClientContract,
190
191
 
191
192
  /**
192
193
  * Single type-safe fetch call
@@ -199,8 +200,8 @@ export const ManduContract = {
199
200
  * });
200
201
  * ```
201
202
  */
202
- fetch: contractFetch,
203
- } as const;
203
+ fetch: contractFetch,
204
+ } as const;
204
205
 
205
206
  /**
206
207
  * Alias for backward compatibility within contract module
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Contract Protection - 보호 필드 시스템
3
+ *
4
+ * Symbol 메타데이터를 사용하여 Contract의 민감/보호 필드를 관리
5
+ *
6
+ * @see docs/plans/09_lockfile_integration_plan.md
7
+ */
8
+
9
+ import { z } from "zod";
10
+ import {
11
+ isSensitiveField,
12
+ isProtectedField,
13
+ getMetadata,
14
+ PROTECTED_FIELD,
15
+ SENSITIVE_FIELD,
16
+ type ProtectedFieldMetadata,
17
+ type SensitiveFieldMetadata,
18
+ } from "../config";
19
+
20
+ // ============================================
21
+ // 타입
22
+ // ============================================
23
+
24
+ export interface ProtectedFieldInfo {
25
+ /** 필드 경로 (예: "body.password") */
26
+ path: string;
27
+ /** 보호 이유 */
28
+ reason: string;
29
+ /** 수정 허용 대상 */
30
+ allowedModifiers: string[];
31
+ /** 민감 필드 여부 */
32
+ isSensitive: boolean;
33
+ }
34
+
35
+ export interface ProtectionViolation {
36
+ /** 위반 필드 경로 */
37
+ field: string;
38
+ /** 보호 이유 */
39
+ reason: string;
40
+ /** 오류 메시지 */
41
+ message: string;
42
+ /** 수정자 */
43
+ modifier: string;
44
+ }
45
+
46
+ export interface ContractChangeValidation {
47
+ /** 유효 여부 */
48
+ valid: boolean;
49
+ /** 보호 위반 목록 */
50
+ violations: ProtectionViolation[];
51
+ }
52
+
53
+ // ============================================
54
+ // 보호 필드 추출
55
+ // ============================================
56
+
57
+ /**
58
+ * Zod 스키마에서 보호된 필드 목록 추출
59
+ *
60
+ * @param schema Zod 스키마
61
+ * @param basePath 기본 경로 (재귀용)
62
+ * @returns 보호된 필드 정보 목록
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const schema = z.object({
67
+ * apiKey: sensitiveToken(),
68
+ * config: z.object({
69
+ * secret: protectedField("Security"),
70
+ * }),
71
+ * });
72
+ *
73
+ * const fields = extractProtectedFields(schema);
74
+ * // [
75
+ * // { path: "apiKey", reason: "Sensitive token...", ... },
76
+ * // { path: "config.secret", reason: "Security", ... },
77
+ * // ]
78
+ * ```
79
+ */
80
+ export function extractProtectedFields(
81
+ schema: z.ZodType,
82
+ basePath = ""
83
+ ): ProtectedFieldInfo[] {
84
+ const fields: ProtectedFieldInfo[] = [];
85
+
86
+ // ZodObject 처리
87
+ if (schema instanceof z.ZodObject) {
88
+ const shape = schema.shape as Record<string, z.ZodType>;
89
+
90
+ for (const [key, value] of Object.entries(shape)) {
91
+ const currentPath = basePath ? `${basePath}.${key}` : key;
92
+
93
+ // 보호된 필드 확인
94
+ if (isProtectedField(value)) {
95
+ const meta = getMetadata(value, PROTECTED_FIELD) as ProtectedFieldMetadata | undefined;
96
+ fields.push({
97
+ path: currentPath,
98
+ reason: meta?.reason ?? "Protected field",
99
+ allowedModifiers: meta?.allowedModifiers ?? ["human"],
100
+ isSensitive: isSensitiveField(value),
101
+ });
102
+ }
103
+ // 민감 필드도 보호 대상
104
+ else if (isSensitiveField(value)) {
105
+ const meta = getMetadata(value, SENSITIVE_FIELD) as SensitiveFieldMetadata | undefined;
106
+ fields.push({
107
+ path: currentPath,
108
+ reason: "Sensitive field - redacted in logs",
109
+ allowedModifiers: ["human"],
110
+ isSensitive: true,
111
+ });
112
+ }
113
+
114
+ // 중첩 객체 재귀 탐색
115
+ if (value instanceof z.ZodObject) {
116
+ const nested = extractProtectedFields(value, currentPath);
117
+ fields.push(...nested);
118
+ }
119
+ // Optional 처리
120
+ else if (value instanceof z.ZodOptional) {
121
+ const inner = value.unwrap();
122
+ if (inner instanceof z.ZodObject) {
123
+ const nested = extractProtectedFields(inner, currentPath);
124
+ fields.push(...nested);
125
+ }
126
+ }
127
+ // Nullable 처리
128
+ else if (value instanceof z.ZodNullable) {
129
+ const inner = value.unwrap();
130
+ if (inner instanceof z.ZodObject) {
131
+ const nested = extractProtectedFields(inner, currentPath);
132
+ fields.push(...nested);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return fields;
139
+ }
140
+
141
+ /**
142
+ * Contract 스키마 전체에서 보호 필드 추출
143
+ */
144
+ export function extractContractProtectedFields(
145
+ contract: { request?: unknown; response?: unknown }
146
+ ): {
147
+ request: ProtectedFieldInfo[];
148
+ response: ProtectedFieldInfo[];
149
+ } {
150
+ const request: ProtectedFieldInfo[] = [];
151
+ const response: ProtectedFieldInfo[] = [];
152
+
153
+ // Request 스키마 탐색
154
+ if (contract.request && typeof contract.request === "object") {
155
+ for (const [method, schema] of Object.entries(contract.request)) {
156
+ if (schema && typeof schema === "object") {
157
+ const methodSchema = schema as Record<string, z.ZodType>;
158
+
159
+ // body, query, params, headers
160
+ for (const [part, partSchema] of Object.entries(methodSchema)) {
161
+ if (partSchema instanceof z.ZodType) {
162
+ const fields = extractProtectedFields(partSchema, `${method}.${part}`);
163
+ request.push(...fields);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Response 스키마 탐색
171
+ if (contract.response && typeof contract.response === "object") {
172
+ for (const [status, schema] of Object.entries(contract.response)) {
173
+ if (schema instanceof z.ZodType) {
174
+ const fields = extractProtectedFields(schema, `${status}`);
175
+ response.push(...fields);
176
+ }
177
+ }
178
+ }
179
+
180
+ return { request, response };
181
+ }
182
+
183
+ // ============================================
184
+ // 변경 검증
185
+ // ============================================
186
+
187
+ /**
188
+ * 객체에서 경로로 값 가져오기
189
+ */
190
+ function getValueByPath(obj: unknown, path: string): unknown {
191
+ const parts = path.split(".");
192
+ let current: unknown = obj;
193
+
194
+ for (const part of parts) {
195
+ if (current === null || current === undefined) {
196
+ return undefined;
197
+ }
198
+ if (typeof current !== "object") {
199
+ return undefined;
200
+ }
201
+ current = (current as Record<string, unknown>)[part];
202
+ }
203
+
204
+ return current;
205
+ }
206
+
207
+ /**
208
+ * Contract 변경 시 보호 필드 검증
209
+ *
210
+ * @param oldSchema 이전 스키마
211
+ * @param newSchema 새 스키마
212
+ * @param modifier 수정자 ("human" | "ai")
213
+ * @returns 검증 결과
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const validation = validateContractChanges(
218
+ * oldContract.request,
219
+ * newContract.request,
220
+ * "ai"
221
+ * );
222
+ *
223
+ * if (!validation.valid) {
224
+ * console.error("AI가 보호된 필드를 수정하려고 합니다:", validation.violations);
225
+ * }
226
+ * ```
227
+ */
228
+ export function validateContractChanges(
229
+ oldSchema: z.ZodType,
230
+ newSchema: z.ZodType,
231
+ modifier: "human" | "ai"
232
+ ): ContractChangeValidation {
233
+ const violations: ProtectionViolation[] = [];
234
+
235
+ // 이전 스키마에서 보호 필드 추출
236
+ const protectedFields = extractProtectedFields(oldSchema);
237
+
238
+ for (const field of protectedFields) {
239
+ // 수정 권한 확인
240
+ if (!field.allowedModifiers.includes(modifier)) {
241
+ // 스키마 구조 변경 감지 (간단한 비교)
242
+ const oldValue = getSchemaDefinition(oldSchema, field.path);
243
+ const newValue = getSchemaDefinition(newSchema, field.path);
244
+
245
+ // 구조가 변경되었는지 확인
246
+ if (hasSchemaChanged(oldValue, newValue)) {
247
+ violations.push({
248
+ field: field.path,
249
+ reason: field.reason,
250
+ message: `${modifier}는 보호된 필드 '${field.path}'를 수정할 수 없습니다`,
251
+ modifier,
252
+ });
253
+ }
254
+ }
255
+ }
256
+
257
+ return {
258
+ valid: violations.length === 0,
259
+ violations,
260
+ };
261
+ }
262
+
263
+ /**
264
+ * 스키마에서 경로로 정의 가져오기
265
+ */
266
+ function getSchemaDefinition(schema: z.ZodType, path: string): z.ZodType | undefined {
267
+ const parts = path.split(".");
268
+ let current: z.ZodType | undefined = schema;
269
+
270
+ for (const part of parts) {
271
+ if (!current) return undefined;
272
+
273
+ if (current instanceof z.ZodObject) {
274
+ current = current.shape[part] as z.ZodType | undefined;
275
+ } else if (current instanceof z.ZodOptional) {
276
+ current = current.unwrap();
277
+ if (current instanceof z.ZodObject) {
278
+ current = current.shape[part] as z.ZodType | undefined;
279
+ }
280
+ } else {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ return current;
286
+ }
287
+
288
+ /**
289
+ * 스키마가 변경되었는지 확인 (간단한 비교)
290
+ */
291
+ function hasSchemaChanged(
292
+ oldSchema: z.ZodType | undefined,
293
+ newSchema: z.ZodType | undefined
294
+ ): boolean {
295
+ // 둘 다 없으면 변경 없음
296
+ if (!oldSchema && !newSchema) return false;
297
+
298
+ // 하나만 있으면 변경됨
299
+ if (!oldSchema || !newSchema) return true;
300
+
301
+ // 타입이 다르면 변경됨
302
+ const oldTypeName = (oldSchema._def as { typeName?: string }).typeName;
303
+ const newTypeName = (newSchema._def as { typeName?: string }).typeName;
304
+ if (oldTypeName !== newTypeName) {
305
+ return true;
306
+ }
307
+
308
+ // ZodObject의 경우 shape 키 비교
309
+ if (oldSchema instanceof z.ZodObject && newSchema instanceof z.ZodObject) {
310
+ const oldKeys = Object.keys(oldSchema.shape);
311
+ const newKeys = Object.keys(newSchema.shape);
312
+
313
+ if (oldKeys.length !== newKeys.length) return true;
314
+
315
+ for (const key of oldKeys) {
316
+ if (!newKeys.includes(key)) return true;
317
+ }
318
+ }
319
+
320
+ return false;
321
+ }
322
+
323
+ // ============================================
324
+ // 포맷팅
325
+ // ============================================
326
+
327
+ /**
328
+ * 보호 필드 목록을 문자열로 포맷
329
+ */
330
+ export function formatProtectedFields(fields: ProtectedFieldInfo[]): string {
331
+ if (fields.length === 0) {
332
+ return "보호된 필드 없음";
333
+ }
334
+
335
+ const lines: string[] = ["보호된 필드:"];
336
+
337
+ for (const field of fields) {
338
+ const sensitive = field.isSensitive ? " 🔐" : "";
339
+ lines.push(` - ${field.path}${sensitive}`);
340
+ lines.push(` 이유: ${field.reason}`);
341
+ lines.push(` 수정 가능: ${field.allowedModifiers.join(", ")}`);
342
+ }
343
+
344
+ return lines.join("\n");
345
+ }
346
+
347
+ /**
348
+ * 보호 위반 목록을 문자열로 포맷
349
+ */
350
+ export function formatProtectionViolations(violations: ProtectionViolation[]): string {
351
+ if (violations.length === 0) {
352
+ return "위반 없음";
353
+ }
354
+
355
+ const lines: string[] = ["🛑 보호 필드 위반:"];
356
+
357
+ for (const v of violations) {
358
+ lines.push(` - ${v.field}`);
359
+ lines.push(` ${v.message}`);
360
+ lines.push(` 이유: ${v.reason}`);
361
+ }
362
+
363
+ return lines.join("\n");
364
+ }