Add SDLC review and eval controls
This commit is contained in:
1 parent
bc14a2fbcb
commit
bbf90a1a62
19 files changed
+927
-2
No files matched your search
@@ -17,6 +17,7 @@ import { ComplianceService } from "../services/compliance-service";
|
||||
import { SearchService } from "../services/search-service";
|
||||
import { IdentityAccessService } from "../services/identity-access-service";
|
||||
import { OperationsService } from "../services/operations-service";
|
||||
import { SdlcService } from "../services/sdlc-service";
|
||||
|
||||
export interface IpcDeps {
|
||||
connection: DatabaseConnection;
|
||||
@@ -38,6 +39,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
const search = new SearchService(deps.connection.db);
|
||||
const identityAccess = new IdentityAccessService(deps.connection.db);
|
||||
const operations = new OperationsService(deps.connection.db);
|
||||
const sdlc = new SdlcService(deps.connection.db);
|
||||
const onboarding = new OnboardingService({
|
||||
db: deps.connection.db,
|
||||
saveProviderCredential: (input) => providers.saveCredential(input),
|
||||
@@ -157,6 +159,9 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
ipcMain.handle(IPC_CHANNELS.createMigrationReport, (_event, input) => mutate(() => migrations.createReport(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setFeatureFlag, (_event, input) => mutate(() => operations.setFeatureFlag(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runChaosDrill, (_event, input) => mutate(() => operations.runChaosDrill(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runSdlcReview, (_event, input) => mutate(() => sdlc.runReview(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.runEvalSuite, (_event, input) => mutate(() => sdlc.runEvalSuite(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setPrBotGate, (_event, input) => mutate(() => sdlc.setPrBotGate(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createComplianceExport, (_event, input) => mutate(() => compliance.createExport(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createLegalHold, (_event, input) => mutate(() => compliance.createLegalHold(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setLegalHoldState, (_event, input) => mutate(() => compliance.setLegalHoldState(input)));
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
export const sdlcControlsSchema = `
|
||||
create table if not exists sdlc_review_agents (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
specialty text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists sdlc_review_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
review_agent_id text not null references sdlc_review_agents(id) on delete cascade,
|
||||
target_type text not null,
|
||||
target_ref text not null,
|
||||
state text not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists eval_suites (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
scope text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists eval_cases (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
title text not null,
|
||||
source_type text not null,
|
||||
source_ref text not null,
|
||||
expected_signal text not null,
|
||||
created_at text not null,
|
||||
unique(suite_id, title)
|
||||
);
|
||||
|
||||
create table if not exists eval_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
state text not null,
|
||||
passed_count integer not null,
|
||||
failed_count integer not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists pr_bot_gates (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
gate_type text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create index if not exists idx_sdlc_review_runs_org_agent_created
|
||||
on sdlc_review_runs(org_id, review_agent_id, created_at);
|
||||
|
||||
create index if not exists idx_eval_runs_org_suite_created
|
||||
on eval_runs(org_id, suite_id, created_at);
|
||||
|
||||
insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at)
|
||||
select 'sra_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.specialty, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'code_reviewer' as key, 'Code Review Agent' as label, 'static quality and architecture' as specialty
|
||||
union all select 'migration_reviewer', 'Migration Review Agent', 'schema diff and rollback evidence'
|
||||
union all select 'policy_reviewer', 'Policy Review Agent', 'provider, approval, and access policy'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from sdlc_review_agents
|
||||
where sdlc_review_agents.org_id = organizations.id and sdlc_review_agents.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at)
|
||||
select 'evs_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.scope, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'contract' as key, 'Contract checks' as label, 'provider adapters, event schemas, slash commands' as scope
|
||||
union all select 'integration', 'Integration checks', 'chat, approval, file, skill resolution flows'
|
||||
union all select 'replay', 'Replay checks', 'audit timeline and historical decision regression'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from eval_suites
|
||||
where eval_suites.org_id = organizations.id and eval_suites.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at)
|
||||
select 'evc_' || lower(hex(randomblob(16))), eval_suites.org_id, eval_suites.id, seed.title, seed.source_type, seed.source_ref, seed.expected_signal, datetime('now')
|
||||
from eval_suites
|
||||
join (
|
||||
select 'contract' as suite_key, 'Provider catalog has adapters' as title, 'provider_gateway' as source_type, 'providers' as source_ref, 'adapter_coverage' as expected_signal
|
||||
union all select 'contract', 'Schema ledger is current', 'database', 'schema_migrations', 'migration_ledger'
|
||||
union all select 'integration', 'Approval routes are assigned', 'org_governance', 'approval_assignments', 'route_coverage'
|
||||
union all select 'integration', 'Skills can resolve runtime snapshots', 'skills_platform', 'skill_runtime_snapshots', 'skill_resolution'
|
||||
union all select 'replay', 'Audit timeline has domain events', 'audit', 'audit_events', 'replayable_timeline'
|
||||
union all select 'replay', 'Operational signals are captured', 'operations', 'operational_events', 'slo_signal'
|
||||
) seed on seed.suite_key = eval_suites.key
|
||||
where not exists (
|
||||
select 1 from eval_cases
|
||||
where eval_cases.suite_id = eval_suites.id and eval_cases.title = seed.title
|
||||
);
|
||||
|
||||
insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at)
|
||||
select 'pbg_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.gate_type, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'static_checks' as key, 'Static checks' as label, 'typecheck_lint_test' as gate_type
|
||||
union all select 'migration_diff', 'Migration diff' as label, 'schema_review' as gate_type
|
||||
union all select 'policy_diff', 'Policy diff' as label, 'policy_review' as gate_type
|
||||
union all select 'human_approval', 'Human approval' as label, 'approval_required' as gate_type
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from pr_bot_gates
|
||||
where pr_bot_gates.org_id = organizations.id and pr_bot_gates.key = seed.key
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (18, 'sdlc_controls', datetime('now'));
|
||||
`;
|
||||
@@ -16,6 +16,7 @@ import { providerPolicyUsageSchema } from "./migrations/014-provider-policy-usag
|
||||
import { identityAccessSchema } from "./migrations/015-identity-access";
|
||||
import { orgGovernanceSchema } from "./migrations/016-org-governance";
|
||||
import { operationsControlsSchema } from "./migrations/017-operations-controls";
|
||||
import { sdlcControlsSchema } from "./migrations/018-sdlc-controls";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -41,6 +42,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 15, name: "identity_access", sql: identityAccessSchema },
|
||||
{ version: 16, name: "org_governance", sql: orgGovernanceSchema },
|
||||
{ version: 17, name: "operations_controls", sql: operationsControlsSchema },
|
||||
{ version: 18, name: "sdlc_controls", sql: sdlcControlsSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -31,6 +31,12 @@ const LEGACY_TABLES = [
|
||||
"operational_events",
|
||||
"chaos_drills",
|
||||
"chaos_drill_runs",
|
||||
"sdlc_review_agents",
|
||||
"sdlc_review_runs",
|
||||
"eval_suites",
|
||||
"eval_cases",
|
||||
"eval_runs",
|
||||
"pr_bot_gates",
|
||||
"chat_spaces",
|
||||
"chat_messages",
|
||||
"chat_threads",
|
||||
@@ -142,6 +148,15 @@ const RELATION_CHECKS: RelationCheck[] = [
|
||||
{ table: "chaos_drills", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chaos_drill_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chaos_drill_runs", column: "drill_id", referencedTable: "chaos_drills", referencedColumn: "id", reason: "dangling_drill_id" },
|
||||
{ table: "sdlc_review_agents", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "sdlc_review_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "sdlc_review_runs", column: "review_agent_id", referencedTable: "sdlc_review_agents", referencedColumn: "id", reason: "dangling_review_agent_id" },
|
||||
{ table: "eval_suites", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_cases", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_cases", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" },
|
||||
{ table: "eval_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "eval_runs", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" },
|
||||
{ table: "pr_bot_gates", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_spaces", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_messages", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "chat_messages", column: "space_id", referencedTable: "chat_spaces", referencedColumn: "id", reason: "dangling_space_id" },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PolicyService } from "./policy-service";
|
||||
import { IdentityAccessService } from "./identity-access-service";
|
||||
import { OrgGovernanceService } from "./org-governance-service";
|
||||
import { OperationsService } from "./operations-service";
|
||||
import { SdlcService } from "./sdlc-service";
|
||||
import { assertUsableMentionName } from "./mention-handles";
|
||||
|
||||
export class OrganizationService {
|
||||
@@ -16,6 +17,7 @@ export class OrganizationService {
|
||||
private readonly identityAccess: IdentityAccessService;
|
||||
private readonly governance: OrgGovernanceService;
|
||||
private readonly operations: OperationsService;
|
||||
private readonly sdlc: SdlcService;
|
||||
|
||||
constructor(private readonly db: DatabaseSync) {
|
||||
this.audit = new AuditService(db);
|
||||
@@ -23,6 +25,7 @@ export class OrganizationService {
|
||||
this.identityAccess = new IdentityAccessService(db);
|
||||
this.governance = new OrgGovernanceService(db);
|
||||
this.operations = new OperationsService(db);
|
||||
this.sdlc = new SdlcService(db);
|
||||
}
|
||||
|
||||
createOrganization(input: CreateOrganizationInput): string {
|
||||
@@ -52,6 +55,7 @@ export class OrganizationService {
|
||||
this.identityAccess.seedEmployeeAccess(orgId, employeeId, now);
|
||||
this.governance.seedOrganizationGovernance(orgId, employeeId, now);
|
||||
this.operations.seedOrganizationOperations(orgId, now);
|
||||
this.sdlc.seedOrganizationSdlc(orgId, now);
|
||||
|
||||
for (const space of [
|
||||
["company", "general", "Company coordination", "workspace"],
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { RunEvalSuiteInput, RunSdlcReviewInput, SetPrBotGateInput } from "../../shared/commands";
|
||||
import {
|
||||
EVAL_SUITE_KEY,
|
||||
PR_BOT_GATE_KEY,
|
||||
SDLC_REVIEW_AGENT_KEY,
|
||||
SDLC_REVIEW_TARGET_TYPE,
|
||||
SDLC_RUN_STATE,
|
||||
type EvalSuiteKey,
|
||||
type SdlcReviewAgentKey,
|
||||
type SdlcReviewTargetType,
|
||||
} from "../../shared/domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
import { createId } from "../../shared/ids";
|
||||
import { AuditService } from "./audit-service";
|
||||
import { providerAdapters } from "./provider-adapters";
|
||||
|
||||
const REVIEW_AGENTS = [
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.CODE_REVIEWER, label: "Code Review Agent", specialty: "static quality and architecture" },
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER, label: "Migration Review Agent", specialty: "schema diff and rollback evidence" },
|
||||
{ key: SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER, label: "Policy Review Agent", specialty: "provider, approval, and access policy" },
|
||||
] as const;
|
||||
|
||||
const EVAL_SUITES = [
|
||||
{ key: EVAL_SUITE_KEY.CONTRACT, label: "Contract checks", scope: "provider adapters, event schemas, slash commands" },
|
||||
{ key: EVAL_SUITE_KEY.INTEGRATION, label: "Integration checks", scope: "chat, approval, file, skill resolution flows" },
|
||||
{ key: EVAL_SUITE_KEY.REPLAY, label: "Replay checks", scope: "audit timeline and historical decision regression" },
|
||||
] as const;
|
||||
|
||||
const EVAL_CASES = [
|
||||
{ suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Provider catalog has adapters", sourceType: "provider_gateway", sourceRef: "providers", expectedSignal: "adapter_coverage" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Schema ledger is current", sourceType: "database", sourceRef: "schema_migrations", expectedSignal: "migration_ledger" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Approval routes are assigned", sourceType: "org_governance", sourceRef: "approval_assignments", expectedSignal: "route_coverage" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Skills can resolve runtime snapshots", sourceType: "skills_platform", sourceRef: "skill_runtime_snapshots", expectedSignal: "skill_resolution" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Audit timeline has domain events", sourceType: "audit", sourceRef: "audit_events", expectedSignal: "replayable_timeline" },
|
||||
{ suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Operational signals are captured", sourceType: "operations", sourceRef: "operational_events", expectedSignal: "slo_signal" },
|
||||
] as const;
|
||||
|
||||
const PR_BOT_GATES = [
|
||||
{ key: PR_BOT_GATE_KEY.STATIC_CHECKS, label: "Static checks", gateType: "typecheck_lint_test" },
|
||||
{ key: PR_BOT_GATE_KEY.MIGRATION_DIFF, label: "Migration diff", gateType: "schema_review" },
|
||||
{ key: PR_BOT_GATE_KEY.POLICY_DIFF, label: "Policy diff", gateType: "policy_review" },
|
||||
{ key: PR_BOT_GATE_KEY.HUMAN_APPROVAL, label: "Human approval", gateType: "approval_required" },
|
||||
] as const;
|
||||
|
||||
interface ReviewAgentRow {
|
||||
id: string;
|
||||
key: SdlcReviewAgentKey;
|
||||
enabled: number;
|
||||
}
|
||||
|
||||
interface EvalSuiteRow {
|
||||
id: string;
|
||||
key: EvalSuiteKey;
|
||||
}
|
||||
|
||||
interface GateCheck {
|
||||
key: string;
|
||||
passed: boolean;
|
||||
value: number | string | boolean;
|
||||
}
|
||||
|
||||
export class SdlcService {
|
||||
private readonly audit: AuditService;
|
||||
|
||||
constructor(private readonly db: DatabaseSync) {
|
||||
this.audit = new AuditService(db);
|
||||
}
|
||||
|
||||
seedOrganizationSdlc(orgId: string, now: string): void {
|
||||
for (const agent of REVIEW_AGENTS) {
|
||||
this.db.prepare(`
|
||||
insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, 1, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("sra"), orgId, agent.key, agent.label, agent.specialty, now, now);
|
||||
}
|
||||
for (const suite of EVAL_SUITES) {
|
||||
this.db.prepare(`
|
||||
insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("evs"), orgId, suite.key, suite.label, suite.scope, now, now);
|
||||
}
|
||||
for (const item of EVAL_CASES) {
|
||||
const suite = this.requireSuiteByKey(orgId, item.suiteKey);
|
||||
this.db.prepare(`
|
||||
insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(suite_id, title) do nothing
|
||||
`).run(createId("evc"), orgId, suite.id, item.title, item.sourceType, item.sourceRef, item.expectedSignal, now);
|
||||
}
|
||||
for (const gate of PR_BOT_GATES) {
|
||||
this.db.prepare(`
|
||||
insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, 1, ?, ?)
|
||||
on conflict(org_id, key) do nothing
|
||||
`).run(createId("pbg"), orgId, gate.key, gate.label, gate.gateType, now, now);
|
||||
}
|
||||
}
|
||||
|
||||
runReview(input: RunSdlcReviewInput): string {
|
||||
const agent = this.db.prepare("select id, key, enabled from sdlc_review_agents where id = ? and org_id = ?")
|
||||
.get(input.reviewAgentId, input.orgId) as ReviewAgentRow | undefined;
|
||||
if (!agent) throw new Error("SDLC review agent was not found.");
|
||||
if (agent.enabled !== 1) throw new Error("SDLC review agent is disabled.");
|
||||
this.assertTarget(input.orgId, input.targetType, input.targetRef);
|
||||
const checks = this.reviewChecks(input.orgId, agent.key, input.targetType);
|
||||
const state = checks.every((check) => check.passed) ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED;
|
||||
const id = createId("srr");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into sdlc_review_runs(id, org_id, review_agent_id, target_type, target_ref, state, result_json, created_at, completed_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, input.orgId, input.reviewAgentId, input.targetType, input.targetRef, state, JSON.stringify({ checks }), now, now);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "sdlc_review_run",
|
||||
entityId: id,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "created",
|
||||
payload: { reviewAgentKey: agent.key, state, targetType: input.targetType },
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
runEvalSuite(input: RunEvalSuiteInput): string {
|
||||
const suite = this.db.prepare("select id, key from eval_suites where id = ? and org_id = ?")
|
||||
.get(input.suiteId, input.orgId) as EvalSuiteRow | undefined;
|
||||
if (!suite) throw new Error("Eval suite was not found.");
|
||||
const checks = this.evalChecks(input.orgId, suite.key);
|
||||
const passedCount = checks.filter((check) => check.passed).length;
|
||||
const failedCount = checks.length - passedCount;
|
||||
const state = failedCount === 0 ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED;
|
||||
const id = createId("evr");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into eval_runs(id, org_id, suite_id, state, passed_count, failed_count, result_json, created_at, completed_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, input.orgId, input.suiteId, state, passedCount, failedCount, JSON.stringify({ checks }), now, now);
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "eval_run",
|
||||
entityId: id,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "created",
|
||||
payload: { suiteKey: suite.key, state, passedCount, failedCount },
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
setPrBotGate(input: SetPrBotGateInput): void {
|
||||
const gate = this.db.prepare("select key from pr_bot_gates where id = ? and org_id = ?")
|
||||
.get(input.gateId, input.orgId) as { key: string } | undefined;
|
||||
if (!gate) throw new Error("PR bot gate was not found.");
|
||||
const now = new Date().toISOString();
|
||||
const result = this.db.prepare("update pr_bot_gates set enabled = ?, updated_at = ? where id = ? and org_id = ?")
|
||||
.run(input.enabled ? 1 : 0, now, input.gateId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("PR bot gate was not found.");
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "pr_bot_gate",
|
||||
entityId: input.gateId,
|
||||
actorType: "system",
|
||||
actorId: null,
|
||||
action: "updated",
|
||||
payload: { key: gate.key, enabled: input.enabled },
|
||||
});
|
||||
}
|
||||
|
||||
private requireSuiteByKey(orgId: string, key: EvalSuiteKey): { id: string } {
|
||||
const suite = this.db.prepare("select id from eval_suites where org_id = ? and key = ?")
|
||||
.get(orgId, key) as { id: string } | undefined;
|
||||
if (!suite) throw new Error("Eval suite is required.");
|
||||
return suite;
|
||||
}
|
||||
|
||||
private assertTarget(orgId: string, type: SdlcReviewTargetType, ref: string): void {
|
||||
if (!Object.values(SDLC_REVIEW_TARGET_TYPE).includes(type)) throw new Error("SDLC review target type is invalid.");
|
||||
const targetRef = ref.trim();
|
||||
if (!targetRef) throw new Error("SDLC review target is required.");
|
||||
if (type === SDLC_REVIEW_TARGET_TYPE.WORKSPACE) {
|
||||
const org = this.db.prepare("select id from organizations where id = ?").get(targetRef);
|
||||
if (!org || targetRef !== orgId) throw new Error("SDLC workspace target is invalid.");
|
||||
return;
|
||||
}
|
||||
if (type === SDLC_REVIEW_TARGET_TYPE.MIGRATION) {
|
||||
const report = this.db.prepare("select id from migration_reports where id = ? and org_id = ?").get(targetRef, orgId);
|
||||
if (!report) throw new Error("SDLC migration target was not found.");
|
||||
return;
|
||||
}
|
||||
const policy = this.db.prepare("select id from policy_rules where id = ? and org_id = ?").get(targetRef, orgId);
|
||||
if (!policy) throw new Error("SDLC policy target was not found.");
|
||||
}
|
||||
|
||||
private reviewChecks(orgId: string, agentKey: SdlcReviewAgentKey, targetType: SdlcReviewTargetType): GateCheck[] {
|
||||
if (agentKey === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) return this.migrationReviewChecks(orgId);
|
||||
if (agentKey === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) return this.policyReviewChecks(orgId);
|
||||
return this.codeReviewChecks(orgId, targetType);
|
||||
}
|
||||
|
||||
private codeReviewChecks(orgId: string, targetType: SdlcReviewTargetType): GateCheck[] {
|
||||
const enabledGates = count(this.db, "select count(*) as count from pr_bot_gates where org_id = ? and enabled = 1", orgId);
|
||||
const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId);
|
||||
return [
|
||||
{ key: "target_type", passed: targetType === SDLC_REVIEW_TARGET_TYPE.WORKSPACE, value: targetType },
|
||||
{ key: "enabled_pr_gates", passed: enabledGates >= 3, value: enabledGates },
|
||||
{ key: "recent_audit_events", passed: auditEvents > 0, value: auditEvents },
|
||||
];
|
||||
}
|
||||
|
||||
private migrationReviewChecks(orgId: string): GateCheck[] {
|
||||
const reports = this.db.prepare("select report_json from migration_reports where org_id = ?").all(orgId) as Array<{ report_json: string }>;
|
||||
const rollbackPacks = reports.filter((report) => hasObjectField(report.report_json, "rollbackPack")).length;
|
||||
const migrationGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.MIGRATION_DIFF);
|
||||
return [
|
||||
{ key: "migration_gate_enabled", passed: migrationGateEnabled, value: migrationGateEnabled },
|
||||
{ key: "rollback_pack_coverage", passed: reports.length === rollbackPacks, value: `${rollbackPacks}/${reports.length}` },
|
||||
];
|
||||
}
|
||||
|
||||
private policyReviewChecks(orgId: string): GateCheck[] {
|
||||
const providerConflicts = count(this.db, "select count(*) as count from providers where org_id = ? and data_policy = 'zero_data_retention' and background_enabled = 1", orgId);
|
||||
const routeRules = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId);
|
||||
const policyGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.POLICY_DIFF);
|
||||
return [
|
||||
{ key: "policy_gate_enabled", passed: policyGateEnabled, value: policyGateEnabled },
|
||||
{ key: "provider_policy_conflicts", passed: providerConflicts === 0, value: providerConflicts },
|
||||
{ key: "approval_routes", passed: routeRules > 0, value: routeRules },
|
||||
];
|
||||
}
|
||||
|
||||
private evalChecks(orgId: string, suiteKey: EvalSuiteKey): GateCheck[] {
|
||||
if (suiteKey === EVAL_SUITE_KEY.CONTRACT) return this.contractEvalChecks();
|
||||
if (suiteKey === EVAL_SUITE_KEY.INTEGRATION) return this.integrationEvalChecks(orgId);
|
||||
return this.replayEvalChecks(orgId);
|
||||
}
|
||||
|
||||
private contractEvalChecks(): GateCheck[] {
|
||||
const catalogKeys = PROVIDER_CATALOG.map((provider) => provider.key);
|
||||
const adapterKeys = Object.keys(providerAdapters);
|
||||
const missing = catalogKeys.filter((key) => !adapterKeys.includes(key));
|
||||
const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations");
|
||||
return [
|
||||
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
|
||||
{ key: "schema_version", passed: schemaVersion >= 18, value: schemaVersion },
|
||||
];
|
||||
}
|
||||
|
||||
private integrationEvalChecks(orgId: string): GateCheck[] {
|
||||
const chatSpaces = count(this.db, "select count(*) as count from chat_spaces where org_id = ?", orgId);
|
||||
const approvalRoutes = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId);
|
||||
const skillSnapshots = count(this.db, `
|
||||
select count(*) as count
|
||||
from skill_runtime_snapshots
|
||||
join agents on agents.id = skill_runtime_snapshots.agent_id
|
||||
where agents.org_id = ?
|
||||
`, orgId);
|
||||
return [
|
||||
{ key: "chat_spaces", passed: chatSpaces > 0, value: chatSpaces },
|
||||
{ key: "approval_routes", passed: approvalRoutes > 0, value: approvalRoutes },
|
||||
{ key: "skill_runtime_snapshots", passed: skillSnapshots > 0, value: skillSnapshots },
|
||||
];
|
||||
}
|
||||
|
||||
private replayEvalChecks(orgId: string): GateCheck[] {
|
||||
const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId);
|
||||
const operationalEvents = count(this.db, "select count(*) as count from operational_events where org_id = ?", orgId);
|
||||
return [
|
||||
{ key: "audit_events", passed: auditEvents > 0, value: auditEvents },
|
||||
{ key: "operational_events", passed: operationalEvents > 0, value: operationalEvents },
|
||||
];
|
||||
}
|
||||
|
||||
private isGateEnabled(orgId: string, key: string): boolean {
|
||||
const row = this.db.prepare("select enabled from pr_bot_gates where org_id = ? and key = ?")
|
||||
.get(orgId, key) as { enabled: number } | undefined;
|
||||
if (!row) throw new Error("PR bot gate is required.");
|
||||
return row.enabled === 1;
|
||||
}
|
||||
}
|
||||
|
||||
function count(db: DatabaseSync, sql: string, ...params: string[]): number {
|
||||
return (db.prepare(sql).get(...params) as { count: number }).count;
|
||||
}
|
||||
|
||||
function hasObjectField(json: string, field: string): boolean {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>;
|
||||
return typeof parsed[field] === "object" && parsed[field] !== null;
|
||||
}
|
||||
@@ -59,6 +59,10 @@ export class SnapshotService {
|
||||
.map((event) => ({ ...event, success: toBool(event.success) }));
|
||||
const chaosDrills = list<AppSnapshot["chaosDrills"][number]>(this.db, "chaos_drills", "key asc")
|
||||
.map((drill) => ({ ...drill, enabled: toBool(drill.enabled) }));
|
||||
const sdlcReviewAgents = list<AppSnapshot["sdlcReviewAgents"][number]>(this.db, "sdlc_review_agents", "key asc")
|
||||
.map((agent) => ({ ...agent, enabled: toBool(agent.enabled) }));
|
||||
const prBotGates = list<AppSnapshot["prBotGates"][number]>(this.db, "pr_bot_gates", "key asc")
|
||||
.map((gate) => ({ ...gate, enabled: toBool(gate.enabled) }));
|
||||
|
||||
return {
|
||||
activeOrgId,
|
||||
@@ -111,6 +115,12 @@ export class SnapshotService {
|
||||
operationalEvents,
|
||||
chaosDrills,
|
||||
chaosDrillRuns: list(this.db, "chaos_drill_runs", "started_at desc"),
|
||||
sdlcReviewAgents,
|
||||
sdlcReviewRuns: list(this.db, "sdlc_review_runs", "created_at desc"),
|
||||
evalSuites: list(this.db, "eval_suites", "key asc"),
|
||||
evalCases: list(this.db, "eval_cases", "title asc"),
|
||||
evalRuns: list(this.db, "eval_runs", "created_at desc"),
|
||||
prBotGates,
|
||||
auditEvents: list<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc").slice(0, 200),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ const api: DesktopApi = {
|
||||
createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input),
|
||||
setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input),
|
||||
runChaosDrill: (input) => invoke(IPC_CHANNELS.runChaosDrill, input),
|
||||
runSdlcReview: (input) => invoke(IPC_CHANNELS.runSdlcReview, input),
|
||||
runEvalSuite: (input) => invoke(IPC_CHANNELS.runEvalSuite, input),
|
||||
setPrBotGate: (input) => invoke(IPC_CHANNELS.setPrBotGate, input),
|
||||
createComplianceExport: (input) => invoke(IPC_CHANNELS.createComplianceExport, input),
|
||||
createLegalHold: (input) => invoke(IPC_CHANNELS.createLegalHold, input),
|
||||
setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BillingPanel } from "./settings/BillingPanel";
|
||||
import { ModelsPanel } from "./settings/ModelsPanel";
|
||||
import { PoliciesPanel } from "./settings/PoliciesPanel";
|
||||
import { OperationsPanel } from "./settings/OperationsPanel";
|
||||
import { SdlcPanel } from "./settings/SdlcPanel";
|
||||
import { MigrationPanel } from "./settings/MigrationPanel";
|
||||
import { CompliancePanel } from "./settings/CompliancePanel";
|
||||
import { NotificationsPanel, ProfilePanel, SecurityPanel } from "./settings/AccountPanels";
|
||||
@@ -22,6 +23,7 @@ type SettingsTab =
|
||||
| "skills"
|
||||
| "policies"
|
||||
| "operations"
|
||||
| "sdlc"
|
||||
| "compliance"
|
||||
| "migration"
|
||||
| "profile"
|
||||
@@ -44,6 +46,7 @@ const AGENTS_GROUP: NavSpec[] = [
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "policies", label: "Policies" },
|
||||
{ id: "operations", label: "Operations" },
|
||||
{ id: "sdlc", label: "SDLC" },
|
||||
{ id: "compliance", label: "Compliance" },
|
||||
{ id: "migration", label: "Migration" },
|
||||
];
|
||||
@@ -91,6 +94,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const operationalEvents = useMemo(() => snapshot.operationalEvents.filter((item) => item.orgId === orgId), [snapshot.operationalEvents, orgId]);
|
||||
const chaosDrills = useMemo(() => snapshot.chaosDrills.filter((item) => item.orgId === orgId), [snapshot.chaosDrills, orgId]);
|
||||
const chaosDrillRuns = useMemo(() => snapshot.chaosDrillRuns.filter((item) => item.orgId === orgId), [snapshot.chaosDrillRuns, orgId]);
|
||||
const sdlcReviewAgents = useMemo(() => snapshot.sdlcReviewAgents.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewAgents, orgId]);
|
||||
const sdlcReviewRuns = useMemo(() => snapshot.sdlcReviewRuns.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewRuns, orgId]);
|
||||
const evalSuites = useMemo(() => snapshot.evalSuites.filter((item) => item.orgId === orgId), [snapshot.evalSuites, orgId]);
|
||||
const evalCases = useMemo(() => snapshot.evalCases.filter((item) => item.orgId === orgId), [snapshot.evalCases, orgId]);
|
||||
const evalRuns = useMemo(() => snapshot.evalRuns.filter((item) => item.orgId === orgId), [snapshot.evalRuns, orgId]);
|
||||
const prBotGates = useMemo(() => snapshot.prBotGates.filter((item) => item.orgId === orgId), [snapshot.prBotGates, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
|
||||
@@ -158,6 +167,20 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "sdlc" && (
|
||||
<SdlcPanel
|
||||
orgId={orgId}
|
||||
reviewAgents={sdlcReviewAgents}
|
||||
reviewRuns={sdlcReviewRuns}
|
||||
evalSuites={evalSuites}
|
||||
evalCases={evalCases}
|
||||
evalRuns={evalRuns}
|
||||
prBotGates={prBotGates}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "compliance" && (
|
||||
<CompliancePanel
|
||||
orgId={orgId}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Button, Pill } from "../../shared/ui";
|
||||
import type {
|
||||
EvalCaseRecord,
|
||||
EvalRunRecord,
|
||||
EvalSuiteRecord,
|
||||
MigrationReportRecord,
|
||||
PolicyRuleRecord,
|
||||
PrBotGateRecord,
|
||||
SdlcReviewAgentRecord,
|
||||
SdlcReviewRunRecord,
|
||||
SdlcReviewTargetType,
|
||||
} from "../../../../shared/domain";
|
||||
import { SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE } from "../../../../shared/domain";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { formatRelative } from "../types";
|
||||
|
||||
export function SdlcPanel({
|
||||
orgId,
|
||||
reviewAgents,
|
||||
reviewRuns,
|
||||
evalSuites,
|
||||
evalCases,
|
||||
evalRuns,
|
||||
prBotGates,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
reviewAgents: SdlcReviewAgentRecord[];
|
||||
reviewRuns: SdlcReviewRunRecord[];
|
||||
evalSuites: EvalSuiteRecord[];
|
||||
evalCases: EvalCaseRecord[];
|
||||
evalRuns: EvalRunRecord[];
|
||||
prBotGates: PrBotGateRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1 className="page-h1">SDLC</h1>
|
||||
<p className="page-sub">{reviewAgents.length} agents / {evalSuites.length} suites / {prBotGates.length} gates.</p>
|
||||
<ReviewAgentsTable
|
||||
orgId={orgId}
|
||||
agents={reviewAgents}
|
||||
runs={reviewRuns}
|
||||
migrationReports={migrationReports}
|
||||
policies={policies}
|
||||
actions={actions}
|
||||
/>
|
||||
<EvalSuitesTable orgId={orgId} suites={evalSuites} cases={evalCases} runs={evalRuns} actions={actions} />
|
||||
<PrBotGatesTable orgId={orgId} gates={prBotGates} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewAgentsTable({
|
||||
orgId,
|
||||
agents,
|
||||
runs,
|
||||
migrationReports,
|
||||
policies,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
agents: SdlcReviewAgentRecord[];
|
||||
runs: SdlcReviewRunRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
policies: PolicyRuleRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Review agents</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px" }}>
|
||||
<div>Agent</div><div>Target</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{agents.map((agent, index) => {
|
||||
const target = resolveReviewTarget(agent, orgId, migrationReports, policies);
|
||||
const run = runs.find((item) => item.reviewAgentId === agent.id) ?? null;
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 96px 82px",
|
||||
borderBottom: index === agents.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{agent.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{agent.specialty}</div>
|
||||
</div>
|
||||
<div><Pill>{target.label}</Pill></div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!agent.enabled || !target.ready}
|
||||
onClick={() => void actions.run(() => window.agentCompany.runSdlcReview({
|
||||
orgId,
|
||||
reviewAgentId: agent.id,
|
||||
targetType: target.type,
|
||||
targetRef: target.ref,
|
||||
}))}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EvalSuitesTable({
|
||||
orgId,
|
||||
suites,
|
||||
cases,
|
||||
runs,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
suites: EvalSuiteRecord[];
|
||||
cases: EvalCaseRecord[];
|
||||
runs: EvalRunRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Eval suites</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 24 }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px" }}>
|
||||
<div>Suite</div><div>Cases</div><div>Last run</div><div>Action</div>
|
||||
</div>
|
||||
{suites.map((suite, index) => {
|
||||
const run = runs.find((item) => item.suiteId === suite.id) ?? null;
|
||||
const caseCount = cases.filter((item) => item.suiteId === suite.id).length;
|
||||
return (
|
||||
<div
|
||||
key={suite.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 90px 96px 82px",
|
||||
borderBottom: index === suites.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{suite.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{suite.scope}</div>
|
||||
</div>
|
||||
<div className="num mono">{caseCount}</div>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}</div>
|
||||
<Button size="small" onClick={() => void actions.run(() => window.agentCompany.runEvalSuite({ orgId, suiteId: suite.id }))}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PrBotGatesTable({
|
||||
orgId,
|
||||
gates,
|
||||
actions,
|
||||
}: {
|
||||
orgId: string;
|
||||
gates: PrBotGateRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>PR bot gates</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)" }}>
|
||||
<div>Gate</div><div>State</div><div>Updated</div>
|
||||
</div>
|
||||
{gates.map((gate, index) => (
|
||||
<div
|
||||
key={gate.id}
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) 112px minmax(0, 1fr)",
|
||||
borderBottom: index === gates.length - 1 ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{gate.label}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{gate.gateType}</div>
|
||||
</div>
|
||||
<label className="row" style={{ gap: 8, fontSize: 12, color: "var(--text-2)" }}>
|
||||
<input
|
||||
aria-label={`${gate.label} enabled`}
|
||||
type="checkbox"
|
||||
checked={gate.enabled}
|
||||
onChange={(event) => {
|
||||
void actions.run(() => window.agentCompany.setPrBotGate({ orgId, gateId: gate.id, enabled: event.target.checked }));
|
||||
}}
|
||||
/>
|
||||
{gate.enabled ? "On" : "Off"}
|
||||
</label>
|
||||
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(gate.updatedAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveReviewTarget(
|
||||
agent: SdlcReviewAgentRecord,
|
||||
orgId: string,
|
||||
migrationReports: MigrationReportRecord[],
|
||||
policies: PolicyRuleRecord[],
|
||||
): { type: SdlcReviewTargetType; ref: string; label: string; ready: boolean } {
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) {
|
||||
const report = migrationReports[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.MIGRATION,
|
||||
ref: report?.id ?? orgId,
|
||||
label: report ? "migration" : "migration report",
|
||||
ready: Boolean(report),
|
||||
};
|
||||
}
|
||||
if (agent.key === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) {
|
||||
const policy = policies[0] ?? null;
|
||||
return {
|
||||
type: SDLC_REVIEW_TARGET_TYPE.POLICY,
|
||||
ref: policy?.id ?? orgId,
|
||||
label: policy ? "policy" : "policy rule",
|
||||
ready: Boolean(policy),
|
||||
};
|
||||
}
|
||||
return { type: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, ref: orgId, label: "workspace", ready: true };
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
RiskLevel,
|
||||
RoleKey,
|
||||
SearchEntityType,
|
||||
SdlcReviewTargetType,
|
||||
SocialPlatform,
|
||||
TaskPriority,
|
||||
TaskStatus,
|
||||
@@ -288,6 +289,24 @@ export interface RunChaosDrillInput {
|
||||
drillId: string;
|
||||
}
|
||||
|
||||
export interface RunSdlcReviewInput {
|
||||
orgId: string;
|
||||
reviewAgentId: string;
|
||||
targetType: SdlcReviewTargetType;
|
||||
targetRef: string;
|
||||
}
|
||||
|
||||
export interface RunEvalSuiteInput {
|
||||
orgId: string;
|
||||
suiteId: string;
|
||||
}
|
||||
|
||||
export interface SetPrBotGateInput {
|
||||
orgId: string;
|
||||
gateId: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BootstrapOnboardingInput {
|
||||
companyName: string;
|
||||
companyDescription: string;
|
||||
|
||||
@@ -27,6 +27,8 @@ import type {
|
||||
LaunchSocialLoginInput,
|
||||
ResolveAgentSkillsInput,
|
||||
RunChaosDrillInput,
|
||||
RunEvalSuiteInput,
|
||||
RunSdlcReviewInput,
|
||||
SaveProviderCredentialInput,
|
||||
SearchInput,
|
||||
SendMessageInput,
|
||||
@@ -34,6 +36,7 @@ import type {
|
||||
SetFilePinnedInput,
|
||||
SetFeatureFlagInput,
|
||||
SetLegalHoldStateInput,
|
||||
SetPrBotGateInput,
|
||||
SetRetentionFreezeStateInput,
|
||||
SetScheduledPostStateInput,
|
||||
SyncEmployeeIdentityInput,
|
||||
@@ -79,6 +82,9 @@ export interface DesktopApi {
|
||||
createMigrationReport(input: CreateMigrationReportInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setFeatureFlag(input: SetFeatureFlagInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runChaosDrill(input: RunChaosDrillInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runSdlcReview(input: RunSdlcReviewInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runEvalSuite(input: RunEvalSuiteInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setPrBotGate(input: SetPrBotGateInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createComplianceExport(input: CreateComplianceExportInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createLegalHold(input: CreateLegalHoldInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
setLegalHoldState(input: SetLegalHoldStateInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
|
||||
@@ -4,12 +4,14 @@ import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, Ser
|
||||
import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain";
|
||||
import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
|
||||
import type { ProviderDataPolicy, ProviderExecutionMode } from "./provider-policy-domain";
|
||||
import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain";
|
||||
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain";
|
||||
export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain";
|
||||
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain";
|
||||
export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain";
|
||||
export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
|
||||
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
|
||||
export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain";
|
||||
export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain";
|
||||
export const TASK_STATUS = { BACKLOG: "backlog", IN_PROGRESS: "in_progress", REVIEW: "review", DONE: "done" } as const;
|
||||
export const APPROVAL_STATE = { PENDING: "pending", APPROVED: "approved", DECLINED: "declined" } as const;
|
||||
@@ -474,5 +476,6 @@ export interface AppSnapshot {
|
||||
employeeIdentities: EmployeeIdentityRecord[]; serviceIdentities: ServiceIdentityRecord[]; roleGrants: RoleGrantRecord[]; accessDecisions: AccessDecisionRecord[];
|
||||
orgDepartments: OrgDepartmentRecord[]; hiringPolicies: HiringPolicyRecord[]; approvalRoutingRules: ApprovalRoutingRuleRecord[]; approvalAssignments: ApprovalAssignmentRecord[];
|
||||
featureFlags: FeatureFlagRecord[]; sloTargets: SloTargetRecord[]; operationalEvents: OperationalEventRecord[]; chaosDrills: ChaosDrillRecord[]; chaosDrillRuns: ChaosDrillRunRecord[];
|
||||
sdlcReviewAgents: SdlcReviewAgentRecord[]; sdlcReviewRuns: SdlcReviewRunRecord[]; evalSuites: EvalSuiteRecord[]; evalCases: EvalCaseRecord[]; evalRuns: EvalRunRecord[]; prBotGates: PrBotGateRecord[];
|
||||
auditEvents: AuditEventRecord[];
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export const IPC_CHANNELS = {
|
||||
createMigrationReport: "agentcompany:create-migration-report",
|
||||
setFeatureFlag: "agentcompany:set-feature-flag",
|
||||
runChaosDrill: "agentcompany:run-chaos-drill",
|
||||
runSdlcReview: "agentcompany:run-sdlc-review",
|
||||
runEvalSuite: "agentcompany:run-eval-suite",
|
||||
setPrBotGate: "agentcompany:set-pr-bot-gate",
|
||||
createComplianceExport: "agentcompany:create-compliance-export",
|
||||
createLegalHold: "agentcompany:create-legal-hold",
|
||||
setLegalHoldState: "agentcompany:set-legal-hold-state",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export const SDLC_REVIEW_AGENT_KEY = {
|
||||
CODE_REVIEWER: "code_reviewer",
|
||||
MIGRATION_REVIEWER: "migration_reviewer",
|
||||
POLICY_REVIEWER: "policy_reviewer",
|
||||
} as const;
|
||||
|
||||
export const SDLC_REVIEW_TARGET_TYPE = { WORKSPACE: "workspace", MIGRATION: "migration", POLICY: "policy" } as const;
|
||||
export const EVAL_SUITE_KEY = { CONTRACT: "contract", INTEGRATION: "integration", REPLAY: "replay" } as const;
|
||||
export const SDLC_RUN_STATE = { PASSED: "passed", FAILED: "failed" } as const;
|
||||
export const PR_BOT_GATE_KEY = {
|
||||
STATIC_CHECKS: "static_checks",
|
||||
MIGRATION_DIFF: "migration_diff",
|
||||
POLICY_DIFF: "policy_diff",
|
||||
HUMAN_APPROVAL: "human_approval",
|
||||
} as const;
|
||||
|
||||
export type SdlcReviewAgentKey = ValueOf<typeof SDLC_REVIEW_AGENT_KEY>;
|
||||
export type SdlcReviewTargetType = ValueOf<typeof SDLC_REVIEW_TARGET_TYPE>;
|
||||
export type EvalSuiteKey = ValueOf<typeof EVAL_SUITE_KEY>;
|
||||
export type SdlcRunState = ValueOf<typeof SDLC_RUN_STATE>;
|
||||
export type PrBotGateKey = ValueOf<typeof PR_BOT_GATE_KEY>;
|
||||
|
||||
export interface SdlcReviewAgentRecord {
|
||||
id: string; orgId: string; key: SdlcReviewAgentKey; label: string; specialty: string; enabled: boolean; createdAt: string; updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SdlcReviewRunRecord {
|
||||
id: string; orgId: string; reviewAgentId: string; targetType: SdlcReviewTargetType; targetRef: string;
|
||||
state: SdlcRunState; resultJson: string; createdAt: string; completedAt: string;
|
||||
}
|
||||
|
||||
export interface EvalSuiteRecord {
|
||||
id: string; orgId: string; key: EvalSuiteKey; label: string; scope: string; createdAt: string; updatedAt: string;
|
||||
}
|
||||
|
||||
export interface EvalCaseRecord {
|
||||
id: string; orgId: string; suiteId: string; title: string; sourceType: string; sourceRef: string; expectedSignal: string; createdAt: string;
|
||||
}
|
||||
|
||||
export interface EvalRunRecord {
|
||||
id: string; orgId: string; suiteId: string; state: SdlcRunState; passedCount: number; failedCount: number; resultJson: string; createdAt: string; completedAt: string;
|
||||
}
|
||||
|
||||
export interface PrBotGateRecord {
|
||||
id: string; orgId: string; key: PrBotGateKey; label: string; gateType: string; enabled: boolean; createdAt: string; updatedAt: string;
|
||||
}
|
||||
Reference in new issue
Block a user