Skip to content

feat: 006 AI advisory output authority 격리 구현 - #287

Open
goodtu02 wants to merge 1 commit into
devfrom
feat/286-006-ai-output-authority
Open

feat: 006 AI advisory output authority 격리 구현#287
goodtu02 wants to merge 1 commit into
devfrom
feat/286-006-ai-output-authority

Conversation

@goodtu02

@goodtu02 goodtu02 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🎋 작업 중인 브랜치 및 이슈

🔎 주요 변경 사항

  • sast-ai-advisory-authority-proof-v1 shared 계약으로 exact intent, advisory/handoff binding, authoritative before/after snapshot 및 zero-authority proof digest를 정의했습니다.
  • proof API 입력을 tenant/advisory ID로 제한하고 caller-provided finding state, severity, lifecycle, waiver, suppression, policy action 및 unknown authority field를 거부합니다.
  • T043 advisory/handoff와 T037 occurrence/lifecycle을 tenant/repository/scan/occurrence/finding/fingerprint/request digest에 다시 결속합니다.
  • serializable transaction에서 authoritative finding, lifecycle, policy decision, waiver 및 suppression 상태의 전후 digest가 동일할 때 proof row만 생성합니다.
  • exact replay 시 현재 authoritative 상태를 다시 캡처해 drift를 fail closed 처리하고, AI plane의 authoritative create/update/delete 권한을 제거했습니다.
  • proof ledger에는 bounded reference/count/digest와 false authority bit만 보존하며 prompt, rationale, evidence, source, secret 또는 policy payload를 저장하지 않습니다.
  • policy 입력은 검증된 advisory/proof reference와 advisoryOnly=true만 허용하고 enforcement/reason/block/ticket 결과는 deterministic finding/coverage 입력으로만 계산합니다.
  • waiver/suppression lifecycle API에 exact key allowlist를 적용해 advisory/proof/authority injection을 차단했습니다.
  • Prisma schema/migration 및 mandatory online-schema 경로에 immutable proof ledger, check/unique/index/composite FK/trigger를 추가했습니다.
  • cross-tenant, binding drift, state drift, replay conflict, suggestedAction/authority injection, source leakage 및 zero authoritative write 회귀 테스트를 추가했습니다.
  • 006 contracts/data-model/research/threat-model/quality-gates/spec/plan/quickstart/tasks와 active-feature/ontology guard를 T044 완료 및 T045 다음 진입점으로 동기화했습니다.

✅ 컨벤션 확인

  • 브랜치명이 type/issue-number-short-feature 형식을 따르나요?
  • 이슈 제목과 PR 제목을 동일하게 작성했나요?
  • 커밋 메시지가 <type>: <description> 형식을 따르나요?

Check List

  • Assignees 등록을 하였나요?
  • 라벨(Label) 등록을 하였나요?
  • PR 머지 전 반드시 CI가 정상적으로 작동하는지 확인했나요?

검증

  • 전체 lint 통과
  • shared/API/AI/web typecheck 통과
  • shared/API/AI/web production build 통과
  • shared 107/107 tests 통과
  • API 103 suites / 663 tests 통과
  • T044 persistence 최종 focused 5/5 tests 통과
  • AI runtime 16/16 tests 통과
  • GitHub Actions guards 35/35 tests 통과
  • runtime guard 1/1 test 통과
  • web 42/42 assertions 통과
  • git diff --check 통과
  • 원격 feature tree와 검증된 로컬 commit tree SHA 일치

006 진행 상태

  • T044 완료
  • 다음 작업: T045 telemetry/metrics boundary 구현

Closes #286

Summary by CodeRabbit

  • New Features
    • Added immutable authority proofs for AI advisory results, recording scoped references, state digests, counts, timestamps, and audit flags.
    • Added an endpoint to create advisory authority proofs.
    • Added tenant- and finding-bound policy references with fail-closed verification.
    • Ensured AI advisory output cannot modify findings, lifecycle, policy, waiver, suppression, or publication decisions.
  • Bug Fixes
    • Strengthened waiver, suppression, and policy payload validation by rejecting unknown or unauthorized fields.
  • Documentation
    • Documented the completed advisory authority-proof safeguards and quality requirements.

@goodtu02 goodtu02 self-assigned this Aug 11, 2026
@goodtu02 goodtu02 added the ✨ feat 새로운 기능 추가 label Aug 11, 2026 — with ChatGPT Codex Connector
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change implements T044 AI advisory authority proofs. It adds shared proof contracts, immutable Prisma persistence, transactional state verification, guarded API creation, display-only policy references, strict lifecycle payload validation, regression tests, and updated T044 design and completion checks.

Changes

AI advisory authority-proof isolation

