diff --git a/spec/SPECIFICATION.md b/spec/SPECIFICATION.md index 44eeb59..a57ce3a 100644 --- a/spec/SPECIFICATION.md +++ b/spec/SPECIFICATION.md @@ -9,7 +9,7 @@ The standard is designed to be: - **Framework-agnostic** — works with Claude Code, OpenAI, LangChain, CrewAI, AutoGen, and others - **Git-native** — version control, branching, diffing, and collaboration built in -- **Compliance-ready** — first-class support for FINRA, Federal Reserve, and interagency regulatory requirements +- **Compliance-ready** — first-class support for FINRA, Federal Reserve, interagency regulatory requirements, and segregation of duties - **Composable** — agents can extend, depend on, and delegate to other agents ## 2. Directory Structure @@ -19,6 +19,7 @@ my-agent/ ├── agent.yaml # [REQUIRED] Agent manifest ├── SOUL.md # [REQUIRED] Identity and personality ├── RULES.md # Hard constraints and boundaries +├── DUTIES.md # Segregation of duties policy and role declaration ├── AGENTS.md # Framework-agnostic fallback instructions ├── README.md # Human documentation ├── skills/ # Reusable capability modules @@ -194,6 +195,48 @@ compliance: soc_report_required: false # SOC 2 report required vendor_ai_notification: true # Vendor must notify of AI changes subcontractor_assessment: false # Fourth-party risk assessed + + # Segregation of duties (multi-agent duty separation) + segregation_of_duties: + roles: # Define roles for agents (min 2) + - id: maker # Initiates/creates + description: Creates proposals and initiates actions + permissions: [create, submit] + - id: checker # Reviews/approves + description: Reviews and approves maker outputs + permissions: [review, approve, reject] + - id: executor # Executes approved work + description: Executes approved actions + permissions: [execute] + - id: auditor # Audits completed work + description: Reviews completed actions for compliance + permissions: [audit, report] + + conflicts: # SOD conflict matrix + - [maker, checker] # Maker cannot approve own work + - [maker, auditor] # Maker cannot audit own work + - [executor, checker] # Executor cannot approve what they execute + - [executor, auditor] # Executor cannot audit own execution + + assignments: # Bind roles to agents + loan-originator: [maker] + credit-reviewer: [checker] + loan-processor: [executor] + compliance-auditor: [auditor] + + isolation: + state: full # full | shared | none + credentials: separate # separate | shared + + handoffs: # Critical actions requiring multi-role handoff + - action: credit_decision + required_roles: [maker, checker] + approval_required: true + - action: loan_disbursement + required_roles: [maker, checker, executor] + approval_required: true + + enforcement: strict # strict | advisory ``` ### Example Minimal agent.yaml @@ -365,6 +408,30 @@ For regulated agents, RULES.md should include explicit regulatory constraints: - Never transmit restricted data across jurisdictional boundaries ``` +## 5a. DUTIES.md — Segregation of Duties + +Declares the agent's duties, role boundaries, and the system-wide SOD policy. DUTIES.md exists at two levels: + +**Root level** (`DUTIES.md`) — Documents the system-wide segregation of duties policy: all roles, the conflict matrix, handoff workflows, isolation policy, and enforcement mode. This is the SOD equivalent of `RULES.md` — it defines the policy that all agents in the system must follow. + +**Per-agent level** (`agents//DUTIES.md`) — Declares this specific agent's role, permissions, boundaries, and handoff participation. Each sub-agent's DUTIES.md answers: what is my role, what can I do, what must I not do, and who do I hand off to. + +### Root DUTIES.md Recommended Sections + +- **Roles** — Table of all roles, assigned agents, and permissions +- **Conflict Matrix** — Which role pairs cannot be held by the same agent +- **Handoff Workflows** — Step-by-step handoff chains for critical actions +- **Isolation Policy** — State and credential isolation levels +- **Enforcement** — Strict vs advisory mode + +### Per-Agent DUTIES.md Recommended Sections + +- **Role** — This agent's assigned role +- **Permissions** — What actions this agent can take +- **Boundaries** — Must/must-not rules specific to this role +- **Handoff Participation** — Where this agent sits in handoff chains +- **Isolation** — This agent's isolation constraints + ## 6. AGENTS.md — Framework-Agnostic Instructions Provides fallback instructions compatible with Cursor, Copilot, and other tools that read `AGENTS.md`. This file supplements `agent.yaml` + `SOUL.md` for systems that don't understand the gitagent format. @@ -864,6 +931,13 @@ A valid gitagent repository must: 5. All referenced tools must exist in `tools/` 6. All referenced sub-agents must exist in `agents/` 7. `hooks.yaml` scripts must exist at specified paths +8. If `compliance.segregation_of_duties` is present: + - `roles` must define at least 2 roles with unique IDs + - `conflicts` pairs must reference defined role IDs + - `assignments` must reference defined role IDs + - No agent in `assignments` may hold roles that appear together in `conflicts` + - `handoffs.required_roles` must reference defined role IDs and include at least 2 + - Assigned agents should exist in the `agents` section ## 19. CLI Commands @@ -942,6 +1016,15 @@ All schemas are in `spec/schemas/`: | SR 21-8 | BSA/AML Model Risk | `compliance.model_risk` for AML agents | | CFPB Circular 2022-03 | Adverse Action + AI | `compliance.data_governance.lda_search` | +### Segregation of Duties References + +| Document | Subject | gitagent Impact | +|----------|---------|-----------------| +| FINOS AI Governance Framework | Multi-Agent Isolation & Segmentation | `compliance.segregation_of_duties` | +| SOC 2 Type II | Logical Access Controls | `segregation_of_duties.isolation` | +| SR 11-7 Section IV | Independent Review | `segregation_of_duties.conflicts` (maker/checker separation) | +| FINRA 3110 | Supervisory Systems (duty separation) | `segregation_of_duties.handoffs` | + --- *This specification is a living document. Contributions welcome.* diff --git a/spec/schemas/agent-yaml.schema.json b/spec/schemas/agent-yaml.schema.json index 6bf666c..acb7d91 100644 --- a/spec/schemas/agent-yaml.schema.json +++ b/spec/schemas/agent-yaml.schema.json @@ -429,6 +429,9 @@ }, "vendor_management": { "$ref": "#/$defs/vendor_management_config" + }, + "segregation_of_duties": { + "$ref": "#/$defs/segregation_of_duties_config" } }, "additionalProperties": false, @@ -662,6 +665,116 @@ } }, "additionalProperties": false + }, + + "segregation_of_duties_config": { + "type": "object", + "description": "Segregation of duties configuration for multi-agent systems. Ensures no single agent has complete control over critical processes.", + "properties": { + "roles": { + "type": "array", + "description": "Roles that agents can hold in this system", + "items": { + "type": "object", + "required": ["id", "description"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Role identifier (snake_case)" + }, + "description": { + "type": "string", + "description": "Human-readable role description" + }, + "permissions": { + "type": "array", + "items": { + "type": "string", + "enum": ["create", "submit", "review", "approve", "reject", "execute", "audit", "report"] + }, + "uniqueItems": true, + "description": "Permissions granted to this role" + } + }, + "additionalProperties": false + }, + "minItems": 2 + }, + "conflicts": { + "type": "array", + "description": "Pairs of role IDs that cannot be held by the same agent (SOD matrix)", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "assignments": { + "type": "object", + "description": "Maps agent names to their assigned roles", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "isolation": { + "type": "object", + "description": "Isolation level between agents with different roles", + "properties": { + "state": { + "type": "string", + "enum": ["full", "shared", "none"], + "description": "'full': agents cannot access each other's state. 'shared': read-only cross-access. 'none': no isolation." + }, + "credentials": { + "type": "string", + "enum": ["separate", "shared"], + "description": "'separate': each role has its own credential scope. 'shared': agents share credentials." + } + }, + "additionalProperties": false + }, + "handoffs": { + "type": "array", + "description": "Critical actions requiring multi-role participation", + "items": { + "type": "object", + "required": ["action", "required_roles"], + "properties": { + "action": { + "type": "string", + "description": "Action type requiring handoff (e.g., credit_decision, loan_disbursement)" + }, + "required_roles": { + "type": "array", + "items": { "type": "string" }, + "minItems": 2, + "description": "Roles that must participate sequentially" + }, + "approval_required": { + "type": "boolean", + "description": "Whether explicit approval is needed at each handoff", + "default": true + } + }, + "additionalProperties": false + } + }, + "enforcement": { + "type": "string", + "enum": ["strict", "advisory"], + "description": "'strict': SOD violations are errors. 'advisory': SOD violations are warnings.", + "default": "strict" + } + }, + "additionalProperties": false } } } diff --git a/src/adapters/claude-code.ts b/src/adapters/claude-code.ts index 104cf59..b9c074a 100644 --- a/src/adapters/claude-code.ts +++ b/src/adapters/claude-code.ts @@ -26,6 +26,12 @@ export function exportToClaudeCode(dir: string): string { parts.push(rules); } + // DUTIES.md → segregation of duties policy + const duty = loadFileIfExists(join(agentDir, 'DUTIES.md')); + if (duty) { + parts.push(duty); + } + // Skills — loaded via skill-loader const skillsDir = join(agentDir, 'skills'); const skills = loadAllSkills(skillsDir); @@ -77,6 +83,36 @@ export function exportToClaudeCode(dir: string): string { complianceParts.push('- All actions are audit-logged'); } + if (c.segregation_of_duties) { + const sod = c.segregation_of_duties; + complianceParts.push('\n### Segregation of Duties'); + complianceParts.push(`Enforcement: ${sod.enforcement ?? 'strict'}`); + if (sod.assignments) { + complianceParts.push('\nRole assignments:'); + for (const [agent, roles] of Object.entries(sod.assignments)) { + complianceParts.push(`- ${agent}: ${roles.join(', ')}`); + } + } + if (sod.conflicts) { + complianceParts.push('\nConflict rules (must not be same agent):'); + for (const [a, b] of sod.conflicts) { + complianceParts.push(`- ${a} <-> ${b}`); + } + } + if (sod.handoffs) { + complianceParts.push('\nRequired handoffs:'); + for (const h of sod.handoffs) { + complianceParts.push(`- ${h.action}: ${h.required_roles.join(' → ')}`); + } + } + if (sod.isolation?.state === 'full') { + complianceParts.push('- Agent state is fully isolated per role'); + } + if (sod.isolation?.credentials === 'separate') { + complianceParts.push('- Credentials are segregated per role'); + } + } + parts.push(complianceParts.join('\n')); } diff --git a/src/adapters/system-prompt.ts b/src/adapters/system-prompt.ts index 782bc12..95f5ede 100644 --- a/src/adapters/system-prompt.ts +++ b/src/adapters/system-prompt.ts @@ -25,6 +25,12 @@ export function exportToSystemPrompt(dir: string): string { parts.push(rules); } + // DUTIES.md + const duty = loadFileIfExists(join(agentDir, 'DUTIES.md')); + if (duty) { + parts.push(duty); + } + // Skills — loaded via skill-loader const skillsDir = join(agentDir, 'skills'); const skills = loadAllSkills(skillsDir); @@ -82,6 +88,37 @@ export function exportToSystemPrompt(dir: string): string { constraints.push('- Do not process any personally identifiable information'); } + if (c.segregation_of_duties) { + const sod = c.segregation_of_duties; + constraints.push('- Segregation of duties is enforced:'); + if (sod.assignments) { + for (const [agentName, roles] of Object.entries(sod.assignments)) { + constraints.push(` - Agent "${agentName}" has role(s): ${roles.join(', ')}`); + } + } + if (sod.conflicts) { + constraints.push('- Duty separation rules (no single agent may hold both):'); + for (const [a, b] of sod.conflicts) { + constraints.push(` - ${a} and ${b}`); + } + } + if (sod.handoffs) { + constraints.push('- The following actions require multi-agent handoff:'); + for (const h of sod.handoffs) { + constraints.push(` - ${h.action}: must pass through roles ${h.required_roles.join(' → ')}${h.approval_required !== false ? ' (approval required)' : ''}`); + } + } + if (sod.isolation?.state === 'full') { + constraints.push('- Agent state/memory is fully isolated per role — do not access another agent\'s state'); + } + if (sod.isolation?.credentials === 'separate') { + constraints.push('- Credentials are segregated per role — use only credentials assigned to your role'); + } + if (sod.enforcement === 'strict') { + constraints.push('- SOD enforcement is STRICT — violations will block execution'); + } + } + if (constraints.length > 0) { parts.push(`## Compliance Constraints\n${constraints.join('\n')}`); } diff --git a/src/commands/audit.ts b/src/commands/audit.ts index cfeda64..1d4a5ed 100644 --- a/src/commands/audit.ts +++ b/src/commands/audit.ts @@ -162,8 +162,65 @@ export const auditCommand = new Command('audit') info(' No vendor dependencies — vendor management not required'); } + // Segregation of Duties + heading('8. Segregation of Duties'); + if (c.segregation_of_duties) { + const sod = c.segregation_of_duties; + auditCheck('Roles defined (≥2)', !!(sod.roles && sod.roles.length >= 2)); + auditCheck('Conflict matrix defined', !!(sod.conflicts && sod.conflicts.length > 0)); + auditCheck('Role assignments configured', !!(sod.assignments && Object.keys(sod.assignments).length > 0)); + auditCheck('State isolation configured', !!sod.isolation?.state); + auditCheck('State isolation is full', sod.isolation?.state === 'full'); + auditCheck('Credential segregation configured', !!sod.isolation?.credentials); + auditCheck('Credentials are separate', sod.isolation?.credentials === 'separate'); + auditCheck('Handoff workflows defined', !!(sod.handoffs && sod.handoffs.length > 0)); + auditCheck('Enforcement is strict', sod.enforcement === 'strict'); + + if (sod.assignments) { + info(' Role assignments:'); + for (const [agent, roles] of Object.entries(sod.assignments)) { + label(' ' + agent, roles.join(', ')); + } + } + + if (sod.conflicts) { + info(' Conflict rules:'); + for (const [a, b] of sod.conflicts) { + info(` ${a} <-> ${b}`); + } + } + + // Check for SOD violations in assignments + if (sod.assignments && sod.conflicts) { + let violationFound = false; + for (const [agentName, assignedRoles] of Object.entries(sod.assignments)) { + for (const [roleA, roleB] of sod.conflicts) { + if (assignedRoles.includes(roleA) && assignedRoles.includes(roleB)) { + error(` VIOLATION: Agent "${agentName}" holds conflicting roles "${roleA}" and "${roleB}"`); + violationFound = true; + } + } + } + if (!violationFound) { + success(' No SOD violations detected in role assignments'); + } + } + + if (sod.handoffs) { + info(' Handoff requirements:'); + for (const h of sod.handoffs) { + label(' ' + h.action, `${h.required_roles.join(' → ')} (approval: ${h.approval_required !== false ? 'yes' : 'no'})`); + } + } + } else if (manifest.agents && Object.keys(manifest.agents).length >= 2) { + warn(' Segregation of duties not configured for multi-agent system'); + warn(' Consider adding segregation_of_duties for duty separation controls'); + } else { + info(' Single-agent system — segregation of duties not applicable'); + } + // Compliance artifacts - heading('8. Compliance Artifacts'); + heading('9. Compliance Artifacts'); auditCheck('compliance/ directory exists', existsSync(join(dir, 'compliance'))); auditCheck('regulatory-map.yaml exists', existsSync(join(dir, 'compliance', 'regulatory-map.yaml'))); auditCheck('validation-schedule.yaml exists', existsSync(join(dir, 'compliance', 'validation-schedule.yaml'))); @@ -171,7 +228,7 @@ export const auditCommand = new Command('audit') auditCheck('RULES.md exists', existsSync(join(dir, 'RULES.md'))); // Hooks for audit trail - heading('9. Audit Hooks'); + heading('10. Audit Hooks'); const hooksExist = existsSync(join(dir, 'hooks', 'hooks.yaml')); auditCheck('hooks/hooks.yaml exists', hooksExist); if (hooksExist) { diff --git a/src/commands/init.ts b/src/commands/init.ts index 3eff35f..38c9f55 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -262,6 +262,36 @@ metadata: Describe the skill instructions here. `; +const FULL_DUTIES_MD = `# Duties + +System-wide segregation of duties policy. + +## Roles + +| Role | Agent | Permissions | Description | +|------|-------|-------------|-------------| +| (define roles) | (assign agents) | (list permissions) | (describe duty) | + +## Conflict Matrix + +No single agent may hold both roles in any pair: + +- (define role conflicts) + +## Handoff Workflows + +(Define critical actions that require multi-role handoff) + +## Isolation Policy + +- **State isolation:** (full | shared | none) +- **Credential segregation:** (separate | shared) + +## Enforcement + +(strict | advisory) +`; + const REGULATORY_MAP = `mappings: [] `; @@ -346,6 +376,7 @@ export const initCommand = new Command('init') createFile(join(dir, 'SOUL.md'), STANDARD_SOUL_MD); createFile(join(dir, 'RULES.md'), FULL_RULES_MD); createFile(join(dir, 'AGENTS.md'), AGENTS_MD); + createFile(join(dir, 'DUTIES.md'), FULL_DUTIES_MD); createDir(join(dir, 'skills', 'example-skill')); createFile(join(dir, 'skills', 'example-skill', 'SKILL.md'), SKILL_MD); @@ -392,6 +423,7 @@ export const initCommand = new Command('init') success('Created SOUL.md'); success('Created RULES.md'); success('Created AGENTS.md'); + success('Created DUTIES.md'); success('Created skills/example-skill/SKILL.md'); success('Created knowledge/index.yaml'); success('Created memory/MEMORY.md + memory.yaml'); diff --git a/src/commands/validate.ts b/src/commands/validate.ts index a5fa696..b568a21 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -236,6 +236,140 @@ function validateCompliance(dir: string): ValidationResult { } } + // Segregation of Duties validation + const sod = c.segregation_of_duties; + if (sod) { + const roleIds = sod.roles?.map(r => r.id) ?? []; + + // Must define at least 2 roles + if (!sod.roles || sod.roles.length < 2) { + result.valid = false; + result.errors.push('[SOD] segregation_of_duties.roles must define at least 2 roles'); + } + + // Role IDs must be unique + if (roleIds.length !== new Set(roleIds).size) { + result.valid = false; + result.errors.push('[SOD] segregation_of_duties.roles contains duplicate role IDs'); + } + + // Conflict pairs must reference defined roles + if (sod.conflicts) { + for (const pair of sod.conflicts) { + for (const roleId of pair) { + if (!roleIds.includes(roleId)) { + result.valid = false; + result.errors.push( + `[SOD] Conflict references undefined role "${roleId}". Defined roles: ${roleIds.join(', ')}` + ); + } + } + if (pair[0] === pair[1]) { + result.valid = false; + result.errors.push(`[SOD] Role "${pair[0]}" cannot conflict with itself`); + } + } + } + + // Assignments must reference defined roles and check for conflicts + if (sod.assignments) { + for (const [agentName, assignedRoles] of Object.entries(sod.assignments)) { + for (const roleId of assignedRoles) { + if (!roleIds.includes(roleId)) { + result.valid = false; + result.errors.push(`[SOD] Agent "${agentName}" assigned undefined role "${roleId}"`); + } + } + + // Core SOD check: no agent holds conflicting roles + if (sod.conflicts) { + for (const [roleA, roleB] of sod.conflicts) { + if (assignedRoles.includes(roleA) && assignedRoles.includes(roleB)) { + const msg = `[SOD] Agent "${agentName}" holds conflicting roles: "${roleA}" and "${roleB}"`; + if (sod.enforcement === 'advisory') { + result.warnings.push(msg); + } else { + result.valid = false; + result.errors.push(msg); + } + } + } + } + + // Assigned agents should exist in manifest.agents + if (manifest.agents && !manifest.agents[agentName]) { + result.warnings.push(`[SOD] Agent "${agentName}" in assignments not found in agents section`); + } + } + } + + // Handoff required_roles must reference defined roles + if (sod.handoffs) { + for (const handoff of sod.handoffs) { + for (const roleId of handoff.required_roles) { + if (!roleIds.includes(roleId)) { + result.valid = false; + result.errors.push( + `[SOD] Handoff for "${handoff.action}" references undefined role "${roleId}"` + ); + } + } + const uniqueRoles = new Set(handoff.required_roles); + if (uniqueRoles.size < 2) { + result.valid = false; + result.errors.push( + `[SOD] Handoff for "${handoff.action}" must require at least 2 distinct roles` + ); + } + } + } + + // High/critical risk tier recommendations + if (c.risk_tier === 'high' || c.risk_tier === 'critical') { + if (sod.enforcement === 'advisory') { + result.warnings.push( + `[SOD] Risk tier "${c.risk_tier}" recommends enforcement: "strict", got "advisory"` + ); + } + if (!sod.isolation || sod.isolation.state !== 'full') { + result.warnings.push( + `[SOD] Risk tier "${c.risk_tier}" recommends isolation.state: "full" for full state segregation` + ); + } + if (!sod.isolation || sod.isolation.credentials !== 'separate') { + result.warnings.push( + `[SOD] Risk tier "${c.risk_tier}" recommends isolation.credentials: "separate"` + ); + } + } + + // SOD without conflicts is meaningless + if (!sod.conflicts || sod.conflicts.length === 0) { + result.warnings.push( + '[SOD] No conflicts defined — segregation_of_duties without conflict rules has no enforcement value' + ); + } + + // Every role should be assigned to at least one agent + if (sod.assignments && sod.roles) { + const assignedRoleIds = new Set(Object.values(sod.assignments).flat()); + for (const role of sod.roles) { + if (!assignedRoleIds.has(role.id)) { + result.warnings.push(`[SOD] Role "${role.id}" is defined but not assigned to any agent`); + } + } + } + } + + // Recommend SOD for multi-agent high/critical risk setups + if (!sod && manifest.agents && Object.keys(manifest.agents).length >= 2) { + if (c.risk_tier === 'high' || c.risk_tier === 'critical') { + result.warnings.push( + '[SOD] Multi-agent system with high/critical risk tier — consider configuring segregation_of_duties' + ); + } + } + return result; } diff --git a/src/utils/loader.ts b/src/utils/loader.ts index 991ebcf..38f5084 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -115,6 +115,25 @@ export interface ComplianceConfig { vendor_ai_notification?: boolean; subcontractor_assessment?: boolean; }; + segregation_of_duties?: { + roles?: Array<{ + id: string; + description: string; + permissions?: string[]; + }>; + conflicts?: Array<[string, string]>; + assignments?: Record; + isolation?: { + state?: string; + credentials?: string; + }; + handoffs?: Array<{ + action: string; + required_roles: string[]; + approval_required?: boolean; + }>; + enforcement?: string; + }; } export function loadAgentManifest(dir: string): AgentManifest {