From a492d0f76556b0e0e05c57fa15943efc8b351b24 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:27:58 +0900 Subject: [PATCH] fix(core): reject identity data that omits the target agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 페르소나 schema를 파생할 때 profileFor가 "정체성 자료를 주지 않음"과 "자료는 줬는데 대상 개인이 없음"을 모두 null로 접었다. 그래서 다른 개인의 프로필만 든 자료나 빈 목록을 넘기면 lockedFor가 전 축을 잠금 해제로 판정하고, digest까지 자료 미제공 경우와 같아져 하류에서 두 상황을 구분할 수 없었다. 제공된 자료는 참여자 전원에 대한 주장이므로 대상이 빠진 것은 정책 부재가 아니라 자료 결함이다. LIFE 경로는 이미 같은 조건을 거부한다 (world/views.ts, world/growth.ts, world/autonomy-rules.ts). persona-schema만 예외였다. 이제 같은 방식으로 거부한다. identity가 null인 기존 입력의 허용 범위는 그대로 두고 반환 타입의 null도 유지한다. 018 작성 계약의 불완전 입력 표에서 자료 미제공은 허용이다. 회귀는 누락을 잠금 해제의 정상 결과로 기대하던 단언을 교체하고, 빈 목록과 타 에이전트 자료 두 형태의 거부, 그리고 부재·manual 전체 잠금·선택 잠금 세 경우의 digest가 서로 다른지를 검사한다. 쌍 두 개만 비교하면 manual이 부재와 같아지는 변형을 놓치므로 세 값을 한 번에 비교한다. Refs: JUN-28 --- .../lina-core/src/agents/persona-schema.ts | 6 +- .../lina-core/test/persona-schema.test.ts | 72 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/packages/lina-core/src/agents/persona-schema.ts b/packages/lina-core/src/agents/persona-schema.ts index df9bcec..e5781a9 100644 --- a/packages/lina-core/src/agents/persona-schema.ts +++ b/packages/lina-core/src/agents/persona-schema.ts @@ -283,7 +283,11 @@ function profileFor( if (identity === null) return null; for (const profile of parseIdentityPolicy(identity).profiles) if (profile.agentId === agentId) return profile; - return null; + // A supplied snapshot is a claim about every participant, so a missing target + // is bad data, not an absent policy. Collapsing it to null would unlock every + // axis and produce the same digest as identity: null. The LIFE paths already + // reject this: world/views.ts, world/growth.ts, world/autonomy-rules.ts. + throw Error("Missing persona identity policy"); } function lockedFor( diff --git a/packages/lina-core/test/persona-schema.test.ts b/packages/lina-core/test/persona-schema.test.ts index e591a4e..63a4fbd 100644 --- a/packages/lina-core/test/persona-schema.test.ts +++ b/packages/lina-core/test/persona-schema.test.ts @@ -256,27 +256,61 @@ test("derivation digest is idempotent and ignores v2-only identity fields", () = const fromV2 = derive(identityV2); expect(fromV1.digest).toBe(fromV2.digest); expect(fromV1.digest).toBe(first.digest); +}); - const missing = personaSchemaFromLifeDefinition({ - agentId: "lina", - revision: 1, - definition, - identity: { - version: 2 as const, - profiles: [ - { - ...lockProfile, - agentId: "mira", - evolution: "manual", - personalBehavior: null, - sourceStamp: null, - }, - ], - }, +test("supplied identity without the target agent is rejected", () => { + const before = derive(); + const otherAgent = { + ...lockProfile, + agentId: "mira", + evolution: "manual" as const, + personalBehavior: null, + sourceStamp: null, + }; + for (const profiles of [[], [otherAgent]]) { + expect(() => + personaSchemaFromLifeDefinition({ + agentId: "lina", + revision: 1, + definition, + identity: { version: 2 as const, profiles }, + }), + ).toThrow("Missing persona identity policy"); + } + expect(derive()).toEqual(before); +}); + +test("absent policy, manual lock and selective lock stay separate cases", () => { + const absent = derive(null); + expect(absent.sourceIdentity).toBeNull(); + expect(absent.dimensions.every((row) => row.locked === false)).toBe(true); + + const selective = derive(); + expect(selective.sourceIdentity).toEqual({ profileRevision: 4 }); + expect(selective.dimensions.map((row) => [row.id, row.locked])).toEqual([ + ["curiosity", false], + ["warmth", true], + ["tea", false], + ["trust", false], + ]); + expect(selective.digest).not.toBe(absent.digest); + + const manual = derive({ + version: 2 as const, + profiles: [ + { + ...lockProfile, + evolution: "manual" as const, + lockedTraitIds: [], + personalBehavior: null, + sourceStamp: null, + }, + ], }); - expect(missing.sourceIdentity).toBeNull(); - expect(missing.dimensions.every((row) => row.locked === false)).toBe(true); - expect(missing.digest).toBe(unlocked.digest); + expect(manual.dimensions.every((row) => row.locked === true)).toBe(true); + expect(new Set([absent.digest, selective.digest, manual.digest]).size).toBe( + 3, + ); }); test.each([identityV1, identityV2])(