Layer / File(s) Summary
Proof contracts and immutable storage
packages/shared/src/types/*, apps/api/prisma/schema.prisma, apps/api/prisma/migrations/..., apps/api/scripts/apply-online-sast-runtime-schema.mjs
Defines versioned proof and policy-reference contracts. Adds bounded digest validation, zero-authority flags, immutable proof storage, scoped uniqueness, foreign keys, indexes, and update/delete rejection triggers.
Transactional proof creation and verification
apps/api/src/ai-plane/sast-ai-advisory-authority.store.ts, apps/api/src/ai-plane/ai-advisory-authority.service.ts
Loads advisory context and authoritative state, computes matching before/after digests, persists one proof in a serializable transaction, supports exact replay, and verifies tenant- and finding-bound references.
API, policy, and lifecycle boundaries
apps/api/src/ai-plane/*, apps/api/src/policy/*
Adds guarded proof creation and fail-closed reference verification. Policy evaluation uses verified advisory visibility only. Waiver and suppression payloads now use exact operation-specific key sets.
Validation and T044 completion coverage
apps/api/test/*, packages/shared/test/*, specs/006-production-sast-runtime-design/*, test/github-actions/*
Adds tests for immutable proof behavior, state drift, replay, cross-tenant references, unauthorized fields, zero authoritative writes, lifecycle rejection, schema safeguards, and T044 completion with T045 as the next task.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AiAdvisoryController
  participant AiAdvisoryAuthorityService
  participant PrismaSastAiAdvisoryAuthorityStore
  participant PrismaDatabase
  Client->>AiAdvisoryController: POST authority proof intent
  AiAdvisoryController->>AiAdvisoryAuthorityService: createProof(intent)
  AiAdvisoryAuthorityService->>PrismaSastAiAdvisoryAuthorityStore: createProof(tenantId, advisoryId, verifiedAt)
  PrismaSastAiAdvisoryAuthorityStore->>PrismaDatabase: read advisory and authoritative state
  PrismaDatabase-->>PrismaSastAiAdvisoryAuthorityStore: scoped rows and state digests
  PrismaSastAiAdvisoryAuthorityStore->>PrismaDatabase: insert immutable proof
  PrismaDatabase-->>PrismaSastAiAdvisoryAuthorityStore: persisted proof or replay
  PrismaSastAiAdvisoryAuthorityStore-->>AiAdvisoryAuthorityService: persisted proof
  AiAdvisoryAuthorityService-->>AiAdvisoryController: proof and policy reference
  AiAdvisoryController-->>Client: authority-proof response
Loading

Possibly related PRs

  • AigisAI/AegisAI_v2#285: Adds the advisory handoff model and flow used by the authority-proof scope.
  • AigisAI/AegisAI_v2#269: Provides fingerprinted normalized findings used by proof state capture and verification.
  • AigisAI/AegisAI_v2#146: Introduces the AI advisory controller, service, shared contracts, and tests extended by this change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation satisfies the authority-proof, zero-authority, validation, immutability, replay, tenant-binding, and regression-test objectives in [#286].
Out of Scope Changes check ✅ Passed The schema, services, policy changes, tests, and documentation directly support the linked issue objectives; no unrelated changes are evident.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: isolating AI advisory output authority through the T044 implementation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13593d54a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +385 to +389
tx.policyDecision.findMany({
where: {
tenantId: scope.tenantId,
findingId: scope.normalizedFindingId
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Snapshot the state used by policy lifecycle services

When policy, waiver, or suppression changes are made through the current APIs, these Prisma queries cannot observe them: PolicyEngineService and PolicyLifecycleService store their authoritative objects only in in-memory arrays and never populate the tables queried by captureAuthorityState. Consequently, a concurrent evaluation, waiver update, or suppression creation can leave both database snapshots identical and produce a proof claiming zero authoritative writes even though application-visible state changed. Persist those services through the same database transaction model, or snapshot their actual stores before treating this proof as valid.

AGENTS.md reference: AGENTS.md:L79-L79

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (8)
apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts (4)

527-547: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the zero-authority columns from the validated proof instead of repeating literals.

buildSastAiAdvisoryAuthorityProof already validates proof.authority and proof.audit against the shared ZERO_AUTHORITY and AUDIT constants. The literal list here duplicates that contract. If the shared contract gains a field, this list drifts silently and the new column is written with a default or omitted.

Spreading the validated objects keeps the same guarantee and removes the duplication.

♻️ Proposed change
-    findingCreateAuthority: false,
-    findingStatusMutationAuthority: false,
-    findingSeverityMutationAuthority: false,
-    lifecycleMutationAuthority: false,
-    waiverMutationAuthority: false,
-    suppressionMutationAuthority: false,
-    policyOverrideAuthority: false,
-    blockDecisionAuthority: false,
-    publicationAuthority: false,
-    scmWriteAuthority: false,
-    advisoryOnly: true,
-    proofLedgerWritten: true,
-    authoritativeFindingWritten: false,
-    lifecycleStateWritten: false,
-    policyDecisionWritten: false,
-    waiverWritten: false,
-    suppressionWritten: false,
-    callerAuthorityFieldsAccepted: false,
-    advisoryContentStored: false,
-    sourceContentStored: false,
-    secretValueStored: false,
+    ...proof.authority,
+    ...proof.audit,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 527 - 547, Update the authority result construction in
buildSastAiAdvisoryAuthorityProof to spread the validated proof.authority and
proof.audit objects instead of repeating individual zero-authority and audit
field literals. Preserve the existing derived fields and ensure all shared
contract fields are persisted automatically.

593-595: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared version constant instead of a literal.

Line 594 hardcodes 'sast-ai-advisory-authority-proof-v1'. The shared package exports SAST_AI_ADVISORY_AUTHORITY_PROOF_VERSION, and buildSastAiAdvisoryAuthorityProof uses that constant. If the shared contract version changes, this literal does not change with it, and isSastAiAdvisoryAuthorityProofShapeValid then rejects every stored proof at line 647.

♻️ Proposed change
 import {
   SAST_AI_ADVISORY_AUTHORITY_LIMITS,
+  SAST_AI_ADVISORY_AUTHORITY_PROOF_VERSION,
   buildSastAiAdvisoryAuthorityProof,
   const proof: SastAiAdvisoryAuthorityProof = {
-    version: 'sast-ai-advisory-authority-proof-v1',
+    version: SAST_AI_ADVISORY_AUTHORITY_PROOF_VERSION,
     proofId: row.id,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 593 - 595, Update the proof construction in the visible proof-mapping
logic to set version from the shared SAST_AI_ADVISORY_AUTHORITY_PROOF_VERSION
constant instead of the hardcoded string, matching
buildSastAiAdvisoryAuthorityProof and keeping stored proofs compatible with
isSastAiAdvisoryAuthorityProofShapeValid.

444-457: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report a missing lifecycle state separately from an over-broad state.

Line 447 treats lifecycleStates.length !== 1 as STATE_TOO_BROAD. When the count is zero, the cause is a missing lifecycle row, not an over-broad state. The shared contract pins lifecycleStateCount: 1, so zero must still fail. Only the reported category is wrong, and it misdirects triage.

♻️ Proposed change
+  if (lifecycleStates.length === 0) {
+    throw new SastAiAdvisoryAuthorityPersistenceError(
+      'CONTEXT_DRIFT'
+    );
+  }
   if (
     findings.length >
       SAST_AI_ADVISORY_AUTHORITY_LIMITS.maximumNormalizedFindings ||
-    lifecycleStates.length !== 1 ||
+    lifecycleStates.length > 1 ||
     policyDecisions.length >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 444 - 457, Update the validation around lifecycleStates in the persistence
flow to distinguish zero lifecycle states from multiple states: throw the
contract’s missing-lifecycle-state error when lifecycleStates.length is zero,
while retaining STATE_TOO_BROAD only for counts above one and preserving the
existing limit checks.

682-684: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The canonical digester is duplicated in two files. Both files define an identical digest function and pass it to the shared builders as the SastAiAdvisoryAuthorityCanonicalDigester. The store uses it to compute and persist proofDigest. The service uses it to validate the same proof and to build the policy reference. If the two definitions ever diverge, the service rejects every persisted proof and verifyPolicyReference fails for all requests. The digester belongs to the proof contract surface, so it should live beside the contract.

  • apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts#L682-L684: remove the local digest function and import the shared canonical digester.
  • apps/api/src/ai-plane/ai-advisory-authority.service.ts#L121-L123: remove the local digest function and import the same shared canonical digester.

As per coding guidelines: "Place shared API contracts in packages/shared."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 682 - 684, The canonical digest implementation is duplicated across the
store and service. Move the shared `digest` implementation to the proof contract
surface in `packages/shared`, then remove the local functions and import that
canonical digester in
`apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` lines 682-684
and `apps/api/src/ai-plane/ai-advisory-authority.service.ts` lines 121-123,
preserving the existing `SastAiAdvisoryAuthorityCanonicalDigester` usage in both
files.

Source: Coding guidelines

apps/api/src/ai-plane/ai-advisory-authority.service.ts (2)

73-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the failure category before returning false.

The empty catch fails closed, which is correct for an authority check. It also silently hides database outages from the policy path. The class already has a logger, and safeErrorCategory produces a leakage-safe value.

♻️ Proposed change
     try {
       return await this.store.verifyPolicyReference(input);
-    } catch {
+    } catch (error) {
+      this.logger.error(
+        `AI advisory policy reference verification failed (${safeErrorCategory(error)}).`
+      );
       return false;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/ai-advisory-authority.service.ts` around lines 73 - 77,
Update the catch block around store.verifyPolicyReference in the policy
authority check to log the failure category using the class logger and
safeErrorCategory, then continue returning false to preserve fail-closed
behavior. Keep the logged value leakage-safe and avoid exposing raw error
details.

53-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Map persistence categories to distinct HTTP statuses.

The catch block converts every failure into a 404. That includes REPLAY_CONFLICT, STATE_DRIFT, serialization conflicts after the retry budget, and database connectivity failures. A caller cannot distinguish "no such advisory" from "retry later", and a database outage is reported as a normal 404, which hides the incident from status-code-based alerting.

Keep the opaque message to avoid leakage. Select the status from safeErrorCategory or from the persistence error category.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/ai-advisory-authority.service.ts` around lines 53 - 58,
Update the catch block in the AI advisory authority proof flow to map
safeErrorCategory(error) or the persistence error category to distinct HTTP
statuses: preserve 404 only for missing advisories, use conflict semantics for
REPLAY_CONFLICT and STATE_DRIFT, and a retryable server/unavailable status for
exhausted serialization conflicts or database connectivity failures. Keep the
opaque error message and existing logging while replacing the unconditional
unavailable() response.
apps/api/prisma/schema.prisma (1)

1735-1784: 🔒 Security & Privacy | 🔵 Trivial

Plan the tenant purge path for the immutable proof ledger.

tenant, repositoryBinding, scanRequest, handoff, advisory, occurrence, and normalizedFinding all use onDelete: Restrict, and the migration adds a BEFORE DELETE trigger. Tenant deletion and scan-data retention jobs will now fail once a proof row exists. Document the privileged maintenance procedure that disables the trigger, and add an operational runbook step for tenant offboarding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/prisma/schema.prisma` around lines 1735 - 1784, Document the
privileged maintenance procedure for purging SastAiAdvisoryAuthorityProof rows,
including temporarily disabling and re-enabling the BEFORE DELETE trigger while
preserving referential integrity. Add an operational runbook step for tenant
offboarding and scan-data retention that invokes this procedure before deleting
restricted parent records, referencing SastAiAdvisoryAuthorityProof and its
tenantId field.
packages/shared/test/sast-ai-advisory-authority.test.mjs (1)

82-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative cases for digest tampering and bounded sets.

The tests cover state drift, non-exact intent, and authority tampering. Two contract invariants remain untested:

  • A mutated proofId, scope, or proofDigest must fail isSastAiAdvisoryAuthorityProofShapeValid. This proves the digest binding, not only the authority bits.
  • buildSastAiAdvisoryAuthorityStateSnapshot must return null for unsorted or duplicate digest arrays, for an empty normalizedFindingDigests, and when targetFindingDigest is absent from normalizedFindingDigests.

These paths carry the zero-authority guarantee described in the PR objectives.

🧪 Proposed additional cases
+test('T044 rejects tampered proof identity and unsorted snapshot sets', () => {
+  const snapshot = authoritySnapshot();
+  const proof = buildSastAiAdvisoryAuthorityProof({
+    scope: authorityScope(),
+    before: snapshot,
+    after: snapshot,
+    verifiedAt: '2026-08-11T05:30:00.000Z',
+    digestCanonical: digest
+  });
+  assert.ok(proof);
+  assert.equal(
+    isSastAiAdvisoryAuthorityProofShapeValid(
+      { ...proof, proofDigest: digest('tampered') },
+      digest
+    ),
+    false
+  );
+  assert.equal(
+    isSastAiAdvisoryAuthorityProofShapeValid(
+      { ...proof, scope: { ...proof.scope, tenantId: 'tenant-other' } },
+      digest
+    ),
+    false
+  );
+  const unsorted = [digest('finding-b'), digest('finding-a')].sort().reverse();
+  assert.equal(
+    buildSastAiAdvisoryAuthorityStateSnapshot({
+      normalizedFindingDigests: unsorted,
+      targetFindingDigest: unsorted[0],
+      lifecycleStateDigests: [digest('lifecycle-open')],
+      policyDecisionDigests: [],
+      waiverDigests: [],
+      suppressionDigests: [],
+      digestCanonical: digest
+    }),
+    null
+  );
+  assert.equal(
+    buildSastAiAdvisoryAuthorityStateSnapshot({
+      normalizedFindingDigests: [digest('finding-a')],
+      targetFindingDigest: digest('finding-missing'),
+      lifecycleStateDigests: [digest('lifecycle-open')],
+      policyDecisionDigests: [],
+      waiverDigests: [],
+      suppressionDigests: [],
+      digestCanonical: digest
+    }),
+    null
+  );
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/test/sast-ai-advisory-authority.test.mjs` around lines 82 -
133, Extend the T044 test coverage with negative cases proving digest binding:
mutate proofId, scope, and proofDigest independently and assert
isSastAiAdvisoryAuthorityProofShapeValid returns false. Also assert
buildSastAiAdvisoryAuthorityStateSnapshot returns null for unsorted or duplicate
digest arrays, an empty normalizedFindingDigests array, and a
targetFindingDigest not present in that array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql`:
- Around line 144-166: Replace ON UPDATE CASCADE with ON UPDATE RESTRICT for all
five SastAiAdvisoryAuthorityProof foreign keys in
apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql
lines 144-166. Apply the same change to the three corresponding constraint
definitions in apps/api/scripts/apply-online-sast-runtime-schema.mjs lines
664-684, and align the SastAiAdvisoryAuthorityProof relation attributes in
apps/api/prisma/schema.prisma.

In `@apps/api/src/ai-plane/ai-advisory.controller.ts`:
- Around line 26-32: Update createAuthorityProof in the AI advisory controller
so it does not trust body.tenantId after InternalServiceGuard authentication.
Obtain the caller’s tenant from tenant-scoped internal credentials or another
trusted authenticated binding, validate or override the request tenant
accordingly, and pass only that authorized tenant to
authorityService.createProof.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts`:
- Around line 140-145: Remove the redundant post-write captureAuthorityState
call and its stateDigest comparison from the transaction callback, while
retaining the initial snapshot and existing serializable transaction protection.
Ensure proof creation no longer performs the unnecessary second state read or
claims in-transaction verification.
- Around line 109-120: Replace the ineffective current-state comparison in the
existing-proof branch of runSerializable with a database fence, version
predicate, or lock that covers the full authoritative scope while keeping proof
insertion atomic. Preserve replay drift rejection and only return replayed from
replayProof when the stored authoritative state still matches; otherwise throw
SastAiAdvisoryAuthorityPersistenceError with STATE_DRIFT.

In `@apps/api/test/ai-plane/ai-advisory-authority.service.e2e-spec.ts`:
- Around line 74-96: Extend the test for
AiAdvisoryAuthorityService.verifyPolicyReference to cover persisted-reference
failures: configure store.verifyPolicyReference to return false and separately
to reject or otherwise represent an unavailable result, then assert both service
calls resolve to false. Preserve the existing valid and locally invalid
reference assertions and update call-count expectations to account for the added
cases.

In `@apps/api/test/ai-plane/sast-ai-advisory-authority-persistence.e2e-spec.ts`:
- Around line 113-146: Extend the test “verifies only a tenant and finding-bound
exact policy reference” to assert verifyPolicyReference returns false for a
foreign normalizedFindingId and for a tampered authority proof reference,
including changed authorityProofDigest or advisoryId. Keep the existing valid
and foreign-tenant assertions unchanged.
- Around line 77-111: Update the transaction fixture and the createProof test to
capture the second $transaction options argument. Assert that runSerializable
invokes $transaction with isolationLevel set to
Prisma.TransactionIsolationLevel.Serializable, plus the expected maxWait and
timeout values.

---

Nitpick comments:
In `@apps/api/prisma/schema.prisma`:
- Around line 1735-1784: Document the privileged maintenance procedure for
purging SastAiAdvisoryAuthorityProof rows, including temporarily disabling and
re-enabling the BEFORE DELETE trigger while preserving referential integrity.
Add an operational runbook step for tenant offboarding and scan-data retention
that invokes this procedure before deleting restricted parent records,
referencing SastAiAdvisoryAuthorityProof and its tenantId field.

In `@apps/api/src/ai-plane/ai-advisory-authority.service.ts`:
- Around line 73-77: Update the catch block around store.verifyPolicyReference
in the policy authority check to log the failure category using the class logger
and safeErrorCategory, then continue returning false to preserve fail-closed
behavior. Keep the logged value leakage-safe and avoid exposing raw error
details.
- Around line 53-58: Update the catch block in the AI advisory authority proof
flow to map safeErrorCategory(error) or the persistence error category to
distinct HTTP statuses: preserve 404 only for missing advisories, use conflict
semantics for REPLAY_CONFLICT and STATE_DRIFT, and a retryable
server/unavailable status for exhausted serialization conflicts or database
connectivity failures. Keep the opaque error message and existing logging while
replacing the unconditional unavailable() response.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts`:
- Around line 527-547: Update the authority result construction in
buildSastAiAdvisoryAuthorityProof to spread the validated proof.authority and
proof.audit objects instead of repeating individual zero-authority and audit
field literals. Preserve the existing derived fields and ensure all shared
contract fields are persisted automatically.
- Around line 593-595: Update the proof construction in the visible
proof-mapping logic to set version from the shared
SAST_AI_ADVISORY_AUTHORITY_PROOF_VERSION constant instead of the hardcoded
string, matching buildSastAiAdvisoryAuthorityProof and keeping stored proofs
compatible with isSastAiAdvisoryAuthorityProofShapeValid.
- Around line 444-457: Update the validation around lifecycleStates in the
persistence flow to distinguish zero lifecycle states from multiple states:
throw the contract’s missing-lifecycle-state error when lifecycleStates.length
is zero, while retaining STATE_TOO_BROAD only for counts above one and
preserving the existing limit checks.
- Around line 682-684: The canonical digest implementation is duplicated across
the store and service. Move the shared `digest` implementation to the proof
contract surface in `packages/shared`, then remove the local functions and
import that canonical digester in
`apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` lines 682-684
and `apps/api/src/ai-plane/ai-advisory-authority.service.ts` lines 121-123,
preserving the existing `SastAiAdvisoryAuthorityCanonicalDigester` usage in both
files.

In `@packages/shared/test/sast-ai-advisory-authority.test.mjs`:
- Around line 82-133: Extend the T044 test coverage with negative cases proving
digest binding: mutate proofId, scope, and proofDigest independently and assert
isSastAiAdvisoryAuthorityProofShapeValid returns false. Also assert
buildSastAiAdvisoryAuthorityStateSnapshot returns null for unsorted or duplicate
digest arrays, an empty normalizedFindingDigests array, and a
targetFindingDigest not present in that array.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5573f27f-b1c3-42f8-b920-7047371cf71b

📥 Commits

Reviewing files that changed from the base of the PR and between 0bbf04e and 13593d5.

📒 Files selected for processing (33)
  • apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/scripts/apply-online-sast-runtime-schema.mjs
  • apps/api/src/ai-plane/ai-advisory-authority.service.ts
  • apps/api/src/ai-plane/ai-advisory.controller.ts
  • apps/api/src/ai-plane/ai-plane.module.ts
  • apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts
  • apps/api/src/ai-plane/sast-ai-advisory-authority.store.ts
  • apps/api/src/policy/policy-engine.service.ts
  • apps/api/src/policy/policy-lifecycle.service.ts
  • apps/api/src/policy/policy.module.ts
  • apps/api/test/ai-plane/ai-advisory-authority.service.e2e-spec.ts
  • apps/api/test/ai-plane/ai-advisory.e2e-spec.ts
  • apps/api/test/ai-plane/sast-ai-advisory-authority-persistence.e2e-spec.ts
  • apps/api/test/policy/policy-decisions.e2e-spec.ts
  • apps/api/test/policy/policy-engine.service.e2e-spec.ts
  • apps/api/test/policy/waiver-suppression-lifecycle.e2e-spec.ts
  • apps/api/test/support/sast-ai-advisory-fixture.ts
  • packages/shared/src/index.ts
  • packages/shared/src/types/production-architecture.ts
  • packages/shared/src/types/sast-ai-advisory-authority.ts
  • packages/shared/test/sast-ai-advisory-authority.test.mjs
  • specs/006-production-sast-runtime-design/contracts/sast-runtime.md
  • specs/006-production-sast-runtime-design/data-model.md
  • specs/006-production-sast-runtime-design/plan.md
  • specs/006-production-sast-runtime-design/quality-gates.md
  • specs/006-production-sast-runtime-design/quickstart.md
  • specs/006-production-sast-runtime-design/research.md
  • specs/006-production-sast-runtime-design/spec.md
  • specs/006-production-sast-runtime-design/tasks.md
  • specs/006-production-sast-runtime-design/threat-model.md
  • test/github-actions/active-feature.test.mjs
  • test/github-actions/ontology.test.mjs

Comment on lines +144 to +166
ALTER TABLE "SastAiAdvisoryAuthorityProof"
ADD CONSTRAINT "SastAiAdvisoryAuthorityProof_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SastAiAdvisoryAuthorityProof"
ADD CONSTRAINT "SastAiAdvisoryAuthorityProof_repository_scope_fkey"
FOREIGN KEY ("repositoryBindingId", "tenantId")
REFERENCES "RepositoryBinding"("id", "tenantId")
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SastAiAdvisoryAuthorityProof"
ADD CONSTRAINT "SastAiAdvisoryAuthorityProof_scan_scope_fkey"
FOREIGN KEY ("scanRequestId", "tenantId", "repositoryBindingId")
REFERENCES "ScanRequest"("id", "tenantId", "repositoryBindingId")
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SastAiAdvisoryAuthorityProof"
ADD CONSTRAINT "SastAiAdvisoryAuthorityProof_handoff_scope_fkey"
FOREIGN KEY ("handoffId", "tenantId")
REFERENCES "SastAiAdvisoryHandoff"("id", "tenantId")
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SastAiAdvisoryAuthorityProof"
ADD CONSTRAINT "SastAiAdvisoryAuthorityProof_advisoryId_fkey"
FOREIGN KEY ("advisoryId") REFERENCES "AiAdvisoryMetadata"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

ON UPDATE CASCADE contradicts the immutable proof ledger. All eight foreign keys on SastAiAdvisoryAuthorityProof declare ON UPDATE CASCADE, but the SastAiAdvisoryAuthorityProof_immutable_update trigger rejects every UPDATE on that table with SQLSTATE 55000. A cascaded parent-key update issues an UPDATE on the proof row and therefore aborts the parent update with a confusing immutability error.

  • apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql#L144-L166: change the five foreign keys to ON DELETE RESTRICT ON UPDATE RESTRICT.
  • apps/api/scripts/apply-online-sast-runtime-schema.mjs#L664-L684: change the three constraint definitions to ON DELETE RESTRICT ON UPDATE RESTRICT, and keep apps/api/prisma/schema.prisma relation attributes aligned.
📍 Affects 2 files
  • apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql#L144-L166 (this comment)
  • apps/api/scripts/apply-online-sast-runtime-schema.mjs#L664-L684
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql`
around lines 144 - 166, Replace ON UPDATE CASCADE with ON UPDATE RESTRICT for
all five SastAiAdvisoryAuthorityProof foreign keys in
apps/api/prisma/migrations/20260811140000_sast_ai_advisory_authority_proof/migration.sql
lines 144-166. Apply the same change to the three corresponding constraint
definitions in apps/api/scripts/apply-online-sast-runtime-schema.mjs lines
664-684, and align the SastAiAdvisoryAuthorityProof relation attributes in
apps/api/prisma/schema.prisma.

Comment on lines +26 to +32
@Post('authority-proofs')
@UseGuards(InternalServiceGuard)
createAuthorityProof(
@Body() body: SastAiAdvisoryAuthorityProofIntent
) {
return this.authorityService.createProof(body);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect InternalServiceGuard to determine whether it binds a tenant.
fd -g 'internal-service.guard.ts' --exec cat -n {}
rg -n -C 3 --type=ts 'CurrentTenant|tenantId' -g '**/internal-service*'

