feat(foundation): FOUNDATION_UPDATE implementation
## Documentation (20 files) - DAARION Ontology Core v1 (Agent → MicroDAO → Node → District) - User Onboarding & Identity Layer (DAIS) - Data Model UPDATE, Event Catalog, Governance & Permissions - Rooms Layer, City/MicroDAO/Agents/Nodes Interface Architecture - Helper files: ontology-summary, lifecycles, event-schemas ## Database Migration (027) - DAIS tables: dais_identities, dais_emails, dais_wallets, dais_keys - agent_assignments table for Assignment Layer - rooms table for Rooms Layer - event_outbox for NATS event delivery - New enums: agent_role, microdao_type, node_kind, node_status, etc. - Updated agents, microdaos, nodes tables with ontology fields ## Backend - DAIS service & routes (/api/v1/dais/*) - Assignment service & routes (/api/v1/assignments/*) - Domain types for DAIS and Ontology ## Frontend - Ontology types (Agent, MicroDAO, Node, DAIS, Assignments) - API clients for DAIS and Assignments - UI components: DaisProfileCard, AssignmentsPanel, OntologyBadge Non-breaking update - all existing functionality preserved.
This commit is contained in:
@@ -15,6 +15,9 @@ import { vendorRoutes } from './api/http/vendor.routes';
|
||||
import { platformsRoutes } from './api/http/platforms.routes';
|
||||
import { agentsRoutes } from './api/http/agents.routes';
|
||||
import { teamsRoutes } from './api/http/teams.routes';
|
||||
// Foundation Update routes
|
||||
import daisRoutes from './http/dais.routes';
|
||||
import assignmentRoutes from './http/assignment.routes';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -32,6 +35,10 @@ app.use('/api/v1/platforms', platformsRoutes);
|
||||
app.use('/api/v1/platforms', vendorRoutes); // Vendor routes under platforms
|
||||
app.use('/api/v1', agentsRoutes);
|
||||
|
||||
// Foundation Update routes (DAIS & Assignments)
|
||||
app.use('/api/v1/dais', daisRoutes);
|
||||
app.use('/api/v1/assignments', assignmentRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
|
||||
93
backend/domain/dais/types.ts
Normal file
93
backend/domain/dais/types.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* DAIS - DAARION Autonomous Identity System
|
||||
* Based on: docs/foundation/DAARION_Identity_And_Access_Draft_v1.md
|
||||
*/
|
||||
|
||||
// Trust levels for DAIS identity
|
||||
export type DaisTrustLevel = 'guest' | 'agent' | 'verified' | 'orchestrator' | 'operator';
|
||||
|
||||
// DAIS Identity
|
||||
export interface DaisIdentity {
|
||||
id: string;
|
||||
did: string; // format: did:daarion:<uuid>
|
||||
defaultEmail?: string;
|
||||
defaultWallet?: string;
|
||||
matrixHandle?: string; // format: @<agent_id>:matrix.daarion.city
|
||||
trustLevel: DaisTrustLevel;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// DAIS Email identity
|
||||
export interface DaisEmail {
|
||||
id: string;
|
||||
daisId: string;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
verifiedAt?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// DAIS Wallet identity
|
||||
export interface DaisWallet {
|
||||
id: string;
|
||||
daisId: string;
|
||||
walletAddress: string;
|
||||
network: 'evm' | 'ton' | 'solana';
|
||||
verified: boolean;
|
||||
verifiedAt?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// DAIS Key types
|
||||
export type DaisKeyType = 'ed25519' | 'x25519' | 'secp256k1';
|
||||
|
||||
// DAIS Public Key
|
||||
export interface DaisKey {
|
||||
id: string;
|
||||
daisId: string;
|
||||
keyType: DaisKeyType;
|
||||
publicKey: string;
|
||||
createdAt: Date;
|
||||
revokedAt?: Date;
|
||||
}
|
||||
|
||||
// Full DAIS profile with all identities
|
||||
export interface DaisProfile {
|
||||
identity: DaisIdentity;
|
||||
emails: DaisEmail[];
|
||||
wallets: DaisWallet[];
|
||||
keys: DaisKey[];
|
||||
}
|
||||
|
||||
// Create DAIS identity request
|
||||
export interface CreateDaisRequest {
|
||||
email?: string;
|
||||
walletAddress?: string;
|
||||
network?: 'evm' | 'ton' | 'solana';
|
||||
}
|
||||
|
||||
// Verify email request
|
||||
export interface VerifyEmailRequest {
|
||||
daisId: string;
|
||||
email: string;
|
||||
otp: string;
|
||||
}
|
||||
|
||||
// Connect wallet request (SIWE)
|
||||
export interface ConnectWalletRequest {
|
||||
daisId: string;
|
||||
walletAddress: string;
|
||||
network: 'evm' | 'ton' | 'solana';
|
||||
signature: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// DAIS creation result
|
||||
export interface DaisCreationResult {
|
||||
identity: DaisIdentity;
|
||||
agentId: string;
|
||||
matrixHandle: string;
|
||||
}
|
||||
|
||||
285
backend/domain/ontology/types.ts
Normal file
285
backend/domain/ontology/types.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* DAARION Ontology Types
|
||||
* Based on: docs/foundation/DAARION_Ontology_Core_v1.md
|
||||
*
|
||||
* Core hierarchy: Agent → MicroDAO → Node → District
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// AGENT
|
||||
// ============================================================================
|
||||
|
||||
export type AgentRole = 'regular' | 'orchestrator';
|
||||
export type ServiceScope = 'microdao' | 'district' | 'city';
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
daisIdentityId?: string;
|
||||
homeMicrodaoId: string; // Required by ontology
|
||||
homeNodeId?: string;
|
||||
role: AgentRole;
|
||||
serviceScope?: ServiceScope;
|
||||
|
||||
// Existing fields
|
||||
name: string;
|
||||
kind: string;
|
||||
isOrchestrator: boolean;
|
||||
isPublic: boolean;
|
||||
visibilityScope?: string;
|
||||
primaryMicrodaoId?: string;
|
||||
nodeId?: string;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type AgentType =
|
||||
| 'personal' // User's personal agent
|
||||
| 'service' // Service/infrastructure agent
|
||||
| 'core-city' // DAARION108 - citywide agents
|
||||
| 'orchestrator'; // Can create MicroDAO
|
||||
|
||||
// ============================================================================
|
||||
// MICRODAO
|
||||
// ============================================================================
|
||||
|
||||
export type MicrodaoType = 'root' | 'standard' | 'district';
|
||||
|
||||
export interface Microdao {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: MicrodaoType;
|
||||
primaryOrchestratorAgentId: string; // Required by ontology
|
||||
parentMicrodaoId?: string; // For districts
|
||||
walletAddress?: string;
|
||||
|
||||
// Existing fields
|
||||
ownerAgentId?: string;
|
||||
orchestratorAgentId?: string;
|
||||
isPlatform: boolean;
|
||||
isActive: boolean;
|
||||
isPublic: boolean;
|
||||
district?: string;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NODE
|
||||
// ============================================================================
|
||||
|
||||
export type NodeKind =
|
||||
| 'smartphone'
|
||||
| 'laptop'
|
||||
| 'edge'
|
||||
| 'datacenter'
|
||||
| 'iot'
|
||||
| 'gpu-cluster';
|
||||
|
||||
export type NodeStatus =
|
||||
| 'provisioning'
|
||||
| 'active'
|
||||
| 'draining'
|
||||
| 'retired';
|
||||
|
||||
export interface NodeCapabilities {
|
||||
cpu?: string;
|
||||
ram?: string;
|
||||
gpu?: {
|
||||
name: string;
|
||||
vram?: string;
|
||||
unified_memory_gb?: number;
|
||||
};
|
||||
network?: {
|
||||
up: string;
|
||||
down: string;
|
||||
};
|
||||
sensors?: string[];
|
||||
modules?: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
port?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
nodeId: string;
|
||||
name: string;
|
||||
microdaoId: string; // Required by ontology
|
||||
kind: NodeKind;
|
||||
status: NodeStatus;
|
||||
capabilities: NodeCapabilities;
|
||||
lastHeartbeat?: Date;
|
||||
|
||||
// Existing fields
|
||||
gpu?: Record<string, unknown>;
|
||||
modules?: Array<Record<string, unknown>>;
|
||||
roles?: string[];
|
||||
version?: string;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DISTRICT (MicroDAO with type='district')
|
||||
// ============================================================================
|
||||
|
||||
export interface District extends Microdao {
|
||||
type: 'district';
|
||||
childMicrodaos: Microdao[];
|
||||
nodePool: Node[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AGENT ASSIGNMENT
|
||||
// ============================================================================
|
||||
|
||||
export type AssignmentScope = 'microdao' | 'district' | 'city';
|
||||
export type AssignmentRole = 'advisor' | 'security' | 'mentor' | 'ops' | 'core-team' | 'member';
|
||||
|
||||
export interface AgentAssignment {
|
||||
id: string;
|
||||
agentId: string;
|
||||
targetMicrodaoId: string;
|
||||
scope: AssignmentScope;
|
||||
role: AssignmentRole;
|
||||
startTs: Date;
|
||||
endTs?: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateAssignmentRequest {
|
||||
agentId: string;
|
||||
targetMicrodaoId: string;
|
||||
scope: AssignmentScope;
|
||||
role: AssignmentRole;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EndAssignmentRequest {
|
||||
assignmentId: string;
|
||||
endTs?: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ROOMS LAYER
|
||||
// ============================================================================
|
||||
|
||||
export type RoomType =
|
||||
| 'city-room'
|
||||
| 'dao-room'
|
||||
| 'front-room'
|
||||
| 'agent-room'
|
||||
| 'event-room'
|
||||
| 'district-room';
|
||||
|
||||
export type RoomVisibility =
|
||||
| 'private'
|
||||
| 'members'
|
||||
| 'public-city'
|
||||
| 'public-global';
|
||||
|
||||
export type SpaceScope = 'city' | 'microdao' | 'district';
|
||||
|
||||
export interface Room {
|
||||
id: string;
|
||||
ownerType: 'city' | 'microdao' | 'district' | 'agent';
|
||||
ownerId: string;
|
||||
type: RoomType;
|
||||
spaceScope: SpaceScope;
|
||||
visibility: RoomVisibility;
|
||||
name: string;
|
||||
description?: string;
|
||||
matrixRoomId?: string;
|
||||
isPortal: boolean;
|
||||
portalTargetMicrodaoId?: string;
|
||||
mapX?: number;
|
||||
mapY?: number;
|
||||
zone?: string;
|
||||
meshId?: string;
|
||||
primaryAgentId?: string;
|
||||
teamAgentIds?: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENTS (for NATS)
|
||||
// ============================================================================
|
||||
|
||||
export interface DomainEvent {
|
||||
eventId: string;
|
||||
timestamp: string; // ISO8601
|
||||
version: string;
|
||||
subject: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentPromotedEvent extends DomainEvent {
|
||||
subject: 'dagion.agent.promoted_to_orchestrator';
|
||||
payload: {
|
||||
agentId: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MicrodaoCreatedEvent extends DomainEvent {
|
||||
subject: 'dagion.microdao.created';
|
||||
payload: {
|
||||
microdaoId: string;
|
||||
primaryOrchestratorAgentId: string;
|
||||
type: MicrodaoType;
|
||||
parentMicrodaoId?: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeRegisteredEvent extends DomainEvent {
|
||||
subject: 'dagion.node.registered';
|
||||
payload: {
|
||||
nodeId: string;
|
||||
microdaoId: string;
|
||||
nodeKind: NodeKind;
|
||||
capabilities: NodeCapabilities;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MicrodaoPromotedToDistrictEvent extends DomainEvent {
|
||||
subject: 'dagion.microdao.promoted_to_district';
|
||||
payload: {
|
||||
microdaoId: string;
|
||||
promotedByAgentId: string;
|
||||
parentMicrodaoId?: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssignmentCreatedEvent extends DomainEvent {
|
||||
subject: 'dagion.agent.assignment_created';
|
||||
payload: {
|
||||
assignmentId: string;
|
||||
agentId: string;
|
||||
targetMicrodaoId: string;
|
||||
scope: AssignmentScope;
|
||||
role: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssignmentEndedEvent extends DomainEvent {
|
||||
subject: 'dagion.agent.assignment_ended';
|
||||
payload: {
|
||||
assignmentId: string;
|
||||
agentId: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
188
backend/http/assignment.routes.ts
Normal file
188
backend/http/assignment.routes.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Agent Assignment API Routes
|
||||
* Based on: docs/foundation/microdao_Governance_And_Permissions_v1.md
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { assignmentService } from '../services/assignment/assignment.service';
|
||||
import { logger } from '../infra/logger/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /api/assignments
|
||||
* Create a new agent assignment
|
||||
*/
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { agentId, targetMicrodaoId, scope, role, metadata } = req.body;
|
||||
|
||||
const assignment = await assignmentService.createAssignment({
|
||||
agentId,
|
||||
targetMicrodaoId,
|
||||
scope,
|
||||
role,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: assignment,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to create assignment', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create assignment',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/assignments/:id
|
||||
* End an agent assignment
|
||||
*/
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await assignmentService.endAssignment(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Assignment ended',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to end assignment', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to end assignment',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/assignments/agent/:agentId
|
||||
* Get all active assignments for an agent
|
||||
*/
|
||||
router.get('/agent/:agentId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { agentId } = req.params;
|
||||
|
||||
const assignments = await assignmentService.getAgentAssignments(agentId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: assignments,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get agent assignments', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get assignments',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/assignments/microdao/:microdaoId
|
||||
* Get all assignments for a MicroDAO
|
||||
*/
|
||||
router.get('/microdao/:microdaoId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { microdaoId } = req.params;
|
||||
|
||||
const assignments = await assignmentService.getMicrodaoAssignments(microdaoId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: assignments,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get microdao assignments', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get assignments',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/assignments/citywide
|
||||
* Get all citywide assignments (DAARION108)
|
||||
*/
|
||||
router.get('/citywide', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const assignments = await assignmentService.getCitywideAssignments();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: assignments,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get citywide assignments', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get assignments',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/assignments/agent/:agentId/scope
|
||||
* Get agent's effective scope
|
||||
*/
|
||||
router.get('/agent/:agentId/scope', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { agentId } = req.params;
|
||||
|
||||
const scope = await assignmentService.getAgentScope(agentId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: scope,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get agent scope', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get scope',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/assignments/check
|
||||
* Check if agent has assignment to target
|
||||
*/
|
||||
router.get('/check', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { agentId, targetMicrodaoId } = req.query;
|
||||
|
||||
if (!agentId || !targetMicrodaoId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'agentId and targetMicrodaoId are required',
|
||||
});
|
||||
}
|
||||
|
||||
const hasAssignment = await assignmentService.hasAssignment(
|
||||
agentId as string,
|
||||
targetMicrodaoId as string
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { hasAssignment },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to check assignment', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to check assignment',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
219
backend/http/dais.routes.ts
Normal file
219
backend/http/dais.routes.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* DAIS API Routes
|
||||
* Based on: docs/foundation/DAARION_Identity_And_Access_Draft_v1.md
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { daisService } from '../services/dais/dais.service';
|
||||
import { logger } from '../infra/logger/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /api/dais/identity
|
||||
* Create a new DAIS identity
|
||||
*/
|
||||
router.post('/identity', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, walletAddress, network } = req.body;
|
||||
|
||||
const result = await daisService.createIdentity({
|
||||
email,
|
||||
walletAddress,
|
||||
network,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to create DAIS identity', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create identity',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/dais/:id
|
||||
* Get DAIS profile
|
||||
*/
|
||||
router.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const profile = await daisService.getProfile(id);
|
||||
|
||||
if (!profile) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'DAIS identity not found',
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: profile,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get DAIS profile', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get profile',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/dais/agent/:agentId
|
||||
* Get DAIS profile by agent ID
|
||||
*/
|
||||
router.get('/agent/:agentId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { agentId } = req.params;
|
||||
const profile = await daisService.getByAgentId(agentId);
|
||||
|
||||
if (!profile) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'DAIS identity not found for agent',
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: profile,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get DAIS by agent', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get profile',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dais/:id/email
|
||||
* Add email to DAIS
|
||||
*/
|
||||
router.post('/:id/email', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { email } = req.body;
|
||||
|
||||
const result = await daisService.addEmail(id, email);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to add email', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to add email',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dais/:id/email/verify
|
||||
* Verify email
|
||||
*/
|
||||
router.post('/:id/email/verify', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { email, otp } = req.body;
|
||||
|
||||
// TODO: Validate OTP
|
||||
await daisService.verifyEmail(id, email);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Email verified',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify email', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to verify email',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dais/:id/wallet
|
||||
* Add wallet to DAIS
|
||||
*/
|
||||
router.post('/:id/wallet', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { walletAddress, network } = req.body;
|
||||
|
||||
const result = await daisService.addWallet(id, walletAddress, network);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to add wallet', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to add wallet',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dais/:id/wallet/verify
|
||||
* Verify wallet (SIWE)
|
||||
*/
|
||||
router.post('/:id/wallet/verify', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { walletAddress, signature, message } = req.body;
|
||||
|
||||
// TODO: Validate SIWE signature
|
||||
await daisService.verifyWallet(id, walletAddress);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Wallet verified',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify wallet', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to verify wallet',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dais/:id/promote-to-orchestrator
|
||||
* Promote DAIS to orchestrator level
|
||||
*/
|
||||
router.post('/:id/promote-to-orchestrator', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await daisService.promoteToOrchestrator(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Promoted to orchestrator',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to promote to orchestrator', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to promote',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
226
backend/services/assignment/assignment.service.ts
Normal file
226
backend/services/assignment/assignment.service.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Agent Assignment Service
|
||||
* Based on: docs/foundation/microdao_Governance_And_Permissions_v1.md
|
||||
*
|
||||
* Manages agent work assignments to other MicroDAO/District/City
|
||||
*/
|
||||
|
||||
import { db } from '../../infra/db/client';
|
||||
import { logger } from '../../infra/logger/logger';
|
||||
import type {
|
||||
AgentAssignment,
|
||||
CreateAssignmentRequest,
|
||||
AssignmentScope,
|
||||
} from '../../domain/ontology/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class AssignmentService {
|
||||
/**
|
||||
* Create a new agent assignment
|
||||
*/
|
||||
async createAssignment(request: CreateAssignmentRequest): Promise<AgentAssignment> {
|
||||
try {
|
||||
const result = await db.query<AgentAssignment>(
|
||||
`INSERT INTO agent_assignments
|
||||
(agent_id, target_microdao_id, scope, role, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[
|
||||
request.agentId,
|
||||
request.targetMicrodaoId,
|
||||
request.scope,
|
||||
request.role,
|
||||
JSON.stringify(request.metadata || {}),
|
||||
]
|
||||
);
|
||||
|
||||
const assignment = result.rows[0];
|
||||
|
||||
// Publish event to outbox
|
||||
await this.publishEvent('dagion.agent.assignment_created', {
|
||||
assignmentId: assignment.id,
|
||||
agentId: request.agentId,
|
||||
targetMicrodaoId: request.targetMicrodaoId,
|
||||
scope: request.scope,
|
||||
role: request.role,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
logger.info(`Created assignment: ${assignment.id} for agent ${request.agentId}`);
|
||||
return assignment;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create assignment', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End an agent assignment
|
||||
*/
|
||||
async endAssignment(assignmentId: string): Promise<void> {
|
||||
try {
|
||||
const result = await db.query<AgentAssignment>(
|
||||
`UPDATE agent_assignments
|
||||
SET end_ts = now()
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[assignmentId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error(`Assignment not found: ${assignmentId}`);
|
||||
}
|
||||
|
||||
const assignment = result.rows[0];
|
||||
|
||||
// Publish event to outbox
|
||||
await this.publishEvent('dagion.agent.assignment_ended', {
|
||||
assignmentId,
|
||||
agentId: assignment.agentId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
logger.info(`Ended assignment: ${assignmentId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to end assignment: ${assignmentId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active assignments for an agent
|
||||
*/
|
||||
async getAgentAssignments(agentId: string): Promise<AgentAssignment[]> {
|
||||
try {
|
||||
const result = await db.query<AgentAssignment>(
|
||||
`SELECT * FROM agent_assignments
|
||||
WHERE agent_id = $1 AND end_ts IS NULL
|
||||
ORDER BY created_at DESC`,
|
||||
[agentId]
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get assignments for agent: ${agentId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assignments for a MicroDAO
|
||||
*/
|
||||
async getMicrodaoAssignments(microdaoId: string): Promise<AgentAssignment[]> {
|
||||
try {
|
||||
const result = await db.query<AgentAssignment>(
|
||||
`SELECT a.*, ag.name as agent_name
|
||||
FROM agent_assignments a
|
||||
JOIN agents ag ON ag.id = a.agent_id
|
||||
WHERE a.target_microdao_id = $1 AND a.end_ts IS NULL
|
||||
ORDER BY a.created_at DESC`,
|
||||
[microdaoId]
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get assignments for microdao: ${microdaoId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get citywide assignments (DAARION108)
|
||||
*/
|
||||
async getCitywideAssignments(): Promise<AgentAssignment[]> {
|
||||
try {
|
||||
const result = await db.query<AgentAssignment>(
|
||||
`SELECT a.*, ag.name as agent_name
|
||||
FROM agent_assignments a
|
||||
JOIN agents ag ON ag.id = a.agent_id
|
||||
WHERE a.scope = 'city' AND a.end_ts IS NULL
|
||||
ORDER BY a.created_at DESC`,
|
||||
[]
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get citywide assignments', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if agent has assignment to target
|
||||
*/
|
||||
async hasAssignment(agentId: string, targetMicrodaoId: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await db.query(
|
||||
`SELECT 1 FROM agent_assignments
|
||||
WHERE agent_id = $1 AND target_microdao_id = $2 AND end_ts IS NULL
|
||||
LIMIT 1`,
|
||||
[agentId, targetMicrodaoId]
|
||||
);
|
||||
|
||||
return result.rows.length > 0;
|
||||
} catch (error) {
|
||||
logger.error('Failed to check assignment', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent's effective scope (home + assignments)
|
||||
*/
|
||||
async getAgentScope(agentId: string): Promise<{
|
||||
homeMicrodaoId: string | null;
|
||||
assignments: AgentAssignment[];
|
||||
effectiveScope: AssignmentScope;
|
||||
}> {
|
||||
try {
|
||||
// Get agent's home MicroDAO
|
||||
const agent = await db.query(
|
||||
`SELECT home_microdao_id, agent_service_scope FROM agents WHERE id = $1`,
|
||||
[agentId]
|
||||
);
|
||||
|
||||
if (agent.rows.length === 0) {
|
||||
throw new Error(`Agent not found: ${agentId}`);
|
||||
}
|
||||
|
||||
const assignments = await this.getAgentAssignments(agentId);
|
||||
|
||||
// Determine effective scope
|
||||
let effectiveScope: AssignmentScope = 'microdao';
|
||||
|
||||
if (agent.rows[0].agent_service_scope === 'city') {
|
||||
effectiveScope = 'city';
|
||||
} else if (assignments.some(a => a.scope === 'city')) {
|
||||
effectiveScope = 'city';
|
||||
} else if (assignments.some(a => a.scope === 'district')) {
|
||||
effectiveScope = 'district';
|
||||
}
|
||||
|
||||
return {
|
||||
homeMicrodaoId: agent.rows[0].home_microdao_id,
|
||||
assignments,
|
||||
effectiveScope,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get agent scope: ${agentId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish event to outbox for NATS
|
||||
*/
|
||||
private async publishEvent(eventType: string, payload: Record<string, unknown>): Promise<void> {
|
||||
await db.query(
|
||||
`INSERT INTO event_outbox (event_type, subject, payload)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[eventType, eventType, JSON.stringify(payload)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const assignmentService = new AssignmentService();
|
||||
|
||||
278
backend/services/dais/dais.service.ts
Normal file
278
backend/services/dais/dais.service.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* DAIS Service - DAARION Autonomous Identity System
|
||||
* Based on: docs/foundation/DAARION_Identity_And_Access_Draft_v1.md
|
||||
*/
|
||||
|
||||
import { db } from '../../infra/db/client';
|
||||
import { logger } from '../../infra/logger/logger';
|
||||
import type {
|
||||
DaisIdentity,
|
||||
DaisProfile,
|
||||
DaisEmail,
|
||||
DaisWallet,
|
||||
CreateDaisRequest,
|
||||
DaisCreationResult,
|
||||
DaisTrustLevel,
|
||||
} from '../../domain/dais/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class DaisService {
|
||||
/**
|
||||
* Create a new DAIS identity
|
||||
*/
|
||||
async createIdentity(request: CreateDaisRequest): Promise<DaisCreationResult> {
|
||||
const id = `dais-${uuidv4()}`;
|
||||
const did = `did:daarion:${uuidv4()}`;
|
||||
|
||||
try {
|
||||
// Create DAIS identity
|
||||
const identity = await db.query<DaisIdentity>(
|
||||
`INSERT INTO dais_identities (id, did, default_email, default_wallet, trust_level)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[id, did, request.email || null, request.walletAddress || null, 'agent']
|
||||
);
|
||||
|
||||
// Add email if provided
|
||||
if (request.email) {
|
||||
await db.query(
|
||||
`INSERT INTO dais_emails (dais_id, email, verified)
|
||||
VALUES ($1, $2, false)`,
|
||||
[id, request.email]
|
||||
);
|
||||
}
|
||||
|
||||
// Add wallet if provided
|
||||
if (request.walletAddress) {
|
||||
await db.query(
|
||||
`INSERT INTO dais_wallets (dais_id, wallet_address, network, verified)
|
||||
VALUES ($1, $2, $3, false)`,
|
||||
[id, request.walletAddress, request.network || 'evm']
|
||||
);
|
||||
}
|
||||
|
||||
// Create agent linked to DAIS
|
||||
const agentId = `agent-${uuidv4()}`;
|
||||
const matrixHandle = `@${agentId}:matrix.daarion.city`;
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO agents (id, name, kind, dais_identity_id, agent_role, home_microdao_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[agentId, 'New Agent', 'personal', id, 'regular', 'daarion']
|
||||
);
|
||||
|
||||
// Update DAIS with matrix handle
|
||||
await db.query(
|
||||
`UPDATE dais_identities SET matrix_handle = $1 WHERE id = $2`,
|
||||
[matrixHandle, id]
|
||||
);
|
||||
|
||||
logger.info(`Created DAIS identity: ${id}, agent: ${agentId}`);
|
||||
|
||||
return {
|
||||
identity: identity.rows[0],
|
||||
agentId,
|
||||
matrixHandle,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to create DAIS identity', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAIS profile with all linked identities
|
||||
*/
|
||||
async getProfile(daisId: string): Promise<DaisProfile | null> {
|
||||
try {
|
||||
const identity = await db.query<DaisIdentity>(
|
||||
`SELECT * FROM dais_identities WHERE id = $1`,
|
||||
[daisId]
|
||||
);
|
||||
|
||||
if (identity.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emails = await db.query<DaisEmail>(
|
||||
`SELECT * FROM dais_emails WHERE dais_id = $1`,
|
||||
[daisId]
|
||||
);
|
||||
|
||||
const wallets = await db.query<DaisWallet>(
|
||||
`SELECT * FROM dais_wallets WHERE dais_id = $1`,
|
||||
[daisId]
|
||||
);
|
||||
|
||||
const keys = await db.query(
|
||||
`SELECT * FROM dais_keys WHERE dais_id = $1 AND revoked_at IS NULL`,
|
||||
[daisId]
|
||||
);
|
||||
|
||||
return {
|
||||
identity: identity.rows[0],
|
||||
emails: emails.rows,
|
||||
wallets: wallets.rows,
|
||||
keys: keys.rows,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get DAIS profile: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAIS by agent ID
|
||||
*/
|
||||
async getByAgentId(agentId: string): Promise<DaisProfile | null> {
|
||||
try {
|
||||
const agent = await db.query(
|
||||
`SELECT dais_identity_id FROM agents WHERE id = $1`,
|
||||
[agentId]
|
||||
);
|
||||
|
||||
if (agent.rows.length === 0 || !agent.rows[0].dais_identity_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getProfile(agent.rows[0].dais_identity_id);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get DAIS by agent: ${agentId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add email to DAIS
|
||||
*/
|
||||
async addEmail(daisId: string, email: string): Promise<DaisEmail> {
|
||||
try {
|
||||
const result = await db.query<DaisEmail>(
|
||||
`INSERT INTO dais_emails (dais_id, email, verified)
|
||||
VALUES ($1, $2, false)
|
||||
RETURNING *`,
|
||||
[daisId, email]
|
||||
);
|
||||
|
||||
logger.info(`Added email to DAIS ${daisId}: ${email}`);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add email to DAIS: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email
|
||||
*/
|
||||
async verifyEmail(daisId: string, email: string): Promise<void> {
|
||||
try {
|
||||
await db.query(
|
||||
`UPDATE dais_emails
|
||||
SET verified = true, verified_at = now()
|
||||
WHERE dais_id = $1 AND email = $2`,
|
||||
[daisId, email]
|
||||
);
|
||||
|
||||
// Update trust level if this is first verified email
|
||||
await this.updateTrustLevel(daisId);
|
||||
|
||||
logger.info(`Verified email for DAIS ${daisId}: ${email}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to verify email: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add wallet to DAIS
|
||||
*/
|
||||
async addWallet(
|
||||
daisId: string,
|
||||
walletAddress: string,
|
||||
network: 'evm' | 'ton' | 'solana' = 'evm'
|
||||
): Promise<DaisWallet> {
|
||||
try {
|
||||
const result = await db.query<DaisWallet>(
|
||||
`INSERT INTO dais_wallets (dais_id, wallet_address, network, verified)
|
||||
VALUES ($1, $2, $3, false)
|
||||
RETURNING *`,
|
||||
[daisId, walletAddress, network]
|
||||
);
|
||||
|
||||
logger.info(`Added wallet to DAIS ${daisId}: ${walletAddress}`);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add wallet to DAIS: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify wallet (after SIWE signature)
|
||||
*/
|
||||
async verifyWallet(daisId: string, walletAddress: string): Promise<void> {
|
||||
try {
|
||||
await db.query(
|
||||
`UPDATE dais_wallets
|
||||
SET verified = true, verified_at = now()
|
||||
WHERE dais_id = $1 AND wallet_address = $2`,
|
||||
[daisId, walletAddress]
|
||||
);
|
||||
|
||||
// Update trust level
|
||||
await this.updateTrustLevel(daisId);
|
||||
|
||||
logger.info(`Verified wallet for DAIS ${daisId}: ${walletAddress}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to verify wallet: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update trust level based on verified identities
|
||||
*/
|
||||
private async updateTrustLevel(daisId: string): Promise<void> {
|
||||
const profile = await this.getProfile(daisId);
|
||||
if (!profile) return;
|
||||
|
||||
const hasVerifiedEmail = profile.emails.some(e => e.verified);
|
||||
const hasVerifiedWallet = profile.wallets.some(w => w.verified);
|
||||
|
||||
let newLevel: DaisTrustLevel = 'guest';
|
||||
|
||||
if (hasVerifiedEmail && hasVerifiedWallet) {
|
||||
newLevel = 'verified';
|
||||
} else if (hasVerifiedEmail) {
|
||||
newLevel = 'agent';
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`UPDATE dais_identities SET trust_level = $1, updated_at = now() WHERE id = $2`,
|
||||
[newLevel, daisId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote agent to orchestrator (updates DAIS trust level)
|
||||
*/
|
||||
async promoteToOrchestrator(daisId: string): Promise<void> {
|
||||
try {
|
||||
await db.query(
|
||||
`UPDATE dais_identities
|
||||
SET trust_level = 'orchestrator', updated_at = now()
|
||||
WHERE id = $1`,
|
||||
[daisId]
|
||||
);
|
||||
|
||||
logger.info(`Promoted DAIS to orchestrator: ${daisId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to promote to orchestrator: ${daisId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const daisService = new DaisService();
|
||||
|
||||
Reference in New Issue
Block a user