forge-openclaw-plugin 0.3.16 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{action-bar-DgrmzFrI.js → action-bar-D3CEYlj4.js} +1 -1
- package/dist/assets/{activity-page-CF451ueg.js → activity-page-Ce1pHIu5.js} +1 -1
- package/dist/assets/{ai-surface-workspace-DtwybxJB.js → ai-surface-workspace-DEliwOjF.js} +1 -1
- package/dist/assets/artifacts-page-BgyFVIVM.js +2 -0
- package/dist/assets/{atlas-panel-CB3uYk_K.js → atlas-panel-VZOnJa1c.js} +1 -1
- package/dist/assets/{board-CuxQRKPJ.js → board-DprRipIG.js} +1 -1
- package/dist/assets/{calendar-page-BYsDuLnh.js → calendar-page-Zstsn4uG.js} +1 -1
- package/dist/assets/{calendar-rules-DXuOv2J-.js → calendar-rules-BGIBuEDk.js} +1 -1
- package/dist/assets/{calendar-week-toolbar-8PJmiUE_.js → calendar-week-toolbar-DXFkMkbq.js} +1 -1
- package/dist/assets/{charts-BzT4pUPg.js → charts-C5S0-BL7.js} +1 -1
- package/dist/assets/{companion-sync-lab-page-DzWNOaBh.js → companion-sync-lab-page-DIPPLFKt.js} +1 -1
- package/dist/assets/{daily-metrics-dashboard-CHcYxDhs.js → daily-metrics-dashboard-CG-OOCJz.js} +1 -1
- package/dist/assets/{define-workbench-box-DcI0_QKW.js → define-workbench-box-BEEdtBkQ.js} +1 -1
- package/dist/assets/{entity-link-multiselect-C--AiCMb.js → entity-link-multiselect-BfDAROHB.js} +1 -1
- package/dist/assets/{entity-note-count-link-xne6mshj.js → entity-note-count-link-BGOiaash.js} +1 -1
- package/dist/assets/{entity-notes-surface-jy6Psist.js → entity-notes-surface-DPsJwqQa.js} +1 -1
- package/dist/assets/execution-board-Dg-2GSKv.js +1 -0
- package/dist/assets/{faceted-token-search-BMQ1Wwzt.js → faceted-token-search-DV7MSVYb.js} +1 -1
- package/dist/assets/{flagship-signal-deck-DjcskRwF.js → flagship-signal-deck-D3wVL3bC.js} +1 -1
- package/dist/assets/{floating-action-menu-cGhbJ2tc.js → floating-action-menu-BUs382px.js} +1 -1
- package/dist/assets/{forms-D1qJ3oOP.js → forms-ByKjodjN.js} +1 -1
- package/dist/assets/{generic-node-view-DNVwfqqq.js → generic-node-view-BY5dbPXJ.js} +1 -1
- package/dist/assets/{goal-detail-page-dAbyCilO.js → goal-detail-page-MYr6j2B4.js} +1 -1
- package/dist/assets/{goal-dialog-DH50D0tP.js → goal-dialog-nkdME5Uy.js} +1 -1
- package/dist/assets/goals-page-BsFeBzU1.js +1 -0
- package/dist/assets/{graph-BF4IsheG.js → graph-CHVj0Z7n.js} +1 -1
- package/dist/assets/habits-page-wO461EVU.js +1 -0
- package/dist/assets/{health-boxes-BPTgGM6G.js → health-boxes-BphMeSJj.js} +1 -1
- package/dist/assets/index-CSd8ylsy.css +1 -0
- package/dist/assets/index-gmPaoLN-.js +2 -0
- package/dist/assets/{inline-note-fields-D4mCM1gO.js → inline-note-fields-BvQwmMAv.js} +1 -1
- package/dist/assets/{insight-flow-dialog-Btp6bkbV.js → insight-flow-dialog-H_eaOmst.js} +1 -1
- package/dist/assets/{insights-page-1qM4mxjS.js → insights-page-DINYe3cB.js} +3 -3
- package/dist/assets/{kanban-boxes-u_A-UJG-.js → kanban-boxes-oACOTDYG.js} +1 -1
- package/dist/assets/{kanban-page-CKCa_3Qb.js → kanban-page-BGLXH91f.js} +1 -1
- package/dist/assets/knowledge-graph-page-VfMvw5jL.js +1 -0
- package/dist/assets/{life-force-page-BZlyA144.js → life-force-page-DQPb6UL-.js} +1 -1
- package/dist/assets/{life-force-workspace-CtAWZXNw.js → life-force-workspace-BJ1aI3JE.js} +1 -1
- package/dist/assets/{maps-BTVHALP8.js → maps-CvEHVkBk.js} +1 -1
- package/dist/assets/{metric-tile-Bkx2btO2.js → metric-tile-CbEe19Gd.js} +1 -1
- package/dist/assets/{motion-DcgUnXhY.js → motion-BibSzp57.js} +1 -1
- package/dist/assets/{movement-boxes-QZ2EIS9q.js → movement-boxes-dtDYhw5c.js} +1 -1
- package/dist/assets/{movement-page-B-3ihggI.js → movement-page-BhmwCHjs.js} +1 -1
- package/dist/assets/{note-markdown-BWedRE3c.js → note-markdown-CG8u2d6p.js} +1 -1
- package/dist/assets/{note-tags-input-DZkCqI1w.js → note-tags-input-D_conqDO.js} +1 -1
- package/dist/assets/{notes-boxes-DGFmt7Dp.js → notes-boxes-BLKucFqW.js} +1 -1
- package/dist/assets/notes-page-CafVxWjb.js +1 -0
- package/dist/assets/{open-in-graph-button-CjvkEj1P.js → open-in-graph-button-DFQ4r6SN.js} +1 -1
- package/dist/assets/{orbit-map-CRHSPenk.js → orbit-map-DsEl3ori.js} +1 -1
- package/dist/assets/overview-page-BffcX-pM.js +1 -0
- package/dist/assets/page-hero-U603l9rQ.js +1 -0
- package/dist/assets/pill-cluster-BYVFNppV.js +1 -0
- package/dist/assets/{preference-entity-handoff-button-CfCQl3cW.js → preference-entity-handoff-button-cPrTpHsV.js} +1 -1
- package/dist/assets/{preferences-page-OltGdsQ2.js → preferences-page-DQjFIOmt.js} +1 -1
- package/dist/assets/{project-collections-FfBIVwXu.js → project-collections-B9OGHGqm.js} +1 -1
- package/dist/assets/project-detail-page-r1Sw2kcO.js +1 -0
- package/dist/assets/{project-dialog-B_J1ESMk.js → project-dialog-B2_PcXAf.js} +1 -1
- package/dist/assets/project-management-hierarchy-page-qLNG8hsh.js +1 -0
- package/dist/assets/{project-management-section-nav-DBNHt9p5.js → project-management-section-nav-DFUlmMEB.js} +1 -1
- package/dist/assets/{projects-boxes-BvlVsO5q.js → projects-boxes-BA7_XaqX.js} +1 -1
- package/dist/assets/projects-page-B6ggXWFw.js +1 -0
- package/dist/assets/psyche-behaviors-page-ByWpXlLq.js +5 -0
- package/dist/assets/{psyche-flashcards-page-05606-97.js → psyche-flashcards-page-B9nSc0mk.js} +1 -1
- package/dist/assets/{psyche-goal-map-page-Cw-aqS3Y.js → psyche-goal-map-page-BQU7qI88.js} +1 -1
- package/dist/assets/{psyche-graph-0E22QHPe.js → psyche-graph-DCeP7r2b.js} +1 -1
- package/dist/assets/{psyche-metrics-page-DBFVPeMa.js → psyche-metrics-page-DD2lOP-J.js} +1 -1
- package/dist/assets/psyche-mode-guide-page-DbtZgnjr.js +1 -0
- package/dist/assets/psyche-modes-page-3zFJWFQf.js +1 -0
- package/dist/assets/psyche-page-D5vXYkfB.js +1 -0
- package/dist/assets/psyche-patterns-page-BneAAeKS.js +5 -0
- package/dist/assets/{psyche-questionnaire-builder-page-DEt0UDsz.js → psyche-questionnaire-builder-page-BH1la5C9.js} +1 -1
- package/dist/assets/{psyche-questionnaire-detail-page-BXQbOE6A.js → psyche-questionnaire-detail-page-DYav634P.js} +1 -1
- package/dist/assets/psyche-questionnaire-run-detail-page-9VaijhJm.js +1 -0
- package/dist/assets/{psyche-questionnaire-run-page-D_EokOQm.js → psyche-questionnaire-run-page-e1T6k_dS.js} +1 -1
- package/dist/assets/{psyche-questionnaires-page-D1Ov2L4n.js → psyche-questionnaires-page-Dyv367z9.js} +1 -1
- package/dist/assets/{psyche-report-detail-page-9HyPr8BJ.js → psyche-report-detail-page-hqxHIXw2.js} +3 -3
- package/dist/assets/psyche-reports-page-DhIfcGbt.js +1 -0
- package/dist/assets/{psyche-schemas-DDol0j-g.js → psyche-schemas-Cg51Ztxz.js} +1 -1
- package/dist/assets/psyche-schemas-beliefs-page-Dgb5o9vx.js +9 -0
- package/dist/assets/{psyche-screen-time-page-DJMDR5oo.js → psyche-screen-time-page-D6MnJGNa.js} +1 -1
- package/dist/assets/{psyche-self-observation-page-DFKXG1Yv.js → psyche-self-observation-page-C1Vyezbv.js} +1 -1
- package/dist/assets/{psyche-values-page-TWKth89J.js → psyche-values-page-B_JWVw1Q.js} +2 -2
- package/dist/assets/{question-flow-dialog-D-_EvoAt.js → question-flow-dialog-yMYVk998.js} +2 -2
- package/dist/assets/{report-chain-fields-Dy-jBQDn.js → report-chain-fields-D4NM9a6I.js} +1 -1
- package/dist/assets/rewards-page-B5-bcL4R.js +1 -0
- package/dist/assets/{scheduling-rules-editor-CAd290IE.js → scheduling-rules-editor-De72IUax.js} +1 -1
- package/dist/assets/{schema-badge-BLaVI1D7.js → schema-badge-jX28kUa6.js} +1 -1
- package/dist/assets/{schemas-B0Pa-V-A.js → schemas-BlFy-uPR.js} +1 -1
- package/dist/assets/{select-menu-DcS4Ukjf.js → select-menu-BXv1rsVc.js} +1 -1
- package/dist/assets/{settings-agents-page-Bt38HhEJ.js → settings-agents-page-DN_np10S.js} +1 -1
- package/dist/assets/settings-bin-page-BM5BpK_i.js +1 -0
- package/dist/assets/{settings-calendar-page-DPSSZV3s.js → settings-calendar-page-Cg0gSSFs.js} +3 -3
- package/dist/assets/{settings-data-page-BdHsD1C_.js → settings-data-page-Dwmp2rLy.js} +1 -1
- package/dist/assets/{settings-logs-page-D9OnWoFq.js → settings-logs-page-DQNM21Re.js} +1 -1
- package/dist/assets/{settings-mobile-page-DFpsXA4M.js → settings-mobile-page-DQdiRa-a.js} +1 -1
- package/dist/assets/settings-models-page-CAEPpuHF.js +1 -0
- package/dist/assets/{settings-page-m_3iYPwU.js → settings-page-Cl2KiCFg.js} +1 -1
- package/dist/assets/{settings-rewards-page-B2aBm-dA.js → settings-rewards-page-DoIUumhe.js} +1 -1
- package/dist/assets/{settings-section-nav-CMeU19Rx.js → settings-section-nav-D_N87IG0.js} +1 -1
- package/dist/assets/{settings-users-page-D8jyewJl.js → settings-users-page-CPesR62N.js} +1 -1
- package/dist/assets/{settings-wiki-page-DjOgQJAh.js → settings-wiki-page-BxMQM5Vi.js} +1 -1
- package/dist/assets/{sleep-page-D8-dA4bE.js → sleep-page-DUYmQKrg.js} +1 -1
- package/dist/assets/{sports-page-CK3PgPKB.js → sports-page-Bzi5TF9T.js} +1 -1
- package/dist/assets/{state-Bpe5dF3T.js → state-BFyKSULP.js} +1 -1
- package/dist/assets/{strategies-page-aFpJath-.js → strategies-page-C-T2MZx4.js} +1 -1
- package/dist/assets/{strategy-detail-page-CobF5qLv.js → strategy-detail-page-3tYuSZu-.js} +1 -1
- package/dist/assets/{strategy-dialog-Fgoaoob5.js → strategy-dialog-BWm4slo4.js} +1 -1
- package/dist/assets/{surface-D0dqgX-k.js → surface-BJObZeRw.js} +1 -1
- package/dist/assets/{table-U7otr5go.js → table-CTLroJKN.js} +1 -1
- package/dist/assets/task-detail-page-ZZOXSpSF.js +1 -0
- package/dist/assets/{task-dialog-eCDTF8xe.js → task-dialog-2wy0xI7f.js} +5 -5
- package/dist/assets/timebox-planning-dialog-C6h0Hp71.js +1 -0
- package/dist/assets/{today-boxes-CYfskVrM.js → today-boxes-B_TdgSDz.js} +1 -1
- package/dist/assets/today-page-Dipns9tl.js +1 -0
- package/dist/assets/training-load-page-B78e1-o-.js +1 -0
- package/dist/assets/{ui-B9TWEtCx.js → ui-uo_bNP7c.js} +1 -1
- package/dist/assets/{use-anchored-overlay-position-BY4kNzPj.js → use-anchored-overlay-position-BSxFX6Hi.js} +1 -1
- package/dist/assets/{use-psyche-focus-target-BhNedCZB.js → use-psyche-focus-target-1GLkhYiv.js} +1 -1
- package/dist/assets/{user-badge-MEZuJiir.js → user-badge-C4eZu1e_.js} +1 -1
- package/dist/assets/{user-select-field-fx129Uh6.js → user-select-field-CmKwcbZt.js} +1 -1
- package/dist/assets/{utility-widgets-h5eG2jvc.js → utility-widgets-B06wQFrd.js} +2 -2
- package/dist/assets/{vendor-BwL6m4SE.js → vendor-B7FKSwHK.js} +222 -212
- package/dist/assets/{vitals-page-eoFnQ3XL.js → vitals-page-BC7QFsm2.js} +1 -1
- package/dist/assets/{weekly-review-page-C7BU1gkK.js → weekly-review-page-BjNsNoWj.js} +1 -1
- package/dist/assets/{weight-loss-page-Cc7Do3XY.js → weight-loss-page-BOahGY9r.js} +1 -1
- package/dist/assets/{wiki-article-markdown-CCKoSptz.js → wiki-article-markdown-DO47HnZf.js} +2 -2
- package/dist/assets/{wiki-editor-page-BpbxZrgz.js → wiki-editor-page-OWRUHl4O.js} +5 -5
- package/dist/assets/{wiki-ingest-history-page-D5Wfr8pc.js → wiki-ingest-history-page-CuHpg2lZ.js} +1 -1
- package/dist/assets/{wiki-ingest-modal-DhRVsEwC.js → wiki-ingest-modal-EZ3kX-vA.js} +1 -1
- package/dist/assets/wiki-page-BPV3HIgu.js +1 -0
- package/dist/assets/{workbench-flow-page-y9FxDbKO.js → workbench-flow-page-C53KueEz.js} +3 -3
- package/dist/assets/{workbench-page-Da16AZvN.js → workbench-page-DJ7nT2JQ.js} +1 -1
- package/dist/assets/workout-detail-page-Dhyu0q7o.js +2 -0
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +10 -0
- package/dist/openclaw/routes.js +78 -0
- package/dist/openclaw/tools.js +42 -0
- package/dist/server/apps/api/migrations/072_artifact_store.sql +106 -0
- package/dist/server/apps/api/src/app.js +364 -3
- package/dist/server/apps/api/src/openapi.js +570 -1
- package/dist/server/apps/api/src/repositories/entity-links.js +66 -0
- package/dist/server/apps/api/src/services/artifacts.js +1059 -0
- package/dist/server/apps/api/src/services/entity-crud.js +22 -0
- package/dist/server/apps/api/src/services/knowledge-graph.js +45 -0
- package/dist/server/apps/api/src/services/psyche-observation-calendar.js +1 -0
- package/dist/server/apps/api/src/types.js +2 -0
- package/dist/server/apps/web/src/lib/api.js +69 -0
- package/dist/server/apps/web/src/lib/entity-visuals.js +10 -1
- package/dist/server/apps/web/src/lib/knowledge-graph-types.js +9 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/server/migrations/072_artifact_store.sql +106 -0
- package/skills/forge-openclaw/SKILL.md +57 -4
- package/skills/forge-openclaw/entity_conversation_playbooks.md +212 -14
- package/skills/forge-openclaw/psyche_entity_playbooks.md +20 -0
- package/dist/assets/execution-board-BSUZCIIx.js +0 -1
- package/dist/assets/goals-page-DkMy9mPD.js +0 -1
- package/dist/assets/habits-page-CCQR8rg6.js +0 -1
- package/dist/assets/index-VzCGTBS_.css +0 -1
- package/dist/assets/index-izRKx6JK.js +0 -2
- package/dist/assets/knowledge-graph-page-BZOxRv19.js +0 -1
- package/dist/assets/notes-page-DYkn04tz.js +0 -1
- package/dist/assets/overview-page-D6LsjPWe.js +0 -1
- package/dist/assets/page-hero-BLhM8FF3.js +0 -1
- package/dist/assets/pill-cluster-B5bYs_e2.js +0 -1
- package/dist/assets/project-detail-page-CCm3iZPQ.js +0 -1
- package/dist/assets/project-management-hierarchy-page-BNzvtNZj.js +0 -1
- package/dist/assets/projects-page-BUf7KV-_.js +0 -1
- package/dist/assets/psyche-behaviors-page-jtyhdpJ1.js +0 -5
- package/dist/assets/psyche-mode-guide-page-DI-LNEDT.js +0 -1
- package/dist/assets/psyche-modes-page-CaMupyfi.js +0 -1
- package/dist/assets/psyche-page-CFBq0YKf.js +0 -1
- package/dist/assets/psyche-patterns-page-sp4ikoef.js +0 -5
- package/dist/assets/psyche-questionnaire-run-detail-page-Cd3ClsSK.js +0 -1
- package/dist/assets/psyche-reports-page-BeyJg5fb.js +0 -1
- package/dist/assets/psyche-schemas-beliefs-page-DNmf-Vma.js +0 -9
- package/dist/assets/rewards-page-D3ymEd2X.js +0 -1
- package/dist/assets/settings-bin-page-DqyVI1i1.js +0 -1
- package/dist/assets/settings-models-page-DaFXLp-O.js +0 -1
- package/dist/assets/task-detail-page-CDK79bzk.js +0 -1
- package/dist/assets/timebox-planning-dialog-CNZIGSpd.js +0 -1
- package/dist/assets/today-page-Bnh8wopq.js +0 -1
- package/dist/assets/training-load-page-C7lq-jqy.js +0 -1
- package/dist/assets/wiki-page-1vf-HErs.js +0 -1
- package/dist/assets/workout-detail-page-Cq0YamXF.js +0 -2
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import AdmZip from "adm-zip";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { getDatabase, resolveDataDir, runInTransaction } from "../db.js";
|
|
8
|
+
import { filterDeletedEntities, isEntityDeleted } from "../repositories/deleted-entities.js";
|
|
9
|
+
import { listEntityLinksForSources, replaceEntityLinksForSource } from "../repositories/entity-links.js";
|
|
10
|
+
import { recordEventLog } from "../repositories/event-log.js";
|
|
11
|
+
import { listWikiLlmProfiles } from "../repositories/wiki-memory.js";
|
|
12
|
+
const MAX_ARTIFACT_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
const MAX_TEXT_EXTRACTION_CHARS = 80_000;
|
|
14
|
+
const MAX_LLM_CONTEXT_CHARS = 24_000;
|
|
15
|
+
const MAX_ZIP_ENTRY_COUNT = 5000;
|
|
16
|
+
const MAX_ZIP_UNCOMPRESSED_BYTES = 250 * 1024 * 1024;
|
|
17
|
+
const MAX_ZIP_RATIO = 100;
|
|
18
|
+
const ALLOWED_EXTENSIONS = [
|
|
19
|
+
"xlsx",
|
|
20
|
+
"xlsm",
|
|
21
|
+
"docx",
|
|
22
|
+
"pptx",
|
|
23
|
+
"pdf",
|
|
24
|
+
"csv",
|
|
25
|
+
"tsv",
|
|
26
|
+
"txt",
|
|
27
|
+
"md",
|
|
28
|
+
"json",
|
|
29
|
+
"yaml",
|
|
30
|
+
"yml",
|
|
31
|
+
"png",
|
|
32
|
+
"jpg",
|
|
33
|
+
"jpeg",
|
|
34
|
+
"webp"
|
|
35
|
+
];
|
|
36
|
+
const extensionToFormatFamily = {
|
|
37
|
+
xlsx: "spreadsheet",
|
|
38
|
+
xlsm: "spreadsheet",
|
|
39
|
+
csv: "spreadsheet",
|
|
40
|
+
tsv: "spreadsheet",
|
|
41
|
+
docx: "document",
|
|
42
|
+
pptx: "presentation",
|
|
43
|
+
pdf: "pdf",
|
|
44
|
+
txt: "text",
|
|
45
|
+
md: "text",
|
|
46
|
+
json: "structured_text",
|
|
47
|
+
yaml: "structured_text",
|
|
48
|
+
yml: "structured_text",
|
|
49
|
+
png: "image",
|
|
50
|
+
jpg: "image",
|
|
51
|
+
jpeg: "image",
|
|
52
|
+
webp: "image"
|
|
53
|
+
};
|
|
54
|
+
export const artifactStateSchema = z.enum([
|
|
55
|
+
"active",
|
|
56
|
+
"quarantined",
|
|
57
|
+
"blocked",
|
|
58
|
+
"archived",
|
|
59
|
+
"metadata_only"
|
|
60
|
+
]);
|
|
61
|
+
export const artifactDangerLevelSchema = z.enum([
|
|
62
|
+
"low",
|
|
63
|
+
"moderate",
|
|
64
|
+
"high",
|
|
65
|
+
"blocked"
|
|
66
|
+
]);
|
|
67
|
+
export const artifactDownloadPolicySchema = z.enum([
|
|
68
|
+
"human_only",
|
|
69
|
+
"disabled"
|
|
70
|
+
]);
|
|
71
|
+
export const artifactFormatFamilySchema = z.enum([
|
|
72
|
+
"spreadsheet",
|
|
73
|
+
"document",
|
|
74
|
+
"presentation",
|
|
75
|
+
"pdf",
|
|
76
|
+
"text",
|
|
77
|
+
"structured_text",
|
|
78
|
+
"image"
|
|
79
|
+
]);
|
|
80
|
+
export const artifactSourceKindSchema = z.enum([
|
|
81
|
+
"upload",
|
|
82
|
+
"agent_upload",
|
|
83
|
+
"wiki_ingest",
|
|
84
|
+
"external_reference",
|
|
85
|
+
"manual"
|
|
86
|
+
]);
|
|
87
|
+
const trimmedString = z.string().trim();
|
|
88
|
+
const optionalTrimmedString = trimmedString.optional().default("");
|
|
89
|
+
const nullableId = trimmedString.nullable().optional().default(null);
|
|
90
|
+
export const entityLinkInputSchema = z.object({
|
|
91
|
+
entityType: z.string().trim().min(1),
|
|
92
|
+
entityId: z.string().trim().min(1),
|
|
93
|
+
anchorKey: z.string().trim().optional().default(""),
|
|
94
|
+
relationship: z.string().trim().optional().default("related")
|
|
95
|
+
});
|
|
96
|
+
export const artifactUploadSchema = z.object({
|
|
97
|
+
title: trimmedString.optional(),
|
|
98
|
+
shortDescription: optionalTrimmedString,
|
|
99
|
+
description: optionalTrimmedString,
|
|
100
|
+
originalFileName: trimmedString.min(1),
|
|
101
|
+
declaredMimeType: optionalTrimmedString,
|
|
102
|
+
contentBase64: z.string().min(1),
|
|
103
|
+
sourceKind: artifactSourceKindSchema.optional(),
|
|
104
|
+
sourceLabel: optionalTrimmedString,
|
|
105
|
+
uploadedByUserId: nullableId,
|
|
106
|
+
uploadedByAgentId: nullableId,
|
|
107
|
+
actingForUserId: nullableId,
|
|
108
|
+
downloadPolicy: artifactDownloadPolicySchema.optional().default("human_only"),
|
|
109
|
+
links: z.array(entityLinkInputSchema).optional().default([]),
|
|
110
|
+
metadata: z.record(z.string(), z.unknown()).optional().default({}),
|
|
111
|
+
useLlmEnrichment: z.boolean().optional().default(false),
|
|
112
|
+
llmProfileId: z.string().trim().optional()
|
|
113
|
+
});
|
|
114
|
+
export const artifactMetadataCreateSchema = z.object({
|
|
115
|
+
title: trimmedString.min(1),
|
|
116
|
+
shortDescription: optionalTrimmedString,
|
|
117
|
+
description: optionalTrimmedString,
|
|
118
|
+
originalFileName: trimmedString.optional().default("metadata-only"),
|
|
119
|
+
sourceKind: artifactSourceKindSchema.optional().default("manual"),
|
|
120
|
+
sourceLabel: optionalTrimmedString,
|
|
121
|
+
uploadedByUserId: nullableId,
|
|
122
|
+
uploadedByAgentId: nullableId,
|
|
123
|
+
actingForUserId: nullableId,
|
|
124
|
+
links: z.array(entityLinkInputSchema).optional().default([]),
|
|
125
|
+
metadata: z.record(z.string(), z.unknown()).optional().default({})
|
|
126
|
+
});
|
|
127
|
+
export const artifactMetadataPatchSchema = z.object({
|
|
128
|
+
title: trimmedString.min(1).optional(),
|
|
129
|
+
shortDescription: trimmedString.optional(),
|
|
130
|
+
description: trimmedString.optional(),
|
|
131
|
+
sourceLabel: trimmedString.optional(),
|
|
132
|
+
artifactState: artifactStateSchema.optional(),
|
|
133
|
+
downloadPolicy: artifactDownloadPolicySchema.optional(),
|
|
134
|
+
links: z.array(entityLinkInputSchema).optional(),
|
|
135
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
136
|
+
});
|
|
137
|
+
export const artifactListQuerySchema = z.object({
|
|
138
|
+
query: trimmedString.optional(),
|
|
139
|
+
artifactState: artifactStateSchema.optional(),
|
|
140
|
+
dangerLevel: artifactDangerLevelSchema.optional(),
|
|
141
|
+
formatFamily: artifactFormatFamilySchema.optional(),
|
|
142
|
+
linkedEntityType: z.string().trim().optional(),
|
|
143
|
+
linkedEntityId: z.string().trim().optional(),
|
|
144
|
+
limit: z.coerce.number().int().min(1).max(500).optional().default(100)
|
|
145
|
+
});
|
|
146
|
+
export const artifactTrustPatchSchema = z.object({
|
|
147
|
+
artifactState: artifactStateSchema,
|
|
148
|
+
reason: trimmedString.min(1),
|
|
149
|
+
downloadPolicy: artifactDownloadPolicySchema.optional()
|
|
150
|
+
});
|
|
151
|
+
export const artifactEnrichmentRequestSchema = z.object({
|
|
152
|
+
llmProfileId: z.string().trim().optional(),
|
|
153
|
+
fillMissingOnly: z.boolean().optional().default(true),
|
|
154
|
+
explicitApiKey: z.string().trim().optional()
|
|
155
|
+
});
|
|
156
|
+
function nowIso() {
|
|
157
|
+
return new Date().toISOString();
|
|
158
|
+
}
|
|
159
|
+
function artifactId() {
|
|
160
|
+
return `artifact_${randomUUID().replaceAll("-", "").slice(0, 16)}`;
|
|
161
|
+
}
|
|
162
|
+
function artifactVersionId() {
|
|
163
|
+
return `artifact_version_${randomUUID().replaceAll("-", "").slice(0, 14)}`;
|
|
164
|
+
}
|
|
165
|
+
function auditId() {
|
|
166
|
+
return `artifact_audit_${randomUUID().replaceAll("-", "").slice(0, 14)}`;
|
|
167
|
+
}
|
|
168
|
+
function parseJsonObject(raw) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
172
|
+
? parsed
|
|
173
|
+
: {};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function normalizeNullableText(value) {
|
|
180
|
+
const text = value?.trim();
|
|
181
|
+
return text && text.length > 0 ? text : null;
|
|
182
|
+
}
|
|
183
|
+
function sanitizeFileName(fileName) {
|
|
184
|
+
return path.basename(fileName).replace(/[^\w.\- ()[\]]+/g, "_").slice(0, 180);
|
|
185
|
+
}
|
|
186
|
+
function extensionFromFileName(fileName) {
|
|
187
|
+
return path.extname(sanitizeFileName(fileName)).replace(/^\./, "").toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
function formatFamilyForExtension(extension) {
|
|
190
|
+
return extensionToFormatFamily[extension] ?? null;
|
|
191
|
+
}
|
|
192
|
+
function sha256(buffer) {
|
|
193
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
194
|
+
}
|
|
195
|
+
function detectMimeType(buffer, extension) {
|
|
196
|
+
if (buffer.subarray(0, 4).toString("utf8") === "%PDF") {
|
|
197
|
+
return "application/pdf";
|
|
198
|
+
}
|
|
199
|
+
if (buffer.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47]))) {
|
|
200
|
+
return "image/png";
|
|
201
|
+
}
|
|
202
|
+
if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
|
|
203
|
+
return "image/jpeg";
|
|
204
|
+
}
|
|
205
|
+
if (buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
206
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
207
|
+
return "image/webp";
|
|
208
|
+
}
|
|
209
|
+
if (buffer.subarray(0, 2).toString("ascii") === "PK") {
|
|
210
|
+
if (extension === "docx") {
|
|
211
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
212
|
+
}
|
|
213
|
+
if (extension === "pptx") {
|
|
214
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
215
|
+
}
|
|
216
|
+
if (extension === "xlsx") {
|
|
217
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
218
|
+
}
|
|
219
|
+
if (extension === "xlsm") {
|
|
220
|
+
return "application/vnd.ms-excel.sheet.macroEnabled.12";
|
|
221
|
+
}
|
|
222
|
+
return "application/zip";
|
|
223
|
+
}
|
|
224
|
+
if (extension === "json") {
|
|
225
|
+
return "application/json";
|
|
226
|
+
}
|
|
227
|
+
if (extension === "csv") {
|
|
228
|
+
return "text/csv";
|
|
229
|
+
}
|
|
230
|
+
if (extension === "tsv") {
|
|
231
|
+
return "text/tab-separated-values";
|
|
232
|
+
}
|
|
233
|
+
if (extension === "md") {
|
|
234
|
+
return "text/markdown";
|
|
235
|
+
}
|
|
236
|
+
if (extension === "yaml" || extension === "yml") {
|
|
237
|
+
return "application/yaml";
|
|
238
|
+
}
|
|
239
|
+
return "text/plain";
|
|
240
|
+
}
|
|
241
|
+
function blobRoot() {
|
|
242
|
+
return path.join(resolveDataDir(), "artifacts", "blobs");
|
|
243
|
+
}
|
|
244
|
+
function storageKeyForHash(hash) {
|
|
245
|
+
return `sha256/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}.bin`;
|
|
246
|
+
}
|
|
247
|
+
function resolveStoragePath(storageKey) {
|
|
248
|
+
const root = path.resolve(blobRoot());
|
|
249
|
+
const resolved = path.resolve(root, storageKey);
|
|
250
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) {
|
|
251
|
+
throw new Error("Artifact storage key resolved outside the artifact root.");
|
|
252
|
+
}
|
|
253
|
+
return resolved;
|
|
254
|
+
}
|
|
255
|
+
async function ensureBlobStored(buffer, detectedMimeType) {
|
|
256
|
+
const contentSha256 = sha256(buffer);
|
|
257
|
+
const storageKey = storageKeyForHash(contentSha256);
|
|
258
|
+
const storagePath = resolveStoragePath(storageKey);
|
|
259
|
+
const createdAt = nowIso();
|
|
260
|
+
const existing = getDatabase()
|
|
261
|
+
.prepare("SELECT content_sha256 FROM artifact_blobs WHERE content_sha256 = ?")
|
|
262
|
+
.get(contentSha256);
|
|
263
|
+
if (!existing && !existsSync(storagePath)) {
|
|
264
|
+
await mkdir(path.dirname(storagePath), { recursive: true });
|
|
265
|
+
const tmpPath = `${storagePath}.${process.pid}.${Date.now()}.tmp`;
|
|
266
|
+
try {
|
|
267
|
+
await writeFile(tmpPath, buffer, { flag: "wx" });
|
|
268
|
+
await rename(tmpPath, storagePath);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
await rm(tmpPath, { force: true }).catch(() => undefined);
|
|
272
|
+
if (!existsSync(storagePath)) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
getDatabase()
|
|
278
|
+
.prepare(`INSERT OR IGNORE INTO artifact_blobs (
|
|
279
|
+
content_sha256, storage_key, byte_size, detected_mime_type, created_at
|
|
280
|
+
) VALUES (?, ?, ?, ?, ?)`)
|
|
281
|
+
.run(contentSha256, storageKey, buffer.byteLength, detectedMimeType, createdAt);
|
|
282
|
+
return {
|
|
283
|
+
contentSha256,
|
|
284
|
+
storageKey,
|
|
285
|
+
storagePath
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function addFinding(findings, severity, code, message) {
|
|
289
|
+
findings.push({ severity, code, message });
|
|
290
|
+
}
|
|
291
|
+
function severityScore(severity) {
|
|
292
|
+
switch (severity) {
|
|
293
|
+
case "blocked":
|
|
294
|
+
return 100;
|
|
295
|
+
case "high":
|
|
296
|
+
return 75;
|
|
297
|
+
case "moderate":
|
|
298
|
+
return 45;
|
|
299
|
+
case "low":
|
|
300
|
+
return 15;
|
|
301
|
+
default:
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function computeDanger(findings) {
|
|
306
|
+
const score = Math.min(100, findings.reduce((max, finding) => Math.max(max, severityScore(finding.severity)), 0) +
|
|
307
|
+
Math.max(0, findings.length - 1) * 4);
|
|
308
|
+
const level = score >= 90 ? "blocked" : score >= 70 ? "high" : score >= 35 ? "moderate" : "low";
|
|
309
|
+
return { score, level };
|
|
310
|
+
}
|
|
311
|
+
function safeUtf8(buffer, limit = MAX_TEXT_EXTRACTION_CHARS) {
|
|
312
|
+
return buffer.subarray(0, limit).toString("utf8").replace(/\u0000/g, "").trim();
|
|
313
|
+
}
|
|
314
|
+
function stripXml(xml) {
|
|
315
|
+
return xml
|
|
316
|
+
.replace(/<[^>]+>/g, " ")
|
|
317
|
+
.replace(/&/g, "&")
|
|
318
|
+
.replace(/</g, "<")
|
|
319
|
+
.replace(/>/g, ">")
|
|
320
|
+
.replace(/\s+/g, " ")
|
|
321
|
+
.trim();
|
|
322
|
+
}
|
|
323
|
+
function zipEntryText(zip, name) {
|
|
324
|
+
const entry = zip.getEntries().find((candidate) => candidate.entryName === name);
|
|
325
|
+
return entry ? entry.getData().toString("utf8") : "";
|
|
326
|
+
}
|
|
327
|
+
function extractOfficeText(zip, extension) {
|
|
328
|
+
if (extension === "docx") {
|
|
329
|
+
return stripXml(zipEntryText(zip, "word/document.xml")).slice(0, MAX_TEXT_EXTRACTION_CHARS);
|
|
330
|
+
}
|
|
331
|
+
if (extension === "pptx") {
|
|
332
|
+
return zip
|
|
333
|
+
.getEntries()
|
|
334
|
+
.filter((entry) => /^ppt\/slides\/slide\d+\.xml$/.test(entry.entryName))
|
|
335
|
+
.map((entry) => stripXml(entry.getData().toString("utf8")))
|
|
336
|
+
.join("\n")
|
|
337
|
+
.slice(0, MAX_TEXT_EXTRACTION_CHARS);
|
|
338
|
+
}
|
|
339
|
+
if (extension === "xlsx" || extension === "xlsm") {
|
|
340
|
+
const sharedStrings = stripXml(zipEntryText(zip, "xl/sharedStrings.xml"));
|
|
341
|
+
const sheetText = zip
|
|
342
|
+
.getEntries()
|
|
343
|
+
.filter((entry) => /^xl\/worksheets\/sheet\d+\.xml$/.test(entry.entryName))
|
|
344
|
+
.slice(0, 5)
|
|
345
|
+
.map((entry) => stripXml(entry.getData().toString("utf8")))
|
|
346
|
+
.join("\n");
|
|
347
|
+
return [sharedStrings, sheetText].filter(Boolean).join("\n").slice(0, MAX_TEXT_EXTRACTION_CHARS);
|
|
348
|
+
}
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
function scanOfficeZip(buffer, extension, findings) {
|
|
352
|
+
let extractedTextSample = "";
|
|
353
|
+
try {
|
|
354
|
+
const zip = new AdmZip(buffer);
|
|
355
|
+
const entries = zip.getEntries();
|
|
356
|
+
const totalUncompressed = entries.reduce((sum, entry) => sum + Math.max(0, entry.header?.size ?? 0), 0);
|
|
357
|
+
const totalCompressed = entries.reduce((sum, entry) => sum + Math.max(1, entry.header?.compressedSize ?? 1), 0);
|
|
358
|
+
const ratio = totalUncompressed / Math.max(1, totalCompressed);
|
|
359
|
+
if (entries.length > MAX_ZIP_ENTRY_COUNT) {
|
|
360
|
+
addFinding(findings, "blocked", "zip_entry_limit", "The archive has too many entries for safe static inspection.");
|
|
361
|
+
}
|
|
362
|
+
if (totalUncompressed > MAX_ZIP_UNCOMPRESSED_BYTES || ratio > MAX_ZIP_RATIO) {
|
|
363
|
+
addFinding(findings, "blocked", "zip_bomb_indicator", "The archive has unsafe compressed-to-uncompressed size characteristics.");
|
|
364
|
+
}
|
|
365
|
+
if (entries.some((entry) => entry.entryName.endsWith("EncryptedPackage"))) {
|
|
366
|
+
addFinding(findings, "high", "office_encrypted_package", "The Office document appears encrypted and cannot be inspected fully.");
|
|
367
|
+
}
|
|
368
|
+
if (entries.some((entry) => /vbaProject\.bin$/i.test(entry.entryName))) {
|
|
369
|
+
addFinding(findings, extension === "xlsm" ? "high" : "blocked", "office_macro_project", "The Office document contains a VBA macro project.");
|
|
370
|
+
}
|
|
371
|
+
if (entries.some((entry) => /oleObject|embeddings\//i.test(entry.entryName))) {
|
|
372
|
+
addFinding(findings, "high", "office_embedded_object", "The Office document contains embedded objects or OLE payloads.");
|
|
373
|
+
}
|
|
374
|
+
const relationshipText = entries
|
|
375
|
+
.filter((entry) => entry.entryName.endsWith(".rels"))
|
|
376
|
+
.map((entry) => entry.getData().toString("utf8"))
|
|
377
|
+
.join("\n");
|
|
378
|
+
if (/TargetMode\s*=\s*["']External["']/i.test(relationshipText)) {
|
|
379
|
+
addFinding(findings, "moderate", "office_external_relationship", "The Office document references external resources.");
|
|
380
|
+
}
|
|
381
|
+
if (extension === "xlsx" || extension === "xlsm") {
|
|
382
|
+
const workbookXml = zipEntryText(zip, "xl/workbook.xml");
|
|
383
|
+
if (/state\s*=\s*["'](?:hidden|veryHidden)["']/i.test(workbookXml)) {
|
|
384
|
+
addFinding(findings, "moderate", "spreadsheet_hidden_sheet", "The workbook contains hidden sheets.");
|
|
385
|
+
}
|
|
386
|
+
if (entries.some((entry) => /^xl\/externalLinks\//.test(entry.entryName))) {
|
|
387
|
+
addFinding(findings, "moderate", "spreadsheet_external_link", "The workbook contains external workbook links.");
|
|
388
|
+
}
|
|
389
|
+
if (entries
|
|
390
|
+
.filter((entry) => /^xl\/worksheets\/sheet\d+\.xml$/.test(entry.entryName))
|
|
391
|
+
.some((entry) => /<f(?:\s|>)/i.test(entry.getData().toString("utf8")))) {
|
|
392
|
+
addFinding(findings, "moderate", "spreadsheet_formulas", "The workbook contains formulas. Forge records this but does not evaluate them.");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
extractedTextSample = extractOfficeText(zip, extension);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
addFinding(findings, "high", "zip_parse_error", "The Office archive could not be parsed safely.");
|
|
399
|
+
}
|
|
400
|
+
return extractedTextSample;
|
|
401
|
+
}
|
|
402
|
+
function scanPdf(buffer, findings) {
|
|
403
|
+
const text = buffer.subarray(0, Math.min(buffer.byteLength, 2_000_000)).toString("latin1");
|
|
404
|
+
if (/\/JavaScript|\/JS\b/i.test(text)) {
|
|
405
|
+
addFinding(findings, "high", "pdf_javascript", "The PDF contains JavaScript actions.");
|
|
406
|
+
}
|
|
407
|
+
if (/\/OpenAction|\/AA\b/i.test(text)) {
|
|
408
|
+
addFinding(findings, "moderate", "pdf_auto_action", "The PDF contains automatic document actions.");
|
|
409
|
+
}
|
|
410
|
+
if (/\/EmbeddedFile|\/Filespec/i.test(text)) {
|
|
411
|
+
addFinding(findings, "high", "pdf_embedded_file", "The PDF contains embedded file references.");
|
|
412
|
+
}
|
|
413
|
+
return text
|
|
414
|
+
.replace(/[^\x09\x0a\x0d\x20-\x7e]+/g, " ")
|
|
415
|
+
.replace(/\s+/g, " ")
|
|
416
|
+
.trim()
|
|
417
|
+
.slice(0, MAX_TEXT_EXTRACTION_CHARS);
|
|
418
|
+
}
|
|
419
|
+
function scanDelimitedText(text, findings) {
|
|
420
|
+
if (/^[=+\-@]/m.test(text) || /[,;\t][=+\-@]/.test(text)) {
|
|
421
|
+
addFinding(findings, "moderate", "spreadsheet_formula_like_text", "The delimited text contains formula-like cells. Forge does not evaluate them.");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function scanStructuredText(extension, text, findings) {
|
|
425
|
+
if (extension === "json") {
|
|
426
|
+
try {
|
|
427
|
+
JSON.parse(text);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
addFinding(findings, "low", "json_parse_error", "The JSON file did not parse as valid JSON.");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
export function scanArtifactBytes(input) {
|
|
435
|
+
const detectedExtension = extensionFromFileName(input.originalFileName);
|
|
436
|
+
const detectedMimeType = detectMimeType(input.buffer, detectedExtension);
|
|
437
|
+
const formatFamily = formatFamilyForExtension(detectedExtension);
|
|
438
|
+
const findings = [];
|
|
439
|
+
if (!formatFamily || !ALLOWED_EXTENSIONS.includes(detectedExtension)) {
|
|
440
|
+
addFinding(findings, "blocked", "unsupported_extension", `Files with extension .${detectedExtension || "unknown"} are not allowed.`);
|
|
441
|
+
}
|
|
442
|
+
if (input.buffer.byteLength > MAX_ARTIFACT_BYTES) {
|
|
443
|
+
addFinding(findings, "blocked", "size_limit_exceeded", "The file exceeds Forge's artifact size limit.");
|
|
444
|
+
}
|
|
445
|
+
if (input.declaredMimeType?.trim() && input.declaredMimeType !== detectedMimeType) {
|
|
446
|
+
addFinding(findings, "low", "mime_mismatch", "The declared MIME type differs from static file detection.");
|
|
447
|
+
}
|
|
448
|
+
let extractedTextSample = "";
|
|
449
|
+
if (formatFamily === "document" || formatFamily === "presentation" || formatFamily === "spreadsheet") {
|
|
450
|
+
if (["docx", "pptx", "xlsx", "xlsm"].includes(detectedExtension)) {
|
|
451
|
+
extractedTextSample = scanOfficeZip(input.buffer, detectedExtension, findings);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
extractedTextSample = safeUtf8(input.buffer);
|
|
455
|
+
scanDelimitedText(extractedTextSample, findings);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else if (formatFamily === "pdf") {
|
|
459
|
+
extractedTextSample = scanPdf(input.buffer, findings);
|
|
460
|
+
}
|
|
461
|
+
else if (formatFamily === "text" || formatFamily === "structured_text") {
|
|
462
|
+
extractedTextSample = safeUtf8(input.buffer);
|
|
463
|
+
scanDelimitedText(extractedTextSample, findings);
|
|
464
|
+
scanStructuredText(detectedExtension, extractedTextSample, findings);
|
|
465
|
+
}
|
|
466
|
+
if (findings.length === 0) {
|
|
467
|
+
addFinding(findings, "info", "static_scan_clean", "Static inspection found no configured danger signal.");
|
|
468
|
+
}
|
|
469
|
+
const danger = computeDanger(findings);
|
|
470
|
+
const artifactState = danger.level === "blocked" ? "blocked" : danger.level === "high" ? "quarantined" : "active";
|
|
471
|
+
return {
|
|
472
|
+
detectedExtension,
|
|
473
|
+
detectedMimeType,
|
|
474
|
+
formatFamily: formatFamily ?? "text",
|
|
475
|
+
dangerScore: danger.score,
|
|
476
|
+
dangerLevel: danger.level,
|
|
477
|
+
artifactState,
|
|
478
|
+
scanResults: {
|
|
479
|
+
scannedAt: nowIso(),
|
|
480
|
+
scannerVersion: "artifact-static-scan-v1",
|
|
481
|
+
declaredExtension: detectedExtension,
|
|
482
|
+
detectedMimeType,
|
|
483
|
+
extensionAllowed: Boolean(formatFamily),
|
|
484
|
+
byteSize: input.buffer.byteLength,
|
|
485
|
+
findings,
|
|
486
|
+
extractedTextSample: extractedTextSample.slice(0, MAX_LLM_CONTEXT_CHARS),
|
|
487
|
+
extractedTextTruncated: extractedTextSample.length > MAX_LLM_CONTEXT_CHARS
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function mapLink(row) {
|
|
492
|
+
return {
|
|
493
|
+
sourceEntityType: row.sourceEntityType,
|
|
494
|
+
sourceEntityId: row.sourceEntityId,
|
|
495
|
+
targetEntityType: row.targetEntityType,
|
|
496
|
+
targetEntityId: row.targetEntityId,
|
|
497
|
+
anchorKey: row.anchorKey,
|
|
498
|
+
relationship: row.relationship,
|
|
499
|
+
createdByActor: row.createdByActor,
|
|
500
|
+
createdAt: row.createdAt
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function mapArtifact(row, links = []) {
|
|
504
|
+
return {
|
|
505
|
+
id: row.id,
|
|
506
|
+
title: row.title,
|
|
507
|
+
shortDescription: row.short_description,
|
|
508
|
+
description: row.description,
|
|
509
|
+
originalFileName: row.original_file_name,
|
|
510
|
+
storageKey: row.storage_key,
|
|
511
|
+
storagePath: row.storage_path,
|
|
512
|
+
contentSha256: row.content_sha256,
|
|
513
|
+
byteSize: row.byte_size,
|
|
514
|
+
detectedExtension: row.detected_extension,
|
|
515
|
+
declaredMimeType: row.declared_mime_type,
|
|
516
|
+
detectedMimeType: row.detected_mime_type,
|
|
517
|
+
formatFamily: row.format_family,
|
|
518
|
+
sourceKind: row.source_kind,
|
|
519
|
+
sourceLabel: row.source_label,
|
|
520
|
+
uploadedByUserId: row.uploaded_by_user_id,
|
|
521
|
+
uploadedByAgentId: row.uploaded_by_agent_id,
|
|
522
|
+
actingForUserId: row.acting_for_user_id,
|
|
523
|
+
artifactState: row.artifact_state,
|
|
524
|
+
dangerScore: row.danger_score,
|
|
525
|
+
dangerLevel: row.danger_level,
|
|
526
|
+
downloadPolicy: row.download_policy,
|
|
527
|
+
scanResults: parseJsonObject(row.scan_results_json),
|
|
528
|
+
enrichmentResults: parseJsonObject(row.enrichment_results_json),
|
|
529
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
530
|
+
links,
|
|
531
|
+
createdAt: row.created_at,
|
|
532
|
+
updatedAt: row.updated_at
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function getArtifactRow(id) {
|
|
536
|
+
return getDatabase()
|
|
537
|
+
.prepare(`SELECT id, title, short_description, description, original_file_name,
|
|
538
|
+
storage_key, storage_path, content_sha256, byte_size,
|
|
539
|
+
detected_extension, declared_mime_type, detected_mime_type,
|
|
540
|
+
format_family, source_kind, source_label, uploaded_by_user_id,
|
|
541
|
+
uploaded_by_agent_id, acting_for_user_id, artifact_state,
|
|
542
|
+
danger_score, danger_level, download_policy, scan_results_json,
|
|
543
|
+
enrichment_results_json, metadata_json, created_at, updated_at
|
|
544
|
+
FROM artifacts
|
|
545
|
+
WHERE id = ?`)
|
|
546
|
+
.get(id);
|
|
547
|
+
}
|
|
548
|
+
function listArtifactRows() {
|
|
549
|
+
return getDatabase()
|
|
550
|
+
.prepare(`SELECT id, title, short_description, description, original_file_name,
|
|
551
|
+
storage_key, storage_path, content_sha256, byte_size,
|
|
552
|
+
detected_extension, declared_mime_type, detected_mime_type,
|
|
553
|
+
format_family, source_kind, source_label, uploaded_by_user_id,
|
|
554
|
+
uploaded_by_agent_id, acting_for_user_id, artifact_state,
|
|
555
|
+
danger_score, danger_level, download_policy, scan_results_json,
|
|
556
|
+
enrichment_results_json, metadata_json, created_at, updated_at
|
|
557
|
+
FROM artifacts
|
|
558
|
+
ORDER BY updated_at DESC`)
|
|
559
|
+
.all();
|
|
560
|
+
}
|
|
561
|
+
function toEventMetadata(metadata) {
|
|
562
|
+
const normalized = {};
|
|
563
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
564
|
+
if (typeof value === "string" ||
|
|
565
|
+
typeof value === "number" ||
|
|
566
|
+
typeof value === "boolean" ||
|
|
567
|
+
value === null) {
|
|
568
|
+
normalized[key] = value;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
normalized[key] = JSON.stringify(value);
|
|
572
|
+
}
|
|
573
|
+
return normalized;
|
|
574
|
+
}
|
|
575
|
+
function recordArtifactAudit(artifactId, eventType, context, metadata = {}) {
|
|
576
|
+
const createdAt = nowIso();
|
|
577
|
+
getDatabase()
|
|
578
|
+
.prepare(`INSERT INTO artifact_audit_events (
|
|
579
|
+
id, artifact_id, event_type, actor, source, metadata_json, created_at
|
|
580
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
581
|
+
.run(auditId(), artifactId, eventType, context.actor ?? context.token?.agentLabel ?? null, context.source, JSON.stringify(metadata), createdAt);
|
|
582
|
+
recordEventLog({
|
|
583
|
+
eventKind: eventType,
|
|
584
|
+
entityType: "artifact",
|
|
585
|
+
entityId: artifactId,
|
|
586
|
+
actor: context.actor ?? context.token?.agentLabel ?? null,
|
|
587
|
+
source: context.source,
|
|
588
|
+
metadata: toEventMetadata(metadata)
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function replaceEntityLinksForArtifact(artifactId, links, context) {
|
|
592
|
+
replaceEntityLinksForSource({
|
|
593
|
+
sourceEntityType: "artifact",
|
|
594
|
+
sourceEntityId: artifactId,
|
|
595
|
+
links,
|
|
596
|
+
actor: context.actor ?? context.token?.agentLabel ?? null
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function insertArtifactVersion(input) {
|
|
600
|
+
const row = getDatabase()
|
|
601
|
+
.prepare(`SELECT COALESCE(MAX(version_number), 0) + 1 AS nextVersion
|
|
602
|
+
FROM artifact_versions
|
|
603
|
+
WHERE artifact_id = ?`)
|
|
604
|
+
.get(input.artifactId);
|
|
605
|
+
getDatabase()
|
|
606
|
+
.prepare(`INSERT INTO artifact_versions (
|
|
607
|
+
id, artifact_id, version_number, content_sha256, storage_key, byte_size,
|
|
608
|
+
original_file_name, scan_results_json, enrichment_results_json,
|
|
609
|
+
created_by_actor, created_at
|
|
610
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
611
|
+
.run(artifactVersionId(), input.artifactId, row.nextVersion, input.contentSha256, input.storageKey, input.byteSize, input.originalFileName, JSON.stringify(input.scanResults), JSON.stringify(input.enrichmentResults), input.context.actor ?? input.context.token?.agentLabel ?? null, nowIso());
|
|
612
|
+
}
|
|
613
|
+
function deriveFallbackTitle(originalFileName) {
|
|
614
|
+
const sanitized = sanitizeFileName(originalFileName);
|
|
615
|
+
return sanitized.replace(/\.[^.]+$/, "").replace(/[_-]+/g, " ").trim() || "Artifact";
|
|
616
|
+
}
|
|
617
|
+
export async function createArtifactFromUpload(input, context, services = {}) {
|
|
618
|
+
const parsed = artifactUploadSchema.parse(input);
|
|
619
|
+
const buffer = Buffer.from(parsed.contentBase64, "base64");
|
|
620
|
+
if (buffer.byteLength === 0) {
|
|
621
|
+
throw new Error("Artifact upload content is empty or invalid base64.");
|
|
622
|
+
}
|
|
623
|
+
const scan = scanArtifactBytes({
|
|
624
|
+
buffer,
|
|
625
|
+
originalFileName: parsed.originalFileName,
|
|
626
|
+
declaredMimeType: parsed.declaredMimeType
|
|
627
|
+
});
|
|
628
|
+
const blob = await ensureBlobStored(buffer, scan.detectedMimeType);
|
|
629
|
+
const id = artifactId();
|
|
630
|
+
const createdAt = nowIso();
|
|
631
|
+
const sourceKind = parsed.sourceKind ?? (context.source === "agent" ? "agent_upload" : "upload");
|
|
632
|
+
const uploadedByAgentId = parsed.uploadedByAgentId ?? context.token?.agentId ?? null;
|
|
633
|
+
const metadata = {
|
|
634
|
+
...parsed.metadata,
|
|
635
|
+
safeHandling: "Forge stores and serves this file for human download only; it must not be executed by agents."
|
|
636
|
+
};
|
|
637
|
+
const initialEnrichment = {
|
|
638
|
+
generated: false,
|
|
639
|
+
status: parsed.useLlmEnrichment ? "pending" : "not_requested"
|
|
640
|
+
};
|
|
641
|
+
runInTransaction(() => {
|
|
642
|
+
getDatabase()
|
|
643
|
+
.prepare(`INSERT INTO artifacts (
|
|
644
|
+
id, title, short_description, description, original_file_name,
|
|
645
|
+
storage_key, storage_path, content_sha256, byte_size, detected_extension,
|
|
646
|
+
declared_mime_type, detected_mime_type, format_family, source_kind,
|
|
647
|
+
source_label, uploaded_by_user_id, uploaded_by_agent_id, acting_for_user_id,
|
|
648
|
+
artifact_state, danger_score, danger_level, download_policy,
|
|
649
|
+
scan_results_json, enrichment_results_json, metadata_json, created_at, updated_at
|
|
650
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
651
|
+
.run(id, parsed.title?.trim() || deriveFallbackTitle(parsed.originalFileName), parsed.shortDescription, parsed.description, sanitizeFileName(parsed.originalFileName), blob.storageKey, blob.storagePath, blob.contentSha256, buffer.byteLength, scan.detectedExtension, parsed.declaredMimeType, scan.detectedMimeType, scan.formatFamily, sourceKind, parsed.sourceLabel, parsed.uploadedByUserId, uploadedByAgentId, parsed.actingForUserId, scan.artifactState, scan.dangerScore, scan.dangerLevel, parsed.downloadPolicy, JSON.stringify(scan.scanResults), JSON.stringify(initialEnrichment), JSON.stringify(metadata), createdAt, createdAt);
|
|
652
|
+
replaceEntityLinksForArtifact(id, parsed.links, context);
|
|
653
|
+
insertArtifactVersion({
|
|
654
|
+
artifactId: id,
|
|
655
|
+
contentSha256: blob.contentSha256,
|
|
656
|
+
storageKey: blob.storageKey,
|
|
657
|
+
byteSize: buffer.byteLength,
|
|
658
|
+
originalFileName: sanitizeFileName(parsed.originalFileName),
|
|
659
|
+
scanResults: scan.scanResults,
|
|
660
|
+
enrichmentResults: initialEnrichment,
|
|
661
|
+
context
|
|
662
|
+
});
|
|
663
|
+
recordArtifactAudit(id, "artifact.created", context, {
|
|
664
|
+
contentSha256: blob.contentSha256,
|
|
665
|
+
dangerScore: scan.dangerScore,
|
|
666
|
+
dangerLevel: scan.dangerLevel,
|
|
667
|
+
sourceKind
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
if (parsed.useLlmEnrichment) {
|
|
671
|
+
await enrichArtifactWithLlm(id, {
|
|
672
|
+
llmProfileId: parsed.llmProfileId,
|
|
673
|
+
fillMissingOnly: true
|
|
674
|
+
}, context, services).catch((error) => {
|
|
675
|
+
runInTransaction(() => {
|
|
676
|
+
updateArtifactEnrichment(id, {
|
|
677
|
+
generated: false,
|
|
678
|
+
status: "failed",
|
|
679
|
+
error: error instanceof Error ? error.message : String(error),
|
|
680
|
+
generatedAt: nowIso()
|
|
681
|
+
});
|
|
682
|
+
recordArtifactAudit(id, "artifact.enrichment_failed", context, {
|
|
683
|
+
error: error instanceof Error ? error.message : String(error)
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return getArtifactById(id);
|
|
689
|
+
}
|
|
690
|
+
export function createArtifactMetadata() {
|
|
691
|
+
throw new Error("Use POST /api/v1/artifacts for artifact creation. Batch CRUD may search, link, update metadata, delete, and restore artifact records, but it must not create file artifacts.");
|
|
692
|
+
}
|
|
693
|
+
export function listArtifacts(input = {}) {
|
|
694
|
+
const parsed = artifactListQuerySchema.parse(input);
|
|
695
|
+
const rows = filterDeletedEntities("artifact", listArtifactRows());
|
|
696
|
+
const linksByArtifactId = new Map();
|
|
697
|
+
for (const linkRow of listEntityLinksForSources("artifact", rows.map((row) => row.id))) {
|
|
698
|
+
const current = linksByArtifactId.get(linkRow.sourceEntityId) ?? [];
|
|
699
|
+
current.push(mapLink(linkRow));
|
|
700
|
+
linksByArtifactId.set(linkRow.sourceEntityId, current);
|
|
701
|
+
}
|
|
702
|
+
const query = parsed.query?.toLowerCase() ?? "";
|
|
703
|
+
return rows
|
|
704
|
+
.map((row) => mapArtifact(row, linksByArtifactId.get(row.id) ?? []))
|
|
705
|
+
.filter((artifact) => parsed.artifactState ? artifact.artifactState === parsed.artifactState : true)
|
|
706
|
+
.filter((artifact) => parsed.dangerLevel ? artifact.dangerLevel === parsed.dangerLevel : true)
|
|
707
|
+
.filter((artifact) => parsed.formatFamily ? artifact.formatFamily === parsed.formatFamily : true)
|
|
708
|
+
.filter((artifact) => query
|
|
709
|
+
? JSON.stringify({
|
|
710
|
+
title: artifact.title,
|
|
711
|
+
shortDescription: artifact.shortDescription,
|
|
712
|
+
description: artifact.description,
|
|
713
|
+
originalFileName: artifact.originalFileName,
|
|
714
|
+
sourceLabel: artifact.sourceLabel,
|
|
715
|
+
metadata: artifact.metadata
|
|
716
|
+
})
|
|
717
|
+
.toLowerCase()
|
|
718
|
+
.includes(query)
|
|
719
|
+
: true)
|
|
720
|
+
.filter((artifact) => parsed.linkedEntityType && parsed.linkedEntityId
|
|
721
|
+
? artifact.links.some((link) => link.targetEntityType === parsed.linkedEntityType &&
|
|
722
|
+
link.targetEntityId === parsed.linkedEntityId)
|
|
723
|
+
: true)
|
|
724
|
+
.slice(0, parsed.limit);
|
|
725
|
+
}
|
|
726
|
+
export function getArtifactById(id) {
|
|
727
|
+
if (isEntityDeleted("artifact", id)) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
const row = getArtifactRow(id);
|
|
731
|
+
if (!row) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
return mapArtifact(row, listEntityLinksForSources("artifact", [id]).map(mapLink));
|
|
735
|
+
}
|
|
736
|
+
export function updateArtifactMetadata(id, input, context) {
|
|
737
|
+
const existing = getArtifactById(id);
|
|
738
|
+
if (!existing) {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
const parsed = artifactMetadataPatchSchema.parse(input);
|
|
742
|
+
const updatedAt = nowIso();
|
|
743
|
+
const nextMetadata = parsed.metadata
|
|
744
|
+
? { ...existing.metadata, ...parsed.metadata }
|
|
745
|
+
: existing.metadata;
|
|
746
|
+
const nextLinks = parsed.links ?? existing.links.map((link) => ({
|
|
747
|
+
entityType: link.targetEntityType,
|
|
748
|
+
entityId: link.targetEntityId,
|
|
749
|
+
anchorKey: link.anchorKey ?? "",
|
|
750
|
+
relationship: link.relationship
|
|
751
|
+
}));
|
|
752
|
+
runInTransaction(() => {
|
|
753
|
+
getDatabase()
|
|
754
|
+
.prepare(`UPDATE artifacts
|
|
755
|
+
SET title = ?, short_description = ?, description = ?, source_label = ?,
|
|
756
|
+
artifact_state = ?, download_policy = ?, metadata_json = ?, updated_at = ?
|
|
757
|
+
WHERE id = ?`)
|
|
758
|
+
.run(parsed.title ?? existing.title, parsed.shortDescription ?? existing.shortDescription, parsed.description ?? existing.description, parsed.sourceLabel ?? existing.sourceLabel, parsed.artifactState ?? existing.artifactState, parsed.downloadPolicy ?? existing.downloadPolicy, JSON.stringify(nextMetadata), updatedAt, id);
|
|
759
|
+
if (parsed.links) {
|
|
760
|
+
replaceEntityLinksForArtifact(id, nextLinks, context);
|
|
761
|
+
}
|
|
762
|
+
recordArtifactAudit(id, "artifact.metadata_updated", context, {
|
|
763
|
+
fields: Object.keys(parsed)
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
return getArtifactById(id);
|
|
767
|
+
}
|
|
768
|
+
export function deleteArtifactMetadata(id, context) {
|
|
769
|
+
const existing = getArtifactById(id);
|
|
770
|
+
if (!existing) {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
runInTransaction(() => {
|
|
774
|
+
replaceEntityLinksForSource({
|
|
775
|
+
sourceEntityType: "artifact",
|
|
776
|
+
sourceEntityId: id,
|
|
777
|
+
links: [],
|
|
778
|
+
actor: context.actor ?? context.token?.agentLabel ?? null
|
|
779
|
+
});
|
|
780
|
+
getDatabase().prepare("DELETE FROM artifacts WHERE id = ?").run(id);
|
|
781
|
+
recordEventLog({
|
|
782
|
+
eventKind: "artifact.metadata_deleted",
|
|
783
|
+
entityType: "artifact",
|
|
784
|
+
entityId: id,
|
|
785
|
+
actor: context.actor ?? context.token?.agentLabel ?? null,
|
|
786
|
+
source: context.source,
|
|
787
|
+
metadata: toEventMetadata({
|
|
788
|
+
contentSha256: existing.contentSha256,
|
|
789
|
+
storageKey: existing.storageKey,
|
|
790
|
+
blobPreserved: true,
|
|
791
|
+
entityLinksRemoved: true
|
|
792
|
+
})
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
return existing;
|
|
796
|
+
}
|
|
797
|
+
export async function readArtifactDownload(id) {
|
|
798
|
+
const artifact = getArtifactById(id);
|
|
799
|
+
if (!artifact) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
if (artifact.downloadPolicy !== "human_only" || artifact.artifactState === "blocked") {
|
|
803
|
+
throw new Error("This artifact is not downloadable in its current state.");
|
|
804
|
+
}
|
|
805
|
+
const storagePath = resolveStoragePath(artifact.storageKey);
|
|
806
|
+
return {
|
|
807
|
+
artifact,
|
|
808
|
+
bytes: await readFile(storagePath)
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
export async function rescanArtifact(id, context) {
|
|
812
|
+
const artifact = getArtifactById(id);
|
|
813
|
+
if (!artifact) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
const storagePath = resolveStoragePath(artifact.storageKey);
|
|
817
|
+
if (!existsSync(storagePath)) {
|
|
818
|
+
runInTransaction(() => {
|
|
819
|
+
recordArtifactAudit(id, "artifact.scan_failed", context, {
|
|
820
|
+
reason: "blob_missing",
|
|
821
|
+
storageKey: artifact.storageKey
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
throw new Error("Artifact blob is missing from local storage.");
|
|
825
|
+
}
|
|
826
|
+
const buffer = await readFile(storagePath);
|
|
827
|
+
const scan = scanArtifactBytes({
|
|
828
|
+
buffer,
|
|
829
|
+
originalFileName: artifact.originalFileName,
|
|
830
|
+
declaredMimeType: artifact.declaredMimeType
|
|
831
|
+
});
|
|
832
|
+
const updatedAt = nowIso();
|
|
833
|
+
runInTransaction(() => {
|
|
834
|
+
getDatabase()
|
|
835
|
+
.prepare(`UPDATE artifacts
|
|
836
|
+
SET detected_extension = ?, detected_mime_type = ?, format_family = ?,
|
|
837
|
+
artifact_state = ?, danger_score = ?, danger_level = ?,
|
|
838
|
+
scan_results_json = ?, updated_at = ?
|
|
839
|
+
WHERE id = ?`)
|
|
840
|
+
.run(scan.detectedExtension, scan.detectedMimeType, scan.formatFamily, scan.artifactState, scan.dangerScore, scan.dangerLevel, JSON.stringify(scan.scanResults), updatedAt, id);
|
|
841
|
+
recordArtifactAudit(id, "artifact.scanned", context, {
|
|
842
|
+
dangerScore: scan.dangerScore,
|
|
843
|
+
dangerLevel: scan.dangerLevel
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
return getArtifactById(id);
|
|
847
|
+
}
|
|
848
|
+
function updateArtifactEnrichment(id, enrichment) {
|
|
849
|
+
getDatabase()
|
|
850
|
+
.prepare(`UPDATE artifacts
|
|
851
|
+
SET enrichment_results_json = ?, updated_at = ?
|
|
852
|
+
WHERE id = ?`)
|
|
853
|
+
.run(JSON.stringify(enrichment), nowIso(), id);
|
|
854
|
+
}
|
|
855
|
+
function extractJsonObject(text) {
|
|
856
|
+
const trimmed = text.trim();
|
|
857
|
+
const start = trimmed.indexOf("{");
|
|
858
|
+
const end = trimmed.lastIndexOf("}");
|
|
859
|
+
if (start < 0 || end <= start) {
|
|
860
|
+
return {};
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
864
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
865
|
+
? parsed
|
|
866
|
+
: {};
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
return {};
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function compactArtifactForPrompt(artifact) {
|
|
873
|
+
const scan = artifact.scanResults;
|
|
874
|
+
return {
|
|
875
|
+
title: artifact.title,
|
|
876
|
+
shortDescription: artifact.shortDescription,
|
|
877
|
+
description: artifact.description,
|
|
878
|
+
originalFileName: artifact.originalFileName,
|
|
879
|
+
detectedExtension: artifact.detectedExtension,
|
|
880
|
+
declaredMimeType: artifact.declaredMimeType,
|
|
881
|
+
detectedMimeType: artifact.detectedMimeType,
|
|
882
|
+
formatFamily: artifact.formatFamily,
|
|
883
|
+
byteSize: artifact.byteSize,
|
|
884
|
+
deterministicDangerScore: artifact.dangerScore,
|
|
885
|
+
deterministicDangerLevel: artifact.dangerLevel,
|
|
886
|
+
findings: Array.isArray(scan.findings) ? scan.findings : [],
|
|
887
|
+
extractedTextSample: typeof scan.extractedTextSample === "string"
|
|
888
|
+
? scan.extractedTextSample.slice(0, MAX_LLM_CONTEXT_CHARS)
|
|
889
|
+
: ""
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
export async function enrichArtifactWithLlm(id, input, context, services = {}) {
|
|
893
|
+
const artifact = getArtifactById(id);
|
|
894
|
+
if (!artifact) {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
const parsed = artifactEnrichmentRequestSchema.parse(input);
|
|
898
|
+
const profile = parsed.llmProfileId
|
|
899
|
+
? listWikiLlmProfiles().find((entry) => entry.id === parsed.llmProfileId)
|
|
900
|
+
: listWikiLlmProfiles().find((entry) => entry.enabled);
|
|
901
|
+
if (!services.llm || !profile) {
|
|
902
|
+
const enrichment = {
|
|
903
|
+
generated: false,
|
|
904
|
+
status: "skipped",
|
|
905
|
+
reason: "No enabled LLM profile is connected.",
|
|
906
|
+
generatedAt: nowIso()
|
|
907
|
+
};
|
|
908
|
+
runInTransaction(() => {
|
|
909
|
+
updateArtifactEnrichment(id, enrichment);
|
|
910
|
+
recordArtifactAudit(id, "artifact.enrichment_skipped", context, {
|
|
911
|
+
reason: "no_llm_profile"
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
return getArtifactById(id);
|
|
915
|
+
}
|
|
916
|
+
const prompt = [
|
|
917
|
+
"You are enriching metadata for a Forge artifact store.",
|
|
918
|
+
"Do not infer executable behavior and do not lower deterministic safety findings.",
|
|
919
|
+
"Return only JSON with keys: title, shortDescription, description, documentType, keywords, suggestedForgeLinks, safetySummary, dangerReasons, dangerScoreAdjustment.",
|
|
920
|
+
JSON.stringify(compactArtifactForPrompt(artifact), null, 2)
|
|
921
|
+
].join("\n\n");
|
|
922
|
+
const result = await services.llm.runTextPrompt(profile, {
|
|
923
|
+
explicitApiKey: parsed.explicitApiKey,
|
|
924
|
+
systemPrompt: "You summarize stored files from static, non-executed text only. You never say a file is safe if deterministic scanning found risk.",
|
|
925
|
+
prompt
|
|
926
|
+
}, (log) => {
|
|
927
|
+
recordArtifactAudit(id, "artifact.enrichment_log", context, {
|
|
928
|
+
level: log.level,
|
|
929
|
+
message: log.message
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
const generated = extractJsonObject(result.outputText);
|
|
933
|
+
const proposedScore = typeof generated.dangerScoreAdjustment === "number"
|
|
934
|
+
? generated.dangerScoreAdjustment
|
|
935
|
+
: artifact.dangerScore;
|
|
936
|
+
const nextDangerScore = Math.max(artifact.dangerScore, Math.min(100, proposedScore));
|
|
937
|
+
const enrichment = {
|
|
938
|
+
generated: true,
|
|
939
|
+
status: "completed",
|
|
940
|
+
provider: profile.provider,
|
|
941
|
+
model: profile.model,
|
|
942
|
+
generatedAt: nowIso(),
|
|
943
|
+
output: {
|
|
944
|
+
...generated,
|
|
945
|
+
dangerScore: nextDangerScore,
|
|
946
|
+
deterministicDangerScorePreserved: artifact.dangerScore
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const title = !parsed.fillMissingOnly || !artifact.title.trim()
|
|
950
|
+
? typeof generated.title === "string" && generated.title.trim()
|
|
951
|
+
? generated.title.trim()
|
|
952
|
+
: artifact.title
|
|
953
|
+
: artifact.title;
|
|
954
|
+
const shortDescription = !parsed.fillMissingOnly || !artifact.shortDescription.trim()
|
|
955
|
+
? typeof generated.shortDescription === "string"
|
|
956
|
+
? generated.shortDescription.trim()
|
|
957
|
+
: artifact.shortDescription
|
|
958
|
+
: artifact.shortDescription;
|
|
959
|
+
const description = !parsed.fillMissingOnly || !artifact.description.trim()
|
|
960
|
+
? typeof generated.description === "string"
|
|
961
|
+
? generated.description.trim()
|
|
962
|
+
: artifact.description
|
|
963
|
+
: artifact.description;
|
|
964
|
+
runInTransaction(() => {
|
|
965
|
+
getDatabase()
|
|
966
|
+
.prepare(`UPDATE artifacts
|
|
967
|
+
SET title = ?, short_description = ?, description = ?,
|
|
968
|
+
danger_score = MAX(danger_score, ?), enrichment_results_json = ?,
|
|
969
|
+
updated_at = ?
|
|
970
|
+
WHERE id = ?`)
|
|
971
|
+
.run(title, shortDescription, description, nextDangerScore, JSON.stringify(enrichment), nowIso(), id);
|
|
972
|
+
recordArtifactAudit(id, "artifact.enriched_with_llm", context, {
|
|
973
|
+
provider: profile.provider,
|
|
974
|
+
model: profile.model,
|
|
975
|
+
dangerScore: nextDangerScore
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
return getArtifactById(id);
|
|
979
|
+
}
|
|
980
|
+
export function replaceArtifactEntityLinks(id, links, context) {
|
|
981
|
+
const artifact = getArtifactById(id);
|
|
982
|
+
if (!artifact) {
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
runInTransaction(() => {
|
|
986
|
+
replaceEntityLinksForArtifact(id, links, context);
|
|
987
|
+
getDatabase()
|
|
988
|
+
.prepare("UPDATE artifacts SET updated_at = ? WHERE id = ?")
|
|
989
|
+
.run(nowIso(), id);
|
|
990
|
+
recordArtifactAudit(id, "artifact.links_updated", context, {
|
|
991
|
+
linkCount: links.length
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
return getArtifactById(id);
|
|
995
|
+
}
|
|
996
|
+
export function patchArtifactTrust(id, input, context) {
|
|
997
|
+
const artifact = getArtifactById(id);
|
|
998
|
+
if (!artifact) {
|
|
999
|
+
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
const parsed = artifactTrustPatchSchema.parse(input);
|
|
1002
|
+
getDatabase()
|
|
1003
|
+
.prepare(`UPDATE artifacts
|
|
1004
|
+
SET artifact_state = ?, download_policy = ?, updated_at = ?
|
|
1005
|
+
WHERE id = ?`)
|
|
1006
|
+
.run(parsed.artifactState, parsed.downloadPolicy ?? artifact.downloadPolicy, nowIso(), id);
|
|
1007
|
+
recordArtifactAudit(id, "artifact.trust_state_updated", context, {
|
|
1008
|
+
from: artifact.artifactState,
|
|
1009
|
+
to: parsed.artifactState,
|
|
1010
|
+
reason: parsed.reason
|
|
1011
|
+
});
|
|
1012
|
+
return getArtifactById(id);
|
|
1013
|
+
}
|
|
1014
|
+
export function listArtifactVersions(id) {
|
|
1015
|
+
return getDatabase()
|
|
1016
|
+
.prepare(`SELECT id, artifact_id, version_number, content_sha256, storage_key,
|
|
1017
|
+
byte_size, original_file_name, scan_results_json,
|
|
1018
|
+
enrichment_results_json, created_by_actor, created_at
|
|
1019
|
+
FROM artifact_versions
|
|
1020
|
+
WHERE artifact_id = ?
|
|
1021
|
+
ORDER BY version_number DESC`)
|
|
1022
|
+
.all(id)
|
|
1023
|
+
.map((row) => {
|
|
1024
|
+
const version = row;
|
|
1025
|
+
return {
|
|
1026
|
+
id: version.id,
|
|
1027
|
+
artifactId: version.artifact_id,
|
|
1028
|
+
versionNumber: version.version_number,
|
|
1029
|
+
contentSha256: version.content_sha256,
|
|
1030
|
+
storageKey: version.storage_key,
|
|
1031
|
+
byteSize: version.byte_size,
|
|
1032
|
+
originalFileName: version.original_file_name,
|
|
1033
|
+
scanResults: parseJsonObject(version.scan_results_json),
|
|
1034
|
+
enrichmentResults: parseJsonObject(version.enrichment_results_json),
|
|
1035
|
+
createdByActor: version.created_by_actor,
|
|
1036
|
+
createdAt: version.created_at
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
export function listArtifactAuditEvents(id) {
|
|
1041
|
+
return getDatabase()
|
|
1042
|
+
.prepare(`SELECT id, artifact_id, event_type, actor, source, metadata_json, created_at
|
|
1043
|
+
FROM artifact_audit_events
|
|
1044
|
+
WHERE artifact_id = ?
|
|
1045
|
+
ORDER BY created_at DESC`)
|
|
1046
|
+
.all(id)
|
|
1047
|
+
.map((row) => {
|
|
1048
|
+
const event = row;
|
|
1049
|
+
return {
|
|
1050
|
+
id: event.id,
|
|
1051
|
+
artifactId: event.artifact_id,
|
|
1052
|
+
eventType: event.event_type,
|
|
1053
|
+
actor: event.actor,
|
|
1054
|
+
source: event.source,
|
|
1055
|
+
metadata: parseJsonObject(event.metadata_json),
|
|
1056
|
+
createdAt: event.created_at
|
|
1057
|
+
};
|
|
1058
|
+
});
|
|
1059
|
+
}
|