Repository: AigisAI/AegisAI_v2

Length of output: 1675


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -t f | rg '(^|/)(ai-advisory|authority|internal-service|tenant).*\\.(ts|tsx)$' | head -80
printf '%s\n' '--- relevant symbols and routes ---'
rg -n -C 4 --type=ts 'createAuthorityProof|createProof|SastAiAdvisoryAuthorityProofIntent|InternalServiceGuard|CurrentTenant' apps
printf '%s\n' '--- tenant checks in authority-proof paths ---'
rg -n -C 4 --type=ts 'authority.?proof|tenantId|tenant_id' apps/api/src | head -300

Repository: AigisAI/AegisAI_v2

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- authority service ---'
sed -n '1,180p' apps/api/src/ai-plane/ai-advisory-authority.service.ts
printf '%s\n' '--- authority store ---'
sed -n '1,240p' apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts
printf '%s\n' '--- controller and tenant decorator ---'
sed -n '1,100p' apps/api/src/ai-plane/ai-advisory.controller.ts
sed -n '1,80p' apps/api/src/auth/decorators/current-tenant.decorator.ts
printf '%s\n' '--- endpoint tests ---'
sed -n '1,180p' apps/api/test/ai-plane/ai-advisory.e2e-spec.ts

Repository: AigisAI/AegisAI_v2

