@mengruo/dsh-vision-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +356 -0
- package/README.zh.md +354 -0
- package/assets/community-group-qr.png +0 -0
- package/assets/dsh-conversation-artifact.png +0 -0
- package/assets/dsh-conversation-image-qa-top.png +0 -0
- package/assets/dsh-conversation-image-qa.png +0 -0
- package/assets/dsh-conversation-pixel-diff.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging-top.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging.png +0 -0
- package/assets/dsh-conversation-tool-call.png +0 -0
- package/assets/dsh-conversation-vision-trace.png +0 -0
- package/assets/dsh-view-example.png +0 -0
- package/assets/hero-v2.png +0 -0
- package/assets/logo_aihubmix.png +0 -0
- package/assets/python-bootstrap.json +48 -0
- package/assets/skill/SKILL.md +329 -0
- package/assets/skill/UPSTREAM.json +71 -0
- package/assets/skill/references/gui.md +88 -0
- package/assets/skill/references/long-screenshot-ocr.md +77 -0
- package/assets/skill/references/restore-graphic.md +84 -0
- package/assets/skill/references/restore-structure.md +45 -0
- package/assets/skill/references/restore-ui.md +202 -0
- package/assets/social-preview.png +0 -0
- package/assets/upstream/README.md +18 -0
- package/assets/upstream/focus-hint-comparison-1.webp +0 -0
- package/assets/upstream/focus-hint-comparison-2.webp +0 -0
- package/assets/upstream/infographic-reference.webp +0 -0
- package/assets/upstream/infographic-result.webp +0 -0
- package/assets/upstream/ui-fast-restore-reference.webp +0 -0
- package/assets/upstream/ui-fast-restore-result.webp +0 -0
- package/assets/upstream/ui-result.webp +0 -0
- package/assets/upstream/ui-sketch.webp +0 -0
- package/assets/vision-model-test.png +0 -0
- package/assets/vision-settings.png +0 -0
- package/assets/wechat-reward.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +6 -0
- package/docs/aihubmix-gemini-vision.md +138 -0
- package/docs/aihubmix-gemini-vision.zh.md +138 -0
- package/docs/assets/aihubmix-api-keys.png +0 -0
- package/docs/assets/aihubmix-create-key.png +0 -0
- package/docs/assets/aihubmix-free-vision-model.png +0 -0
- package/docs/assets/aihubmix-home.png +0 -0
- package/docs/assets/aihubmix-sign-up.png +0 -0
- package/docs/assets/groq-console-home.png +0 -0
- package/docs/assets/groq-console-keys-login.png +0 -0
- package/docs/assets/groq-docs-quickstart.png +0 -0
- package/docs/assets/groq-docs-qwen3.6.png +0 -0
- package/docs/assets/groq-docs-vision.png +0 -0
- package/docs/assets/vision-settings.png +0 -0
- package/docs/dsh-desktop-install.md +67 -0
- package/docs/dsh-desktop-install.zh.md +67 -0
- package/docs/groq-qwen3.6-vision.i18n.yaml +6 -0
- package/docs/groq-qwen3.6-vision.md +236 -0
- package/docs/groq-qwen3.6-vision.zh.md +236 -0
- package/docs/python-runtime.i18n.yaml +6 -0
- package/docs/python-runtime.md +89 -0
- package/docs/python-runtime.zh.md +89 -0
- package/docs/requirements-traceability/README.i18n.yaml +6 -0
- package/docs/requirements-traceability/README.md +75 -0
- package/docs/requirements-traceability/README.zh.md +75 -0
- package/examples/ui-restoration/README.i18n.yaml +6 -0
- package/examples/ui-restoration/README.md +70 -0
- package/examples/ui-restoration/README.zh.md +70 -0
- package/examples/ui-restoration/assets/final-heatmap.png +0 -0
- package/examples/ui-restoration/assets/final-report.json +83 -0
- package/examples/ui-restoration/assets/implementation.png +0 -0
- package/examples/ui-restoration/assets/initial-heatmap.png +0 -0
- package/examples/ui-restoration/assets/initial-report.json +83 -0
- package/examples/ui-restoration/assets/initial.png +0 -0
- package/examples/ui-restoration/assets/metrics.json +12 -0
- package/examples/ui-restoration/assets/reference.png +0 -0
- package/examples/ui-restoration/implementation.html +94 -0
- package/examples/ui-restoration/initial.html +57 -0
- package/lib/artifact-access.js +369 -0
- package/lib/artifact-access.js.map +1 -0
- package/lib/artifacts.js +56 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/client.js +2099 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +172 -0
- package/lib/config.js.map +1 -0
- package/lib/defaults.js +6 -0
- package/lib/defaults.js.map +1 -0
- package/lib/errors.js +56 -0
- package/lib/errors.js.map +1 -0
- package/lib/evidence-cache.js +350 -0
- package/lib/evidence-cache.js.map +1 -0
- package/lib/exposure.js +247 -0
- package/lib/exposure.js.map +1 -0
- package/lib/image-input-variants.js +882 -0
- package/lib/image-input-variants.js.map +1 -0
- package/lib/index.js +105 -0
- package/lib/index.js.map +1 -0
- package/lib/paste-images.js +217 -0
- package/lib/paste-images.js.map +1 -0
- package/lib/paths.js +354 -0
- package/lib/paths.js.map +1 -0
- package/lib/plugin-update.js +1003 -0
- package/lib/plugin-update.js.map +1 -0
- package/lib/runtime-install.js +964 -0
- package/lib/runtime-install.js.map +1 -0
- package/lib/runtime-manager.js +133 -0
- package/lib/runtime-manager.js.map +1 -0
- package/lib/runtime.js +1678 -0
- package/lib/runtime.js.map +1 -0
- package/lib/skill.js +25 -0
- package/lib/skill.js.map +1 -0
- package/lib/tools.js +549 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/artifact-access.d.ts +61 -0
- package/lib/types/artifact-access.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +42 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/client/display-config.d.ts +24 -0
- package/lib/types/client/display-config.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +350 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/model-variants-hider.d.ts +40 -0
- package/lib/types/client/model-variants-hider.d.ts.map +1 -0
- package/lib/types/client/paste-images.d.ts +126 -0
- package/lib/types/client/paste-images.d.ts.map +1 -0
- package/lib/types/config.d.ts +123 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/errors.d.ts +35 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/evidence-cache.d.ts +91 -0
- package/lib/types/evidence-cache.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +50 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/image-input-variants.d.ts +144 -0
- package/lib/types/image-input-variants.d.ts.map +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +73 -0
- package/lib/types/paste-images.d.ts.map +1 -0
- package/lib/types/paths.d.ts +117 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/plugin-update.d.ts +119 -0
- package/lib/types/plugin-update.d.ts.map +1 -0
- package/lib/types/runtime-install.d.ts +92 -0
- package/lib/types/runtime-install.d.ts.map +1 -0
- package/lib/types/runtime-manager.d.ts +60 -0
- package/lib/types/runtime-manager.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +412 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/skill.d.ts +14 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/tools.d.ts +35 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +233 -0
- package/lib/types/upstream.d.ts.map +1 -0
- package/lib/types/version.d.ts +15 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/web-request.d.ts +11 -0
- package/lib/types/web-request.d.ts.map +1 -0
- package/lib/types/web.d.ts +118 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/upstream.js +860 -0
- package/lib/upstream.js.map +1 -0
- package/lib/version.js +18 -0
- package/lib/version.js.map +1 -0
- package/lib/web-request.js +29 -0
- package/lib/web-request.js.map +1 -0
- package/lib/web.js +462 -0
- package/lib/web.js.map +1 -0
- package/package.json +149 -0
- package/patches/vision-tools-dsh.patch +970 -0
- package/runtime/requirements.lock +3 -0
- package/src/artifact-access.ts +386 -0
- package/src/artifacts.ts +85 -0
- package/src/client/display-config.ts +62 -0
- package/src/client/index.tsx +1580 -0
- package/src/client/model-variants-hider.ts +159 -0
- package/src/client/paste-images.tsx +747 -0
- package/src/config.ts +290 -0
- package/src/defaults.ts +5 -0
- package/src/errors.ts +62 -0
- package/src/evidence-cache.ts +418 -0
- package/src/exposure.ts +265 -0
- package/src/image-input-variants.ts +996 -0
- package/src/index.ts +141 -0
- package/src/paste-images.ts +286 -0
- package/src/paths.ts +383 -0
- package/src/plugin-update.ts +1143 -0
- package/src/runtime-install.ts +1161 -0
- package/src/runtime-manager.ts +174 -0
- package/src/runtime.ts +2189 -0
- package/src/skill.ts +35 -0
- package/src/tools.ts +691 -0
- package/src/upstream.ts +1078 -0
- package/src/version.ts +37 -0
- package/src/web-request.ts +27 -0
- package/src/web.ts +594 -0
- package/vendor/agent-vision-toolkit/CHANGELOG.md +30 -0
- package/vendor/agent-vision-toolkit/LICENSE +21 -0
- package/vendor/agent-vision-toolkit/README.md +405 -0
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +94 -0
- package/vendor/agent-vision-toolkit/bin/crop +90 -0
- package/vendor/agent-vision-toolkit/bin/detect +13 -0
- package/vendor/agent-vision-toolkit/bin/glance +93 -0
- package/vendor/agent-vision-toolkit/bin/ground +13 -0
- package/vendor/agent-vision-toolkit/bin/trace +129 -0
- package/vendor/agent-vision-toolkit/detect.py +60 -0
- package/vendor/agent-vision-toolkit/ground.py +267 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/html_shot.py +420 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/vendor/agent-vision-toolkit/tests/test_vision_client.py +450 -0
- package/vendor/agent-vision-toolkit/vision_client.py +310 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"sourceTool": "vision_pixel_diff",
|
|
4
|
+
"original": {
|
|
5
|
+
"path": "assets/reference.png",
|
|
6
|
+
"bytes": 137315,
|
|
7
|
+
"width": 1200,
|
|
8
|
+
"height": 720,
|
|
9
|
+
"format": "png"
|
|
10
|
+
},
|
|
11
|
+
"rebuilt": {
|
|
12
|
+
"path": "assets/initial.png",
|
|
13
|
+
"bytes": 69296,
|
|
14
|
+
"width": 1200,
|
|
15
|
+
"height": 720,
|
|
16
|
+
"format": "png"
|
|
17
|
+
},
|
|
18
|
+
"scaled": false,
|
|
19
|
+
"overallDifferencePct": 6.04,
|
|
20
|
+
"grid": 8,
|
|
21
|
+
"worstRegions": [
|
|
22
|
+
{
|
|
23
|
+
"index": 1,
|
|
24
|
+
"differencePct": 27.51,
|
|
25
|
+
"box": {
|
|
26
|
+
"x1": 300,
|
|
27
|
+
"y1": 540,
|
|
28
|
+
"x2": 450,
|
|
29
|
+
"y2": 630
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"index": 2,
|
|
34
|
+
"differencePct": 25.99,
|
|
35
|
+
"box": {
|
|
36
|
+
"x1": 750,
|
|
37
|
+
"y1": 360,
|
|
38
|
+
"x2": 900,
|
|
39
|
+
"y2": 450
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"index": 3,
|
|
44
|
+
"differencePct": 24.16,
|
|
45
|
+
"box": {
|
|
46
|
+
"x1": 750,
|
|
47
|
+
"y1": 450,
|
|
48
|
+
"x2": 900,
|
|
49
|
+
"y2": 540
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"index": 4,
|
|
54
|
+
"differencePct": 24.03,
|
|
55
|
+
"box": {
|
|
56
|
+
"x1": 600,
|
|
57
|
+
"y1": 540,
|
|
58
|
+
"x2": 750,
|
|
59
|
+
"y2": 630
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"index": 5,
|
|
64
|
+
"differencePct": 23.88,
|
|
65
|
+
"box": {
|
|
66
|
+
"x1": 450,
|
|
67
|
+
"y1": 540,
|
|
68
|
+
"x2": 600,
|
|
69
|
+
"y2": 630
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"index": 6,
|
|
74
|
+
"differencePct": 22.93,
|
|
75
|
+
"box": {
|
|
76
|
+
"x1": 300,
|
|
77
|
+
"y1": 450,
|
|
78
|
+
"x2": 450,
|
|
79
|
+
"y2": 540
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Vision QA Reconstruction</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
html, body { width: 1200px; height: 720px; margin: 0; overflow: hidden; }
|
|
10
|
+
body { background: #eef1f8; color: #182033; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
|
|
11
|
+
button { font: inherit; }
|
|
12
|
+
.app { position: absolute; left: 48px; top: 36px; width: 1104px; height: 648px; overflow: hidden; border: 1px solid #dfe3ee; border-radius: 28px; background: #fff; box-shadow: 0 24px 60px rgba(45,56,90,.14); }
|
|
13
|
+
.toolbar { position: absolute; inset: 0 0 auto; height: 78px; border-bottom: 1px solid #e8eaf1; background: #fff; }
|
|
14
|
+
.identity { position: absolute; left: 27px; top: 22px; display: flex; align-items: center; gap: 11px; font-size: 17px; font-weight: 760; letter-spacing: -.25px; }
|
|
15
|
+
.identity-icon { position: relative; width: 32px; height: 32px; border-radius: 10px; background: #5a4ff3; box-shadow: inset 0 0 0 1px rgba(255,255,255,.22); }
|
|
16
|
+
.identity-icon::before, .identity-icon::after { content: ""; position: absolute; border-radius: 99px; background: #fff; transform: rotate(-25deg); }
|
|
17
|
+
.identity-icon::before { left: 8px; top: 8px; width: 16px; height: 5px; }
|
|
18
|
+
.identity-icon::after { left: 11px; top: 17px; width: 13px; height: 5px; opacity: .72; }
|
|
19
|
+
.switcher { position: absolute; left: 380px; top: 19px; display: flex; height: 40px; align-items: center; gap: 8px; padding: 4px; border: 1px solid #e8eaf1; border-radius: 13px; background: #f7f8fb; }
|
|
20
|
+
.switcher span { min-width: 82px; padding: 8px 14px; border-radius: 9px; color: #71798e; font-size: 13px; font-weight: 650; text-align: center; }
|
|
21
|
+
.switcher .selected { color: #2b255e; background: #fff; box-shadow: 0 2px 8px rgba(37,43,70,.08); }
|
|
22
|
+
.person { position: absolute; right: 26px; top: 20px; width: 38px; height: 38px; border: 3px solid #fff; border-radius: 50%; background: linear-gradient(135deg,#ffcf88,#f08bb7); box-shadow: 0 0 0 1px #e5e8f0; }
|
|
23
|
+
.ready { position: absolute; right: 79px; top: 28px; display: flex; align-items: center; gap: 7px; color: #697187; font-size: 12px; font-weight: 650; }
|
|
24
|
+
.ready::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #29b77a; box-shadow: 0 0 0 4px #e7f8f1; }
|
|
25
|
+
.rail { position: absolute; left: 0; top: 78px; bottom: 0; width: 216px; padding: 28px 20px; border-right: 1px solid #e8eaf1; background: #fbfbfd; }
|
|
26
|
+
.rail-title { margin: 0 10px 13px; color: #a0a6b6; font-size: 10px; font-weight: 760; letter-spacing: 1.15px; text-transform: uppercase; }
|
|
27
|
+
.rail-link { position: relative; height: 44px; margin-bottom: 6px; padding: 13px 14px 13px 43px; border-radius: 12px; color: #737b90; font-size: 13px; font-weight: 650; }
|
|
28
|
+
.rail-link.current { color: #3f36cf; background: #eeecff; }
|
|
29
|
+
.rail-link::before { content: ""; position: absolute; left: 15px; top: 14px; width: 14px; height: 14px; border: 2px solid currentColor; border-radius: 5px; opacity: .88; }
|
|
30
|
+
.runtime-card { position: absolute; left: 20px; right: 20px; bottom: 22px; padding: 16px; border: 1px solid #e4e6ef; border-radius: 16px; background: #fff; }
|
|
31
|
+
.runtime-card strong { display: block; margin-bottom: 5px; font-size: 12px; }
|
|
32
|
+
.runtime-card small { display: block; color: #8a91a3; font-size: 10px; line-height: 1.45; }
|
|
33
|
+
.runtime-bar { height: 5px; margin-top: 13px; overflow: hidden; border-radius: 99px; background: #eceef4; }
|
|
34
|
+
.runtime-bar::after { content: ""; display: block; width: 72%; height: 100%; border-radius: inherit; background: #6559f5; }
|
|
35
|
+
.workspace { position: absolute; left: 216px; right: 0; top: 78px; bottom: 0; padding: 30px 34px 28px; background: #f6f7fb; }
|
|
36
|
+
.kicker { margin: 0 0 7px; color: #675bf2; font-size: 10px; font-weight: 800; letter-spacing: 1.25px; text-transform: uppercase; }
|
|
37
|
+
h1 { margin: 0; color: #20263a; font-size: 27px; font-weight: 780; letter-spacing: -.75px; line-height: 1.1; }
|
|
38
|
+
.lede { margin: 7px 0 0; color: #8990a2; font-size: 12px; }
|
|
39
|
+
.run { position: absolute; right: 34px; top: 31px; height: 38px; padding: 0 17px; border: 0; border-radius: 11px; background: #5b50ed; color: #fff; font-size: 12px; font-weight: 720; box-shadow: 0 8px 18px rgba(83,72,225,.24); }
|
|
40
|
+
.summary { display: flex; gap: 13px; margin-top: 23px; }
|
|
41
|
+
.stat { width: 260px; height: 80px; padding: 15px 17px; border: 1px solid #e2e5ef; border-radius: 15px; background: #fff; box-shadow: 0 5px 14px rgba(39,47,76,.035); }
|
|
42
|
+
.stat-label { display: flex; align-items: center; justify-content: space-between; color: #8b92a4; font-size: 10px; font-weight: 700; }
|
|
43
|
+
.stat-label i { width: 8px; height: 8px; border-radius: 50%; }
|
|
44
|
+
.stat-value { margin-top: 7px; font-size: 24px; font-weight: 790; letter-spacing: -.6px; }
|
|
45
|
+
.stat-value em { margin-left: 7px; color: #37a878; font-size: 10px; font-style: normal; font-weight: 760; }
|
|
46
|
+
.comparison { display: grid; grid-template-columns: 500px 286px; gap: 13px; margin-top: 13px; }
|
|
47
|
+
.card { height: 236px; border: 1px solid #e2e5ef; border-radius: 17px; background: #fff; box-shadow: 0 5px 14px rgba(39,47,76,.035); }
|
|
48
|
+
.card-header { display: flex; align-items: center; justify-content: space-between; height: 48px; padding: 0 17px; border-bottom: 1px solid #eceef4; }
|
|
49
|
+
.card-header b { font-size: 12px; font-weight: 760; }
|
|
50
|
+
.badge { padding: 5px 8px; border-radius: 8px; background: #ebf8f2; color: #25825c; font-size: 9px; font-weight: 780; }
|
|
51
|
+
.visual { position: relative; height: 187px; padding: 15px; }
|
|
52
|
+
.visual-stage { position: relative; width: 100%; height: 157px; overflow: hidden; border-radius: 12px; background: linear-gradient(145deg,#11182a,#1b2440); }
|
|
53
|
+
.visual-stage::before { content: ""; position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px); background-size: 22px 22px; }
|
|
54
|
+
.shape { position: absolute; left: 51px; top: 33px; width: 112px; height: 112px; border-radius: 35px; background: linear-gradient(135deg,#7d6cff,#41d5ca); transform: rotate(-9deg); box-shadow: 0 22px 42px rgba(73,95,225,.36); }
|
|
55
|
+
.shape::after { content: ""; position: absolute; right: 18px; top: 17px; width: 37px; height: 37px; border-radius: 50%; background: rgba(255,255,255,.52); filter: blur(1px); }
|
|
56
|
+
.copy-lines { position: absolute; left: 199px; top: 42px; width: 229px; }
|
|
57
|
+
.copy-lines i { display: block; height: 10px; margin-bottom: 12px; border-radius: 99px; background: #394563; }
|
|
58
|
+
.copy-lines i:nth-child(1) { width: 86%; background: #eef1ff; }
|
|
59
|
+
.copy-lines i:nth-child(2) { width: 64%; }
|
|
60
|
+
.copy-lines i:nth-child(3) { width: 94%; }
|
|
61
|
+
.copy-lines i:nth-child(4) { width: 74%; }
|
|
62
|
+
.copy-lines i:nth-child(5) { width: 48%; background: #665af4; }
|
|
63
|
+
.target { position: absolute; left: 38px; top: 21px; width: 144px; height: 139px; border: 2px solid #ffcf5a; border-radius: 14px; box-shadow: 0 0 0 1px rgba(255,207,90,.18); }
|
|
64
|
+
.target-label { position: absolute; left: 38px; top: 12px; padding: 4px 7px; border-radius: 6px; background: #ffcf5a; color: #322a19; font-size: 8px; font-weight: 820; }
|
|
65
|
+
.analysis { padding: 14px 17px; }
|
|
66
|
+
.score { display: flex; align-items: baseline; gap: 6px; }
|
|
67
|
+
.score strong { font-size: 32px; font-weight: 800; letter-spacing: -1px; }
|
|
68
|
+
.score span { color: #9298a8; font-size: 10px; font-weight: 650; }
|
|
69
|
+
.score-bar { position: relative; height: 7px; margin: 10px 0 12px; border-radius: 99px; background: #eceef4; }
|
|
70
|
+
.score-bar::after { content: ""; display: block; width: 93%; height: 100%; border-radius: inherit; background: linear-gradient(90deg,#6b5ef5,#34c6ac); }
|
|
71
|
+
.detail { display: flex; align-items: center; justify-content: space-between; height: 28px; border-top: 1px solid #eef0f5; color: #7f8799; font-size: 10px; }
|
|
72
|
+
.detail strong { color: #30374a; font-size: 10px; }
|
|
73
|
+
.artifacts { margin-top: 13px; height: 82px; overflow: hidden; border: 1px solid #e2e5ef; border-radius: 15px; background: #fff; }
|
|
74
|
+
.artifact-head, .artifact-row { display: grid; grid-template-columns: 1.5fr .8fr .8fr .65fr; align-items: center; padding: 0 16px; }
|
|
75
|
+
.artifact-head { height: 30px; border-bottom: 1px solid #eceef4; color: #9aa0b0; font-size: 9px; font-weight: 760; letter-spacing: .4px; text-transform: uppercase; }
|
|
76
|
+
.artifact-row { height: 26px; color: #50586c; font-size: 10px; }
|
|
77
|
+
.artifact-name { display: flex; align-items: center; gap: 8px; font-weight: 700; }
|
|
78
|
+
.artifact-name::before { content: ""; width: 16px; height: 16px; border-radius: 5px; background: #eeecff; box-shadow: inset 0 0 0 1px #d9d5ff; }
|
|
79
|
+
.artifact-state { justify-self: start; padding: 4px 7px; border-radius: 7px; background: #eaf8f2; color: #27815d; font-size: 9px; font-weight: 800; }
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<main class="app">
|
|
84
|
+
<header class="toolbar"><div class="identity"><span class="identity-icon"></span>Vision Forge</div><nav class="switcher"><span class="selected">Workspace</span><span>Runs</span><span>Artifacts</span></nav><div class="ready">Runtime ready</div><span class="person"></span></header>
|
|
85
|
+
<aside class="rail"><p class="rail-title">Vision tools</p><div class="rail-link current">Visual QA</div><div class="rail-link">Grounding</div><div class="rail-link">OCR batches</div><div class="rail-link">Asset trace</div><div class="runtime-card"><strong>Runtime generation 4</strong><small>Pinned upstream and local browser checks passed.</small><div class="runtime-bar"></div></div></aside>
|
|
86
|
+
<section class="workspace">
|
|
87
|
+
<p class="kicker">Latest verification</p><h1>Visual QA run</h1><p class="lede">Reference and rebuilt page compared at 1200 × 720.</p><button class="run">Run comparison</button>
|
|
88
|
+
<section class="summary"><article class="stat"><div class="stat-label">Similarity <i style="background:#5c50ee"></i></div><div class="stat-value">99.86% <em>+3.2%</em></div></article><article class="stat"><div class="stat-label">Largest delta <i style="background:#ffbd4a"></i></div><div class="stat-value">18 × 12 <em>px</em></div></article><article class="stat"><div class="stat-label">Artifacts <i style="background:#35b98a"></i></div><div class="stat-value">4 <em>ready</em></div></article></section>
|
|
89
|
+
<section class="comparison"><article class="card"><header class="card-header"><b>Annotated comparison</b><span class="badge">PASS</span></header><div class="visual"><div class="visual-stage"><span class="shape"></span><div class="copy-lines"><i></i><i></i><i></i><i></i><i></i></div><span class="target"></span><span class="target-label">#1 0.14%</span></div></div></article><article class="card"><header class="card-header"><b>Difference score</b><span class="badge">LOW</span></header><div class="analysis"><div class="score"><strong>0.14%</strong><span>overall</span></div><div class="score-bar"></div><div class="detail"><span>Viewport</span><strong>1200 × 720</strong></div><div class="detail"><span>Worst region</span><strong>Top left</strong></div><div class="detail"><span>Threshold</span><strong>≤ 0.50%</strong></div></div></article></section>
|
|
90
|
+
<section class="artifacts"><div class="artifact-head"><span>Artifact</span><span>Tool</span><span>Size</span><span>Status</span></div><div class="artifact-row"><span class="artifact-name">implementation.png</span><span>html_screenshot</span><span>86 KB</span><span class="artifact-state">READY</span></div><div class="artifact-row"><span class="artifact-name">heatmap.png</span><span>pixel_diff</span><span>41 KB</span><span class="artifact-state">READY</span></div></section>
|
|
91
|
+
</section>
|
|
92
|
+
</main>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Vision QA Initial Reconstruction</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
html, body { width: 1200px; height: 720px; margin: 0; overflow: hidden; }
|
|
10
|
+
body { background: #e8edf7; color: #20263a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
|
|
11
|
+
.app { position: absolute; left: 58px; top: 46px; width: 1084px; height: 628px; overflow: hidden; border: 1px solid #d7dce8; border-radius: 20px; background: #fff; box-shadow: 0 18px 48px rgba(45,56,90,.12); }
|
|
12
|
+
.top { height: 72px; border-bottom: 1px solid #e5e8f0; padding: 20px 24px; font-size: 18px; font-weight: 760; }
|
|
13
|
+
.mark { display: inline-block; width: 28px; height: 28px; margin-right: 10px; vertical-align: middle; border-radius: 8px; background: #695cf4; }
|
|
14
|
+
.nav { position: absolute; left: 390px; top: 22px; color: #747c90; font-size: 13px; word-spacing: 32px; }
|
|
15
|
+
.side { position: absolute; left: 0; top: 72px; bottom: 0; width: 198px; padding: 26px 17px; border-right: 1px solid #e5e8f0; background: #fafbfe; }
|
|
16
|
+
.side h2 { margin: 0 10px 15px; color: #a2a8b6; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; }
|
|
17
|
+
.link { margin-bottom: 7px; padding: 13px 14px; border-radius: 10px; color: #71798d; font-size: 13px; font-weight: 650; }
|
|
18
|
+
.link.active { color: #483ed8; background: #e8e6ff; }
|
|
19
|
+
.content { position: absolute; left: 198px; right: 0; top: 72px; bottom: 0; padding: 28px 30px; background: #f7f8fc; }
|
|
20
|
+
.content small { color: #6c60ef; font-size: 10px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }
|
|
21
|
+
h1 { margin: 8px 0 6px; font-size: 29px; letter-spacing: -.7px; }
|
|
22
|
+
p { margin: 0; color: #8c93a4; font-size: 12px; }
|
|
23
|
+
button { position: absolute; right: 30px; top: 29px; height: 36px; padding: 0 16px; border: 0; border-radius: 9px; background: #6659f3; color: #fff; font-size: 12px; font-weight: 700; }
|
|
24
|
+
.metrics { display: flex; gap: 12px; margin-top: 22px; }
|
|
25
|
+
.metric { width: 248px; height: 78px; padding: 14px 15px; border: 1px solid #e1e4ed; border-radius: 12px; background: #fff; }
|
|
26
|
+
.metric span { display: block; color: #8c93a4; font-size: 10px; }
|
|
27
|
+
.metric strong { display: block; margin-top: 8px; font-size: 23px; }
|
|
28
|
+
.work { display: grid; grid-template-columns: 470px 280px; gap: 12px; margin-top: 12px; }
|
|
29
|
+
.panel { height: 250px; border: 1px solid #e1e4ed; border-radius: 14px; background: #fff; }
|
|
30
|
+
.panel h3 { height: 46px; margin: 0; padding: 16px; border-bottom: 1px solid #eceef4; font-size: 12px; }
|
|
31
|
+
.preview { position: relative; height: 176px; margin: 14px; overflow: hidden; border-radius: 10px; background: #19223b; }
|
|
32
|
+
.shape { position: absolute; left: 36px; top: 27px; width: 104px; height: 104px; border-radius: 50%; background: linear-gradient(135deg,#7c6df7,#54c9c1); }
|
|
33
|
+
.copy { position: absolute; left: 178px; top: 38px; width: 220px; }
|
|
34
|
+
.copy i { display: block; width: 80%; height: 9px; margin-bottom: 13px; border-radius: 99px; background: #3b4765; }
|
|
35
|
+
.copy i:first-child { background: #eef1ff; }
|
|
36
|
+
.score { padding: 18px; }
|
|
37
|
+
.score strong { font-size: 36px; }
|
|
38
|
+
.bar { height: 8px; margin: 18px 0; border-radius: 99px; background: linear-gradient(90deg,#695df4 76%,#eceef4 76%); }
|
|
39
|
+
.row { display: flex; justify-content: space-between; padding: 11px 0; border-top: 1px solid #eef0f5; color: #81889a; font-size: 10px; }
|
|
40
|
+
.table { height: 86px; margin-top: 12px; border: 1px solid #e1e4ed; border-radius: 12px; background: #fff; }
|
|
41
|
+
.table div { display: grid; grid-template-columns: 1.5fr .8fr .7fr .6fr; padding: 9px 14px; color: #596175; font-size: 10px; }
|
|
42
|
+
.table div:first-child { border-bottom: 1px solid #eceef4; color: #9aa0b0; font-size: 9px; font-weight: 760; text-transform: uppercase; }
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<main class="app">
|
|
47
|
+
<header class="top"><span class="mark"></span>Vision Forge <span class="nav">Workspace Runs Artifacts</span></header>
|
|
48
|
+
<aside class="side"><h2>Vision tools</h2><div class="link active">Visual QA</div><div class="link">Grounding</div><div class="link">OCR batches</div><div class="link">Asset trace</div></aside>
|
|
49
|
+
<section class="content">
|
|
50
|
+
<small>Latest verification</small><h1>Visual QA run</h1><p>Reference and rebuilt page compared at 1200 × 720.</p><button>Run comparison</button>
|
|
51
|
+
<div class="metrics"><article class="metric"><span>Similarity</span><strong>96.66%</strong></article><article class="metric"><span>Largest delta</span><strong>44 × 28</strong></article><article class="metric"><span>Artifacts</span><strong>4 ready</strong></article></div>
|
|
52
|
+
<div class="work"><article class="panel"><h3>Annotated comparison</h3><div class="preview"><span class="shape"></span><div class="copy"><i></i><i></i><i></i><i></i><i></i></div></div></article><article class="panel"><h3>Difference score</h3><div class="score"><strong>3.34%</strong><div class="bar"></div><div class="row"><span>Viewport</span><b>1200 × 720</b></div><div class="row"><span>Worst region</span><b>Top left</b></div><div class="row"><span>Threshold</span><b>≤ 0.50%</b></div></div></article></div>
|
|
53
|
+
<div class="table"><div><span>Artifact</span><span>Tool</span><span>Size</span><span>Status</span></div><div><b>implementation.png</b><span>html_screenshot</span><span>86 KB</span><b>READY</b></div></div>
|
|
54
|
+
</section>
|
|
55
|
+
</main>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability-gated HTTP delivery for managed Vision Toolkit artifacts.
|
|
3
|
+
* Signed tokens are durable across process restarts, expose no secret, and
|
|
4
|
+
* are accepted only for the exact artifact facts projected into a tool result.
|
|
5
|
+
* @module dsh-vision-toolkit/artifact-access
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
8
|
+
import { constants as fsConstants } from 'node:fs';
|
|
9
|
+
import { chmod, lstat, mkdir, open, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { basename, dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
|
|
11
|
+
import { isWithin } from "./paths.js";
|
|
12
|
+
import { visionToolkitStateRoot } from "./runtime-install.js";
|
|
13
|
+
/** Prefix owned by the plugin's artifact capability route. */
|
|
14
|
+
export const ARTIFACT_ROUTE_PREFIX = '/_dsh/vision-toolkit/artifacts';
|
|
15
|
+
/** Presentation metadata key reserved by the browser half of this package. */
|
|
16
|
+
export const PRESENTATION_META_KEY = '$dshVisionToolkit';
|
|
17
|
+
const KEY_BYTES = 32;
|
|
18
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
19
|
+
const MAX_TOKEN_LENGTH = 16 * 1024;
|
|
20
|
+
const MIME_BY_EXTENSION = new Map([
|
|
21
|
+
['.png', { mimeType: 'image/png', kind: 'image' }],
|
|
22
|
+
['.jpg', { mimeType: 'image/jpeg', kind: 'image' }],
|
|
23
|
+
['.jpeg', { mimeType: 'image/jpeg', kind: 'image' }],
|
|
24
|
+
['.gif', { mimeType: 'image/gif', kind: 'image' }],
|
|
25
|
+
['.webp', { mimeType: 'image/webp', kind: 'image' }],
|
|
26
|
+
['.svg', { mimeType: 'image/svg+xml', kind: 'svg' }],
|
|
27
|
+
['.md', { mimeType: 'text/markdown', kind: 'markdown' }],
|
|
28
|
+
['.json', { mimeType: 'application/json', kind: 'json' }],
|
|
29
|
+
]);
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function isArtifactKind(value) {
|
|
34
|
+
return value === 'image' || value === 'svg' || value === 'markdown' || value === 'json';
|
|
35
|
+
}
|
|
36
|
+
function artifactFrom(value) {
|
|
37
|
+
if (!isRecord(value))
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof value.path !== 'string'
|
|
40
|
+
|| typeof value.filename !== 'string'
|
|
41
|
+
|| typeof value.mimeType !== 'string'
|
|
42
|
+
|| !isArtifactKind(value.kind)
|
|
43
|
+
|| typeof value.description !== 'string'
|
|
44
|
+
|| typeof value.sourceTool !== 'string'
|
|
45
|
+
|| (value.previewIntent !== 'image' && value.previewIntent !== 'svg' && value.previewIntent !== 'text' && value.previewIntent !== 'download')
|
|
46
|
+
|| typeof value.bytes !== 'number'
|
|
47
|
+
|| !Number.isSafeInteger(value.bytes)
|
|
48
|
+
|| value.bytes < 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function collectArtifacts(value, found, depth = 0) {
|
|
53
|
+
if (depth > 16)
|
|
54
|
+
return;
|
|
55
|
+
const artifact = artifactFrom(value);
|
|
56
|
+
if (artifact !== undefined) {
|
|
57
|
+
found.set(artifact.path, artifact);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
for (const entry of value)
|
|
62
|
+
collectArtifacts(entry, found, depth + 1);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(value))
|
|
66
|
+
return;
|
|
67
|
+
for (const entry of Object.values(value))
|
|
68
|
+
collectArtifacts(entry, found, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
function parsePayload(value) {
|
|
71
|
+
if (!isRecord(value))
|
|
72
|
+
return undefined;
|
|
73
|
+
if (value.v !== 1
|
|
74
|
+
|| typeof value.path !== 'string'
|
|
75
|
+
|| !isAbsolute(value.path)
|
|
76
|
+
|| typeof value.filename !== 'string'
|
|
77
|
+
|| basename(value.path) !== value.filename
|
|
78
|
+
|| typeof value.mimeType !== 'string'
|
|
79
|
+
|| !isArtifactKind(value.kind)
|
|
80
|
+
|| typeof value.bytes !== 'number'
|
|
81
|
+
|| !Number.isSafeInteger(value.bytes)
|
|
82
|
+
|| value.bytes < 0)
|
|
83
|
+
return undefined;
|
|
84
|
+
const expected = MIME_BY_EXTENSION.get(extname(value.path).toLowerCase());
|
|
85
|
+
if (expected === undefined || expected.mimeType !== value.mimeType || expected.kind !== value.kind)
|
|
86
|
+
return undefined;
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function mac(key, payload) {
|
|
90
|
+
return createHmac('sha256', key).update(payload).digest();
|
|
91
|
+
}
|
|
92
|
+
function safeEqual(left, right) {
|
|
93
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
94
|
+
}
|
|
95
|
+
async function readKey(path) {
|
|
96
|
+
const info = await lstat(path);
|
|
97
|
+
if (info.isSymbolicLink() || !info.isFile())
|
|
98
|
+
throw new Error('artifact access key is not a regular file');
|
|
99
|
+
const key = await readFile(path);
|
|
100
|
+
if (key.length !== KEY_BYTES)
|
|
101
|
+
throw new Error('artifact access key has an invalid length');
|
|
102
|
+
await chmod(path, 0o600);
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Load or atomically create the per-DSH-home signing key.
|
|
107
|
+
* @param root - state root override used by tests; defaults to the plugin cache.
|
|
108
|
+
* @returns the 32-byte signing key.
|
|
109
|
+
*/
|
|
110
|
+
export async function prepareArtifactAccessKey(root = visionToolkitStateRoot()) {
|
|
111
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
112
|
+
const path = join(root, 'artifact-access.key');
|
|
113
|
+
try {
|
|
114
|
+
return await readKey(path);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== 'ENOENT')
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const candidate = randomBytes(KEY_BYTES);
|
|
121
|
+
try {
|
|
122
|
+
await writeFile(path, candidate, { flag: 'wx', mode: 0o600 });
|
|
123
|
+
return candidate;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'EEXIST')
|
|
127
|
+
throw error;
|
|
128
|
+
return readKey(path);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function artifactRoot(path) {
|
|
132
|
+
let current = dirname(path);
|
|
133
|
+
while (true) {
|
|
134
|
+
if (basename(current) === 'artifacts' && basename(dirname(current)) === '.dsh-vision-toolkit')
|
|
135
|
+
return current;
|
|
136
|
+
const parent = dirname(current);
|
|
137
|
+
if (parent === current)
|
|
138
|
+
return undefined;
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function assertNoSymlinkPath(root, path) {
|
|
143
|
+
const rootInfo = await lstat(root);
|
|
144
|
+
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())
|
|
145
|
+
throw new Error('artifact root is not a real directory');
|
|
146
|
+
const rel = relative(root, path);
|
|
147
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
148
|
+
throw new Error('artifact path escaped its managed root');
|
|
149
|
+
}
|
|
150
|
+
let current = root;
|
|
151
|
+
const parts = rel.split(sep);
|
|
152
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
153
|
+
const part = parts[index];
|
|
154
|
+
if (part === undefined || part.length === 0 || part === '.' || part === '..')
|
|
155
|
+
throw new Error('artifact path is malformed');
|
|
156
|
+
current = join(current, part);
|
|
157
|
+
const info = await lstat(current);
|
|
158
|
+
if (info.isSymbolicLink())
|
|
159
|
+
throw new Error('artifact path contains a symbolic link');
|
|
160
|
+
const final = index === parts.length - 1;
|
|
161
|
+
if (final ? !info.isFile() : !info.isDirectory())
|
|
162
|
+
throw new Error('artifact path contains an unexpected entry type');
|
|
163
|
+
}
|
|
164
|
+
const [realRoot, realFile] = await Promise.all([realpath(root), realpath(path)]);
|
|
165
|
+
if (!isWithin(realRoot, realFile))
|
|
166
|
+
throw new Error('artifact path escaped its managed root');
|
|
167
|
+
}
|
|
168
|
+
function sameFile(opened, current) {
|
|
169
|
+
if (opened.dev === 0 || current.dev === 0 || opened.ino === 0 || current.ino === 0)
|
|
170
|
+
return true;
|
|
171
|
+
return opened.dev === current.dev && opened.ino === current.ino;
|
|
172
|
+
}
|
|
173
|
+
async function openVerifiedArtifact(payload) {
|
|
174
|
+
const root = artifactRoot(payload.path);
|
|
175
|
+
if (root === undefined)
|
|
176
|
+
throw new Error('artifact path is outside the managed delivery tree');
|
|
177
|
+
await assertNoSymlinkPath(root, payload.path);
|
|
178
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
179
|
+
const handle = await open(payload.path, fsConstants.O_RDONLY | noFollow);
|
|
180
|
+
try {
|
|
181
|
+
const info = await handle.stat();
|
|
182
|
+
if (!info.isFile() || info.size !== payload.bytes)
|
|
183
|
+
throw new Error('artifact no longer matches its delivered descriptor');
|
|
184
|
+
const current = await lstat(payload.path);
|
|
185
|
+
if (current.isSymbolicLink() || !current.isFile() || !sameFile(info, current)) {
|
|
186
|
+
throw new Error('artifact changed while it was being opened');
|
|
187
|
+
}
|
|
188
|
+
await assertNoSymlinkPath(root, payload.path);
|
|
189
|
+
return { handle, info };
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await handle.close().catch(() => { });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function asciiFilename(filename) {
|
|
197
|
+
const fallback = filename.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160);
|
|
198
|
+
return fallback.length === 0 ? 'artifact' : fallback;
|
|
199
|
+
}
|
|
200
|
+
function contentDisposition(filename, download) {
|
|
201
|
+
const mode = download ? 'attachment' : 'inline';
|
|
202
|
+
return `${mode}; filename="${asciiFilename(filename)}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
203
|
+
}
|
|
204
|
+
function securityHeaders(res, payload, download) {
|
|
205
|
+
res.setHeader('Content-Type', payload.mimeType);
|
|
206
|
+
res.setHeader('Content-Length', String(payload.bytes));
|
|
207
|
+
res.setHeader('Content-Disposition', contentDisposition(payload.filename, download));
|
|
208
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
209
|
+
res.setHeader('Cache-Control', 'private, no-store, max-age=0');
|
|
210
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
211
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
212
|
+
res.setHeader('Content-Security-Policy', payload.kind === 'svg'
|
|
213
|
+
? "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'"
|
|
214
|
+
: "sandbox; default-src 'none'");
|
|
215
|
+
}
|
|
216
|
+
/** Signed-capability encoder and safe Artifact route handler. */
|
|
217
|
+
export class ArtifactAccessController {
|
|
218
|
+
key;
|
|
219
|
+
routeCount = 0;
|
|
220
|
+
constructor(key) {
|
|
221
|
+
this.key = key;
|
|
222
|
+
if (key.length !== KEY_BYTES)
|
|
223
|
+
throw new TypeError(`artifact access key must be ${KEY_BYTES} bytes`);
|
|
224
|
+
}
|
|
225
|
+
/** Whether at least one HTTP carrier currently owns the route. */
|
|
226
|
+
get routeAvailable() {
|
|
227
|
+
return this.routeCount > 0;
|
|
228
|
+
}
|
|
229
|
+
/** Mark one route attachment; the returned disposer removes that attachment. */
|
|
230
|
+
attachRoute() {
|
|
231
|
+
this.routeCount += 1;
|
|
232
|
+
let active = true;
|
|
233
|
+
return () => {
|
|
234
|
+
if (!active)
|
|
235
|
+
return;
|
|
236
|
+
active = false;
|
|
237
|
+
this.routeCount -= 1;
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Purely enrich a canonical tool-result value with browser access grants.
|
|
242
|
+
* @param value - schema-validated tool result.
|
|
243
|
+
* @returns the unchanged value when no route/artifact exists, otherwise a detached metadata envelope.
|
|
244
|
+
*/
|
|
245
|
+
presentationMeta(value) {
|
|
246
|
+
if (!this.routeAvailable || !isRecord(value))
|
|
247
|
+
return value;
|
|
248
|
+
const artifacts = new Map();
|
|
249
|
+
collectArtifacts(value, artifacts);
|
|
250
|
+
if (artifacts.size === 0)
|
|
251
|
+
return value;
|
|
252
|
+
const grants = [...artifacts.values()].map((artifact) => {
|
|
253
|
+
const token = this.sign(artifact);
|
|
254
|
+
const previewUrl = `${ARTIFACT_ROUTE_PREFIX}/${token}`;
|
|
255
|
+
return { path: artifact.path, previewUrl, downloadUrl: `${previewUrl}?download=1` };
|
|
256
|
+
});
|
|
257
|
+
const envelope = { schemaVersion: 1, artifacts: grants };
|
|
258
|
+
return { ...value, [PRESENTATION_META_KEY]: envelope };
|
|
259
|
+
}
|
|
260
|
+
/** Mint a deterministic, tamper-evident capability for one descriptor. */
|
|
261
|
+
sign(artifact) {
|
|
262
|
+
const payload = {
|
|
263
|
+
v: 1,
|
|
264
|
+
path: artifact.path,
|
|
265
|
+
filename: artifact.filename,
|
|
266
|
+
mimeType: artifact.mimeType,
|
|
267
|
+
kind: artifact.kind,
|
|
268
|
+
bytes: artifact.bytes,
|
|
269
|
+
};
|
|
270
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
271
|
+
return `${encoded}.${mac(this.key, encoded).toString('base64url')}`;
|
|
272
|
+
}
|
|
273
|
+
/** Verify and decode one capability without touching the filesystem. */
|
|
274
|
+
verify(token) {
|
|
275
|
+
if (token.length === 0 || token.length > MAX_TOKEN_LENGTH || !TOKEN_PATTERN.test(token))
|
|
276
|
+
return undefined;
|
|
277
|
+
const [encoded, signature] = token.split('.');
|
|
278
|
+
if (encoded === undefined || signature === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
let supplied;
|
|
281
|
+
try {
|
|
282
|
+
supplied = Buffer.from(signature, 'base64url');
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
if (!safeEqual(mac(this.key, encoded), supplied))
|
|
288
|
+
return undefined;
|
|
289
|
+
try {
|
|
290
|
+
return parsePayload(JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8')));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Serve one GET/HEAD capability request with MIME, CSP, and symlink checks.
|
|
298
|
+
* @param req - Node HTTP request matched under {@link ARTIFACT_ROUTE_PREFIX}.
|
|
299
|
+
* @param res - response owned by this handler.
|
|
300
|
+
*/
|
|
301
|
+
async handle(req, res) {
|
|
302
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
303
|
+
res.setHeader('Allow', 'GET, HEAD');
|
|
304
|
+
res.writeHead(405);
|
|
305
|
+
res.end();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let url;
|
|
309
|
+
try {
|
|
310
|
+
url = new URL(req.url ?? '/', 'http://dsh.local');
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
res.writeHead(400);
|
|
314
|
+
res.end();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const prefix = `${ARTIFACT_ROUTE_PREFIX}/`;
|
|
318
|
+
if (!url.pathname.startsWith(prefix) || url.pathname.slice(prefix.length).includes('/')) {
|
|
319
|
+
res.writeHead(404);
|
|
320
|
+
res.end();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
let token;
|
|
324
|
+
try {
|
|
325
|
+
token = decodeURIComponent(url.pathname.slice(prefix.length));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
res.writeHead(404);
|
|
329
|
+
res.end();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const payload = this.verify(token);
|
|
333
|
+
if (payload === undefined) {
|
|
334
|
+
res.writeHead(404);
|
|
335
|
+
res.end();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const downloadValue = url.searchParams.get('download');
|
|
339
|
+
if ([...url.searchParams.keys()].some(key => key !== 'download') || (downloadValue !== null && downloadValue !== '1')) {
|
|
340
|
+
res.writeHead(400);
|
|
341
|
+
res.end();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
let opened;
|
|
345
|
+
try {
|
|
346
|
+
opened = await openVerifiedArtifact(payload);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
res.writeHead(404);
|
|
350
|
+
res.end();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
securityHeaders(res, payload, downloadValue === '1');
|
|
354
|
+
res.writeHead(200);
|
|
355
|
+
if (req.method === 'HEAD') {
|
|
356
|
+
await opened.handle.close().catch(() => { });
|
|
357
|
+
res.end();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const stream = opened.handle.createReadStream({ autoClose: true });
|
|
361
|
+
stream.on('error', () => {
|
|
362
|
+
if (!res.headersSent)
|
|
363
|
+
res.writeHead(500);
|
|
364
|
+
res.destroy();
|
|
365
|
+
});
|
|
366
|
+
stream.pipe(res);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
//# sourceMappingURL=artifact-access.js.map
|