Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@
# Core CLI
ANTHROPIC_API_KEY=<set-anthropic-api-key>

# Governed model intelligence layer
# disabled keeps local and CI deterministic. Set to anthropic or openai only on the server.
DISHA_MODEL_PROVIDER=disabled
DISHA_MODEL_TIMEOUT_MS=20000
ANTHROPIC_MODEL=claude-sonnet-4-5
ANTHROPIC_BASE_URL=https://api.anthropic.com/v1
ANTHROPIC_VERSION=2023-06-01

# AI Platform Backend
OPENAI_API_KEY=<set-openai-api-key>
OPENAI_MODEL=gpt-5.4-mini
OPENAI_BASE_URL=https://api.openai.com/v1
NEO4J_URI=bolt://neo4j:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=<set-neo4j-password>
Expand Down
69 changes: 69 additions & 0 deletions docs/product/GOVERNED_MODEL_INTELLIGENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# DISHA Governed Model Intelligence Layer

DISHA can now run an agentic mission through one governed product path:

1. Normalize the request into `DishaSignal`.
2. Run selected lenses.
3. Apply the policy gate.
4. Append evidence events.
5. Optionally ask a configured model provider for advisory intelligence.
6. Record evidence-backed learning memory.

The model is never the authority. The policy gate, evidence ledger, and human approval boundary remain authoritative.

## API

`POST /api/v1/agentic/mission`

The route accepts the normal mission payload plus optional `operatorInstruction`. It returns:

- `mission`: the normal `MissionResult`.
- `modelIntelligence`: provider status, advisory summary, reasoning notes, safe next steps, and verification gaps.
- `learning`: an evidence-backed `LearningMemoryRecord`, or `null` if learning is blocked.
- `evidenceEventIds`: the complete event chain for the mission.

## Provider Configuration

Local and CI default to deterministic mode:

```env
DISHA_MODEL_PROVIDER=disabled
```

Production may enable one provider on the server:

```env
DISHA_MODEL_PROVIDER=anthropic
ANTHROPIC_API_KEY=...
ANTHROPIC_MODEL=claude-sonnet-4-5
ANTHROPIC_BASE_URL=https://api.anthropic.com/v1
ANTHROPIC_VERSION=2023-06-01
```

or:

```env
DISHA_MODEL_PROVIDER=openai
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-5.4-mini
OPENAI_BASE_URL=https://api.openai.com/v1
```

If the provider is configured without the matching key, DISHA falls back to deterministic advisory output.

## Safety and Evidence Rules

- Denied missions do not call the model provider.
- Provider output is advisory JSON, not an execution instruction.
- Unsupported claims remain `[VERIFY REQUIRED]` through the mission result.
- Controlled and classified missions store redacted learning memory only.
- Learning memory stores evidence event references and hashes, not silent training data.

## Production Gaps

The current layer is production-shaped but still requires deployment-specific hardening before public operation:

- Durable memory store with retention and export policy.
- Provider rate limits and billing controls.
- Prompt-injection regression tests for each connected provider.
- Operational monitoring for provider latency and error rates.
44 changes: 44 additions & 0 deletions web/app/api/v1/agentic/mission/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

import { runAgenticMission } from "@/lib/unified/agentic-executor";
import { withContext } from "@/lib/unified/api";

const agenticMissionRequestSchema = z.object({
rawText: z.string().default(""),
requestedAction: z.string().optional(),
operatorInstruction: z.string().optional(),
sensitivity: z.enum(["public", "internal", "controlled", "classified"]).default("public"),
deviceId: z.string().optional(),
deviceTrust: z.number().min(0).max(1).optional(),
missionId: z.string().optional(),
indicators: z.array(z.object({
type: z.enum(["ip", "domain", "url", "hash", "cve", "email", "other"]),
value: z.string(),
source: z.string().optional(),
})).default([]),
locations: z.array(z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
accuracyM: z.number().nonnegative().optional(),
label: z.string().optional(),
})).default([]),
dataSources: z.array(z.object({
sourceId: z.string().min(1),
name: z.string().min(1),
url: z.string().url().optional(),
sensitivity: z.enum(["public", "internal", "controlled", "classified"]).default("public"),
})).default([]),
});