Length of output: 17210


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re

root = Path("apps/api/src")
guard = (root / "common/security/internal-service.guard.ts").read_text()
controller = (root / "ai-plane/ai-advisory.controller.ts").read_text()
service = (root / "ai-plane/ai-advisory-authority.service.ts").read_text()
store = (root / "ai-plane/prisma-sast-ai-advisory-authority.store.ts").read_text()

print("guard_reads_tenant:", bool(re.search(r"\btenantId\b|\bCurrentTenant\b", guard)))
print("controller_uses_internal_guard:", "InternalServiceGuard" in controller)
print("controller_body_to_createProof:",
      bool(re.search(r"createProof\s*\(\s*body\s*\)", controller)))
print("service_forwards_body_tenant:",
      bool(re.search(r"tenantId\s*:\s*input\.tenantId", service)))
print("store_advisory_tenant_filter:",
      bool(re.search(r"where:\s*\{\s*id:\s*input\.advisoryId,\s*tenantId:\s*input\.tenantId", store, re.S)))
print("store_authenticated_tenant_reference:",
      bool(re.search(r"request|user|CurrentTenant|session|principal", store, re.I)))

assert not re.search(r"\btenantId\b|\bCurrentTenant\b", guard)
assert "InternalServiceGuard" in controller
assert re.search(r"createProof\s*\(\s*body\s*\)", controller)
assert re.search(r"tenantId\s*:\s*input\.tenantId", service)
assert re.search(
    r"where:\s*\{\s*id:\s*input\.advisoryId,\s*tenantId:\s*input\.tenantId",
    store, re.S
)
PY
printf '%s\n' '--- proof return construction ---'
rg -n -C 5 'return replayProof|function replayProof|proofData|buildSastAiAdvisoryAuthorityProof' \
  apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts

