吸收数字员工风险策略远端下发
This commit is contained in:
@@ -4,9 +4,11 @@ import { getDomainTokenKey } from "@shared/utils/domain";
|
||||
import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from "../../db";
|
||||
import {
|
||||
normalizePlanStepDispatchPolicy,
|
||||
normalizeRiskApprovalPolicy,
|
||||
readDigitalEmployeeUiState,
|
||||
saveDigitalEmployeeUiState,
|
||||
type DigitalEmployeePlanStepDispatchPolicy,
|
||||
type DigitalEmployeeRiskApprovalPolicy,
|
||||
} from "./stateService";
|
||||
import {
|
||||
SKILL_POLICIES_SETTING_KEY,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
const DEFAULT_SYNC_PATH = "/api/digital-employee/sync/outbox";
|
||||
const DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH = "/api/digital-employee/policies/plan-step-dispatch";
|
||||
const DEFAULT_SKILL_POLICY_PATH = "/api/digital-employee/policies/skills";
|
||||
const DEFAULT_RISK_APPROVAL_POLICY_PATH = "/api/digital-employee/policies/risk-approval";
|
||||
const DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
|
||||
const MANAGEMENT_SYNC_RECORD_PATH =
|
||||
"/system/content/content-digital-employee-sync";
|
||||
@@ -38,6 +41,8 @@ let policyPulling = false;
|
||||
let lastPolicyPullAttemptMs = 0;
|
||||
let skillPolicyPulling = false;
|
||||
let lastSkillPolicyPullAttemptMs = 0;
|
||||
let riskApprovalPolicyPulling = false;
|
||||
let lastRiskApprovalPolicyPullAttemptMs = 0;
|
||||
|
||||
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
|
||||
if (!ensureDigitalEmployeeSchema()) return null;
|
||||
@@ -49,6 +54,7 @@ interface DigitalEmployeeSyncConfig {
|
||||
endpoint: string | null;
|
||||
policyEndpoint: string | null;
|
||||
skillPolicyEndpoint: string | null;
|
||||
riskApprovalPolicyEndpoint: string | null;
|
||||
leaseEndpoint: string | null;
|
||||
clientKey: string | null;
|
||||
authToken: string | null;
|
||||
@@ -73,6 +79,15 @@ export interface DigitalEmployeePlanStepDispatchPolicyPullResult {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeRiskApprovalPolicyPullResult {
|
||||
ok: boolean;
|
||||
skipped?: boolean;
|
||||
endpoint: string | null;
|
||||
pulled_at: string | null;
|
||||
policy: DigitalEmployeeRiskApprovalPolicy;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepRemoteLease {
|
||||
lease_id: string;
|
||||
state: "active" | "released" | "expired";
|
||||
@@ -368,6 +383,63 @@ export async function pullDigitalEmployeeSkillPolicies(
|
||||
}
|
||||
}
|
||||
|
||||
export async function pullDigitalEmployeeRiskApprovalPolicy(
|
||||
options: { force?: boolean } = {},
|
||||
): Promise<DigitalEmployeeRiskApprovalPolicyPullResult> {
|
||||
const currentState = readDigitalEmployeeUiState();
|
||||
const currentPolicy = currentState.riskApprovalPolicy ?? normalizeRiskApprovalPolicy(null);
|
||||
if (riskApprovalPolicyPulling) {
|
||||
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
if (!options.force && nowMs - lastRiskApprovalPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
|
||||
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
|
||||
}
|
||||
lastRiskApprovalPolicyPullAttemptMs = nowMs;
|
||||
|
||||
const config = resolveSyncConfig();
|
||||
const missingCredentials = missingSyncCredentialLabels(config);
|
||||
if (!config.riskApprovalPolicyEndpoint || missingCredentials.length > 0) {
|
||||
const error = !config.riskApprovalPolicyEndpoint ? "management risk approval policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
|
||||
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, error);
|
||||
}
|
||||
|
||||
riskApprovalPolicyPulling = true;
|
||||
try {
|
||||
const response = await fetchJsonWithAuth(config, config.riskApprovalPolicyEndpoint);
|
||||
const responseError = readSyncResponseError(response as SyncResponse);
|
||||
if (responseError) throw new Error(responseError);
|
||||
const remotePolicy = readRemoteRiskApprovalPolicy(response);
|
||||
if (!remotePolicy) throw new Error("management policy response missing risk approval policy");
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policy = normalizeRiskApprovalPolicy({
|
||||
...remotePolicy,
|
||||
source: "remote",
|
||||
remote_updated_at: stringField(remotePolicy.updated_at) || stringField(remotePolicy.updatedAt) || stringField(remotePolicy.remote_updated_at) || null,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: null,
|
||||
});
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "pull_risk_approval_policy",
|
||||
metadata: {
|
||||
source: "management-api",
|
||||
endpoint: config.riskApprovalPolicyEndpoint,
|
||||
ok: true,
|
||||
risk_approval_policy: policy,
|
||||
},
|
||||
state: {
|
||||
...currentState,
|
||||
riskApprovalPolicy: policy,
|
||||
},
|
||||
});
|
||||
return { ok: true, endpoint: config.riskApprovalPolicyEndpoint, pulled_at: pulledAt, policy, error: null };
|
||||
} catch (error) {
|
||||
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, normalizeError(error));
|
||||
} finally {
|
||||
riskApprovalPolicyPulling = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireDigitalEmployeePlanStepRemoteLease(
|
||||
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
|
||||
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
|
||||
@@ -577,6 +649,11 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
? config.skillPolicyEndpoint.trim()
|
||||
: null;
|
||||
const skillPolicyEndpoint = skillPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_SKILL_POLICY_PATH);
|
||||
const riskApprovalPolicyEndpointOverride =
|
||||
typeof config.riskApprovalPolicyEndpoint === "string" && config.riskApprovalPolicyEndpoint.trim()
|
||||
? config.riskApprovalPolicyEndpoint.trim()
|
||||
: null;
|
||||
const riskApprovalPolicyEndpoint = riskApprovalPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_RISK_APPROVAL_POLICY_PATH);
|
||||
const leaseEndpointOverride =
|
||||
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
|
||||
? config.planStepDispatchLeaseEndpoint.trim()
|
||||
@@ -593,6 +670,7 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
endpoint,
|
||||
policyEndpoint,
|
||||
skillPolicyEndpoint,
|
||||
riskApprovalPolicyEndpoint,
|
||||
leaseEndpoint,
|
||||
clientKey: readClientKey(serverHost),
|
||||
authToken: readAuthToken(serverHost),
|
||||
@@ -746,6 +824,14 @@ function readRemoteSkillPolicyRecords(payload: Record<string, unknown>): Record<
|
||||
?? (looksLikeSkillPolicyMap(payload) ? payload : null);
|
||||
}
|
||||
|
||||
function readRemoteRiskApprovalPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const data = objectRecord(response.data) ?? response;
|
||||
return objectRecord(data.risk_approval_policy)
|
||||
?? objectRecord(data.riskApprovalPolicy)
|
||||
?? objectRecord(data.policy)
|
||||
?? (hasRiskApprovalPolicyFields(data) ? data : null);
|
||||
}
|
||||
|
||||
function readPlanStepLeaseRejectionReason(response: Record<string, unknown>): string | null {
|
||||
if (response.ok === false || response.success === false || response.accepted === false || response.granted === false) {
|
||||
return stringField(response.reason ?? response.error ?? response.message) || "remote_lease_rejected";
|
||||
@@ -813,6 +899,15 @@ function hasSkillPolicyWrapperFields(record: Record<string, unknown>): boolean {
|
||||
|| "policies" in record;
|
||||
}
|
||||
|
||||
function hasRiskApprovalPolicyFields(record: Record<string, unknown>): boolean {
|
||||
return "approval_required_risk_levels" in record
|
||||
|| "approvalRequiredRiskLevels" in record
|
||||
|| "approval_required_actions" in record
|
||||
|| "approvalRequiredActions" in record
|
||||
|| "auto_reject_actions" in record
|
||||
|| "autoRejectActions" in record;
|
||||
}
|
||||
|
||||
function looksLikeSkillPolicyMap(record: Record<string, unknown>): boolean {
|
||||
return Object.values(record).some((value) => {
|
||||
if (typeof value === "boolean") return true;
|
||||
@@ -865,6 +960,35 @@ function recordSkillPolicyPullFailure(
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policies, error };
|
||||
}
|
||||
|
||||
function recordRiskApprovalPolicyPullFailure(
|
||||
state: ReturnType<typeof readDigitalEmployeeUiState>,
|
||||
currentPolicy: DigitalEmployeeRiskApprovalPolicy,
|
||||
endpoint: string | null,
|
||||
error: string,
|
||||
): DigitalEmployeeRiskApprovalPolicyPullResult {
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policy = normalizeRiskApprovalPolicy({
|
||||
...currentPolicy,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: error,
|
||||
});
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "pull_risk_approval_policy",
|
||||
metadata: {
|
||||
source: "management-api",
|
||||
endpoint,
|
||||
ok: false,
|
||||
error,
|
||||
risk_approval_policy: policy,
|
||||
},
|
||||
state: {
|
||||
...state,
|
||||
riskApprovalPolicy: policy,
|
||||
},
|
||||
});
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
|
||||
}
|
||||
|
||||
function buildSyncItem(row: OutboxRow): Record<string, unknown> {
|
||||
const payload = parsePayload(row.payload);
|
||||
const summary = readEntitySummary(row.entity_type, row.entity_id);
|
||||
@@ -1058,6 +1182,7 @@ function eventSpecificBusinessView(
|
||||
return artifactEventBusinessView(payload);
|
||||
}
|
||||
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
||||
if (kind === "risk_approval_required" || kind === "risk_policy_rejected") return riskPolicyBusinessView(payload);
|
||||
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
|
||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
|
||||
@@ -1132,6 +1257,24 @@ function approvalSubscriptionBusinessView(payload: Record<string, unknown>): Rec
|
||||
};
|
||||
}
|
||||
|
||||
function riskPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
action_kind: stringField(payload.action_kind ?? payload.actionKind) || null,
|
||||
action_label: stringField(payload.action_label ?? payload.actionLabel) || null,
|
||||
risk_level: stringField(payload.risk_level ?? payload.riskLevel) || null,
|
||||
decision: stringField(payload.decision) || null,
|
||||
status: stringField(payload.status) || null,
|
||||
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
|
||||
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
|
||||
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
|
||||
correlation_id: stringField(payload.correlation_id ?? payload.correlationId) || null,
|
||||
actor: stringField(payload.actor) || null,
|
||||
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
|
||||
task_id: stringField(payload.task_id ?? payload.taskId) || null,
|
||||
run_id: stringField(payload.run_id ?? payload.runId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactEventBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const retention = objectRecord(payload.retention_summary);
|
||||
return {
|
||||
@@ -1266,6 +1409,7 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
|
||||
function eventBusinessCategory(kind: string): string {
|
||||
if (kind === "route_decision") return "route";
|
||||
if (kind.startsWith("approval_")) return "approval";
|
||||
if (kind.startsWith("risk_")) return "approval";
|
||||
if (isApprovalSubscriptionEvent(kind)) return "approval";
|
||||
if (kind.startsWith("artifact_")) return "artifact";
|
||||
if (kind.startsWith("skill_call_")) return "skill";
|
||||
|
||||
Reference in New Issue
Block a user