export async function POST(req: NextRequest) {
return withContext(req, "agent:run", async (ctx) => {
const body = agenticMissionRequestSchema.parse(await req.json());
const result = await runAgenticMission({
...body,
userId: ctx.principal.userId,
userRole: ctx.principal.roles[0] ?? "analyst",
});
return NextResponse.json(result, { headers: { "X-Request-ID": ctx.requestId } });
});
}
5 changes: 5 additions & 0 deletions web/app/api/v1/health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { NextResponse } from "next/server";
import { lensRegistry } from "@/lib/unified/lenses";
import { openDataSources } from "@/lib/unified/data-integration";
import { listAgentSkills } from "@/lib/unified/agentic-readiness";
import { getEnv } from "@/lib/server/env";

export async function GET() {
const env = getEnv();
return NextResponse.json({
status: "ok",
product: "DISHA v6.6 Unified Policy-Gated Cognitive Intelligence OS",
Expand All @@ -13,5 +15,8 @@ export async function GET() {
openDataSources: openDataSources.length,
policyGate: "enabled",
evidenceLedger: "enabled",
agenticMission: "enabled",
modelProvider: env.DISHA_MODEL_PROVIDER,
learningMemory: "evidence_memory",
});
}
12 changes: 12 additions & 0 deletions web/lib/server/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const envSchema = z.object({
DISHA_WORKFLOW_ALLOWED_HOSTS: z.string().optional(),

// Model provider (OpenAI) - server-side only.
DISHA_MODEL_PROVIDER: z.enum(["disabled", "anthropic", "openai"]).default("disabled"),
DISHA_MODEL_TIMEOUT_MS: z.coerce.number().int().positive().default(20_000),
ANTHROPIC_API_KEY: z.string().optional(),
ANTHROPIC_MODEL: z.string().default("claude-sonnet-4-5"),
ANTHROPIC_BASE_URL: z.string().url().default("https://api.anthropic.com/v1"),
ANTHROPIC_VERSION: z.string().default("2023-06-01"),
OPENAI_API_KEY: z.string().optional(),
OPENAI_MODEL: z.string().default("gpt-5.4-mini"),
OPENAI_BASE_URL: z.string().url().default("https://api.openai.com/v1"),
Expand Down Expand Up @@ -75,6 +81,12 @@ export function getEnv(): RuntimeEnv {
DISHA_WORKFLOW_NODE_TIMEOUT_MS: 15000,
DISHA_WORKFLOW_TOTAL_TIMEOUT_MS: 60000,
DISHA_WORKFLOW_ALLOWED_HOSTS: undefined,
DISHA_MODEL_PROVIDER: "disabled",
DISHA_MODEL_TIMEOUT_MS: 20000,
ANTHROPIC_API_KEY: undefined,
ANTHROPIC_MODEL: "claude-sonnet-4-5",
ANTHROPIC_BASE_URL: "https://api.anthropic.com/v1",
ANTHROPIC_VERSION: "2023-06-01",
OPENAI_API_KEY: undefined,
OPENAI_MODEL: "gpt-5.4-mini",
OPENAI_BASE_URL: "https://api.openai.com/v1",
Expand Down
67 changes: 67 additions & 0 deletions web/lib/unified/agentic-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { appendEvidenceEvent } from "./evidence-ledger";
import { learnFromMission, type LearningMemoryRecord } from "./learning-memory";
import { runModelIntelligence, type ModelIntelligenceResult } from "./model-provider";
import { runMission, type MissionInput, type MissionResult } from "./orchestrator";

export type AgenticMissionInput = MissionInput & {
operatorInstruction?: string;
};

export type AgenticMissionResult = {
mission: MissionResult;
modelIntelligence: ModelIntelligenceResult;
learning: LearningMemoryRecord | null;
evidenceEventIds: string[];
};

export async function runAgenticMission(input: AgenticMissionInput): Promise<AgenticMissionResult> {
const mission = await runMission(input);
const modelIntelligence = await runModelIntelligence({ mission, operatorInstruction: input.operatorInstruction });
const modelEvent = appendEvidenceEvent({
missionId: mission.missionId,
actor: "model-intelligence-adapter",
action: "model_intelligence_completed",
input: {
missionId: mission.missionId,
provider: modelIntelligence.provider,
model: modelIntelligence.model,
policyDecision: mission.policyDecision.decision,
},
output: {
status: modelIntelligence.status,
summary: modelIntelligence.summary,
verifyRequired: modelIntelligence.verifyRequired,
recommendedNextSteps: modelIntelligence.recommendedNextSteps,
},
policyDecision: mission.policyDecision,
});
mission.evidenceEventIds = [...mission.evidenceEventIds, modelEvent.eventId];

const learning = learnFromMission(mission, modelIntelligence);
const learningEvent = appendEvidenceEvent({
missionId: mission.missionId,
actor: "evidence-learning-memory",
action: "learning_memory_recorded",
input: { missionId: mission.missionId, modelEventId: modelEvent.eventId },
output: learning
? {
memoryId: learning.memoryId,
redacted: learning.redacted,
evidenceHashes: learning.evidenceHashes,
}
: { recorded: false },
policyDecision: mission.policyDecision,
parentEventId: modelEvent.eventId,
});
mission.evidenceEventIds = [...mission.evidenceEventIds, learningEvent.eventId];

return {
mission,
modelIntelligence: {
...modelIntelligence,
evidenceEventIds: mission.evidenceEventIds,
},
learning,
evidenceEventIds: mission.evidenceEventIds,
};
}
36 changes: 25 additions & 11 deletions web/lib/unified/agentic-readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,30 +174,44 @@ export function listAgentSkills(): AgentSkillDefinition[] {
{
id: "memory-learning",
title: "Memory and Learning Agent",
purpose: "Learn only from approved evidence, verified source records, and explicit mission memory references.",
purpose: "Record evidence-backed mission memory without silent training or raw controlled-data retention.",
inputContract: "missionId + approved evidence events",
outputContract: "memoryRefs",
outputContract: "LearningMemoryRecord",
allowedData: ["evidence ledger events", "verified source records", "operator-approved summaries"],
policyBoundary: "No silent learning from controlled data, personal data, or unverified claims.",
evidenceBoundary: "Learning input must reference evidence event ids and source provenance.",
learningMode: "evidence_memory",
status: "partial",
readinessScore: 0.58,
blockers: ["Durable memory store adapter requires auth, retention policy, redaction, and audit export."],
readinessScore: 0.68,
blockers: ["Durable memory store adapter requires retention policy, export, and production database persistence."],
},
{
id: "claude-orchestrator-bridge",
title: "Claude Orchestrator Bridge",
purpose: "Allow Claude to call DISHA mission/lens APIs as an orchestrator without direct data or action access.",
inputContract: "/api/v1/mission or /api/v1/lenses/{lens}/analyze",
outputContract: "MissionResult or DishaLensResult",
purpose: "Allow Claude or another provider to enrich DISHA missions without direct data or action access.",
inputContract: "/api/v1/agentic/mission, /api/v1/mission, or /api/v1/lenses/{lens}/analyze",
outputContract: "AgenticMissionResult, MissionResult, or DishaLensResult",
allowedData: ["API responses returned after policy/evidence controls"],
policyBoundary: "Claude cannot bypass policy, controlled connectors, or evidence ledger.",
evidenceBoundary: "Every Claude-driven mission remains a normal DISHA mission with event hashes.",
learningMode: "blocked",
learningMode: "evidence_memory",
status: "partial",
readinessScore: 0.64,
blockers: ["Provider adapter needs production API key handling, request signing, rate limits, and prompt injection tests."],
readinessScore: 0.74,
blockers: ["Production provider keys, rate limits, and prompt-injection regression tests must be configured per deployment."],
},
{
id: "governed-model-intelligence",
title: "Governed Model Intelligence Agent",
purpose: "Run optional Anthropic or OpenAI analysis after policy gating and return advisory JSON with verification gaps.",
inputContract: "MissionResult + operatorInstruction?",
outputContract: "ModelIntelligenceResult",
allowedData: ["mission summaries", "lens summaries", "policy decision", "evidence event ids"],
policyBoundary: "Provider execution is skipped when policy denies the mission.",
evidenceBoundary: "Model output is appended to the mission evidence chain before learning memory is recorded.",
learningMode: "evidence_memory",
status: "ready",
readinessScore: 0.82,
blockers: [],
},
];
}
Expand All @@ -215,7 +229,7 @@ export function getAgenticReadinessReport(): AgenticReadinessReport {
orchestrationContract: "Claude or any model may orchestrate only through DISHA API v1, DishaSignal, DishaLensResult, policy gate, and evidence ledger.",
claudeBridge: {
role: "reasoning and orchestration client, not a privileged data plane",
allowedEntryPoints: ["/api/v1/mission", "/api/v1/lenses/{lens}/analyze", "/api/v1/data/open/query", "/api/v1/evidence/export"],
allowedEntryPoints: ["/api/v1/agentic/mission", "/api/v1/mission", "/api/v1/lenses/{lens}/analyze", "/api/v1/data/open/query", "/api/v1/evidence/export"],
deniedCapabilities: ["direct controlled-data access", "offensive cyber action", "silent learning", "unsourced factual publication", "policy bypass"],
},
openSourceConnectorMesh: openDataSources.map((source) => ({
Expand Down
63 changes: 63 additions & 0 deletions web/lib/unified/learning-memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { hashValue } from "./hash";
import type { MissionResult } from "./orchestrator";
import type { ModelIntelligenceResult } from "./model-provider";

export type LearningMemoryRecord = {
memoryId: string;
missionId: string;
createdAt: string;
sensitivity: MissionResult["signal"]["context"]["sensitivity"];
policyDecision: MissionResult["policyDecision"]["decision"];
sourceEventIds: string[];
evidenceHashes: string[];
summary: string;
reusablePatterns: string[];
redacted: boolean;
};

const memoryByMission = new Map<string, LearningMemoryRecord>();

export function learnFromMission(mission: MissionResult, model: ModelIntelligenceResult): LearningMemoryRecord | null {
if (mission.policyDecision.decision === "DENY") return null;
if (!mission.evidenceEventIds.length) return null;

const redacted = ["controlled", "classified"].includes(mission.signal.context.sensitivity);
const evidenceHashes = mission.evidenceEventIds.map((eventId) => hashValue(`${mission.missionId}:${eventId}`));
const reusablePatterns = [
...mission.fusedIntelligence.topFindings,
...model.recommendedNextSteps,
]
.filter((item) => item && !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(item))
.slice(0, 8);

const summary = redacted
? `Redacted ${mission.signal.context.sensitivity} mission memory. Evidence and policy metadata retained; raw mission text excluded.`
: model.summary;

const record: LearningMemoryRecord = {
memoryId: hashValue(`${mission.missionId}:${mission.evidenceEventIds.join(":")}`).slice(0, 24),
missionId: mission.missionId,
createdAt: new Date().toISOString(),
sensitivity: mission.signal.context.sensitivity,
policyDecision: mission.policyDecision.decision,
sourceEventIds: mission.evidenceEventIds,
evidenceHashes,
summary,
reusablePatterns,
redacted,
};
memoryByMission.set(mission.missionId, record);
return record;
}

export function getLearningMemory(missionId?: string): LearningMemoryRecord[] {
if (missionId) {
const record = memoryByMission.get(missionId);
return record ? [record] : [];
}
return Array.from(memoryByMission.values());
}

export function clearLearningMemoryForTests(): void {
memoryByMission.clear();
}
Loading
Loading