Repository: AigisAI/AegisAI_v2

Length of output: 2449


🏁 Script executed:

#!/bin/bash
set -e
sed -n '200,360p' apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts
sed -n '490,610p' apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts

Repository: AigisAI/AegisAI_v2

Length of output: 9141


Bind tenantId to the authenticated internal caller. InternalServiceGuard validates only the shared bearer secret, and createProof forwards the body-supplied tenantId to the store. The store validates the advisory and tenant pair but does not authorize the caller for that tenant. Use tenant-scoped internal credentials or another trusted tenant binding before creating the proof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/ai-advisory.controller.ts` around lines 26 - 32, Update
createAuthorityProof in the AI advisory controller so it does not trust
body.tenantId after InternalServiceGuard authentication. Obtain the caller’s
tenant from tenant-scoped internal credentials or another trusted authenticated
binding, validate or override the request tenant accordingly, and pass only that
authorized tenant to authorityService.createProof.

Comment on lines +109 to +120
if (existing) {
const replayed = replayProof(existing, context);
const current = await captureAuthorityState(tx, context);
if (
current.stateDigest !== replayed.proof.before.stateDigest
) {
throw new SastAiAdvisoryAuthorityPersistenceError(
'STATE_DRIFT'
);
}
return replayed;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate the replay and drift requirements for T044/FR-052 in the spec files.
fd -e md . specs/006-production-sast-runtime-design | while IFS= read -r file; do
  rg -n -C 5 'FR-052|T044|replay|REPLAY_CONFLICT|STATE_DRIFT' "$file"
done

Repository: AigisAI/AegisAI_v2

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Relevant specification text ---'
fd -e md . specs/006-production-sast-runtime-design | while IFS= read -r file; do
  if rg -q 'T044|FR-052|exact advisory replay|changed state|STATE_DRIFT|identical before/after' "$file"; then
    printf '\nFILE: %s\n' "$file"
    rg -n -C 4 --max-count 20 'T044|FR-052|exact advisory replay|changed state|STATE_DRIFT|identical before/after' "$file"
  fi
done

printf '%s\n' '--- Implementation and callers ---'
rg -n -C 8 'replayProof|captureAuthorityState|STATE_DRIFT|createProof' apps/api/src/ai-plane

Repository: AigisAI/AegisAI_v2

Length of output: 31104


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Replay validation and error mapping ---'
sed -n '553,590p' apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts
sed -n '35,75p' apps/api/src/ai-plane/ai-advisory-authority.service.ts

printf '%s\n' '--- State digest fields ---'
sed -n '334,430p' apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts
rg -n -C 5 'updatedAt|reconciliation|lifecycleState.*update|waiver.*update|suppression.*update' apps/api/src packages

Repository: AigisAI/AegisAI_v2

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

path = Path("apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts")
text = path.read_text()

create = re.search(
    r"const before = await captureAuthorityState\(tx, context\);(?P<body>.*?)"
    r"const after = await captureAuthorityState\(tx, context\);",
    text,
    re.S,
)
if not create:
    raise SystemExit("createProof before/after sequence not found")

body = create.group("body")
writes = re.findall(r"\btx\.([A-Za-z0-9_]+)\.(create|createMany|update|updateMany|upsert|delete|deleteMany)\b", body)
print("writes_between_authority_captures:", writes)
print("only_proof_ledger_write:",
      writes == [("sastAiAdvisoryAuthorityProof", "create")])

run = re.search(
    r"async function runSerializable|private async runSerializable",
    text,
)
print("runSerializable_declared_in_file:", bool(run))

print("\nrelevant_sequence:")
for i, line in enumerate(text.splitlines(), 1):
    if 120 <= i <= 146:
        print(f"{i}: {line}")
PY

Repository: AigisAI/AegisAI_v2

Length of output: 1304


🏁 Script executed:

#!/bin/bash
set -e
rg -n -C 12 'runSerializable|isolationLevel|SastAiAdvisoryAuthorityProof' \
  apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts \
  apps/api/src/ai-plane/prisma.service.ts \
  apps/api/prisma/schema.prisma prisma/schema.prisma 2>/dev/null || true

Repository: AigisAI/AegisAI_v2

Length of output: 48486


Replace the tautological post-write drift check

runSerializable uses one SERIALIZABLE transaction snapshot. The only intervening write is SastAiAdvisoryAuthorityProof.create, which captureAuthorityState does not read. Therefore, before.stateDigest !== after.stateDigest cannot detect authoritative drift. Use a database fence, version predicate, or locking strategy that covers the complete authoritative scope while preserving atomic proof insertion. Keep replay drift rejection because changed state must fail closed; exact retry applies only when the stored state still matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 109 - 120, Replace the ineffective current-state comparison in the
existing-proof branch of runSerializable with a database fence, version
predicate, or lock that covers the full authoritative scope while keeping proof
insertion atomic. Preserve replay drift rejection and only return replayed from
replayProof when the stored authoritative state still matches; otherwise throw
SastAiAdvisoryAuthorityPersistenceError with STATE_DRIFT.

Comment on lines +140 to +145
const after = await captureAuthorityState(tx, context);
if (before.stateDigest !== after.stateDigest) {
throw new SastAiAdvisoryAuthorityPersistenceError(
'STATE_DRIFT'
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

The after snapshot cannot differ from before inside the same serializable transaction.

Lines 122 and 140 capture state twice within one transaction. PostgreSQL SERIALIZABLE uses a single consistent snapshot per transaction, and the transaction writes only to sastAiAdvisoryAuthorityProof. Therefore after.stateDigest always equals before.stateDigest, and the check at line 141 can never fail.

Two consequences:

  1. The proof claims "authoritative state unchanged during proof creation", but the in-transaction re-read provides no additional evidence.
  2. Each proof creation runs five extra findMany queries for no benefit.

The real protection comes from serializable conflict detection plus the replay-time comparison at lines 112-118. Consider removing the second capture, or move the second capture outside the transaction if a genuine post-commit verification is required.

♻️ Proposed simplification
         const created =
           await tx.sastAiAdvisoryAuthorityProof.create({
             data: proofData(proof)
           });
-        const after = await captureAuthorityState(tx, context);
-        if (before.stateDigest !== after.stateDigest) {
-          throw new SastAiAdvisoryAuthorityPersistenceError(
-            'STATE_DRIFT'
-          );
-        }
         return replayProof(created, context, false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const after = await captureAuthorityState(tx, context);
if (before.stateDigest !== after.stateDigest) {
throw new SastAiAdvisoryAuthorityPersistenceError(
'STATE_DRIFT'
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts` around
lines 140 - 145, Remove the redundant post-write captureAuthorityState call and
its stateDigest comparison from the transaction callback, while retaining the
initial snapshot and existing serializable transaction protection. Ensure proof
creation no longer performs the unnecessary second state read or claims
in-transaction verification.

