For agentic workers / runner: REQUIRED SUB-SKILL: superpowers:subagent-driven-development / executing-plans.
- [ ]checkbox syntax.依賴:FR-056.1 / .2 / .3(收尾整合階段)。 母案設計見
design.md。
Goal: 把 FR-056.1~.3 串成完整可用流程:任務執行抽屜「開始執行」→ 派工 → Agent 掃描回報 → 報告自動存回 job_evidences(source=DETECTION_TOOL)+ detection_executions 落執行史 + 發 mail 通知負責人 → 依完成模式 auto 自動完成 / manual 留任務給人工。執行歷史可查、報告可下載(沿用既有下載端點)。
Architecture: 結果轉證據照抄 ImportDriveFileHandler(DRIVE_SYNC)三段式:報告存 Minio → 組 JobEvidenceEntity(source="DETECTION_TOOL", created_user="detection-agent@system") → domain service .add()。通知照 job_batch_complete_service 的 threading.Thread + send_mail_notification。完成模式不新增任何 JobStatus 狀態——auto 呼叫既有 complete_job,manual 什麼都不做(任務留 PROCESSING,人工用既有「完成任務」按鈕)。報告下載沿用既有 /file/download/<uid>,零新端點。
Tech Stack: 同前。
| 項目 | 定案 | 來源 |
|---|---|---|
| 完成模式語意 | 不是狀態。auto=系統掃完自動呼叫既有 complete_job;manual=系統不做事,任務留 PROCESSING,人工按既有「完成任務」 |
user 澄清 2026-07-26 |
| 不動 jedi_flow_engine | JobStatus(TODO/PROCESSING/COMPLETED/CANCEL)零新增,complete_job 前置檢查不改 |
user 澄清(原「待覆核新狀態」方案作廢) |
| 轉證據模板 | 抄 ImportDriveFileHandler.handle() 三段式,新系統帳號常數 detection-agent@system |
Explore |
| source 新值 | job_evidences.source CHECK constraint 加 DETECTION_TOOL(需 migration) |
Explore |
| evidence_type(D11 定案) | 用既有 FILE,不用 REPORT。REPORT 型別會牽動「其他判斷證據檔案的地方」,改動面大;第一版用 FILE 最安全,要區分檢測報告類證據未來另開需求 |
user 拍板 2026-07-26 |
| 通知模板 | job_batch_complete_service._send_batch_summary_notification(threading.Thread + send_mail_notification,收件人查 task_assignees/ParticipantRole) |
Explore |
| 報告下載 | 不新建端點,沿用 /file/download/<uid>(UploadFileDownloadRoute),report 存 upload_files 拿 uid 即可 |
Explore |
| 完成模式讀取來源 | FR-056.2 建的 config.job_execution_detection_tools.completion_mode |
FR-056.2 |
| 重掃 | PROCESSING 下既有「再執行」能力(再建一筆 agent_task),每次一筆 detection_executions 獨立紀錄 | design |
| Task | 對應 | 產出 | 依賴 | Repo |
|---|---|---|---|---|
| Task 1 | T-4.1 | job_evidences source 加 DETECTION_TOOL + detection_executions 表 + 轉證據 handler | FR-056.3 | BE |
| Task 2 | T-4.2 | 執行編排(開始執行串派工 / 完成模式分岔 / 通知) | Task 1 + FR-056.2 | BE |
| Task 3 | T-4.3 | FE 任務執行抽屜(開始執行 / 重掃 / 執行歷史 / 下載) | Task 2 | FE |
Files:
scripts/sql/2026-07-26-fr056-4-job-evidences-detection-source.sql(改 CHECK constraint)scripts/sql/2026-07-26-fr056-4-detection-executions.sql(執行史表)detection_execution 模組app/detection_tools/service/detection_result_handler.py(轉證據,mirror ImportDriveFileHandler)infra/flow_engine/models/job_evidence.py(comment 更新 source 值)test/test_detection_result_handler.py-- Date: 2026-07-26
-- FR-056.4 job_evidences.source 加 DETECTION_TOOL
ALTER TABLE compliance.job_evidences DROP CONSTRAINT IF EXISTS chk_job_evidences_source;
ALTER TABLE compliance.job_evidences ADD CONSTRAINT chk_job_evidences_source
CHECK (source IN ('SYSTEM_UPLOAD','DRIVE_SYNC','DETECTION_TOOL'));
INSERT INTO public.schema_migrations(filename, note) VALUES
('2026-07-26-fr056-4-job-evidences-detection-source.sql', 'FR-056.4 job_evidences source 加 DETECTION_TOOL') ON CONFLICT (filename) DO NOTHING;-- Date: 2026-07-26
-- FR-056.4 檢測執行歷史(一次執行一筆,可查可下載)
CREATE TABLE compliance.detection_executions (
id BIGSERIAL PRIMARY KEY,
uid VARCHAR(36) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
agent_task_uid VARCHAR(36), -- soft-ref → compliance.agent_tasks.uid
job_execution_uid VARCHAR(50) NOT NULL, -- soft-ref → job_executions.uid
detection_tool_id BIGINT NOT NULL,
started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ,
status VARCHAR(20) NOT NULL DEFAULT 'running', -- running/succeeded/failed
summary JSONB, -- 摘要(發現幾項等)
report_file_id BIGINT, -- soft-ref → upload_files.id(原始報告,供下載)
evidence_id BIGINT, -- soft-ref → job_evidences.id(轉出的證據)
error_message TEXT,
created_user VARCHAR(255), updated_user VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE compliance.detection_executions IS 'FR-056.4 檢測執行歷史(每次執行一筆,重掃不覆蓋)';
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.detection_executions TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.detection_executions_id_seq TO cm_app;
ALTER TABLE compliance.detection_executions ENABLE ROW LEVEL SECURITY;
CREATE POLICY detection_executions_tenant_isolation ON compliance.detection_executions
USING (current_setting('app.is_super_admin', TRUE) = 'true'
OR tenant_id = ANY (string_to_array(current_setting('app.allowed_tenant_paths', TRUE), ',')::BIGINT[]));
INSERT INTO public.schema_migrations(filename, note) VALUES
('2026-07-26-fr056-4-detection-executions.sql', 'FR-056.4 檢測執行歷史表') ON CONFLICT (filename) DO NOTHING;套進 DEV + psql 驗證。
test/test_detection_result_handler.py:mock file_upload + evidence domain service,驗證 handler 收到 agent 結果後:(a) 報告存 Minio;(b) JobEvidenceEntity 的 source="DETECTION_TOOL"、created_user="detection-agent@system";(c) detection_executions 落一筆 status=succeeded、report_file_id/evidence_id 有值。
app/detection_tools/service/detection_result_handler.py:
DETECTION_AGENT_USER = "detection-agent@system"
class DetectionResultHandler:
def __init__(self, file_upload_service, job_evidence_domain_service,
detection_execution_domain_service, job_execution_domain_service):
...
@transaction
def handle_result(self, agent_task, report_file_bytes, summary):
"""agent 回報成功後呼叫:報告存 Minio → 寫 job_evidences(DETECTION_TOOL) → detection_executions 落史。"""
job = self._job_exec.get_job_execution(JobExecutionQueryEntity(uid=agent_task.job_execution_uid))
# (1) 存 Minio(mirror ImportDriveFileHandler)
fs = FileStorage(stream=io.BytesIO(report_file_bytes), filename=f"detection_report_{agent_task.uid}.xml",
content_type="application/xml")
save_dir = f"JOB_EVIDENCES/{job.uid}"
uploaded = self._file_upload.upload_files([fs], DETECTION_AGENT_USER, save_dir)[0]
# (2) 寫 job_evidences(source=DETECTION_TOOL)
evidence = self._evidence.add(JobEvidenceEntity(
main_workflow_execution_id=job.main_workflow_execution_id,
workflow_execution_id=job.workflow_execution_id,
job_execution_id=job.id,
evidence_type="FILE", # D11 定案:用 FILE(REPORT 改動面大,未來另開需求)
source="DETECTION_TOOL",
file_id=uploaded.id,
content_hash=getattr(uploaded, "checksum", None), hash_algorithm="MD5",
description=f"[檢測工具] {agent_task.uid}",
created_user=DETECTION_AGENT_USER, updated_user=DETECTION_AGENT_USER,
))
# (3) detection_executions 落史
self._exec.write_success(agent_task.uid, job.uid, report_file_id=uploaded.id,
evidence_id=evidence.id, summary=summary, user=DETECTION_AGENT_USER)
return evidence注意: 不改
job_evidence_service.add_job_evidence(那是 user 手動上傳路徑)——照 DRIVE_SYNC 慣例直接呼叫 domain service.add()自訂 source。
git commit -m "feat(fr056): detection result → evidence handler + execution history (T-4.1)"Files:
app/detection_tools/service/detection_orchestration_service.py(開始執行 + 完成模式分岔)app/remote_agent/service/remote_agent_service.py 的 receive_result(FR-056.3 留的 TODO 掛鉤點:成功時觸發 Task 1 handler + 本 Task 的完成/通知)api/detection_tools/routes/detection_tool_route.py(「開始執行」endpoint)test/test_detection_orchestration.py驗證:(a) 開始執行 → 建 agent_task(pending) + detection_executions(running);(b) 結果成功 + completion_mode=auto → 呼叫 complete_job;(c) completion_mode=manual → 不呼叫 complete_job(任務留 PROCESSING);(d) 兩情況都發 mail 給負責人。
@transaction
def start_execution(self, job_uid, curr_user, tenant_id):
binding = self._jedt_domain.get_one(job_execution_uid=job_uid) # FR-056.2 綁定
if not binding:
raise BadRequestError(DetectionToolsErrorCode.DETECTION_TOOL_NOT_BOUND)
# 選一台有 detection_scan capability 的 agent(tenant 內)
agent = self._pick_agent(tenant_id)
# 解密該租戶工具憑證,隨派工下發(走 mTLS,見 FR-056.3 憑證下發決策)
creds = self._resolve_credentials(binding, tenant_id)
task = self._agent_task_domain.create(AgentTaskEntity(
uid=str(uuid.uuid4()), tenant_id=tenant_id, agent_id=agent.id,
job_execution_uid=job_uid, detection_tool_id=binding.detection_tool_id,
params={**binding.tool_params, "_credentials": creds}, status="pending",
created_user=curr_user, updated_user=curr_user))
self._exec_domain.create(DetectionExecutionEntity(
uid=str(uuid.uuid4()), tenant_id=tenant_id, agent_task_uid=task.uid,
job_execution_uid=job_uid, detection_tool_id=binding.detection_tool_id,
status="running", started_at=datetime.now(), created_user=curr_user))
return {"agent_task_uid": task.uid}FR-056.3 Task 2 的 receive_result 成功時,於此觸發(把 TODO(FR-056.4) 換成實作):
def _on_scan_succeeded(self, agent_task, report_bytes, summary):
evidence = self._result_handler.handle_result(agent_task, report_bytes, summary) # Task 1
binding = self._jedt_domain.get_one(job_execution_uid=agent_task.job_execution_uid)
self._notify_owner(agent_task.job_execution_uid, evidence) # 發信
if binding.completion_mode == "auto":
self._workflow_execution_service.complete_job(...) # 既有 complete_job,PROCESSING→COMPLETED
# manual:什麼都不做,任務留 PROCESSING,等人工手動完成通知照 job_batch_complete_service pattern:
def _notify_owner(self, job_uid, evidence):
targets = self._get_job_notify_targets(job_uid) # 查 task_assignees / ParticipantRole email
subject = _("detection_scan_completed_subject")
for info in targets:
if info["email"]:
content = _("detection_scan_completed_content") % {"nickname": info["nickname"]}
threading.Thread(target=notification_service.send_mail_notification,
kwargs={"to": info["email"], "subject": subject, "content": content, "is_html": True}).start()掃描失敗(
receive_resultfailed 分支):detection_executions標 failed(Task 1 的 write_failure),任務留 PROCESSING(不 silent,FE 執行歷史看得到 failed),可重試。
POST /detection-tools/jobs/<job_uid>/execute(掛 @jwt_required() + 專案參與者守門;這是 GRC 專案資源,用軸③ project-role 或既有 job 操作守門,非軸④ capability——runner 對齊既有 job 操作 endpoint 的守門方式)。
手測:建 detection 任務綁 auto → 執行 → 掃完任務自動 COMPLETED + 收信 + 證據入庫;建另一任務綁 manual → 執行 → 掃完任務仍 PROCESSING + 收信 + 證據入庫,手動按完成才 COMPLETED。
git commit -m "feat(fr056): execution orchestration + completion mode + notify (T-4.2)
開始執行串派工;掃完轉證據 + 發信;completion_mode=auto 呼叫既有 complete_job,manual 留任務 PROCESSING 等人工。零新增 JobStatus。"FE repo。 任務執行抽屜(既有元件,runner 先定位 detection 任務執行時開的抽屜 Vue 檔)。
git commit -m "feat(fr056): detection task execution drawer (T-4.3)"FILE(不用 REPORT)。REPORT 會牽動其他判斷證據檔案的地方、改動面大,第一版 FILE 最安全,未來要區分檢測報告類證據另開需求。app/cloud_integration/service/handlers/import_drive_file_handler.py::handle(DRIVE_SYNC)scripts/sql/2026-04-24-job-evidences-add-drive-source.sqlapp/grc/service/job_batch_complete_service.py::_send_batch_summary_notification + notification_service.send_mail_notificationinfra/grc/repository/job_batch_complete_query.py::get_batch_notification_data(task_assignees/ParticipantRole)app/flow_engine/service/workflow_execution_service.py::complete_job(不改,auto 模式直接呼叫)api/uploadfile/routes/uploadfile_route.py::UploadFileDownloadRoute(/file/download/<uid>,沿用)