Comment on lines +74 to +96
it('fails closed when a policy reference is missing, drifted, or unavailable', async () => {
const store = {
createProof: jest.fn(),
verifyPolicyReference: jest.fn().mockResolvedValue(true)
};
const service = new AiAdvisoryAuthorityService(store as never);
const reference = aiPolicyReference();

await expect(
service.verifyPolicyReference({
tenantId: 'tenant-ai',
normalizedFindingId: 'normalized-finding-ai',
reference
})
).resolves.toBe(true);
await expect(
service.verifyPolicyReference({
tenantId: 'tenant-ai',
normalizedFindingId: 'normalized-finding-ai',
reference: { ...reference, suggestedAction: 'BLOCK' } as never
})
).resolves.toBe(false);
expect(store.verifyPolicyReference).toHaveBeenCalledTimes(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add missing and unavailable reference cases.

This test verifies one valid reference and one locally invalid reference. It does not make store.verifyPolicyReference return false or reject. A service that accepts a missing, drifted, or unavailable persisted proof can pass this test.

Add cases for a false result and an unavailable store result. Assert that both resolve to false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/ai-plane/ai-advisory-authority.service.e2e-spec.ts` around
lines 74 - 96, Extend the test for
AiAdvisoryAuthorityService.verifyPolicyReference to cover persisted-reference
failures: configure store.verifyPolicyReference to return false and separately
to reject or otherwise represent an unavailable result, then assert both service
calls resolve to false. Preserve the existing valid and locally invalid
reference assertions and update call-count expectations to account for the added
cases.

Comment on lines +77 to +111
it('uses one serializable proof write and no authoritative model writes', async () => {
const fixture = prismaFixture();
const store = new PrismaSastAiAdvisoryAuthorityStore(
fixture.prisma as never
);
const handoff = aiHandoff();

const first = await store.createProof({
tenantId: handoff.tenantId,
advisoryId: handoff.advisoryId,
verifiedAt: '2026-08-11T05:30:00.000Z'
});
expect(first.replayed).toBe(false);
expect(first.proof.before.stateDigest).toBe(
first.proof.after.stateDigest
);
expect(fixture.proof.create).toHaveBeenCalledTimes(1);
expect(fixture.finding.findMany).toHaveBeenCalledTimes(2);
expect(fixture.lifecycle.findMany).toHaveBeenCalledTimes(2);
expect(fixture.policy.findMany).toHaveBeenCalledTimes(2);
expect(fixture.waiver.findMany).toHaveBeenCalledTimes(2);
expect(fixture.suppression.findMany).toHaveBeenCalledTimes(2);
expect(storeSource).not.toMatch(
/\b(?:normalizedFinding|sastFindingLifecycleState|policyDecision|waiver|suppression)\.(?:create|createMany|update|updateMany|upsert|delete|deleteMany)\b/u
);

await expect(
store.createProof({
tenantId: handoff.tenantId,
advisoryId: handoff.advisoryId,
verifiedAt: '2026-08-11T06:00:00.000Z'
})
).resolves.toMatchObject({ replayed: true, proof: first.proof });
expect(fixture.proof.create).toHaveBeenCalledTimes(1);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 'runSerializable|\$transaction|isolationLevel' \
  apps/api/src/ai-plane/prisma-sast-ai-advisory-authority.store.ts \
  apps/api/test/ai-plane/sast-ai-advisory-authority-persistence.e2e-spec.ts

Repository: AigisAI/AegisAI_v2

Length of output: 5397


Assert the $transaction options.

The fixture ignores the second $transaction argument. Capture it and assert isolationLevel: Prisma.TransactionIsolationLevel.Serializable, maxWait, and timeout from runSerializable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/ai-plane/sast-ai-advisory-authority-persistence.e2e-spec.ts`
around lines 77 - 111, Update the transaction fixture and the createProof test
to capture the second $transaction options argument. Assert that runSerializable
invokes $transaction with isolationLevel set to
Prisma.TransactionIsolationLevel.Serializable, plus the expected maxWait and
timeout values.

Comment on lines +113 to +146
it('verifies only a tenant and finding-bound exact policy reference', async () => {
const fixture = prismaFixture();
const store = new PrismaSastAiAdvisoryAuthorityStore(
fixture.prisma as never
);
const handoff = aiHandoff();
const persisted = await store.createProof({
tenantId: handoff.tenantId,
advisoryId: handoff.advisoryId,
verifiedAt: '2026-08-11T05:30:00.000Z'
});
const reference = {
...aiPolicyReference(),
authorityProofId: persisted.proof.proofId,
authorityProofDigest: persisted.proof.proofDigest
};

await expect(
store.verifyPolicyReference({
tenantId: handoff.tenantId,
normalizedFindingId:
handoff.normalizedFinding.normalizedFindingId,
reference
})
).resolves.toBe(true);
await expect(
store.verifyPolicyReference({
tenantId: 'foreign-tenant',
normalizedFindingId:
handoff.normalizedFinding.normalizedFindingId,
reference
})
).resolves.toBe(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Test the finding and digest bindings.

The negative case changes only tenantId. A verifier that ignores normalizedFindingId, authorityProofDigest, or advisoryId can still pass this test.

Add rejection cases for a foreign finding and a tampered proof reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/ai-plane/sast-ai-advisory-authority-persistence.e2e-spec.ts`
around lines 113 - 146, Extend the test “verifies only a tenant and
finding-bound exact policy reference” to assert verifyPolicyReference returns
false for a foreign normalizedFindingId and for a tampered authority proof
reference, including changed authorityProofDigest or advisoryId. Keep the
existing valid and foreign-tenant assertions unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 006 AI advisory output authority 격리 구현

1 participant