@capagents/forgeqe 0.0.0-stage → 1.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/HOW_TO_USE.md +1639 -0
- package/README.md +338 -2
- package/bin/forgeqe.d.ts +3 -0
- package/bin/forgeqe.js +188 -0
- package/docs/ARCHITECTURE.md +408 -0
- package/docs/security-data-flow.html +1870 -0
- package/docs/user-guide.html +2094 -0
- package/evals/domain/access-role-change.yaml +37 -0
- package/evals/domain/banking-payment-transfer.yaml +41 -0
- package/evals/domain/gdpr-account-erasure.yaml +30 -0
- package/evals/domain/insurance-claim-fnol.yaml +30 -0
- package/evals/domain/insurance-policy-renewal.yaml +37 -0
- package/evals/domain/lending-loan-application.yaml +36 -0
- package/forgeqe-architecture.html +718 -0
- package/forgeqe.example.yaml +203 -0
- package/knowledge/README.md +18 -0
- package/knowledge/business-rules.md +10 -0
- package/knowledge/compliance.md +16 -0
- package/knowledge/environments.md +5 -0
- package/knowledge/glossary.md +5 -0
- package/knowledge/product.md +16 -0
- package/knowledge/roles.md +9 -0
- package/knowledge/test-data.md +7 -0
- package/llms.example.json +51 -0
- package/package.json +63 -4
- package/prompts/README.md +34 -0
- package/prompts/api-scenarios.md +21 -0
- package/prompts/api-tests.md +39 -0
- package/prompts/brain.md +166 -0
- package/prompts/browser-capture.md +7 -0
- package/prompts/browser-case-report.md +7 -0
- package/prompts/browser-checks.md +14 -0
- package/prompts/browser-efficiency.md +7 -0
- package/prompts/browser-error-recovery.md +15 -0
- package/prompts/browser-extra.md +5 -0
- package/prompts/browser-goal-verification.md +13 -0
- package/prompts/browser-skill-select.md +6 -0
- package/prompts/domain-rule.md +13 -0
- package/prompts/eval-judge.md +19 -0
- package/prompts/explore-charters.md +27 -0
- package/prompts/find-tests.md +28 -0
- package/prompts/heal.md +40 -0
- package/prompts/pr-review.md +29 -0
- package/prompts/skill-rule.md +6 -0
- package/prompts/test-agent.md +34 -0
- package/prompts/test-reviewer.md +30 -0
- package/prompts/test-rules.md +28 -0
- package/py/forgeqe_engine/__init__.py +3 -0
- package/py/forgeqe_engine/__main__.py +3 -0
- package/py/forgeqe_engine/brand.py +453 -0
- package/py/forgeqe_engine/brand_assets.py +6 -0
- package/py/forgeqe_engine/browser.py +196 -0
- package/py/forgeqe_engine/capture.py +538 -0
- package/py/forgeqe_engine/checks.py +98 -0
- package/py/forgeqe_engine/cli.py +188 -0
- package/py/forgeqe_engine/diagnostics.py +609 -0
- package/py/forgeqe_engine/llm.py +195 -0
- package/py/forgeqe_engine/logins.py +251 -0
- package/py/forgeqe_engine/loops.py +224 -0
- package/py/forgeqe_engine/memory.py +1004 -0
- package/py/forgeqe_engine/network.py +244 -0
- package/py/forgeqe_engine/prompts.py +20 -0
- package/py/forgeqe_engine/replay.py +240 -0
- package/py/forgeqe_engine/report.py +173 -0
- package/py/forgeqe_engine/service.py +1335 -0
- package/py/forgeqe_engine/settings.py +93 -0
- package/py/forgeqe_engine/skills.py +88 -0
- package/py/forgeqe_engine/summary.py +239 -0
- package/py/forgeqe_engine/urls.py +81 -0
- package/py/forgeqe_engine/workflow_yaml.py +689 -0
- package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
- package/skills/dynamics-365/SKILL.md +21 -0
- package/skills/playwright-typescript/SKILL.md +35 -0
- package/skills/writing-skills/SKILL.md +30 -0
- package/src/api/plan.ts +246 -0
- package/src/api/run.ts +204 -0
- package/src/api/scenario.ts +502 -0
- package/src/api/schema.ts +170 -0
- package/src/api/spec.ts +624 -0
- package/src/api/store.ts +232 -0
- package/src/api/traffic.ts +281 -0
- package/src/api/types.ts +163 -0
- package/src/api/write.ts +272 -0
- package/src/auth/signin.ts +494 -0
- package/src/auth/store.ts +170 -0
- package/src/brain/index.ts +189 -0
- package/src/brain/prompt.ts +19 -0
- package/src/capabilities/docs.ts +101 -0
- package/src/capabilities/guide.ts +455 -0
- package/src/capabilities/registry.ts +343 -0
- package/src/capabilities/render.ts +106 -0
- package/src/capabilities/status.ts +137 -0
- package/src/card.ts +11 -0
- package/src/cases/ado.ts +325 -0
- package/src/cases/api.ts +117 -0
- package/src/cases/bugs.ts +223 -0
- package/src/cases/confluence.ts +111 -0
- package/src/cases/context.ts +223 -0
- package/src/cases/dashboards.ts +137 -0
- package/src/cases/failure.ts +97 -0
- package/src/cases/files.ts +295 -0
- package/src/cases/goal.ts +53 -0
- package/src/cases/http.ts +111 -0
- package/src/cases/index.ts +131 -0
- package/src/cases/jira.ts +122 -0
- package/src/cases/qtest.ts +108 -0
- package/src/cases/quality.ts +190 -0
- package/src/cases/rag.ts +100 -0
- package/src/cases/readiness.ts +284 -0
- package/src/cases/save.ts +149 -0
- package/src/cases/sprints.ts +148 -0
- package/src/cases/testrail.ts +176 -0
- package/src/cases/types.ts +107 -0
- package/src/cases/wiki.ts +105 -0
- package/src/cases/xray.ts +206 -0
- package/src/cases/zephyr.ts +153 -0
- package/src/childEnv.ts +86 -0
- package/src/cli/args.ts +114 -0
- package/src/cli/help.ts +167 -0
- package/src/cli/index.ts +1142 -0
- package/src/cli/terminal.ts +187 -0
- package/src/code/areas.ts +337 -0
- package/src/code/attention.ts +214 -0
- package/src/code/find.ts +540 -0
- package/src/code/handoff.ts +541 -0
- package/src/code/history.ts +170 -0
- package/src/code/impact.ts +231 -0
- package/src/code/index.ts +34 -0
- package/src/code/livebrowser.ts +67 -0
- package/src/code/loop.ts +603 -0
- package/src/code/notes.ts +75 -0
- package/src/code/profile.ts +226 -0
- package/src/code/prompt.ts +303 -0
- package/src/code/prreview.ts +649 -0
- package/src/code/prtests.ts +103 -0
- package/src/code/replay.ts +772 -0
- package/src/code/review.ts +202 -0
- package/src/code/runtime.ts +781 -0
- package/src/code/slices.ts +159 -0
- package/src/code/testrun.ts +187 -0
- package/src/code/traceability.ts +100 -0
- package/src/code/worktree.ts +153 -0
- package/src/commands.ts +101 -0
- package/src/config.ts +970 -0
- package/src/detail.ts +65 -0
- package/src/domain.ts +19 -0
- package/src/engine/bridge.ts +359 -0
- package/src/engine/mock.ts +102 -0
- package/src/engine/setup.ts +298 -0
- package/src/entra.ts +154 -0
- package/src/env.ts +24 -0
- package/src/evals/domain.ts +338 -0
- package/src/explore/charters.ts +151 -0
- package/src/explore/run.ts +280 -0
- package/src/explore/story.ts +106 -0
- package/src/guard.ts +51 -0
- package/src/heal/loop.ts +357 -0
- package/src/heal/prompt.ts +143 -0
- package/src/heal/report.ts +95 -0
- package/src/index.ts +12 -0
- package/src/init.ts +102 -0
- package/src/knowledge.ts +114 -0
- package/src/legacyEnv.ts +9 -0
- package/src/llm.ts +61 -0
- package/src/locale.ts +25 -0
- package/src/logins.ts +148 -0
- package/src/managed.ts +101 -0
- package/src/mcp/apitools.ts +335 -0
- package/src/mcp/captools.ts +25 -0
- package/src/mcp/casetools.ts +223 -0
- package/src/mcp/commands.ts +421 -0
- package/src/mcp/exploretools.ts +129 -0
- package/src/mcp/external.ts +172 -0
- package/src/mcp/git.ts +78 -0
- package/src/mcp/notifytools.ts +49 -0
- package/src/mcp/prtools.ts +261 -0
- package/src/mcp/qualitytools.ts +154 -0
- package/src/mcp/repotests.ts +286 -0
- package/src/mcp/server.ts +234 -0
- package/src/mcp/tools.ts +810 -0
- package/src/memory.ts +250 -0
- package/src/money.ts +39 -0
- package/src/monitor/alerts.ts +94 -0
- package/src/monitor/background.ts +68 -0
- package/src/monitor/run.ts +263 -0
- package/src/monitor/state.ts +105 -0
- package/src/notify/events.ts +144 -0
- package/src/notify/notice.ts +177 -0
- package/src/prices.ts +175 -0
- package/src/print.ts +54 -0
- package/src/privacy.ts +35 -0
- package/src/prompts.ts +165 -0
- package/src/quality/budgets.ts +249 -0
- package/src/quality/history.ts +149 -0
- package/src/quality/visual.ts +201 -0
- package/src/report/data.ts +475 -0
- package/src/report/html.ts +106 -0
- package/src/report/template.html +84 -0
- package/src/run.ts +243 -0
- package/src/sessionLog.ts +216 -0
- package/src/sessionLogins.ts +94 -0
- package/src/skills/registry.ts +217 -0
- package/src/skills/scaffold.ts +65 -0
- package/src/skills/select.ts +37 -0
- package/src/skills/view.ts +44 -0
- package/src/suite/report.ts +135 -0
- package/src/suite/run.ts +303 -0
- package/src/summary.ts +188 -0
- package/src/tools.ts +130 -0
- package/src/tui/App.tsx +983 -0
- package/src/tui/clipboard.ts +36 -0
- package/src/tui/components.tsx +263 -0
- package/src/tui/dialogs.ts +65 -0
- package/src/tui/main.tsx +20 -0
- package/src/tui/text.ts +28 -0
- package/src/tui/theme.ts +104 -0
- package/src/tui/transcript.tsx +344 -0
- package/src/types.ts +146 -0
- package/src/update.ts +311 -0
- package/src/usage.ts +158 -0
|
@@ -0,0 +1,1870 @@
|
|
|
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>ForgeQE data security</title>
|
|
7
|
+
<script>try{var t=localStorage.getItem("wp-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
|
|
8
|
+
<meta name="description" content="How ForgeQE works, how data moves between its agents, your machine and your organisation's LLM, and the controls that keep it there.">
|
|
9
|
+
<link rel="icon" type="image/png" href="data:image/png;base64,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">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Montserrat:ital,wght@0,400;0,500;0,600;0,700;0,800;1,700;1,800&display=swap" rel="stylesheet">
|
|
13
|
+
<style>
|
|
14
|
+
/* this gets exported as style.css and can be used for the default theming */
|
|
15
|
+
/* these are the necessary styles for React Flow, they get used by base.css and style.css */
|
|
16
|
+
.react-flow {
|
|
17
|
+
direction: ltr;
|
|
18
|
+
}
|
|
19
|
+
.react-flow__container {
|
|
20
|
+
position: absolute;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
top: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
}
|
|
26
|
+
.react-flow__pane {
|
|
27
|
+
z-index: 1;
|
|
28
|
+
cursor: -webkit-grab;
|
|
29
|
+
cursor: grab;
|
|
30
|
+
}
|
|
31
|
+
.react-flow__pane.selection {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
.react-flow__pane.dragging {
|
|
35
|
+
cursor: -webkit-grabbing;
|
|
36
|
+
cursor: grabbing;
|
|
37
|
+
}
|
|
38
|
+
.react-flow__viewport {
|
|
39
|
+
transform-origin: 0 0;
|
|
40
|
+
z-index: 2;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
}
|
|
43
|
+
.react-flow__renderer {
|
|
44
|
+
z-index: 4;
|
|
45
|
+
}
|
|
46
|
+
.react-flow__selection {
|
|
47
|
+
z-index: 6;
|
|
48
|
+
}
|
|
49
|
+
.react-flow__nodesselection-rect:focus,
|
|
50
|
+
.react-flow__nodesselection-rect:focus-visible {
|
|
51
|
+
outline: none;
|
|
52
|
+
}
|
|
53
|
+
.react-flow .react-flow__edges {
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
overflow: visible;
|
|
56
|
+
}
|
|
57
|
+
.react-flow__edge-path,
|
|
58
|
+
.react-flow__connection-path {
|
|
59
|
+
stroke: #b1b1b7;
|
|
60
|
+
stroke-width: 1;
|
|
61
|
+
fill: none;
|
|
62
|
+
}
|
|
63
|
+
.react-flow__edge {
|
|
64
|
+
pointer-events: visibleStroke;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
.react-flow__edge.animated path {
|
|
68
|
+
stroke-dasharray: 5;
|
|
69
|
+
-webkit-animation: dashdraw 0.5s linear infinite;
|
|
70
|
+
animation: dashdraw 0.5s linear infinite;
|
|
71
|
+
}
|
|
72
|
+
.react-flow__edge.animated path.react-flow__edge-interaction {
|
|
73
|
+
stroke-dasharray: none;
|
|
74
|
+
-webkit-animation: none;
|
|
75
|
+
animation: none;
|
|
76
|
+
}
|
|
77
|
+
.react-flow__edge.inactive {
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
.react-flow__edge.selected,
|
|
81
|
+
.react-flow__edge:focus,
|
|
82
|
+
.react-flow__edge:focus-visible {
|
|
83
|
+
outline: none;
|
|
84
|
+
}
|
|
85
|
+
.react-flow__edge.selected .react-flow__edge-path,
|
|
86
|
+
.react-flow__edge:focus .react-flow__edge-path,
|
|
87
|
+
.react-flow__edge:focus-visible .react-flow__edge-path {
|
|
88
|
+
stroke: #555;
|
|
89
|
+
}
|
|
90
|
+
.react-flow__edge-textwrapper {
|
|
91
|
+
pointer-events: all;
|
|
92
|
+
}
|
|
93
|
+
.react-flow__edge-textbg {
|
|
94
|
+
fill: white;
|
|
95
|
+
}
|
|
96
|
+
.react-flow__edge .react-flow__edge-text {
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
-webkit-user-select: none;
|
|
99
|
+
-moz-user-select: none;
|
|
100
|
+
user-select: none;
|
|
101
|
+
}
|
|
102
|
+
.react-flow__connection {
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
.react-flow__connection .animated {
|
|
106
|
+
stroke-dasharray: 5;
|
|
107
|
+
-webkit-animation: dashdraw 0.5s linear infinite;
|
|
108
|
+
animation: dashdraw 0.5s linear infinite;
|
|
109
|
+
}
|
|
110
|
+
.react-flow__connectionline {
|
|
111
|
+
z-index: 1001;
|
|
112
|
+
}
|
|
113
|
+
.react-flow__nodes {
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
transform-origin: 0 0;
|
|
116
|
+
}
|
|
117
|
+
.react-flow__node {
|
|
118
|
+
position: absolute;
|
|
119
|
+
-webkit-user-select: none;
|
|
120
|
+
-moz-user-select: none;
|
|
121
|
+
user-select: none;
|
|
122
|
+
pointer-events: all;
|
|
123
|
+
transform-origin: 0 0;
|
|
124
|
+
box-sizing: border-box;
|
|
125
|
+
cursor: -webkit-grab;
|
|
126
|
+
cursor: grab;
|
|
127
|
+
}
|
|
128
|
+
.react-flow__node.dragging {
|
|
129
|
+
cursor: -webkit-grabbing;
|
|
130
|
+
cursor: grabbing;
|
|
131
|
+
}
|
|
132
|
+
.react-flow__nodesselection {
|
|
133
|
+
z-index: 3;
|
|
134
|
+
transform-origin: left top;
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
.react-flow__nodesselection-rect {
|
|
138
|
+
position: absolute;
|
|
139
|
+
pointer-events: all;
|
|
140
|
+
cursor: -webkit-grab;
|
|
141
|
+
cursor: grab;
|
|
142
|
+
}
|
|
143
|
+
.react-flow__handle {
|
|
144
|
+
position: absolute;
|
|
145
|
+
pointer-events: none;
|
|
146
|
+
min-width: 5px;
|
|
147
|
+
min-height: 5px;
|
|
148
|
+
width: 6px;
|
|
149
|
+
height: 6px;
|
|
150
|
+
background: #1a192b;
|
|
151
|
+
border: 1px solid white;
|
|
152
|
+
border-radius: 100%;
|
|
153
|
+
}
|
|
154
|
+
.react-flow__handle.connectionindicator {
|
|
155
|
+
pointer-events: all;
|
|
156
|
+
cursor: crosshair;
|
|
157
|
+
}
|
|
158
|
+
.react-flow__handle-bottom {
|
|
159
|
+
top: auto;
|
|
160
|
+
left: 50%;
|
|
161
|
+
bottom: -4px;
|
|
162
|
+
transform: translate(-50%, 0);
|
|
163
|
+
}
|
|
164
|
+
.react-flow__handle-top {
|
|
165
|
+
left: 50%;
|
|
166
|
+
top: -4px;
|
|
167
|
+
transform: translate(-50%, 0);
|
|
168
|
+
}
|
|
169
|
+
.react-flow__handle-left {
|
|
170
|
+
top: 50%;
|
|
171
|
+
left: -4px;
|
|
172
|
+
transform: translate(0, -50%);
|
|
173
|
+
}
|
|
174
|
+
.react-flow__handle-right {
|
|
175
|
+
right: -4px;
|
|
176
|
+
top: 50%;
|
|
177
|
+
transform: translate(0, -50%);
|
|
178
|
+
}
|
|
179
|
+
.react-flow__edgeupdater {
|
|
180
|
+
cursor: move;
|
|
181
|
+
pointer-events: all;
|
|
182
|
+
}
|
|
183
|
+
.react-flow__panel {
|
|
184
|
+
position: absolute;
|
|
185
|
+
z-index: 5;
|
|
186
|
+
margin: 15px;
|
|
187
|
+
}
|
|
188
|
+
.react-flow__panel.top {
|
|
189
|
+
top: 0;
|
|
190
|
+
}
|
|
191
|
+
.react-flow__panel.bottom {
|
|
192
|
+
bottom: 0;
|
|
193
|
+
}
|
|
194
|
+
.react-flow__panel.left {
|
|
195
|
+
left: 0;
|
|
196
|
+
}
|
|
197
|
+
.react-flow__panel.right {
|
|
198
|
+
right: 0;
|
|
199
|
+
}
|
|
200
|
+
.react-flow__panel.center {
|
|
201
|
+
left: 50%;
|
|
202
|
+
transform: translateX(-50%);
|
|
203
|
+
}
|
|
204
|
+
.react-flow__attribution {
|
|
205
|
+
font-size: 10px;
|
|
206
|
+
background: rgba(255, 255, 255, 0.5);
|
|
207
|
+
padding: 2px 3px;
|
|
208
|
+
margin: 0;
|
|
209
|
+
}
|
|
210
|
+
.react-flow__attribution a {
|
|
211
|
+
text-decoration: none;
|
|
212
|
+
color: #999;
|
|
213
|
+
}
|
|
214
|
+
@-webkit-keyframes dashdraw {
|
|
215
|
+
from {
|
|
216
|
+
stroke-dashoffset: 10;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
@keyframes dashdraw {
|
|
220
|
+
from {
|
|
221
|
+
stroke-dashoffset: 10;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
.react-flow__edgelabel-renderer {
|
|
225
|
+
position: absolute;
|
|
226
|
+
width: 100%;
|
|
227
|
+
height: 100%;
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
-webkit-user-select: none;
|
|
230
|
+
-moz-user-select: none;
|
|
231
|
+
user-select: none;
|
|
232
|
+
}
|
|
233
|
+
.react-flow__edge.updating .react-flow__edge-path {
|
|
234
|
+
stroke: #777;
|
|
235
|
+
}
|
|
236
|
+
.react-flow__edge-text {
|
|
237
|
+
font-size: 10px;
|
|
238
|
+
}
|
|
239
|
+
.react-flow__node.selectable:focus,
|
|
240
|
+
.react-flow__node.selectable:focus-visible {
|
|
241
|
+
outline: none;
|
|
242
|
+
}
|
|
243
|
+
.react-flow__node-default,
|
|
244
|
+
.react-flow__node-input,
|
|
245
|
+
.react-flow__node-output,
|
|
246
|
+
.react-flow__node-group {
|
|
247
|
+
padding: 10px;
|
|
248
|
+
border-radius: 3px;
|
|
249
|
+
width: 150px;
|
|
250
|
+
font-size: 12px;
|
|
251
|
+
color: #222;
|
|
252
|
+
text-align: center;
|
|
253
|
+
border-width: 1px;
|
|
254
|
+
border-style: solid;
|
|
255
|
+
border-color: #1a192b;
|
|
256
|
+
background-color: white;
|
|
257
|
+
}
|
|
258
|
+
.react-flow__node-default.selectable:hover, .react-flow__node-input.selectable:hover, .react-flow__node-output.selectable:hover, .react-flow__node-group.selectable:hover {
|
|
259
|
+
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
|
260
|
+
}
|
|
261
|
+
.react-flow__node-default.selectable.selected,
|
|
262
|
+
.react-flow__node-default.selectable:focus,
|
|
263
|
+
.react-flow__node-default.selectable:focus-visible,
|
|
264
|
+
.react-flow__node-input.selectable.selected,
|
|
265
|
+
.react-flow__node-input.selectable:focus,
|
|
266
|
+
.react-flow__node-input.selectable:focus-visible,
|
|
267
|
+
.react-flow__node-output.selectable.selected,
|
|
268
|
+
.react-flow__node-output.selectable:focus,
|
|
269
|
+
.react-flow__node-output.selectable:focus-visible,
|
|
270
|
+
.react-flow__node-group.selectable.selected,
|
|
271
|
+
.react-flow__node-group.selectable:focus,
|
|
272
|
+
.react-flow__node-group.selectable:focus-visible {
|
|
273
|
+
box-shadow: 0 0 0 0.5px #1a192b;
|
|
274
|
+
}
|
|
275
|
+
.react-flow__node-group {
|
|
276
|
+
background-color: rgba(240, 240, 240, 0.25);
|
|
277
|
+
}
|
|
278
|
+
.react-flow__nodesselection-rect,
|
|
279
|
+
.react-flow__selection {
|
|
280
|
+
background: rgba(0, 89, 220, 0.08);
|
|
281
|
+
border: 1px dotted rgba(0, 89, 220, 0.8);
|
|
282
|
+
}
|
|
283
|
+
.react-flow__nodesselection-rect:focus,
|
|
284
|
+
.react-flow__nodesselection-rect:focus-visible,
|
|
285
|
+
.react-flow__selection:focus,
|
|
286
|
+
.react-flow__selection:focus-visible {
|
|
287
|
+
outline: none;
|
|
288
|
+
}
|
|
289
|
+
.react-flow__controls {
|
|
290
|
+
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
|
291
|
+
}
|
|
292
|
+
.react-flow__controls-button {
|
|
293
|
+
border: none;
|
|
294
|
+
background: #fefefe;
|
|
295
|
+
border-bottom: 1px solid #eee;
|
|
296
|
+
box-sizing: content-box;
|
|
297
|
+
display: flex;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
align-items: center;
|
|
300
|
+
width: 16px;
|
|
301
|
+
height: 16px;
|
|
302
|
+
cursor: pointer;
|
|
303
|
+
-webkit-user-select: none;
|
|
304
|
+
-moz-user-select: none;
|
|
305
|
+
user-select: none;
|
|
306
|
+
padding: 5px;
|
|
307
|
+
}
|
|
308
|
+
.react-flow__controls-button:hover {
|
|
309
|
+
background: #f4f4f4;
|
|
310
|
+
}
|
|
311
|
+
.react-flow__controls-button svg {
|
|
312
|
+
width: 100%;
|
|
313
|
+
max-width: 12px;
|
|
314
|
+
max-height: 12px;
|
|
315
|
+
}
|
|
316
|
+
.react-flow__controls-button:disabled {
|
|
317
|
+
pointer-events: none;
|
|
318
|
+
}
|
|
319
|
+
.react-flow__controls-button:disabled svg {
|
|
320
|
+
fill-opacity: 0.4;
|
|
321
|
+
}
|
|
322
|
+
.react-flow__minimap {
|
|
323
|
+
background-color: #fff;
|
|
324
|
+
}
|
|
325
|
+
.react-flow__minimap svg {
|
|
326
|
+
display: block;
|
|
327
|
+
}
|
|
328
|
+
.react-flow__resize-control {
|
|
329
|
+
position: absolute;
|
|
330
|
+
}
|
|
331
|
+
.react-flow__resize-control.left,
|
|
332
|
+
.react-flow__resize-control.right {
|
|
333
|
+
cursor: ew-resize;
|
|
334
|
+
}
|
|
335
|
+
.react-flow__resize-control.top,
|
|
336
|
+
.react-flow__resize-control.bottom {
|
|
337
|
+
cursor: ns-resize;
|
|
338
|
+
}
|
|
339
|
+
.react-flow__resize-control.top.left,
|
|
340
|
+
.react-flow__resize-control.bottom.right {
|
|
341
|
+
cursor: nwse-resize;
|
|
342
|
+
}
|
|
343
|
+
.react-flow__resize-control.bottom.left,
|
|
344
|
+
.react-flow__resize-control.top.right {
|
|
345
|
+
cursor: nesw-resize;
|
|
346
|
+
}
|
|
347
|
+
/* handle styles */
|
|
348
|
+
.react-flow__resize-control.handle {
|
|
349
|
+
width: 4px;
|
|
350
|
+
height: 4px;
|
|
351
|
+
border: 1px solid #fff;
|
|
352
|
+
border-radius: 1px;
|
|
353
|
+
background-color: #3367d9;
|
|
354
|
+
transform: translate(-50%, -50%);
|
|
355
|
+
}
|
|
356
|
+
.react-flow__resize-control.handle.left {
|
|
357
|
+
left: 0;
|
|
358
|
+
top: 50%;
|
|
359
|
+
}
|
|
360
|
+
.react-flow__resize-control.handle.right {
|
|
361
|
+
left: 100%;
|
|
362
|
+
top: 50%;
|
|
363
|
+
}
|
|
364
|
+
.react-flow__resize-control.handle.top {
|
|
365
|
+
left: 50%;
|
|
366
|
+
top: 0;
|
|
367
|
+
}
|
|
368
|
+
.react-flow__resize-control.handle.bottom {
|
|
369
|
+
left: 50%;
|
|
370
|
+
top: 100%;
|
|
371
|
+
}
|
|
372
|
+
.react-flow__resize-control.handle.top.left {
|
|
373
|
+
left: 0;
|
|
374
|
+
}
|
|
375
|
+
.react-flow__resize-control.handle.bottom.left {
|
|
376
|
+
left: 0;
|
|
377
|
+
}
|
|
378
|
+
.react-flow__resize-control.handle.top.right {
|
|
379
|
+
left: 100%;
|
|
380
|
+
}
|
|
381
|
+
.react-flow__resize-control.handle.bottom.right {
|
|
382
|
+
left: 100%;
|
|
383
|
+
}
|
|
384
|
+
/* line styles */
|
|
385
|
+
.react-flow__resize-control.line {
|
|
386
|
+
border-color: #3367d9;
|
|
387
|
+
border-width: 0;
|
|
388
|
+
border-style: solid;
|
|
389
|
+
}
|
|
390
|
+
.react-flow__resize-control.line.left,
|
|
391
|
+
.react-flow__resize-control.line.right {
|
|
392
|
+
width: 1px;
|
|
393
|
+
transform: translate(-50%, 0);
|
|
394
|
+
top: 0;
|
|
395
|
+
height: 100%;
|
|
396
|
+
}
|
|
397
|
+
.react-flow__resize-control.line.left {
|
|
398
|
+
left: 0;
|
|
399
|
+
border-left-width: 1px;
|
|
400
|
+
}
|
|
401
|
+
.react-flow__resize-control.line.right {
|
|
402
|
+
left: 100%;
|
|
403
|
+
border-right-width: 1px;
|
|
404
|
+
}
|
|
405
|
+
.react-flow__resize-control.line.top,
|
|
406
|
+
.react-flow__resize-control.line.bottom {
|
|
407
|
+
height: 1px;
|
|
408
|
+
transform: translate(0, -50%);
|
|
409
|
+
left: 0;
|
|
410
|
+
width: 100%;
|
|
411
|
+
}
|
|
412
|
+
.react-flow__resize-control.line.top {
|
|
413
|
+
top: 0;
|
|
414
|
+
border-top-width: 1px;
|
|
415
|
+
}
|
|
416
|
+
.react-flow__resize-control.line.bottom {
|
|
417
|
+
border-bottom-width: 1px;
|
|
418
|
+
top: 100%;
|
|
419
|
+
}
|
|
420
|
+
</style>
|
|
421
|
+
<style>
|
|
422
|
+
/* ============ tokens ============ */
|
|
423
|
+
:root{
|
|
424
|
+
--bg:#F2F6FA; --band:#E7EEF5; --card:#FFFFFF; --canvas:#FAFCFE; --code:#EDF3F9;
|
|
425
|
+
--ink:#0B1F3A; --ink2:#22395A; --muted:#4F6079; --hair:#D5E0EB; --rule:#A7BBD0;
|
|
426
|
+
--grid:#E9F0F6; --grid2:#DCE6F0;
|
|
427
|
+
--local:#0079C8; --local-bg:#E2F0FA;
|
|
428
|
+
--org:#007A69; --org-bg:#DCF0EB;
|
|
429
|
+
--pub:#A35700; --pub-bg:#FAEDDB;
|
|
430
|
+
--agent:#5A3DAE; --agent-bg:#ECE7F7;
|
|
431
|
+
--block:#BE2830; --block-bg:#F9E3E4;
|
|
432
|
+
--focus:#0079C8;
|
|
433
|
+
--brand:#0079C8; --brand-ink:#FFFFFF; --brand-soft:#CFE7F7;
|
|
434
|
+
--sans:'Montserrat',ui-sans-serif,'Segoe UI',Arial,sans-serif;
|
|
435
|
+
--display:'Montserrat',ui-sans-serif,'Segoe UI',Arial,sans-serif;
|
|
436
|
+
--mono:'Montserrat',ui-sans-serif,'Segoe UI',Arial,sans-serif;
|
|
437
|
+
--codefont:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace;
|
|
438
|
+
color-scheme:light;
|
|
439
|
+
}
|
|
440
|
+
@media (prefers-color-scheme:dark){
|
|
441
|
+
:root:not([data-theme="light"]){
|
|
442
|
+
--bg:#04162B; --band:#061D36; --card:#0A2442; --canvas:#05192F; --code:#0C2948;
|
|
443
|
+
--ink:#E6F0FA; --ink2:#C3D4E6; --muted:#8FA6BF; --hair:#17345A; --rule:#2D5682;
|
|
444
|
+
--grid:#0A2340; --grid2:#103059;
|
|
445
|
+
--local:#4DB3F2; --local-bg:#0B2A4D;
|
|
446
|
+
--org:#3FCFB0; --org-bg:#0A2E2D;
|
|
447
|
+
--pub:#F2B25C; --pub-bg:#2B2010;
|
|
448
|
+
--agent:#B6A2FF; --agent-bg:#1E1A3C;
|
|
449
|
+
--block:#FF7B76; --block-bg:#34171B;
|
|
450
|
+
--focus:#4DB3F2;
|
|
451
|
+
--brand:#06325C; --brand-ink:#FFFFFF; --brand-soft:#9CCBEB;
|
|
452
|
+
color-scheme:dark;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
:root[data-theme="dark"]{
|
|
456
|
+
--bg:#04162B; --band:#061D36; --card:#0A2442; --canvas:#05192F; --code:#0C2948;
|
|
457
|
+
--ink:#E6F0FA; --ink2:#C3D4E6; --muted:#8FA6BF; --hair:#17345A; --rule:#2D5682;
|
|
458
|
+
--grid:#0A2340; --grid2:#103059;
|
|
459
|
+
--local:#4DB3F2; --local-bg:#0B2A4D;
|
|
460
|
+
--org:#3FCFB0; --org-bg:#0A2E2D;
|
|
461
|
+
--pub:#F2B25C; --pub-bg:#2B2010;
|
|
462
|
+
--agent:#B6A2FF; --agent-bg:#1E1A3C;
|
|
463
|
+
--block:#FF7B76; --block-bg:#34171B;
|
|
464
|
+
--focus:#4DB3F2;
|
|
465
|
+
--brand:#06325C; --brand-ink:#FFFFFF; --brand-soft:#9CCBEB;
|
|
466
|
+
color-scheme:dark;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* ============ base ============ */
|
|
470
|
+
*{box-sizing:border-box}
|
|
471
|
+
html,body{height:100%}
|
|
472
|
+
html{-webkit-text-size-adjust:100%}
|
|
473
|
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:12.75px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
474
|
+
button{font:inherit;color:inherit}
|
|
475
|
+
:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
|
|
476
|
+
code{font-family:var(--codefont);font-size:.88em;background:var(--code);padding:0 4px;border:1px solid var(--hair);border-radius:2px;white-space:nowrap}
|
|
477
|
+
p{margin:0}
|
|
478
|
+
h2,h3{margin:0}
|
|
479
|
+
.muted{color:var(--muted)}
|
|
480
|
+
.mono{font-family:var(--mono);font-size:10px;letter-spacing:.09em;text-transform:uppercase;font-weight:500}
|
|
481
|
+
.noscript{padding:24px}
|
|
482
|
+
|
|
483
|
+
/* corner ticks (registration marks) */
|
|
484
|
+
.ticks{position:relative}
|
|
485
|
+
.ticks::before{content:"";position:absolute;inset:-4px;pointer-events:none;--tc:var(--rule);
|
|
486
|
+
background:
|
|
487
|
+
linear-gradient(var(--tc),var(--tc)) left top/9px 1px no-repeat,
|
|
488
|
+
linear-gradient(var(--tc),var(--tc)) left top/1px 9px no-repeat,
|
|
489
|
+
linear-gradient(var(--tc),var(--tc)) right top/9px 1px no-repeat,
|
|
490
|
+
linear-gradient(var(--tc),var(--tc)) right top/1px 9px no-repeat,
|
|
491
|
+
linear-gradient(var(--tc),var(--tc)) left bottom/9px 1px no-repeat,
|
|
492
|
+
linear-gradient(var(--tc),var(--tc)) left bottom/1px 9px no-repeat,
|
|
493
|
+
linear-gradient(var(--tc),var(--tc)) right bottom/9px 1px no-repeat,
|
|
494
|
+
linear-gradient(var(--tc),var(--tc)) right bottom/1px 9px no-repeat}
|
|
495
|
+
|
|
496
|
+
/* classification stamp */
|
|
497
|
+
.where{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;padding:1px 6px 1px 5px;border:1px solid currentColor;border-radius:2px;line-height:1.5;white-space:nowrap;background:var(--card)}
|
|
498
|
+
.where::before{content:"";width:5px;height:5px;background:currentColor;flex:none}
|
|
499
|
+
.where.local{color:var(--local);background:var(--local-bg)}
|
|
500
|
+
.where.org{color:var(--org);background:var(--org-bg)}
|
|
501
|
+
.where.pub{color:var(--pub);background:var(--pub-bg)}
|
|
502
|
+
.where.block{color:var(--block);background:var(--block-bg)}
|
|
503
|
+
.where.block::before{background:none;border:1px solid currentColor;width:6px;height:6px;transform:rotate(45deg)}
|
|
504
|
+
|
|
505
|
+
/* ============ shell ============ */
|
|
506
|
+
#app{height:100vh;height:100dvh;display:grid;grid-template-columns:216px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
|
|
507
|
+
.mast{grid-column:1/-1;display:flex;align-items:center;gap:16px;height:52px;padding:0 20px;border-bottom:0;background:var(--brand);color:var(--brand-ink)}
|
|
508
|
+
.mark{display:flex;align-items:center;gap:9px;flex:none}
|
|
509
|
+
.mark img{display:block;width:30px;height:30px;padding:2px;background:#fff;border-radius:7px}
|
|
510
|
+
.mark b{font-family:var(--sans);font-weight:700;font-size:14px;letter-spacing:-.01em}
|
|
511
|
+
.mast .sep{width:1px;height:22px;background:color-mix(in srgb,var(--brand-ink) 40%,transparent);flex:none}
|
|
512
|
+
.mast .ttl{display:flex;align-items:baseline;gap:12px;min-width:0}
|
|
513
|
+
.mast h1{font-family:var(--display);font-weight:700;font-size:16px;line-height:1;margin:0;white-space:nowrap;letter-spacing:-.005em}
|
|
514
|
+
.mast .sub{color:var(--brand-soft);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
515
|
+
.mast .right{margin-left:auto;display:flex;align-items:center;gap:14px;flex:none}
|
|
516
|
+
.status{display:flex;align-items:center;gap:8px;color:var(--brand-soft);white-space:nowrap}
|
|
517
|
+
.status .pulse{width:7px;height:7px;background:#7FE3C9;box-shadow:0 0 0 3px color-mix(in srgb,#7FE3C9 30%,transparent);flex:none}
|
|
518
|
+
.status em{font-style:normal;color:var(--brand-ink);font-weight:700}
|
|
519
|
+
.theme{display:grid;place-items:center;width:30px;height:30px;border:1px solid color-mix(in srgb,var(--brand-ink) 50%,transparent);background:transparent;cursor:pointer;border-radius:0;color:var(--brand-ink)}
|
|
520
|
+
.theme:hover{background:var(--brand-ink);color:var(--brand);border-color:var(--brand-ink)}
|
|
521
|
+
.mast :focus-visible{outline-color:var(--brand-ink)}
|
|
522
|
+
.theme svg{width:14px;height:14px}
|
|
523
|
+
|
|
524
|
+
/* chapter rail */
|
|
525
|
+
.rail{grid-row:2;border-right:1px solid var(--hair);background:var(--band);display:flex;flex-direction:column;min-height:0;overflow:auto}
|
|
526
|
+
.rail .cap{padding:16px 18px 8px;color:var(--muted)}
|
|
527
|
+
.chapters{display:flex;flex-direction:column;padding:0 0 12px}
|
|
528
|
+
.chapters button{position:relative;display:grid;grid-template-columns:28px minmax(0,1fr);gap:0 6px;text-align:left;background:none;border:0;border-top:1px solid var(--hair);padding:11px 16px 11px 18px;cursor:pointer;color:var(--muted)}
|
|
529
|
+
.chapters button:last-child{border-bottom:1px solid var(--hair)}
|
|
530
|
+
.chapters button .n{font-family:var(--mono);font-size:10.5px;font-weight:600;padding-top:2px;color:var(--muted)}
|
|
531
|
+
.chapters button .t{font-weight:600;font-size:12.5px;color:var(--ink2);line-height:1.3}
|
|
532
|
+
.chapters button .d{grid-column:2;font-size:11px;line-height:1.4;margin-top:2px}
|
|
533
|
+
.chapters button::before{content:"";position:absolute;left:0;top:-1px;bottom:-1px;width:3px;background:var(--ink);transform:scaleY(0);transform-origin:top;transition:transform .25s ease}
|
|
534
|
+
.chapters button:hover{background:var(--card)}
|
|
535
|
+
.chapters button:hover .t{color:var(--ink)}
|
|
536
|
+
.chapters button[aria-selected="true"]{background:var(--card);color:var(--ink2)}
|
|
537
|
+
.chapters button[aria-selected="true"] .t,.chapters button[aria-selected="true"] .n{color:var(--ink)}
|
|
538
|
+
.chapters button[aria-selected="true"]::before{transform:scaleY(1)}
|
|
539
|
+
.chapters button:focus-visible{outline-offset:-2px}
|
|
540
|
+
.rail .foot{margin-top:auto;padding:14px 18px 16px;border-top:1px solid var(--hair);display:grid;gap:6px;color:var(--muted)}
|
|
541
|
+
.rail .foot .k{display:flex;align-items:center;gap:8px;font-size:11px}
|
|
542
|
+
.rail .foot .k i{width:16px;height:0;border-top:2px solid;flex:none}
|
|
543
|
+
|
|
544
|
+
/* content */
|
|
545
|
+
.panel{grid-row:2;min-width:0;min-height:0;overflow:auto;padding:14px 22px 18px;display:flex;flex-direction:column;gap:12px}
|
|
546
|
+
.chead{flex:none;display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;padding-bottom:9px;border-bottom:1px solid var(--hair)}
|
|
547
|
+
.chead .mono{color:var(--muted)}
|
|
548
|
+
.chead h2{font-family:var(--display);font-weight:700;font-size:20px;line-height:1.1;letter-spacing:-.01em}
|
|
549
|
+
.chead p{color:var(--muted);font-size:12px;flex:1 1 280px}
|
|
550
|
+
.fill{flex:1;min-height:0}
|
|
551
|
+
@media (prefers-reduced-motion:no-preference){
|
|
552
|
+
.enter{animation:enter .4s cubic-bezier(.2,.7,.2,1) both}
|
|
553
|
+
.enter:nth-child(2){animation-delay:.05s}.enter:nth-child(3){animation-delay:.1s}.enter:nth-child(4){animation-delay:.15s}
|
|
554
|
+
@keyframes enter{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* buttons */
|
|
558
|
+
.btn{font-size:11.5px;font-weight:600;background:var(--card);color:var(--ink);border:1px solid var(--rule);border-radius:0;padding:6px 11px;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}
|
|
559
|
+
.btn:hover{border-color:var(--ink)}
|
|
560
|
+
.btn:active{transform:translateY(1px)}
|
|
561
|
+
.btn.primary{background:var(--ink);color:var(--bg);border-color:var(--ink)}
|
|
562
|
+
.btn.primary:hover{background:var(--ink2)}
|
|
563
|
+
|
|
564
|
+
/* ============ overview ============ */
|
|
565
|
+
.ov{max-width:1200px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:22px;padding-top:6px}
|
|
566
|
+
.cover{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:36px;align-items:start}
|
|
567
|
+
.cover .kick{color:var(--org);display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
568
|
+
.cover .kick::after{content:"";flex:1;height:1px;background:var(--hair)}
|
|
569
|
+
.cover h2{font-family:var(--display);font-weight:800;font-size:clamp(28px,3.6vw,46px);line-height:1.04;letter-spacing:-.02em;margin:0 0 14px}
|
|
570
|
+
.cover h2 em{font-style:italic;color:var(--org)}
|
|
571
|
+
.cover .lead{font-size:13.5px;line-height:1.6;color:var(--ink2);max-width:58ch}
|
|
572
|
+
.cover .lead b{color:var(--ink)}
|
|
573
|
+
.jump{list-style:none;margin:18px 0 0;padding:0;display:grid;border-top:1px solid var(--ink);max-width:460px}
|
|
574
|
+
.jump button{width:100%;display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:8px;text-align:left;background:none;border:0;border-bottom:1px solid var(--hair);padding:9px 2px;cursor:pointer}
|
|
575
|
+
.jump button .n{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
|
|
576
|
+
.jump button .t{font-weight:600;font-size:12.5px}
|
|
577
|
+
.jump button .a{font-family:var(--mono);font-size:12px;color:var(--muted);transition:transform .2s}
|
|
578
|
+
.jump button:hover .a{transform:translateX(4px);color:var(--ink)}
|
|
579
|
+
.jump button:hover .t{text-decoration:underline;text-underline-offset:3px}
|
|
580
|
+
.ledger{background:var(--card);border:1px solid var(--hair);padding:14px 18px 6px}
|
|
581
|
+
.ledger header{display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px solid var(--ink);padding-bottom:7px;color:var(--muted)}
|
|
582
|
+
.ledger header b{font-family:var(--display);font-weight:700;font-size:16px;color:var(--ink);text-transform:none;letter-spacing:0}
|
|
583
|
+
.ledger ol{list-style:none;margin:0;padding:0}
|
|
584
|
+
.ledger li{display:grid;grid-template-columns:40px minmax(0,1fr) auto;gap:10px;align-items:center;padding:10px 0;border-bottom:1px solid var(--hair);font-size:12.5px}
|
|
585
|
+
.ledger li:last-child{border-bottom:0}
|
|
586
|
+
.ledger li .n{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
|
|
587
|
+
.ledger li .ok{display:grid;place-items:center;width:18px;height:18px;border:1px solid var(--org);color:var(--org)}
|
|
588
|
+
.ledger li .ok svg{width:11px;height:11px}
|
|
589
|
+
|
|
590
|
+
.perim{display:flex;flex-direction:column;gap:8px}
|
|
591
|
+
.perim .phead{display:flex;align-items:baseline;gap:12px;color:var(--muted)}
|
|
592
|
+
.perim .phead::after{content:"";flex:1;height:1px;background:var(--hair)}
|
|
593
|
+
.band{position:relative;border:1px solid;padding:26px 16px 16px}
|
|
594
|
+
.band>.lbl{position:absolute;top:-1px;left:-1px;display:flex;align-items:center;gap:8px;padding:3px 9px;font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase}
|
|
595
|
+
.band .row{display:grid;grid-template-columns:minmax(0,220px) minmax(0,1fr);gap:8px 22px;align-items:start;margin-bottom:14px}
|
|
596
|
+
.band .row b{display:block;font-size:13.5px;font-weight:700;color:var(--ink)}
|
|
597
|
+
.band .row p{color:var(--muted);font-size:12px;margin-top:2px}
|
|
598
|
+
.band .row ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}
|
|
599
|
+
.band .row li{font-size:11.5px;color:var(--ink2);background:var(--card);border:1px solid var(--hair);padding:3px 8px}
|
|
600
|
+
.band.p{border-color:var(--pub);border-style:dashed;background:repeating-linear-gradient(-45deg,transparent 0 7px,color-mix(in srgb,var(--pub) 7%,transparent) 7px 8px)}
|
|
601
|
+
.band.p>.lbl{background:var(--pub);color:var(--bg)}
|
|
602
|
+
.band.o{border-color:var(--org);background:var(--bg)}
|
|
603
|
+
.band.o>.lbl{background:var(--org);color:var(--bg)}
|
|
604
|
+
.band.l{border-color:var(--local);border-width:1.5px;background:var(--local-bg);margin-bottom:0}
|
|
605
|
+
.band.l>.lbl{background:var(--local);color:var(--bg)}
|
|
606
|
+
.band.l .row{margin-bottom:0}
|
|
607
|
+
.band.l::before{--tc:var(--local)}
|
|
608
|
+
@media (max-width:900px){.cover{grid-template-columns:minmax(0,1fr);gap:20px}.band .row{grid-template-columns:minmax(0,1fr)}.band{padding:26px 10px 10px}}
|
|
609
|
+
|
|
610
|
+
/* ============ split views ============ */
|
|
611
|
+
.split{height:100%;display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:16px}
|
|
612
|
+
.split>*{min-width:0}
|
|
613
|
+
.fig{display:flex;flex-direction:column;min-height:0;border:1px solid var(--ink);background:var(--canvas)}
|
|
614
|
+
.fig>header{flex:none;display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:6px 10px;border-bottom:1px solid var(--hair);background:var(--card)}
|
|
615
|
+
.fig>header .mono{color:var(--ink)}
|
|
616
|
+
.key{display:flex;flex-wrap:wrap;gap:4px 14px;margin-left:auto}
|
|
617
|
+
.key span{display:inline-flex;align-items:center;gap:6px;font-size:10.5px;color:var(--muted);white-space:nowrap}
|
|
618
|
+
.key i{width:18px;height:0;border-top:3px dotted}
|
|
619
|
+
.canvas{position:relative;flex:1;min-height:0;overflow:hidden}
|
|
620
|
+
.side{min-height:0;overflow:auto;display:flex;flex-direction:column;gap:14px;padding-right:2px}
|
|
621
|
+
.sec{border-top:1px solid var(--ink);padding-top:10px}
|
|
622
|
+
.sec>.mono{color:var(--muted);display:block;margin-bottom:4px}
|
|
623
|
+
.sec h3{font-family:var(--display);font-weight:700;font-size:16.5px;line-height:1.1;margin-bottom:4px}
|
|
624
|
+
.sec h4{margin:0;font-size:12.5px;font-weight:700}
|
|
625
|
+
.hintline{font-size:11.5px;color:var(--muted)}
|
|
626
|
+
@media (max-width:1023px){.split{height:auto;grid-template-columns:minmax(0,1fr)}.fig .canvas{height:60vh;min-height:340px;flex:none}.side{overflow:visible}}
|
|
627
|
+
|
|
628
|
+
.info{position:absolute;left:10px;bottom:10px;z-index:5;max-width:min(360px,calc(100% - 70px));background:var(--card);border:1px solid var(--ink);padding:9px 30px 10px 12px;font-size:11.5px;box-shadow:4px 4px 0 var(--grid2)}
|
|
629
|
+
.info .mono{color:var(--muted);display:block;margin-bottom:2px}
|
|
630
|
+
.info b{display:block;margin-bottom:3px;font-size:12.5px}
|
|
631
|
+
.info button{position:absolute;right:4px;top:4px;width:22px;height:22px;border:0;background:none;color:var(--muted);cursor:pointer;font-size:15px;line-height:1}
|
|
632
|
+
.info button:hover{color:var(--ink)}
|
|
633
|
+
|
|
634
|
+
/* scenario index */
|
|
635
|
+
.scn{list-style:none;margin:8px 0 0;padding:0;border-top:1px solid var(--hair)}
|
|
636
|
+
.scn button{position:relative;width:100%;display:grid;grid-template-columns:30px minmax(0,1fr) auto;align-items:center;gap:6px;text-align:left;background:none;border:0;border-bottom:1px solid var(--hair);padding:6px 6px 6px 10px;cursor:pointer;font-size:12px;color:var(--ink2)}
|
|
637
|
+
.scn button .n{font-family:var(--mono);font-size:10px;color:var(--muted)}
|
|
638
|
+
.scn button .c{font-family:var(--mono);font-size:9.5px;color:var(--muted)}
|
|
639
|
+
.scn button:hover{background:var(--card);color:var(--ink)}
|
|
640
|
+
.scn button[aria-pressed="true"]{background:var(--card);color:var(--ink);font-weight:600}
|
|
641
|
+
.scn button[aria-pressed="true"]::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--org)}
|
|
642
|
+
.ctrl{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:10px;border:1px solid var(--rule)}
|
|
643
|
+
.ctrl .btn{border:0;border-left:1px solid var(--rule);justify-content:center;padding:7px 4px;font-size:11px}
|
|
644
|
+
.ctrl .btn:first-child{border-left:0}
|
|
645
|
+
.ctrl .btn:focus-visible{outline-offset:-2px}
|
|
646
|
+
|
|
647
|
+
/* step timeline */
|
|
648
|
+
.steps{list-style:none;margin:8px 0 0;padding:0;position:relative}
|
|
649
|
+
.steps li{position:relative;display:grid;grid-template-columns:26px minmax(0,1fr);gap:8px;padding:0 4px 12px 0;cursor:pointer}
|
|
650
|
+
.steps li::after{content:"";position:absolute;left:10px;top:22px;bottom:0;width:1px;background:var(--hair)}
|
|
651
|
+
.steps li:last-child::after{display:none}
|
|
652
|
+
.steps li .dot{width:21px;height:21px;display:grid;place-items:center;border:1px solid var(--rule);background:var(--card);font-family:var(--mono);font-size:10px;font-weight:600;color:var(--muted);transition:background .2s,color .2s}
|
|
653
|
+
.steps li b{display:block;font-size:12.5px;line-height:1.35}
|
|
654
|
+
.steps li p{font-size:11.75px;color:var(--muted);margin:2px 0 6px;line-height:1.45}
|
|
655
|
+
.steps li:hover b{text-decoration:underline;text-underline-offset:3px}
|
|
656
|
+
.steps li.on .dot{background:var(--ink);color:var(--bg);border-color:var(--ink)}
|
|
657
|
+
.steps li.on b{color:var(--ink)}
|
|
658
|
+
.steps li.on>div{background:var(--card);box-shadow:-8px 0 0 var(--card),8px 0 0 var(--card);outline:1px solid var(--hair);outline-offset:4px}
|
|
659
|
+
.steps li.done .dot{color:var(--ink);border-color:var(--ink)}
|
|
660
|
+
|
|
661
|
+
/* agent notes */
|
|
662
|
+
.anote{border-top:1px solid var(--hair);padding-top:9px}
|
|
663
|
+
.anote header{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
|
|
664
|
+
.anote header .mono{color:var(--agent)}
|
|
665
|
+
.anote ul{list-style:none;margin:0;padding:0}
|
|
666
|
+
.anote li{position:relative;padding:3px 0 3px 16px;font-size:12px;color:var(--ink2)}
|
|
667
|
+
.anote li::before{content:"";position:absolute;left:2px;top:10px;width:7px;height:1px;background:var(--ink2)}
|
|
668
|
+
|
|
669
|
+
/* ============ react flow skin ============ */
|
|
670
|
+
.react-flow__attribution{background:transparent;font-size:9px}
|
|
671
|
+
.react-flow__attribution a{color:var(--muted)}
|
|
672
|
+
.react-flow__controls{box-shadow:none;border:1px solid var(--ink);border-radius:0;overflow:hidden}
|
|
673
|
+
.react-flow__controls-button{background:var(--card);border-bottom:1px solid var(--hair);fill:var(--ink);width:24px;height:24px}
|
|
674
|
+
.react-flow__controls-button:hover{background:var(--code)}
|
|
675
|
+
.react-flow__controls-button svg{fill:var(--ink)}
|
|
676
|
+
.react-flow__background path{stroke:var(--grid)}
|
|
677
|
+
.react-flow__background.major path{stroke:var(--grid2)}
|
|
678
|
+
.react-flow__node{cursor:pointer}
|
|
679
|
+
.react-flow__node.nopan{cursor:grab}
|
|
680
|
+
.react-flow__node.dragging{cursor:grabbing}
|
|
681
|
+
.canvas .reset{position:absolute;top:10px;right:10px;z-index:6;font-size:11px;padding:5px 10px}
|
|
682
|
+
.react-flow__node.dim{opacity:.3;transition:opacity .25s}
|
|
683
|
+
.react-flow__node-zone{cursor:grab}
|
|
684
|
+
.react-flow__node-zone.dragging{cursor:grabbing}
|
|
685
|
+
.react-flow__handle{opacity:0;width:6px;height:6px;min-width:0;min-height:0;border:0}
|
|
686
|
+
|
|
687
|
+
.zone{position:relative;width:100%;height:100%;border:1px solid;padding:0}
|
|
688
|
+
.zone::before{content:"";position:absolute;inset:-5px;pointer-events:none;--tc:currentColor;
|
|
689
|
+
background:
|
|
690
|
+
linear-gradient(var(--tc),var(--tc)) left top/12px 1.5px no-repeat,linear-gradient(var(--tc),var(--tc)) left top/1.5px 12px no-repeat,
|
|
691
|
+
linear-gradient(var(--tc),var(--tc)) right top/12px 1.5px no-repeat,linear-gradient(var(--tc),var(--tc)) right top/1.5px 12px no-repeat,
|
|
692
|
+
linear-gradient(var(--tc),var(--tc)) left bottom/12px 1.5px no-repeat,linear-gradient(var(--tc),var(--tc)) left bottom/1.5px 12px no-repeat,
|
|
693
|
+
linear-gradient(var(--tc),var(--tc)) right bottom/12px 1.5px no-repeat,linear-gradient(var(--tc),var(--tc)) right bottom/1.5px 12px no-repeat}
|
|
694
|
+
.zone span{position:absolute;left:10px;top:-8px;background:var(--canvas);padding:0 6px;font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:600;white-space:nowrap;line-height:15px}
|
|
695
|
+
.zone.local{color:var(--local);background:color-mix(in srgb,var(--local) 5%,transparent);border-width:1.5px}
|
|
696
|
+
.zone.org{color:var(--org);background:color-mix(in srgb,var(--org) 5%,transparent)}
|
|
697
|
+
.zone.pub{color:var(--pub);border-style:dashed;background:repeating-linear-gradient(-45deg,transparent 0 7px,color-mix(in srgb,var(--pub) 9%,transparent) 7px 8px)}
|
|
698
|
+
.zone.lane{color:var(--muted);border-color:var(--hair);border-left:3px solid var(--agent);background:color-mix(in srgb,var(--card) 70%,transparent)}
|
|
699
|
+
.zone.lane::before{display:none}
|
|
700
|
+
.zone.lane span{top:7px;left:10px;background:none;padding:0;color:var(--ink2)}
|
|
701
|
+
|
|
702
|
+
.cn{width:100%;height:100%;background:var(--card);border:1px solid var(--rule);padding:6px 9px 7px;display:flex;flex-direction:column;gap:2px;overflow:hidden;transition:box-shadow .25s,border-color .25s}
|
|
703
|
+
.cn .meta{display:flex;justify-content:space-between;align-items:center;gap:6px;border-bottom:1px solid var(--hair);padding-bottom:3px;margin-bottom:2px;font-family:var(--mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;font-weight:600;color:var(--muted)}
|
|
704
|
+
.cn .meta .tag{display:inline-flex;align-items:center;gap:4px}
|
|
705
|
+
.cn .meta .tag::before{content:"";width:5px;height:5px;background:currentColor}
|
|
706
|
+
.cn b{font-size:13px;line-height:1.22;font-weight:700}
|
|
707
|
+
.cn .sub{font-size:11.5px;color:var(--ink2);line-height:1.32}
|
|
708
|
+
.cn.k-local .tag{color:var(--local)}
|
|
709
|
+
.cn.k-agent{border-color:var(--agent);box-shadow:inset 3px 0 0 var(--agent)}
|
|
710
|
+
.cn.k-agent .tag{color:var(--agent)}
|
|
711
|
+
.cn.k-org .tag{color:var(--org)}
|
|
712
|
+
.cn.k-pub .tag{color:var(--pub)}
|
|
713
|
+
.cn.k-user{background:var(--ink);border-color:var(--ink)}
|
|
714
|
+
.cn.k-user b,.cn.k-user .sub,.cn.k-user .meta{color:var(--bg)}
|
|
715
|
+
.cn.k-user .meta{border-color:color-mix(in srgb,var(--bg) 25%,transparent)}
|
|
716
|
+
.cn.k-user .tag{color:var(--bg)}
|
|
717
|
+
.cn .rows{margin-top:5px;border-top:1px solid var(--hair)}
|
|
718
|
+
.cn .rows div{display:flex;justify-content:space-between;align-items:center;gap:6px;padding:4px 0;border-bottom:1px solid var(--hair);font-size:10.5px;line-height:1.3;color:var(--ink2)}
|
|
719
|
+
.cn .rows em{font-style:normal;font-family:var(--mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:var(--org);white-space:nowrap;border:1px solid currentColor;padding:1px 4px}
|
|
720
|
+
.cn ul{list-style:none;margin:6px 0 0;padding:0;font-size:11.5px;color:var(--ink2);border-top:1px solid var(--hair)}
|
|
721
|
+
.cn ul li{padding:5px 0 5px 12px;border-bottom:1px solid var(--hair);position:relative;line-height:1.35}
|
|
722
|
+
.cn ul li::before{content:"";position:absolute;left:1px;top:11px;width:6px;height:1px;background:var(--org)}
|
|
723
|
+
.react-flow__node.hl .cn{border-color:var(--ink);box-shadow:0 0 0 1px var(--ink),0 0 0 5px color-mix(in srgb,var(--org) 22%,transparent)}
|
|
724
|
+
.react-flow__node.hl .cn.k-agent{box-shadow:inset 3px 0 0 var(--agent),0 0 0 1px var(--ink),0 0 0 5px color-mix(in srgb,var(--org) 22%,transparent)}
|
|
725
|
+
.react-flow__node:focus-visible .cn{outline:2px solid var(--focus);outline-offset:2px}
|
|
726
|
+
|
|
727
|
+
.st{width:100%;height:100%;background:var(--card);border:1px solid var(--rule);padding:6px 9px;display:flex;flex-direction:column;justify-content:center;gap:4px}
|
|
728
|
+
.st b{font-size:11px;line-height:1.25;font-weight:600}
|
|
729
|
+
.st .where{align-self:flex-start;font-size:8.5px}
|
|
730
|
+
.st.w-org{border-color:var(--org);box-shadow:inset 0 2px 0 var(--org)}
|
|
731
|
+
.st.end{border-color:var(--ink);box-shadow:3px 3px 0 var(--grid2)}
|
|
732
|
+
|
|
733
|
+
.react-flow__edge-path{stroke-width:1.5}
|
|
734
|
+
.react-flow__edge.e-local .react-flow__edge-path{stroke:var(--local)}
|
|
735
|
+
.react-flow__edge.e-org .react-flow__edge-path{stroke:var(--org);stroke-width:2}
|
|
736
|
+
.react-flow__edge.e-secret .react-flow__edge-path{stroke:var(--agent)}
|
|
737
|
+
.react-flow__edge.e-pub .react-flow__edge-path{stroke:var(--pub)}
|
|
738
|
+
.react-flow__edge.e-muted .react-flow__edge-path{stroke:var(--muted)}
|
|
739
|
+
.react-flow__edge .react-flow__edge-path{stroke-opacity:.55;stroke-width:1.6}
|
|
740
|
+
.flow-dots{fill:none;stroke-width:3.4;stroke-linecap:round;stroke-dasharray:0 8;animation:flowfwd 1.2s linear infinite;pointer-events:none}
|
|
741
|
+
.flow-dots.rev{stroke-width:2.4;opacity:.75;stroke-dashoffset:4;animation-name:flowrev}
|
|
742
|
+
@keyframes flowfwd{from{stroke-dashoffset:0}to{stroke-dashoffset:-16}}
|
|
743
|
+
@keyframes flowrev{from{stroke-dashoffset:4}to{stroke-dashoffset:20}}
|
|
744
|
+
.react-flow__edge.e-local .flow-dots{stroke:var(--local)}
|
|
745
|
+
.react-flow__edge.e-org .flow-dots{stroke:var(--org)}
|
|
746
|
+
.react-flow__edge.e-secret .flow-dots{stroke:var(--agent)}
|
|
747
|
+
.react-flow__edge.e-pub .flow-dots{stroke:var(--pub)}
|
|
748
|
+
.react-flow__edge.e-muted .flow-dots{stroke:var(--muted)}
|
|
749
|
+
.react-flow__edge.on .flow-dots{stroke-width:4.2;animation-duration:.7s}
|
|
750
|
+
.react-flow__edge.on .flow-dots.rev{stroke-width:3}
|
|
751
|
+
.react-flow__edge.off{opacity:.12}
|
|
752
|
+
.react-flow__edge.on .react-flow__edge-path{stroke-width:2.6;filter:drop-shadow(0 0 3px color-mix(in srgb,var(--org) 55%,transparent))}
|
|
753
|
+
.react-flow__edge.on.e-local .react-flow__edge-path{filter:drop-shadow(0 0 3px color-mix(in srgb,var(--local) 55%,transparent))}
|
|
754
|
+
.react-flow__edge.on.e-secret .react-flow__edge-path{filter:drop-shadow(0 0 3px color-mix(in srgb,var(--agent) 55%,transparent))}
|
|
755
|
+
.react-flow__edge.on.e-pub .react-flow__edge-path{filter:drop-shadow(0 0 3px color-mix(in srgb,var(--pub) 55%,transparent))}
|
|
756
|
+
.react-flow__edge-text{fill:var(--ink);font-size:10.5px;font-family:var(--mono);font-weight:600}
|
|
757
|
+
.react-flow__edge-textbg{fill:var(--canvas)}
|
|
758
|
+
.react-flow__edge.on .react-flow__edge-text{font-weight:700}
|
|
759
|
+
@media (prefers-reduced-motion:reduce){
|
|
760
|
+
.react-flow__edge.animated path{animation:none!important}
|
|
761
|
+
*,*::before,*::after{transition:none!important;animation:none!important}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* ============ data tables ============ */
|
|
765
|
+
.wrapc{max-width:1200px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
|
766
|
+
.seg{display:flex;align-items:flex-end;gap:0;border-bottom:1px solid var(--ink)}
|
|
767
|
+
.seg button{position:relative;background:none;border:1px solid transparent;border-bottom:0;padding:8px 14px 7px;cursor:pointer;color:var(--muted);font-weight:600;font-size:12px;display:flex;align-items:baseline;gap:8px;margin-bottom:-1px}
|
|
768
|
+
.seg button .mono{font-size:9.5px}
|
|
769
|
+
.seg button:hover{color:var(--ink)}
|
|
770
|
+
.seg button[aria-selected="true"]{color:var(--ink);background:var(--card);border-color:var(--ink);border-bottom:1px solid var(--card)}
|
|
771
|
+
.seg button:focus-visible{outline-offset:-3px}
|
|
772
|
+
.seg .count{margin-left:auto;padding:0 2px 7px;color:var(--muted)}
|
|
773
|
+
.tw{overflow-x:auto;border:1px solid var(--hair);border-top:0;background:var(--card)}
|
|
774
|
+
table{width:100%;border-collapse:collapse;font-size:12px;min-width:760px}
|
|
775
|
+
th,td{text-align:left;vertical-align:top;padding:8px 12px;border-top:1px solid var(--hair)}
|
|
776
|
+
thead th{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:600;border-top:0;border-bottom:1px solid var(--ink);background:var(--card);position:sticky;top:0;white-space:nowrap}
|
|
777
|
+
tbody tr:hover td{background:var(--bg)}
|
|
778
|
+
td.rn{font-family:var(--mono);font-size:10px;color:var(--muted);width:36px;padding-right:0}
|
|
779
|
+
td.nm{font-weight:600;color:var(--ink)}
|
|
780
|
+
td{color:var(--ink2)}
|
|
781
|
+
.tnote{display:flex;gap:8px;align-items:baseline;color:var(--muted);font-size:12px}
|
|
782
|
+
|
|
783
|
+
/* ============ controls ============ */
|
|
784
|
+
.grid2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}
|
|
785
|
+
@media (max-width:900px){.grid2{grid-template-columns:minmax(0,1fr)}}
|
|
786
|
+
.spec{background:var(--card);border:1px solid var(--hair);display:flex;flex-direction:column}
|
|
787
|
+
.spec>.hd{display:flex;align-items:baseline;gap:10px;padding:10px 14px 8px;border-bottom:1px solid var(--hair)}
|
|
788
|
+
.spec>.hd .mono{color:var(--org)}
|
|
789
|
+
.spec>.hd h3{font-family:var(--display);font-weight:700;font-size:15.5px;line-height:1.15}
|
|
790
|
+
.spec>.ds{padding:8px 14px 10px;color:var(--muted);font-size:12px}
|
|
791
|
+
.sheet{margin:0 14px 14px;border:1px solid var(--ink);background:var(--canvas)}
|
|
792
|
+
.sheet .file{display:flex;justify-content:space-between;gap:8px;padding:4px 10px;background:var(--ink);color:var(--bg);font-family:var(--mono);font-size:10px;letter-spacing:.06em}
|
|
793
|
+
.sheet pre{margin:0;padding:8px 0;overflow-x:auto;font-family:var(--codefont);font-size:11px;line-height:1.6;counter-reset:ln}
|
|
794
|
+
.sheet pre code{background:none;border:0;padding:0;font-size:inherit;white-space:pre;display:block}
|
|
795
|
+
.sheet .ln{display:block;padding:0 12px 0 0;counter-increment:ln}
|
|
796
|
+
.sheet .ln::before{content:counter(ln,decimal-leading-zero);display:inline-block;width:34px;padding-right:10px;margin-right:10px;text-align:right;color:var(--muted);border-right:1px solid var(--hair);opacity:.8}
|
|
797
|
+
.sheet .ln.cm{color:var(--muted);font-style:italic}
|
|
798
|
+
.sheet .ln.hi{background:color-mix(in srgb,var(--org) 10%,transparent)}
|
|
799
|
+
.plain{list-style:none;margin:0;padding:4px 14px 12px}
|
|
800
|
+
.plain li{display:grid;grid-template-columns:28px minmax(0,1fr);gap:4px;padding:7px 0;border-bottom:1px solid var(--hair);font-size:12.25px}
|
|
801
|
+
.plain li:last-child{border-bottom:0}
|
|
802
|
+
.plain li .n{font-family:var(--mono);font-size:10px;color:var(--muted);padding-top:2px}
|
|
803
|
+
.vchk{list-style:none;margin:0;padding:4px 14px 12px}
|
|
804
|
+
.vchk li{border-bottom:1px solid var(--hair)}
|
|
805
|
+
.vchk li:last-child{border-bottom:0}
|
|
806
|
+
.vchk label{display:grid;grid-template-columns:22px minmax(0,1fr);gap:6px;padding:8px 0;cursor:pointer;font-size:12.25px}
|
|
807
|
+
.vchk input{appearance:none;-webkit-appearance:none;margin:2px 0 0;width:15px;height:15px;border:1.5px solid var(--org);background:var(--card);display:grid;place-items:center;cursor:pointer;border-radius:0}
|
|
808
|
+
.vchk input:checked{background:var(--org)}
|
|
809
|
+
.vchk input:checked::after{content:"";width:7px;height:4px;border:solid var(--bg);border-width:0 0 2px 2px;transform:translate(0,-1px) rotate(-45deg)}
|
|
810
|
+
.vchk input:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
|
|
811
|
+
.vchk li.ck span{color:var(--muted);text-decoration:line-through;text-decoration-color:var(--rule)}
|
|
812
|
+
.prog{display:flex;align-items:center;gap:8px;margin-left:auto;color:var(--muted)}
|
|
813
|
+
.prog i{display:block;width:60px;height:4px;background:var(--hair);position:relative}
|
|
814
|
+
.prog i b{position:absolute;inset:0 auto 0 0;background:var(--org);transition:width .25s}
|
|
815
|
+
.notice{position:relative;border:1px solid var(--pub);background:var(--card);padding:0}
|
|
816
|
+
.notice>.hd{display:flex;align-items:center;gap:10px;padding:7px 14px;background:repeating-linear-gradient(-45deg,var(--pub-bg) 0 8px,transparent 8px 16px);border-bottom:1px solid var(--pub);color:var(--pub)}
|
|
817
|
+
.notice>.hd h3{font-family:var(--display);font-weight:700;font-size:15.5px;color:var(--ink)}
|
|
818
|
+
.notice ul{list-style:none;margin:0;padding:6px 14px 10px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 28px}
|
|
819
|
+
.notice li{padding:8px 0 8px 16px;border-bottom:1px solid var(--hair);font-size:12.25px;color:var(--ink2);position:relative}
|
|
820
|
+
.notice li::before{content:"!";position:absolute;left:0;top:8px;font-family:var(--mono);font-weight:700;font-size:11px;color:var(--pub)}
|
|
821
|
+
.notice li b{color:var(--ink)}
|
|
822
|
+
@media (max-width:900px){.notice ul{grid-template-columns:minmax(0,1fr)}}
|
|
823
|
+
|
|
824
|
+
/* ============ responsive shell ============ */
|
|
825
|
+
@media (max-width:1023px){
|
|
826
|
+
#app{grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto minmax(0,1fr)}
|
|
827
|
+
.rail{grid-row:2;border-right:0;border-bottom:1px solid var(--hair);overflow:visible}
|
|
828
|
+
.rail .cap,.rail .foot{display:none}
|
|
829
|
+
.chapters{flex-direction:row;overflow-x:auto;scrollbar-width:none;padding:0}
|
|
830
|
+
.chapters::-webkit-scrollbar{display:none}
|
|
831
|
+
.chapters button{flex:none;grid-template-columns:auto auto;gap:6px;border-top:0;border-right:1px solid var(--hair);padding:10px 14px}
|
|
832
|
+
.chapters button:last-child{border-bottom:0}
|
|
833
|
+
.chapters button .d{display:none}
|
|
834
|
+
.chapters button::before{left:0;right:0;top:auto;bottom:0;width:auto;height:3px;transform:scaleX(0);transform-origin:left}
|
|
835
|
+
.chapters button[aria-selected="true"]::before{transform:scaleX(1)}
|
|
836
|
+
.panel{grid-row:3;padding:12px 16px 18px}
|
|
837
|
+
.fill{flex:none}
|
|
838
|
+
.mast .sub{display:none}
|
|
839
|
+
}
|
|
840
|
+
@media (max-width:640px){
|
|
841
|
+
.mast{padding:0 12px;gap:10px;height:48px}
|
|
842
|
+
.mast .sep,.mark b{display:none}
|
|
843
|
+
.mast h1{font-size:18px}
|
|
844
|
+
.status .lt{display:none}
|
|
845
|
+
.status{font-size:10px}
|
|
846
|
+
.panel{padding:12px 12px 18px}
|
|
847
|
+
.chead h2{font-size:21px}
|
|
848
|
+
.seg .count{display:none}
|
|
849
|
+
.seg button{padding:8px 10px 7px;font-size:11.5px}
|
|
850
|
+
}
|
|
851
|
+
@media (max-width:420px){.status{display:none}}
|
|
852
|
+
</style>
|
|
853
|
+
</head>
|
|
854
|
+
<body>
|
|
855
|
+
<div id="app"><noscript><p class="noscript">This page needs JavaScript to draw its diagrams.</p></noscript></div>
|
|
856
|
+
|
|
857
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
|
|
858
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
|
|
859
|
+
<script src="https://unpkg.com/reactflow@11.11.4/dist/umd/index.js"></script>
|
|
860
|
+
<script>
|
|
861
|
+
(function () {
|
|
862
|
+
const { useState, useEffect, useMemo, useRef, useCallback, Fragment } = React;
|
|
863
|
+
const RF = window.ReactFlow;
|
|
864
|
+
const Flow = RF.default || RF.ReactFlow;
|
|
865
|
+
const { Handle, Position, Background, Controls, MarkerType, ReactFlowProvider } = RF;
|
|
866
|
+
const h = React.createElement;
|
|
867
|
+
const SIDE = { top: Position.Top, right: Position.Right, bottom: Position.Bottom, left: Position.Left };
|
|
868
|
+
|
|
869
|
+
/* ---------- theme-aware marker colours ---------- */
|
|
870
|
+
function useDark() {
|
|
871
|
+
const q = () => {
|
|
872
|
+
const t = document.documentElement.getAttribute("data-theme");
|
|
873
|
+
if (t) return t === "dark";
|
|
874
|
+
return window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches;
|
|
875
|
+
};
|
|
876
|
+
const [dark, setDark] = useState(q);
|
|
877
|
+
useEffect(() => {
|
|
878
|
+
const m = window.matchMedia && matchMedia("(prefers-color-scheme: dark)");
|
|
879
|
+
const on = () => setDark(q());
|
|
880
|
+
m && m.addEventListener && m.addEventListener("change", on);
|
|
881
|
+
const mo = new MutationObserver(on);
|
|
882
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
883
|
+
return () => { m && m.removeEventListener && m.removeEventListener("change", on); mo.disconnect(); };
|
|
884
|
+
}, []);
|
|
885
|
+
return dark;
|
|
886
|
+
}
|
|
887
|
+
const PALETTE = {
|
|
888
|
+
light: { local: "#0079C8", org: "#007A69", pub: "#A35700", secret: "#5A3DAE", muted: "#4F6079" },
|
|
889
|
+
dark: { local: "#4DB3F2", org: "#3FCFB0", pub: "#F2B25C", secret: "#B6A2FF", muted: "#8FA6BF" },
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
/* ---------- node types ---------- */
|
|
893
|
+
function handles(hs) {
|
|
894
|
+
return (hs || []).map(([type, side, pct, id]) =>
|
|
895
|
+
h(Handle, {
|
|
896
|
+
key: id, id, type: type === "s" ? "source" : "target", position: SIDE[side], isConnectable: false,
|
|
897
|
+
style: side === "left" || side === "right" ? { top: pct + "%" } : { left: pct + "%" },
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
const REF = { ws: "EXT·01", you: "USR·00", pub: "NET·01", repo: "LOC·01", docs: "KNW·02", kn: "KNW·01", orch: "AGT·01", store: "LOC·02", tw: "AGT·03", br: "AGT·02", llm: "ORG·01", aut: "ORG·02" };
|
|
901
|
+
function CardNode({ id, data }) {
|
|
902
|
+
return h("div", { className: "cn k-" + data.kind },
|
|
903
|
+
handles(data.hs),
|
|
904
|
+
h("div", { className: "meta" }, h("span", null, REF[id] || ""), data.tag ? h("span", { className: "tag" }, data.tag) : null),
|
|
905
|
+
h("b", null, data.title),
|
|
906
|
+
h("div", { className: "sub" }, data.sub),
|
|
907
|
+
data.rows ? h("div", { className: "rows" }, data.rows.map(([a, m]) => h("div", { key: a }, h("span", null, a), h("em", null, m)))) : null,
|
|
908
|
+
data.list ? h("ul", null, data.list.map((x) => h("li", { key: x }, x))) : null);
|
|
909
|
+
}
|
|
910
|
+
function ZoneNode({ data }) {
|
|
911
|
+
return h("div", { className: "zone " + data.kind }, h("span", null, data.label));
|
|
912
|
+
}
|
|
913
|
+
function StepNode({ data }) {
|
|
914
|
+
return h("div", { className: "st w-" + data.where + (data.end ? " end" : "") },
|
|
915
|
+
handles(data.hs),
|
|
916
|
+
h("b", null, data.title),
|
|
917
|
+
h("span", { className: "where " + data.where }, WHERE[data.where]));
|
|
918
|
+
}
|
|
919
|
+
const NODE_TYPES = { card: CardNode, zone: ZoneNode, step: StepNode };
|
|
920
|
+
const WHERE = { local: "On your machine", org: "Your LLM / your systems", pub: "Public download", block: "Blocked" };
|
|
921
|
+
|
|
922
|
+
const zone = (id, x, y, w, hgt, kind, label) => ({
|
|
923
|
+
id, type: "zone", position: { x, y }, data: { kind, label }, style: { width: w, height: hgt },
|
|
924
|
+
draggable: false, selectable: false, focusable: false, zIndex: -1,
|
|
925
|
+
});
|
|
926
|
+
const card = (id, x, y, kind, title, sub, tag, hs, extra) => ({
|
|
927
|
+
id, type: "card", position: { x, y }, data: Object.assign({ kind, title, sub, tag, hs }, extra || {}),
|
|
928
|
+
style: { width: 200, height: (extra && extra.height) || 96 }, draggable: false,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
/* ---------- architecture diagram ---------- */
|
|
932
|
+
const ARCH_NODES = [
|
|
933
|
+
zone("z-kn", -16, -325, 232, 277, "org", "Your product knowledge"),
|
|
934
|
+
zone("z-ws", -16, 6, 232, 146, "org", "Your work systems"),
|
|
935
|
+
zone("z-local", 284, -194, 552, 728, "local", "ForgeQE · runs on your machine or CI agent"),
|
|
936
|
+
zone("z-llm", 924, 6, 232, 370, "org", "Your organisation cloud"),
|
|
937
|
+
zone("z-aut", 924, 392, 232, 142, "org", "Your applications"),
|
|
938
|
+
zone("z-pub", -16, 392, 232, 142, "pub", "Public internet · setup only"),
|
|
939
|
+
card("kn", 0, -281, "org", "Product repos · Confluence · SharePoint", "Read-only, never changed", "READ ONLY",
|
|
940
|
+
[["t", "right", 50, "r"]],
|
|
941
|
+
{ height: 225, rows: [["Repos (cloned)", "Local folder"], ["Repos (hosted)", "API"], ["Confluence pages", "API"], ["SharePoint · wikis · specs", "RAG search"]] }),
|
|
942
|
+
card("ws", 0, 50, "org", "Azure DevOps · Jira", "Test cases, stories and bugs, reached with your own token", "YOUR SYSTEMS",
|
|
943
|
+
[["t", "right", 50, "r"]]),
|
|
944
|
+
card("you", 0, 240, "user", "You", "Ask in plain language, from a terminal or a CI pipeline", "START HERE",
|
|
945
|
+
[["s", "right", 50, "r"]]),
|
|
946
|
+
card("pub", 0, 430, "pub", "Component downloads", "Browser and runtime parts, once, checksum-verified. No data sent.", "DOWNLOAD ONLY",
|
|
947
|
+
[["s", "right", 50, "r"]]),
|
|
948
|
+
card("repo", 300, -150, "local", "Your test repository", "Generated tests are written here, in a separate local copy", "LOCAL",
|
|
949
|
+
[["t", "right", 50, "r"], ["s", "right", 50, "rs"]]),
|
|
950
|
+
card("docs", 300, 50, "local", "Project docs & skills", "Case files, specs and skill notes in your project folder", "KNOWLEDGE · LOCAL",
|
|
951
|
+
[["s", "bottom", 50, "b"]]),
|
|
952
|
+
card("orch", 300, 240, "agent", "Orchestrator agent", "Understands your request, plans, and calls ForgeQE's tools", "AGENT · LOCAL",
|
|
953
|
+
[["t", "left", 50, "l"], ["s", "left", 20, "lw"], ["s", "left", 6, "lk"], ["t", "top", 50, "t"], ["s", "right", 20, "r1"], ["s", "right", 50, "r2"], ["s", "right", 80, "r3"]]),
|
|
954
|
+
card("store", 300, 430, "local", "Local storage & secrets", "Page memory, reports, screenshots, logs, saved logins and keys", "LOCAL",
|
|
955
|
+
[["t", "left", 50, "l"], ["t", "right", 35, "rin"], ["s", "right", 75, "rout"]]),
|
|
956
|
+
card("tw", 620, 50, "agent", "Test-writing agent", "Turns a passed run into a test in your repo's own framework", "AGENT · LOCAL",
|
|
957
|
+
[["t", "left", 75, "l"], ["s", "left", 30, "lr"], ["s", "right", 50, "r"]]),
|
|
958
|
+
card("br", 620, 430, "agent", "Browser agent + Chrome", "Carries out the task in a real browser on this machine", "AGENT · LOCAL",
|
|
959
|
+
[["t", "left", 12, "l"], ["s", "left", 35, "lo"], ["t", "left", 75, "li"], ["s", "right", 20, "rl"], ["s", "right", 50, "ra"]]),
|
|
960
|
+
card("llm", 940, 50, "org", "Your LLM deployment", "For example Azure OpenAI in your tenant, behind your private endpoint", "YOUR CLOUD",
|
|
961
|
+
[["t", "left", 15.5, "l1"], ["t", "left", 76.8, "l2"], ["t", "left", 90, "l3"]],
|
|
962
|
+
{ height: 310, list: ["Your endpoint, key and region", "The only place prompts, page content and code excerpts are sent", "Nothing reaches a public AI service unless you configure one"] }),
|
|
963
|
+
card("aut", 940, 430, "org", "App under test", "Staging or production sites on your allow-list", "YOUR APPS",
|
|
964
|
+
[["t", "left", 50, "l"]]),
|
|
965
|
+
];
|
|
966
|
+
const INFO = {
|
|
967
|
+
you: ["You", "You describe what you want: a task to carry out in a browser, test cases to run, or a test to write. Everything starts here and the result comes back here."],
|
|
968
|
+
orch: ["Orchestrator agent", "Runs on your machine. It reads your request, asks your organisation's LLM how to handle it, and calls ForgeQE's tools to do the work. The tools are on a loopback-only connection protected by a one-time token, so nothing outside this machine can call them."],
|
|
969
|
+
tw: ["Test-writing agent", "Runs on your machine. It studies your repo in a separate working copy, writes the test with help from your LLM, and runs it locally until it passes. It cannot push to git."],
|
|
970
|
+
br: ["Browser agent + Chrome", "Runs on your machine and drives a local Chrome. At each step it sends the page's text and a low-detail screenshot to your LLM and gets back the next action. Saved logins are typed in locally. The model only ever sees placeholders."],
|
|
971
|
+
repo: ["Your test repository", "Where generated tests are written, in a separate git working copy on this machine. Tests stay local until you ask to push a branch, and the main branch is never pushed. If your tests live inside a product repo, only test files are written."],
|
|
972
|
+
store: ["Local storage & secrets", "Page memory (elements and locators), run reports and screenshots, redacted session logs, saved sign-in sessions and your keys. All on disk on this machine. ForgeQE never uploads them."],
|
|
973
|
+
pub: ["Component downloads", "On first use ForgeQE downloads the browser and runtime parts it needs, checks their checksums, and keeps them on this machine. Only downloads: nothing about you, your code or your runs is sent. Usage telemetry is switched off everywhere."],
|
|
974
|
+
kn: ["Your product knowledge", "Read-only context for the agents; ForgeQE never changes it. Product repos are read from a local clone, or from your own Azure DevOps or GitHub with your token, to understand the app and which areas a change touches. Confluence pages linked from your stories are read over HTTPS with your own token. For SharePoint, wikis and other specs, you point ForgeQE at your own knowledge search (RAG) service by URL or local command; ForgeQE sends a query and gets back only the relevant passages. Nothing is copied or indexed anywhere else."],
|
|
975
|
+
docs: ["Project docs & skills", "Test case files and specs in your project folder (YAML, JSON, CSV, Gherkin, plain text) and skill notes that teach the agents your app and conventions. Read on this machine; only the parts a task needs are sent to your LLM."],
|
|
976
|
+
ws: ["Your work systems", "Only the systems you list in the config. Test cases and stories are read over HTTPS with your own token, and results or bugs are written back only when you ask."],
|
|
977
|
+
llm: ["Your LLM deployment", "The single destination for model traffic: your request, page content, screenshots, test case text and code excerpts. Point the profile at your organisation's tenant, for example Azure OpenAI with a private endpoint, and the data stays in your cloud."],
|
|
978
|
+
aut: ["App under test", "The sites the browser agent visits. Set an allow-list so it can only open your own hosts."],
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const EDGE_DEFS = [
|
|
982
|
+
["you-orch", "you", "r", "orch", "l", "request", "local"],
|
|
983
|
+
["orch-ws", "orch", "lw", "ws", "r", "API: cases ⇄ results", "org", true],
|
|
984
|
+
["orch-kn", "orch", "lk", "kn", "r", "read: API · RAG · folder", "org", true],
|
|
985
|
+
["docs-orch", "docs", "b", "orch", "t", "local folder: specs, skills", "local"],
|
|
986
|
+
["orch-tw", "orch", "r1", "tw", "l", "passed run", "local"],
|
|
987
|
+
["orch-br", "orch", "r3", "br", "l", "start task", "local"],
|
|
988
|
+
["orch-llm", "orch", "r2", "llm", "l2", "request ⇄ plan", "org", true],
|
|
989
|
+
["tw-llm", "tw", "r", "llm", "l1", "code + steps ⇄ test", "org", true],
|
|
990
|
+
["br-llm", "br", "rl", "llm", "l3", "page ⇄ next action", "org", true],
|
|
991
|
+
["br-aut", "br", "ra", "aut", "l", "browses", "org", true],
|
|
992
|
+
["tw-repo", "tw", "lr", "repo", "r", "reads ⇄ writes", "local", true],
|
|
993
|
+
["br-store", "br", "lo", "store", "rin", "memory, reports", "local"],
|
|
994
|
+
["store-br", "store", "rout", "br", "li", "login values", "secret"],
|
|
995
|
+
["pub-store", "pub", "r", "store", "l", "download", "pub"],
|
|
996
|
+
];
|
|
997
|
+
|
|
998
|
+
const SCENARIOS = [
|
|
999
|
+
{ name: "Run a browser task", steps: [
|
|
1000
|
+
{ t: "You ask for a task", d: "For example: “Get a quote on our staging site for a family of four.”", e: ["you-orch"], n: ["you", "orch"], w: "local" },
|
|
1001
|
+
{ t: "The orchestrator plans with your LLM", d: "Your request and the list of available tools go to your organisation's model. Nothing else is sent.", e: ["orch-llm"], n: ["orch", "llm"], w: "org" },
|
|
1002
|
+
{ t: "It starts the browser agent", d: "Over a loopback-only connection on this machine, protected by a one-time token.", e: ["orch-br"], n: ["orch", "br"], w: "local" },
|
|
1003
|
+
{ t: "Chrome opens your site", d: "Only hosts on your allow-list can be opened.", e: ["br-aut"], n: ["br", "aut"], w: "org" },
|
|
1004
|
+
{ t: "Each step: page in, action out", d: "The page's text and a low-detail screenshot go to your LLM, which answers with the next click or entry. A task seen before is replayed from page memory with no model calls.", e: ["br-llm", "br-aut"], n: ["br", "llm", "aut"], w: "org" },
|
|
1005
|
+
{ t: "The run is saved locally", d: "Steps, screenshots and every element's locators go into page memory and the run report on disk.", e: ["br-store"], n: ["br", "store"], w: "local" },
|
|
1006
|
+
{ t: "You get the result", d: "The outcome comes back to your terminal. Nothing is uploaded.", e: ["orch-br", "you-orch"], n: ["you", "orch", "br"], w: "local" },
|
|
1007
|
+
] },
|
|
1008
|
+
{ name: "Write a test", steps: [
|
|
1009
|
+
{ t: "A passed run is handed over", d: "The orchestrator gives the test-writing agent the run's steps and the locators from page memory.", e: ["orch-tw"], n: ["orch", "tw"], w: "local" },
|
|
1010
|
+
{ t: "It studies your repo locally", d: "It reads your framework, page objects and helpers in a separate git working copy, so your own checkout is untouched.", e: ["tw-repo"], n: ["tw", "repo"], w: "local" },
|
|
1011
|
+
{ t: "It drafts the test with your LLM", d: "Relevant code excerpts and the run's steps go to your organisation's model, which returns the test code.", e: ["tw-llm"], n: ["tw", "llm"], w: "org" },
|
|
1012
|
+
{ t: "It runs the test locally", d: "Your own test command runs on this machine, without ForgeQE's keys in its environment. Failures go back to the model until the test passes.", e: ["tw-repo", "tw-llm"], n: ["tw", "repo", "llm"], w: "local" },
|
|
1013
|
+
{ t: "Nothing is pushed for you", d: "The agents can't run git push. A branch reaches your remote only when you ask, and never main, master or the default branch.", e: ["tw-repo"], n: ["repo"], w: "local" },
|
|
1014
|
+
] },
|
|
1015
|
+
{ name: "Run cases from ADO / Jira", steps: [
|
|
1016
|
+
{ t: "You name the cases", d: "For example: “Run SHOP-12 and SHOP-14, two browsers at once.”", e: ["you-orch"], n: ["you", "orch"], w: "local" },
|
|
1017
|
+
{ t: "Cases are fetched from your system", d: "Over HTTPS with your own token, which stays in your environment and out of prompts and logs.", e: ["orch-ws"], n: ["orch", "ws"], w: "org" },
|
|
1018
|
+
{ t: "Your LLM plans the run", d: "The case text goes to your organisation's model.", e: ["orch-llm"], n: ["orch", "llm"], w: "org" },
|
|
1019
|
+
{ t: "Each case runs in the browser", d: "Exactly as in “Run a browser task”: page data to your LLM, actions to your app, results to local storage.", e: ["orch-br", "br-aut", "br-llm", "br-store"], n: ["br", "aut", "llm", "store"], w: "org" },
|
|
1020
|
+
{ t: "Results go back only if you ask", d: "Results or bugs are written to your own system on request.", e: ["orch-ws"], n: ["ws"], w: "org" },
|
|
1021
|
+
] },
|
|
1022
|
+
{ name: "Use product knowledge", steps: [
|
|
1023
|
+
{ t: "Project docs are read locally", d: "Case files, specs and skill notes in your project folder are read on this machine.", e: ["docs-orch"], n: ["docs", "orch"], w: "local" },
|
|
1024
|
+
{ t: "Confluence and your knowledge search", d: "Confluence pages linked from a story are read with your own token. SharePoint, wikis and specs are reached only through a knowledge search service you run; ForgeQE asks it for relevant passages.", e: ["orch-kn"], n: ["orch", "kn"], w: "org" },
|
|
1025
|
+
{ t: "Only relevant passages go to your LLM", d: "The passages a task needs are sent to your organisation's model as context. Nothing is copied or indexed elsewhere.", e: ["orch-llm"], n: ["orch", "llm"], w: "org" },
|
|
1026
|
+
{ t: "Product repos are read, never changed", d: "Product code is read as knowledge, from a local clone or from your own Azure DevOps or GitHub with your token. ForgeQE never changes a product repo, and only the excerpts a task needs go to your LLM.", e: ["orch-kn", "orch-llm"], n: ["orch", "kn", "llm"], w: "org" },
|
|
1027
|
+
{ t: "What the agents learn stays local", d: "Notes about your repo and page memory are kept in local storage on this machine.", e: [], n: ["store"], w: "local" },
|
|
1028
|
+
] },
|
|
1029
|
+
{ name: "Sign in safely", steps: [
|
|
1030
|
+
{ t: "Login values stay on disk", d: "Usernames, passwords and authenticator secrets are read from your environment or saved sessions on this machine.", e: ["store-br"], n: ["store", "br"], w: "local" },
|
|
1031
|
+
{ t: "The model sees placeholders only", d: "Your LLM is told to type <secret>shop_password</secret>, never the real value.", e: ["br-llm"], n: ["br", "llm"], w: "org" },
|
|
1032
|
+
{ t: "Chrome types the real value", d: "The browser agent fills in the real value directly on the page, and only on the login's own domains.", e: ["br-aut"], n: ["br", "aut"], w: "org" },
|
|
1033
|
+
{ t: "The session is reused", d: "The signed-in session is saved locally, so later runs skip the login. Logs redact any value that looks like a secret.", e: ["br-store"], n: ["store"], w: "local" },
|
|
1034
|
+
] },
|
|
1035
|
+
{ name: "First-time setup", steps: [
|
|
1036
|
+
{ t: "Components are downloaded once", d: "The browser and runtime parts are fetched from public release sites, checksum-checked and kept on this machine. Only downloads: nothing is uploaded.", e: ["pub-store"], n: ["pub", "store"], w: "pub" },
|
|
1037
|
+
{ t: "Telemetry is switched off", d: "Usage analytics, cloud sync, share links and trace export are forced off in every process ForgeQE starts, and your environment can't turn them back on.", e: [], n: ["store", "orch", "tw", "br"], w: "block" },
|
|
1038
|
+
] },
|
|
1039
|
+
];
|
|
1040
|
+
|
|
1041
|
+
/* ---------- agents diagram ---------- */
|
|
1042
|
+
const step = (id, x, y, title, where, hs, end) => ({
|
|
1043
|
+
id, type: "step", position: { x, y }, data: { title, where, hs, end }, style: { width: 160, height: 58 }, draggable: false,
|
|
1044
|
+
});
|
|
1045
|
+
const LR = [["t", "left", 50, "l"], ["s", "right", 50, "r"], ["s", "bottom", 50, "bs"], ["t", "bottom", 50, "bt"], ["s", "top", 50, "ts"], ["t", "top", 50, "tt"]];
|
|
1046
|
+
const AGENT_NODES = [
|
|
1047
|
+
zone("lane1", -14, -10, 1144, 170, "lane", "Orchestrator agent · started by your request"),
|
|
1048
|
+
zone("lane2", -14, 175, 1144, 180, "lane", "Browser agent · started by the orchestrator for a browser task"),
|
|
1049
|
+
zone("lane3", -14, 370, 1144, 170, "lane", "Test-writing agent · started after a run passes"),
|
|
1050
|
+
step("o1", 10, 40, "Read your request", "local", LR),
|
|
1051
|
+
step("o2", 200, 40, "Plan with your LLM", "org", LR),
|
|
1052
|
+
step("o3", 390, 40, "Call a ForgeQE tool", "local", LR),
|
|
1053
|
+
step("o4", 580, 40, "Check the result", "local", LR),
|
|
1054
|
+
step("o5", 960, 40, "Reply to you", "local", LR, true),
|
|
1055
|
+
step("b1", 10, 245, "Seen this task before?", "local", LR),
|
|
1056
|
+
step("b2", 200, 245, "Read the page", "local", LR),
|
|
1057
|
+
step("b3", 390, 245, "Ask your LLM for the next action", "org", LR),
|
|
1058
|
+
step("b4", 580, 245, "Act in Chrome", "org", LR),
|
|
1059
|
+
step("b5", 770, 245, "Check what changed", "local", LR),
|
|
1060
|
+
step("b6", 960, 245, "Save run & page memory", "local", LR, true),
|
|
1061
|
+
step("t1", 10, 420, "Study your repo & the run", "local", LR),
|
|
1062
|
+
step("t2", 200, 420, "Draft the test with your LLM", "org", LR),
|
|
1063
|
+
step("t3", 390, 420, "Run the test locally", "local", LR),
|
|
1064
|
+
step("t4", 580, 420, "Fix the failure with your LLM", "org", LR),
|
|
1065
|
+
step("t5", 960, 420, "Test passes · stays local", "local", LR, true),
|
|
1066
|
+
];
|
|
1067
|
+
const AGENT_EDGES = [
|
|
1068
|
+
["o1", "r", "o2", "l", ""], ["o2", "r", "o3", "l", ""], ["o3", "r", "o4", "l", ""], ["o4", "r", "o5", "l", "done"],
|
|
1069
|
+
["o4", "bs", "o2", "bt", "needs another step", "muted"],
|
|
1070
|
+
["b1", "r", "b2", "l", "new"], ["b2", "r", "b3", "l", ""], ["b3", "r", "b4", "l", ""], ["b4", "r", "b5", "l", ""], ["b5", "r", "b6", "l", "done"],
|
|
1071
|
+
["b5", "bs", "b2", "bt", "next step", "muted"],
|
|
1072
|
+
["b1", "ts", "b4", "tt", "seen before: replay from page memory, no model calls", "local"],
|
|
1073
|
+
["t1", "r", "t2", "l", ""], ["t2", "r", "t3", "l", ""], ["t3", "r", "t4", "l", "fails"],
|
|
1074
|
+
["t4", "bs", "t3", "bt", "retry", "muted"],
|
|
1075
|
+
["t3", "ts", "t5", "tt", "passes", "local"],
|
|
1076
|
+
];
|
|
1077
|
+
|
|
1078
|
+
/* ---------- moving-dot edge: dots travel source → target; two-way links also carry a return stream ---------- */
|
|
1079
|
+
function FlowEdge(p) {
|
|
1080
|
+
const dy = (p.data && p.data.dy) || 0;
|
|
1081
|
+
const [path, lx, ly] = RF.getSmoothStepPath({
|
|
1082
|
+
sourceX: p.sourceX, sourceY: p.sourceY, sourcePosition: p.sourcePosition,
|
|
1083
|
+
targetX: p.targetX, targetY: p.targetY, targetPosition: p.targetPosition,
|
|
1084
|
+
centerX: p.data && p.data.cx,
|
|
1085
|
+
centerY: p.data && p.data.cy,
|
|
1086
|
+
});
|
|
1087
|
+
return h(Fragment, null,
|
|
1088
|
+
h("path", { d: path, className: "flow-dots fwd" }),
|
|
1089
|
+
p.data && p.data.bi ? h("path", { d: path, className: "flow-dots rev" }) : null,
|
|
1090
|
+
h(RF.BaseEdge, {
|
|
1091
|
+
id: p.id, path, labelX: lx, labelY: ly + dy, label: p.label, labelStyle: p.labelStyle, labelShowBg: true,
|
|
1092
|
+
labelBgPadding: p.labelBgPadding, labelBgBorderRadius: p.labelBgBorderRadius,
|
|
1093
|
+
markerEnd: p.markerEnd, markerStart: p.markerStart, interactionWidth: 12,
|
|
1094
|
+
}));
|
|
1095
|
+
}
|
|
1096
|
+
const EDGE_TYPES = { flow: FlowEdge };
|
|
1097
|
+
|
|
1098
|
+
/* ---------- flow canvas ---------- */
|
|
1099
|
+
function FitCanvas(props) {
|
|
1100
|
+
const ref = useRef(null);
|
|
1101
|
+
const inst = useRef(null);
|
|
1102
|
+
const fit = useCallback(() => inst.current && inst.current.fitView({ padding: 0.05 }), []);
|
|
1103
|
+
useEffect(() => {
|
|
1104
|
+
if (!ref.current || !window.ResizeObserver) return;
|
|
1105
|
+
const ro = new ResizeObserver(() => requestAnimationFrame(fit));
|
|
1106
|
+
ro.observe(ref.current);
|
|
1107
|
+
return () => ro.disconnect();
|
|
1108
|
+
}, [fit]);
|
|
1109
|
+
return h("div", { ref, style: { width: "100%", height: "100%" } },
|
|
1110
|
+
h(Flow, {
|
|
1111
|
+
nodes: props.nodes, edges: props.edges, nodeTypes: NODE_TYPES, edgeTypes: EDGE_TYPES, fitView: true, fitViewOptions: { padding: 0.05 },
|
|
1112
|
+
onInit: (i) => { inst.current = i; setTimeout(fit, 30); }, minZoom: 0.2, maxZoom: 2.5,
|
|
1113
|
+
nodesDraggable: !!props.onNodesChange, onNodesChange: props.onNodesChange, autoPanOnNodeDrag: false, nodesConnectable: false, elementsSelectable: true, zoomOnScroll: false, panOnScroll: false,
|
|
1114
|
+
zoomOnDoubleClick: false, preventScrolling: false, onNodeClick: props.onNodeClick, onPaneClick: props.onPaneClick,
|
|
1115
|
+
},
|
|
1116
|
+
props.plain ? null : h(Background, { id: "minor", variant: "lines", gap: 20, lineWidth: 1 }),
|
|
1117
|
+
props.plain ? null : h(Background, { id: "major", className: "major", variant: "lines", gap: 100, lineWidth: 1 }),
|
|
1118
|
+
h(Controls, { showInteractive: false, position: "bottom-right" })));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const LANES = { "orch-kn": 232, "orch-ws": 266 };
|
|
1122
|
+
const LABEL_DY = { "tw-llm": -12 };
|
|
1123
|
+
function buildEdges(defs, pal, active, moved) {
|
|
1124
|
+
return defs.map(([id, s, sh, t, th, label, kind, bi]) => {
|
|
1125
|
+
const on = active ? active.has(id) : false;
|
|
1126
|
+
const col = pal[kind] || pal.local;
|
|
1127
|
+
const mk = { type: MarkerType.ArrowClosed, color: col, width: 17, height: 17 };
|
|
1128
|
+
return {
|
|
1129
|
+
id, source: s, sourceHandle: sh, target: t, targetHandle: th, type: "flow", label, data: { bi: !!bi, cx: moved ? undefined : LANES[id], dy: LABEL_DY[id] },
|
|
1130
|
+
labelBgPadding: [4, 2], labelBgBorderRadius: 0, markerEnd: mk, markerStart: bi ? mk : undefined,
|
|
1131
|
+
className: "e-" + kind + (active ? (on ? " on" : " off") : ""), zIndex: on ? 10 : 0,
|
|
1132
|
+
};
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function ChapterHead({ n, title, desc }) {
|
|
1137
|
+
return h("header", { className: "chead" },
|
|
1138
|
+
h("span", { className: "mono" }, "Ch. " + n),
|
|
1139
|
+
h("h2", null, title),
|
|
1140
|
+
desc ? h("p", null, desc) : null);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const KEY = [["local", "Stays on your machine"], ["org", "Goes to your organisation"], ["secret", "Secret, typed locally"], ["pub", "Download only"]];
|
|
1144
|
+
function MapKey({ pal, items }) {
|
|
1145
|
+
return h("div", { className: "key", "aria-label": "Map key" }, (items || KEY).map(([k, l]) =>
|
|
1146
|
+
h("span", { key: k }, h("i", { style: { borderTopColor: pal[k] } }), l)));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/* ---------- draggable cards: each one stays inside its own boundary box ---------- */
|
|
1150
|
+
/* Boxes drag with their cards; each card is a child of its box and can't leave it. Cards outside any box move freely. */
|
|
1151
|
+
function withExtents(list) {
|
|
1152
|
+
const zones = list.filter((n) => n.type === "zone");
|
|
1153
|
+
return list.map((n) => {
|
|
1154
|
+
const size = { width: n.style.width, height: n.style.height };
|
|
1155
|
+
if (n.type === "zone") return Object.assign({}, n, size, { draggable: true });
|
|
1156
|
+
const cx = n.position.x + n.style.width / 2, cy = n.position.y + n.style.height / 2;
|
|
1157
|
+
const z = zones.find((z) => cx > z.position.x && cx < z.position.x + z.style.width && cy > z.position.y && cy < z.position.y + z.style.height);
|
|
1158
|
+
if (!z) return Object.assign({}, n, size, { draggable: true });
|
|
1159
|
+
return Object.assign({}, n, size, {
|
|
1160
|
+
draggable: true, parentNode: z.id,
|
|
1161
|
+
position: { x: n.position.x - z.position.x, y: n.position.y - z.position.y },
|
|
1162
|
+
// React Flow ignores the card's own size for box-relative limits, so subtract it here.
|
|
1163
|
+
extent: [[10, 26], [z.style.width - 10 - n.style.width, z.style.height - 8 - n.style.height]],
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const DRAG_ARCH = withExtents(ARCH_NODES);
|
|
1168
|
+
const DRAG_AGENTS = withExtents(AGENT_NODES);
|
|
1169
|
+
function useDragLayout() {
|
|
1170
|
+
const [pos, setPos] = useState({});
|
|
1171
|
+
const onNodesChange = useCallback((changes) => setPos((p) => {
|
|
1172
|
+
let next = p;
|
|
1173
|
+
for (const c of changes) if (c.type === "position" && c.position) next = Object.assign({}, next, { [c.id]: c.position });
|
|
1174
|
+
return next;
|
|
1175
|
+
}), []);
|
|
1176
|
+
return [pos, onNodesChange, () => setPos({})];
|
|
1177
|
+
}
|
|
1178
|
+
const place = (n, pos) => (pos[n.id] ? Object.assign({}, n, { position: pos[n.id] }) : n);
|
|
1179
|
+
const resetBtn = (pos, reset) => (Object.keys(pos).length ? h("button", { className: "btn reset", onClick: reset }, "Reset layout") : null);
|
|
1180
|
+
|
|
1181
|
+
function Architecture() {
|
|
1182
|
+
const dark = useDark();
|
|
1183
|
+
const pal = dark ? PALETTE.dark : PALETTE.light;
|
|
1184
|
+
const [sc, setSc] = useState(0);
|
|
1185
|
+
const [st, setSt] = useState(-1);
|
|
1186
|
+
const [play, setPlay] = useState(false);
|
|
1187
|
+
const [info, setInfo] = useState(null);
|
|
1188
|
+
const [pos, onNodesChange, resetLayout] = useDragLayout();
|
|
1189
|
+
const scenario = SCENARIOS[sc];
|
|
1190
|
+
const cur = st >= 0 ? scenario.steps[st] : null;
|
|
1191
|
+
|
|
1192
|
+
useEffect(() => {
|
|
1193
|
+
if (!play) return;
|
|
1194
|
+
const id = setInterval(() => setSt((s) => {
|
|
1195
|
+
if (s + 1 >= scenario.steps.length) { setPlay(false); return s; }
|
|
1196
|
+
return s + 1;
|
|
1197
|
+
}), 2600);
|
|
1198
|
+
return () => clearInterval(id);
|
|
1199
|
+
}, [play, scenario]);
|
|
1200
|
+
|
|
1201
|
+
const activeE = cur ? new Set(cur.e) : null;
|
|
1202
|
+
const activeN = cur ? new Set(cur.n) : null;
|
|
1203
|
+
const nodes = useMemo(() => DRAG_ARCH.map((n) => n.type === "zone" ? place(n, pos) :
|
|
1204
|
+
Object.assign({}, place(n, pos), { className: activeN ? (activeN.has(n.id) ? "hl" : "dim") : "" })), [st, sc, pos]);
|
|
1205
|
+
const edges = useMemo(() => buildEdges(EDGE_DEFS, pal, activeE, Object.keys(pos).length > 0), [st, sc, dark, pos]);
|
|
1206
|
+
|
|
1207
|
+
const pick = (i) => { setSc(i); setSt(0); setPlay(false); setInfo(null); };
|
|
1208
|
+
|
|
1209
|
+
return h(Fragment, null,
|
|
1210
|
+
h(ChapterHead, { n: "02", title: "Architecture & data flow", desc: "The machine's perimeter, and every hop data makes across it." }),
|
|
1211
|
+
h("div", { className: "split fill" },
|
|
1212
|
+
h("figure", { className: "fig", style: { margin: 0 } },
|
|
1213
|
+
h("header", null,
|
|
1214
|
+
h("span", { className: "mono" }, "Fig. 02 · System perimeter"),
|
|
1215
|
+
h(MapKey, { pal })),
|
|
1216
|
+
h("div", { className: "canvas" },
|
|
1217
|
+
h(ReactFlowProvider, null, h(FitCanvas, {
|
|
1218
|
+
nodes, edges, plain: true, onNodesChange,
|
|
1219
|
+
onNodeClick: (_, n) => INFO[n.id] && setInfo([REF[n.id]].concat(INFO[n.id])),
|
|
1220
|
+
onPaneClick: () => setInfo(null),
|
|
1221
|
+
})),
|
|
1222
|
+
resetBtn(pos, resetLayout),
|
|
1223
|
+
info ? h("div", { className: "info", role: "status" },
|
|
1224
|
+
h("button", { onClick: () => setInfo(null), "aria-label": "Close" }, "×"),
|
|
1225
|
+
h("span", { className: "mono" }, info[0]),
|
|
1226
|
+
h("b", null, info[1]), h("span", { className: "muted" }, info[2])) : null)),
|
|
1227
|
+
h("aside", { className: "side" },
|
|
1228
|
+
h("section", { className: "sec" },
|
|
1229
|
+
h("span", { className: "mono" }, "Trace"),
|
|
1230
|
+
h("h3", null, "Follow the data"),
|
|
1231
|
+
h("p", { className: "hintline" }, "Pick a scenario, then step through it. The diagram highlights each hop. Click any box for details, or drag it to rearrange."),
|
|
1232
|
+
h("ol", { className: "scn" }, SCENARIOS.map((s, i) =>
|
|
1233
|
+
h("li", { key: s.name },
|
|
1234
|
+
h("button", { "aria-pressed": i === sc && st >= 0, onClick: () => pick(i) },
|
|
1235
|
+
h("span", { className: "n" }, "S" + (i + 1)),
|
|
1236
|
+
h("span", null, s.name),
|
|
1237
|
+
h("span", { className: "c" }, s.steps.length + " steps"))))),
|
|
1238
|
+
h("div", { className: "ctrl" },
|
|
1239
|
+
h("button", { className: "btn", onClick: () => { setPlay(false); setSt((s) => Math.max(0, s - 1)); } }, "‹ Back"),
|
|
1240
|
+
h("button", { className: "btn primary", onClick: () => { if (st >= scenario.steps.length - 1 || st < 0) setSt(0); setPlay((p) => !p); } }, play ? "Pause" : "▶ Play"),
|
|
1241
|
+
h("button", { className: "btn", onClick: () => { setPlay(false); setSt((s) => Math.min(scenario.steps.length - 1, s + 1)); } }, "Next ›"),
|
|
1242
|
+
h("button", { className: "btn", onClick: () => { setPlay(false); setSt(-1); } }, "Show all"))),
|
|
1243
|
+
h("section", { className: "sec" },
|
|
1244
|
+
h("span", { className: "mono" }, "S" + (sc + 1) + " · " + (st >= 0 ? "Step " + (st + 1) + " of " + scenario.steps.length : scenario.steps.length + " steps")),
|
|
1245
|
+
h("h3", null, scenario.name),
|
|
1246
|
+
h("ol", { className: "steps" }, scenario.steps.map((s, i) =>
|
|
1247
|
+
h("li", { key: i, className: i === st ? "on" : i < st ? "done" : "", onClick: () => { setPlay(false); setSt(i); } },
|
|
1248
|
+
h("span", { className: "dot" }, String(i + 1).padStart(2, "0")),
|
|
1249
|
+
h("div", null, h("b", null, s.t), h("p", null, s.d), h("span", { className: "where " + s.w }, WHERE[s.w])))))))));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function Agents() {
|
|
1253
|
+
const dark = useDark();
|
|
1254
|
+
const pal = dark ? PALETTE.dark : PALETTE.light;
|
|
1255
|
+
const [pos, onNodesChange, resetLayout] = useDragLayout();
|
|
1256
|
+
const agentNodes = useMemo(() => DRAG_AGENTS.map((n) => place(n, pos)), [pos]);
|
|
1257
|
+
const edges = useMemo(() => AGENT_EDGES.map(([s, sh, t, th, label, kind], i) => {
|
|
1258
|
+
const k = kind || "local";
|
|
1259
|
+
const col = pal[k];
|
|
1260
|
+
return {
|
|
1261
|
+
id: "a" + i, source: s, sourceHandle: sh, target: t, targetHandle: th, type: "flow", label,
|
|
1262
|
+
labelBgPadding: [4, 2], labelBgBorderRadius: 0, className: "e-" + k,
|
|
1263
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: col, width: 17, height: 17 },
|
|
1264
|
+
};
|
|
1265
|
+
}), [dark]);
|
|
1266
|
+
const note = (n, title, items) => h("section", { className: "anote" },
|
|
1267
|
+
h("header", null, h("span", { className: "mono" }, n), h("h4", null, title)),
|
|
1268
|
+
h("ul", null, items.map((x) => h("li", { key: x }, x))));
|
|
1269
|
+
return h(Fragment, null,
|
|
1270
|
+
h(ChapterHead, { n: "03", title: "How the agents work", desc: "Three local agents, drawn as swimlanes. Each step is stamped with where its data goes." }),
|
|
1271
|
+
h("div", { className: "split fill" },
|
|
1272
|
+
h("figure", { className: "fig", style: { margin: 0 } },
|
|
1273
|
+
h("header", null,
|
|
1274
|
+
h("span", { className: "mono" }, "Fig. 03 · Agent swimlanes"),
|
|
1275
|
+
h("div", { className: "key" },
|
|
1276
|
+
h("span", null, h("span", { className: "where local" }, "On your machine")),
|
|
1277
|
+
h("span", null, h("span", { className: "where org" }, "Your LLM / your systems")),
|
|
1278
|
+
h("span", null, h("i", { style: { borderTopColor: pal.muted, borderTopStyle: "dotted" } }), "Loop back"))),
|
|
1279
|
+
h("div", { className: "canvas" },
|
|
1280
|
+
h(ReactFlowProvider, null, h(FitCanvas, { nodes: agentNodes, edges, plain: true, onNodesChange })),
|
|
1281
|
+
resetBtn(pos, resetLayout))),
|
|
1282
|
+
h("aside", { className: "side" },
|
|
1283
|
+
h("section", { className: "sec" },
|
|
1284
|
+
h("span", { className: "mono" }, "Summary"),
|
|
1285
|
+
h("h3", null, "How the agents work"),
|
|
1286
|
+
h("p", { className: "muted", style: { fontSize: "12.25px" } },
|
|
1287
|
+
"Three agents run on your machine. None of them hosts a model: each one thinks by sending a request to your organisation's LLM, then acts locally. Green steps are the only moments data leaves the machine.")),
|
|
1288
|
+
note("AGT·01", "Orchestrator agent", [
|
|
1289
|
+
"Talks to ForgeQE's tools only over a loopback connection with a one-time token",
|
|
1290
|
+
"Reaches your work systems only for the ones you configure",
|
|
1291
|
+
"Cannot push to git"]),
|
|
1292
|
+
note("AGT·02", "Browser agent", [
|
|
1293
|
+
"Opens only hosts on your allow-list",
|
|
1294
|
+
"Sees logins as placeholders; the real values are typed locally",
|
|
1295
|
+
"Replays known tasks from page memory without calling the model"]),
|
|
1296
|
+
note("AGT·03", "Test-writing agent", [
|
|
1297
|
+
"Works in a separate local copy of your repo",
|
|
1298
|
+
"Runs your test command without ForgeQE's keys in its environment",
|
|
1299
|
+
"Never deletes, skips or loosens tests, and cannot push"]))));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/* ---------- static tabs ---------- */
|
|
1303
|
+
const CHECK = h("svg", { viewBox: "0 0 12 12", "aria-hidden": "true" }, h("path", { d: "M2 6.4 4.8 9 10 3", fill: "none", stroke: "currentColor", strokeWidth: 1.8 }));
|
|
1304
|
+
function Overview({ go }) {
|
|
1305
|
+
const promises = [
|
|
1306
|
+
"No ForgeQE servers: there is no ForgeQE cloud for data to go to",
|
|
1307
|
+
"Model calls go only to the endpoint you configure",
|
|
1308
|
+
"Passwords and keys are never sent to the model",
|
|
1309
|
+
"Your repo is edited locally and never pushed for you",
|
|
1310
|
+
"Usage telemetry is switched off everywhere and can't be turned back on",
|
|
1311
|
+
];
|
|
1312
|
+
const band = (cls, lbl, title, desc, items, inner) => h("div", { className: "band ticks " + cls },
|
|
1313
|
+
h("span", { className: "lbl" }, lbl),
|
|
1314
|
+
h("div", { className: "row" },
|
|
1315
|
+
h("div", null, h("b", null, title), h("p", null, desc)),
|
|
1316
|
+
h("ul", null, items.map((x) => h("li", { key: x }, x)))),
|
|
1317
|
+
inner || null);
|
|
1318
|
+
return h("div", { className: "ov" },
|
|
1319
|
+
h("section", { className: "cover enter" },
|
|
1320
|
+
h("div", null,
|
|
1321
|
+
h("div", { className: "kick mono" }, "Ch. 01 · Overview"),
|
|
1322
|
+
h("h2", null, "Your data stays on your machine ", h("em", null, "and in your cloud")),
|
|
1323
|
+
h("p", { className: "lead" }, "ForgeQE runs entirely on your computer or CI agent. Its agents think by calling the LLM ", h("b", null, "your organisation deploys"), ", such as Azure OpenAI in your own tenant. Your repo, your credentials and everything ForgeQE records stay on disk where they are."),
|
|
1324
|
+
h("ul", { className: "jump" },
|
|
1325
|
+
[["02", "See the data flow", "architecture"], ["03", "How the agents work", "agents"], ["05", "Lock it down", "controls"]].map(([n, l, k]) =>
|
|
1326
|
+
h("li", { key: k }, h("button", { onClick: () => go(k) },
|
|
1327
|
+
h("span", { className: "n" }, n), h("span", { className: "t" }, l), h("span", { className: "a", "aria-hidden": "true" }, "⟶")))))),
|
|
1328
|
+
h("div", { className: "ledger ticks" },
|
|
1329
|
+
h("header", { className: "mono" }, h("b", null, "Five commitments"), h("span", null, "Status")),
|
|
1330
|
+
h("ol", null, promises.map((p, i) =>
|
|
1331
|
+
h("li", { key: p },
|
|
1332
|
+
h("span", { className: "n" }, "P·" + String(i + 1).padStart(2, "0")),
|
|
1333
|
+
h("span", null, p),
|
|
1334
|
+
h("span", { className: "ok", title: "Holds by design" }, CHECK)))))),
|
|
1335
|
+
h("section", { className: "perim enter" },
|
|
1336
|
+
h("div", { className: "phead" }, h("span", { className: "mono" }, "Fig. 01 · Where data lives, from the outside in")),
|
|
1337
|
+
band("p", "Zone C · Public internet: setup only", "Public internet: setup only", "Downloads only. No prompts, code or run data.",
|
|
1338
|
+
["Browser and runtime parts, once", "A public model price list, daily (can be turned off)", "Updates, only when you run them"],
|
|
1339
|
+
band("o", "Zone B · Your organisation", "Goes to your organisation", "Only to systems you own and configure.",
|
|
1340
|
+
["Prompts, page content, screenshots and code excerpts → your LLM", "Test case queries and results → your ADO or Jira", "Browser traffic → your apps under test"],
|
|
1341
|
+
band("l", "Zone A · Your machine", "Stays on your machine", "Never uploaded by ForgeQE.",
|
|
1342
|
+
["Your code repository", "Page memory, reports and screenshots", "Saved logins, API keys and tokens", "Redacted session logs"])))));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const DATA_ROWS = [
|
|
1346
|
+
["Your request text", "Orchestrator → your LLM", ["org", "Your LLM only"], "Active profile in `llms.json`"],
|
|
1347
|
+
["Page content and step screenshots", "Browser agent → your LLM, each step", ["org", "Your LLM only"], "`browser.vision_detail: low` · `browser.allowed_hosts`"],
|
|
1348
|
+
["Repo source code", "Test-writing agent reads it locally and sends excerpts to your LLM", ["org", "Your LLM only"], "`--repo` / `code.repo` · `--no-code` turns test writing off"],
|
|
1349
|
+
["Test cases, stories, bugs", "Your ADO or Jira ⇄ this machine ⇄ your LLM", ["org", "Your systems + LLM"], "`sources:` in `forgeqe.yaml` · scoped tokens"],
|
|
1350
|
+
["Login usernames, passwords, one-time codes", "Typed into the page locally; the model sees a placeholder", ["local", "Never to the LLM"], "Scoped to each login's own domains"],
|
|
1351
|
+
["API keys and tokens", "Your environment → only the process and endpoint that need them", ["local", "Never in prompts or logs"], "Filtered environments · log redaction"],
|
|
1352
|
+
["Page memory", "On disk next to your config", ["local", "No"], "`forgeqe memory clear` · `--no-memory`"],
|
|
1353
|
+
["Reports and screenshots", "On disk, one folder per run", ["local", "No"], "`report.screenshots: all | failures | off`"],
|
|
1354
|
+
["Session logs", "On disk, secrets redacted", ["local", "No"], "Shared only if you bundle and send them yourself"],
|
|
1355
|
+
["Generated tests", "Your repo, in a separate local working copy", ["local", "Only if you ask to push"], "Push is blocked for agents; default branches are refused"],
|
|
1356
|
+
];
|
|
1357
|
+
const CONN_ROWS = [
|
|
1358
|
+
["Your LLM endpoint", ["org", "Your cloud"], "Every agent step (not on replays)", "Prompts, page text, screenshots, code excerpts, case text", "The endpoint in your `llms.json` profile"],
|
|
1359
|
+
["Sites under test", ["org", "Your apps"], "Browser tasks", "Normal browser traffic, including what the agent types", "`browser.allowed_hosts` (empty allows every host)"],
|
|
1360
|
+
["ADO, Jira, Confluence, Xray, TestRail, Zephyr, qTest", ["org", "Your systems"], "When a request names cases or files a bug", "Queries, results, new bugs", "Only systems listed under `sources:`"],
|
|
1361
|
+
["Your git remote", ["org", "Your systems"], "Only when you ask to push a branch", "The test branch", "Agents can't push; default branches refused"],
|
|
1362
|
+
["Teams / email alerts", ["org", "Your systems"], "Monitoring, if configured", "Alert summary", "`monitor.alerts`"],
|
|
1363
|
+
["Atlassian-hosted Jira connector", ["pub", "Vendor cloud"], "Only if you choose it", "Jira queries", "Off by default; a local connector is used instead"],
|
|
1364
|
+
["Public package registries", ["pub", "Public"], "First start of the ADO or Jira connector", "Download only", "Turn the connector off; built-in API access still works"],
|
|
1365
|
+
["Component downloads", ["pub", "Public"], "First run or after an upgrade", "Download only, checksum-verified", "Pre-install, or route through your proxy (`HTTPS_PROXY`)"],
|
|
1366
|
+
["Chrome Web Store", ["pub", "Public"], "Once, for the ad-block and cookie-banner extensions", "Download only", "Block it; runs work without them"],
|
|
1367
|
+
["Public model price list", ["pub", "Public"], "At most once a day", "A plain download, nothing about your runs", "`FORGEQE_PRICES=off` and set `price` in `llms.json`"],
|
|
1368
|
+
["Package registry update check", ["pub", "Public"], "Only when you run it", "Version lookup", "Don't run `forgeqe update`, or use an internal mirror"],
|
|
1369
|
+
["Web pages the orchestrator reads", ["pub", "Agent's choice"], "When it decides to fetch a page", "An HTTP GET to that URL", "Egress proxy allow-list for strict environments"],
|
|
1370
|
+
["Usage analytics, tracing, cloud sync, share links", ["block", "Blocked"], "Never", "—", "Forced off in every process ForgeQE starts"],
|
|
1371
|
+
];
|
|
1372
|
+
const rich = (s) => s.split("`").map((x, i) => i % 2 ? h("code", { key: i }, x) : x);
|
|
1373
|
+
const tag = ([k, l]) => h("span", { className: "where " + k }, l);
|
|
1374
|
+
|
|
1375
|
+
function Data() {
|
|
1376
|
+
const [sub, setSub] = useState(0);
|
|
1377
|
+
const subs = [["inv", "What data goes where", DATA_ROWS.length + " data types"], ["out", "Every outbound connection", CONN_ROWS.length + " destinations"]];
|
|
1378
|
+
const onKey = (e) => {
|
|
1379
|
+
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
|
|
1380
|
+
const n = sub === 0 ? 1 : 0; setSub(n);
|
|
1381
|
+
const b = document.getElementById("sub-" + subs[n][0]); b && b.focus();
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
const head = (cols) => h("thead", null, h("tr", null, h("th", { "aria-label": "Row" }, "#"), cols.map((x) => h("th", { key: x }, x))));
|
|
1385
|
+
return h(Fragment, null,
|
|
1386
|
+
h(ChapterHead, { n: "04", title: "Data & connections", desc: "A ledger of everything ForgeQE handles, and every place it can connect to." }),
|
|
1387
|
+
h("div", { className: "wrapc" },
|
|
1388
|
+
h("div", { className: "seg", role: "tablist", "aria-label": "Data views", onKeyDown: onKey },
|
|
1389
|
+
subs.map(([k, l, c], i) => h("button", {
|
|
1390
|
+
key: k, id: "sub-" + k, role: "tab", "aria-selected": sub === i, "aria-controls": "subpanel", tabIndex: sub === i ? 0 : -1, onClick: () => setSub(i),
|
|
1391
|
+
}, h("span", { className: "mono" }, "4." + (i + 1)), l)),
|
|
1392
|
+
h("span", { className: "count mono" }, subs[sub][2])),
|
|
1393
|
+
h("div", { id: "subpanel", role: "tabpanel", "aria-labelledby": "sub-" + subs[sub][0] },
|
|
1394
|
+
sub === 0
|
|
1395
|
+
? h(Fragment, null,
|
|
1396
|
+
h("div", { className: "tw" }, h("table", null,
|
|
1397
|
+
head(["Data", "Where it goes", "Leaves the machine?", "Control"]),
|
|
1398
|
+
h("tbody", null, DATA_ROWS.map((r, i) => h("tr", { key: r[0] }, h("td", { className: "rn" }, "D" + String(i + 1).padStart(2, "0")), h("td", { className: "nm" }, r[0]), h("td", null, r[1]), h("td", null, tag(r[2])), h("td", null, rich(r[3]))))))),
|
|
1399
|
+
h("p", { className: "tnote", style: { marginTop: "10px" } }, h("span", { className: "mono" }, "Note"), "Every kind of data ForgeQE handles, where it goes, and what controls it."))
|
|
1400
|
+
: h(Fragment, null,
|
|
1401
|
+
h("div", { className: "tw" }, h("table", null,
|
|
1402
|
+
head(["Destination", "Type", "When", "What is sent", "How to control it"]),
|
|
1403
|
+
h("tbody", null, CONN_ROWS.map((r, i) => h("tr", { key: r[0] }, h("td", { className: "rn" }, "C" + String(i + 1).padStart(2, "0")), h("td", { className: "nm" }, r[0]), h("td", null, tag(r[1])), h("td", null, r[2]), h("td", null, r[3]), h("td", null, rich(r[4]))))))),
|
|
1404
|
+
h("p", { className: "tnote", style: { marginTop: "10px" } }, h("span", { className: "mono" }, "Note"), "The complete list of places ForgeQE can connect to. On a locked-down network, allow the first rows and block or mirror the rest.")))));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function Sheet({ file, kind, text, hi }) {
|
|
1408
|
+
const mark = hi || /org-azure|allowed_hosts|FORGEQE_PRICES|HTTPS_PROXY|"endpoint"/;
|
|
1409
|
+
return h("div", { className: "sheet" },
|
|
1410
|
+
h("div", { className: "file" }, h("span", null, file), h("span", null, kind)),
|
|
1411
|
+
h("pre", null, h("code", null, text.split("\n").map((l, i) =>
|
|
1412
|
+
h("span", { key: i, className: "ln" + (/^\s*#/.test(l) ? " cm" : "") + (!/^\s*#/.test(l) && mark.test(l) ? " hi" : "") }, l || " ")))));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const VERIFY = [
|
|
1416
|
+
h(Fragment, null, h("code", null, "forgeqe models"), " lists only your organisation's profile"),
|
|
1417
|
+
h(Fragment, null, "During a run, ", h("code", null, "lsof -iTCP -sTCP:LISTEN -P"), " shows ForgeQE listening on 127.0.0.1 only"),
|
|
1418
|
+
"Run behind an egress proxy that allows only your LLM, your apps and your work systems. Runs still pass.",
|
|
1419
|
+
"Open the latest session log and check that secrets appear only as redacted values",
|
|
1420
|
+
h(Fragment, null, "Delete ", h("code", null, "~/.forgeqe"), " and ", h("code", null, ".forgeqe/"), " to remove everything ForgeQE stored"),
|
|
1421
|
+
];
|
|
1422
|
+
|
|
1423
|
+
function Controls_() {
|
|
1424
|
+
const [done, setDone] = useState(() => VERIFY.map(() => false));
|
|
1425
|
+
const count = done.filter(Boolean).length;
|
|
1426
|
+
const safeguards = [
|
|
1427
|
+
"Logins reach the model only as placeholders, scoped to each login's domains",
|
|
1428
|
+
"Each process ForgeQE starts gets only the keys it needs",
|
|
1429
|
+
"Logs and log bundles redact keys, tokens, passwords and login values",
|
|
1430
|
+
"Internal tools listen on 127.0.0.1 only, behind a one-time token",
|
|
1431
|
+
"Telemetry, tracing, cloud sync and share links are forced off",
|
|
1432
|
+
"Chrome runs with background networking, sync and pings disabled",
|
|
1433
|
+
"Agents can't push to git; pushes on request refuse default branches",
|
|
1434
|
+
];
|
|
1435
|
+
return h(Fragment, null,
|
|
1436
|
+
h(ChapterHead, { n: "05", title: "Controls & checks", desc: "Two spec sheets to lock ForgeQE to your organisation, then verify it yourself." }),
|
|
1437
|
+
h("div", { className: "wrapc" },
|
|
1438
|
+
h("div", { className: "grid2" },
|
|
1439
|
+
h("section", { className: "spec enter" },
|
|
1440
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Spec 01"), h("h3", null, "Point ForgeQE at your organisation's LLM")),
|
|
1441
|
+
h("p", { className: "ds" }, "The example llms.json defaults to public OpenAI. Replace it with a single profile for your own deployment and remove the others."),
|
|
1442
|
+
h(Sheet, { file: "llms.json", kind: "JSON", text:
|
|
1443
|
+
`{
|
|
1444
|
+
"default": "org-azure",
|
|
1445
|
+
"models": {
|
|
1446
|
+
"org-azure": {
|
|
1447
|
+
"provider": "azure",
|
|
1448
|
+
"model": "gpt-4o",
|
|
1449
|
+
"deployment": "gpt-4o",
|
|
1450
|
+
"endpoint": "https://<your-resource>.openai.azure.com/",
|
|
1451
|
+
"api_key_env": "WP_AZURE_API_KEY",
|
|
1452
|
+
"api_version": "2024-05-01-preview",
|
|
1453
|
+
"deployment_type": "data_zone"
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}` })),
|
|
1457
|
+
h("section", { className: "spec enter" },
|
|
1458
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Spec 02"), h("h3", null, "Restrict browsing and outbound traffic")),
|
|
1459
|
+
h("p", { className: "ds" }, "Give the Azure resource a private endpoint. With an egress proxy that allows only your LLM, your apps and your work systems, nothing else can leave."),
|
|
1460
|
+
h(Sheet, { file: "forgeqe.yaml + environment", kind: "YAML / ENV", text:
|
|
1461
|
+
`# forgeqe.yaml
|
|
1462
|
+
llm:
|
|
1463
|
+
active: org-azure
|
|
1464
|
+
browser:
|
|
1465
|
+
allowed_hosts: ["*.yourcompany.com"]
|
|
1466
|
+
vision_detail: low
|
|
1467
|
+
report:
|
|
1468
|
+
screenshots: failures
|
|
1469
|
+
# code.model: leave unset so every agent
|
|
1470
|
+
# uses the org-azure profile
|
|
1471
|
+
|
|
1472
|
+
# environment / CI variables
|
|
1473
|
+
FORGEQE_PRICES=off
|
|
1474
|
+
HTTPS_PROXY=http://egress.yourcompany.internal:8080` }))),
|
|
1475
|
+
h("div", { className: "grid2" },
|
|
1476
|
+
h("section", { className: "spec enter" },
|
|
1477
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Built in"), h("h3", null, "Built-in safeguards")),
|
|
1478
|
+
h("ol", { className: "plain" }, safeguards.map((s, i) =>
|
|
1479
|
+
h("li", { key: s }, h("span", { className: "n" }, "G" + (i + 1)), h("span", null, s))))),
|
|
1480
|
+
h("section", { className: "spec enter" },
|
|
1481
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Checklist"), h("h3", null, "Verify it yourself"),
|
|
1482
|
+
h("span", { className: "prog mono", "aria-live": "polite" }, count + " / " + VERIFY.length,
|
|
1483
|
+
h("i", { "aria-hidden": "true" }, h("b", { style: { width: (count / VERIFY.length * 100) + "%" } })))),
|
|
1484
|
+
h("ul", { className: "vchk" }, VERIFY.map((v, i) =>
|
|
1485
|
+
h("li", { key: i, className: done[i] ? "ck" : "" },
|
|
1486
|
+
h("label", null,
|
|
1487
|
+
h("input", { type: "checkbox", checked: done[i], onChange: () => setDone((d) => d.map((x, j) => j === i ? !x : x)) }),
|
|
1488
|
+
h("span", null, v))))))),
|
|
1489
|
+
h("section", { className: "notice", "aria-labelledby": "gtk" },
|
|
1490
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Notice"), h("h3", { id: "gtk" }, "Good to know")),
|
|
1491
|
+
h("ul", null,
|
|
1492
|
+
h("li", null, h("b", null, "The model sees real content."), " Page text, screenshots, case text and code excerpts are what it works from. They stay in your organisation because the profile points at your tenant."),
|
|
1493
|
+
h("li", null, h("b", null, "Keep code.model unset"), " or pointed at the same deployment. It can override the provider for the orchestrator and the test-writing agent."),
|
|
1494
|
+
h("li", null, h("b", null, "The orchestrator can read web pages."), " Use an egress allow-list if that matters."),
|
|
1495
|
+
h("li", null, h("b", null, "An empty allowed_hosts allows every site."), " Set it for any environment with policy requirements."),
|
|
1496
|
+
h("li", null, h("b", null, "Values typed into forms"), " other than saved logins are part of what the model sees. Use saved logins for anything sensitive."),
|
|
1497
|
+
h("li", null, h("b", null, "Reports and screenshots on disk"), " can hold sensitive page data. Keep ", h("code", null, ".forgeqe/"), " out of version control.")))));
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/* ---------- 06 Azure deployment: Foundry on a private network, used from AVD and org laptops ---------- */
|
|
1501
|
+
const ALL_HANDLES = ["l", "r", "t", "b"].flatMap((s) => [25, 50, 75].flatMap((p) => [
|
|
1502
|
+
["s", { l: "left", r: "right", t: "top", b: "bottom" }[s], p, s + p + "s"],
|
|
1503
|
+
["t", { l: "left", r: "right", t: "top", b: "bottom" }[s], p, s + p + "t"],
|
|
1504
|
+
]));
|
|
1505
|
+
const azCard = (id, x, y, kind, title, sub, tagText, extra) => card(id, x, y, kind, title, sub, tagText, ALL_HANDLES, extra);
|
|
1506
|
+
const AZ_NODES = [
|
|
1507
|
+
zone("az-z-pub", -16, 0, 232, 176, "pub", "Public internet · setup only"),
|
|
1508
|
+
zone("az-z-sys", 284, 0, 532, 176, "org", "Company systems"),
|
|
1509
|
+
zone("az-z-avd", -16, 216, 232, 156, "local", "Azure · AVD spoke VNet"),
|
|
1510
|
+
zone("az-z-lap", -16, 396, 232, 356, "local", "Company network · offices & VPN"),
|
|
1511
|
+
zone("az-z-hub", 284, 216, 232, 536, "org", "Azure · hub VNet"),
|
|
1512
|
+
zone("az-z-ai", 584, 216, 232, 536, "org", "Azure · AI spoke VNet (private)"),
|
|
1513
|
+
zone("az-z-fdy", 884, 216, 232, 156, "org", "Azure AI Foundry · PaaS"),
|
|
1514
|
+
zone("az-z-host", 884, 396, 232, 356, "lane", "On every ForgeQE machine"),
|
|
1515
|
+
azCard("dl", 0, 40, "pub", "Setup downloads", "One time, or never with a pre-built image", "DOWNLOAD ONLY"),
|
|
1516
|
+
azCard("ado", 300, 40, "org", "Azure DevOps · Entra ID", "Cases, bugs, repos; sign-in with a PAT or az login", "YOUR SYSTEMS"),
|
|
1517
|
+
azCard("aut", 600, 40, "org", "Apps under test", "Internal staging and production sites", "YOUR APPS"),
|
|
1518
|
+
azCard("avd", 0, 260, "user", "AVD session host", "ForgeQE + Chrome, multi- or single-session", "CLIENT"),
|
|
1519
|
+
azCard("lap", 0, 440, "user", "Org laptop", "ForgeQE + Chrome, in the office or on VPN", "CLIENT"),
|
|
1520
|
+
azCard("cdns", 0, 620, "local", "Corporate DNS", "Conditional forwarders for the Foundry domains", "DNS"),
|
|
1521
|
+
azCard("fw", 300, 260, "org", "Azure Firewall / NVA", "All spoke and VPN egress; FQDN rules, TLS inspection", "HUB"),
|
|
1522
|
+
azCard("gw", 300, 440, "org", "VPN / ExpressRoute gateway", "Brings office and VPN traffic into Azure", "HUB"),
|
|
1523
|
+
azCard("dnsr", 300, 620, "org", "DNS Private Resolver", "Inbound endpoint answers private names", "HUB"),
|
|
1524
|
+
azCard("pe", 600, 260, "org", "Private endpoint", "Foundry's private IP in snet-private-endpoints", "PRIVATE LINK"),
|
|
1525
|
+
azCard("apim", 600, 440, "org", "API Management (optional)", "Internal gateway if key access must stay off", "OPTIONAL"),
|
|
1526
|
+
azCard("pdz", 600, 620, "org", "Private DNS zones", "privatelink.openai / cognitiveservices / services.ai", "DNS"),
|
|
1527
|
+
azCard("fdy", 900, 260, "org", "Foundry model deployment", "Public network access: disabled", "YOUR CLOUD"),
|
|
1528
|
+
azCard("nin", 900, 440, "local", "Inbound: none", "ForgeQE opens no ports on the network", "NO RULES"),
|
|
1529
|
+
azCard("loop", 900, 620, "local", "Loopback only", "127.0.0.1, ephemeral ports: tool bridge, agents, Chrome", "LOCAL"),
|
|
1530
|
+
];
|
|
1531
|
+
Object.assign(REF, { dl: "NET·01", ado: "SYS·01", aut: "SYS·02", avd: "CLI·01", lap: "CLI·02", cdns: "DNS·01", fw: "HUB·01", gw: "HUB·02", dnsr: "DNS·02", pe: "AI·01", apim: "AI·02", pdz: "DNS·03", fdy: "AI·03", nin: "HOST·01", loop: "HOST·02" });
|
|
1532
|
+
const AZ_INFO = {
|
|
1533
|
+
avd: ["AVD session host", "Runs ForgeQE and Chrome inside your AVD spoke VNet. Its VNet is peered to the hub, a route table sends 0.0.0.0/0 to the firewall, and its DNS servers point at the DNS Private Resolver inbound endpoint (or the firewall's DNS proxy)."],
|
|
1534
|
+
lap: ["Org laptop", "Runs ForgeQE and Chrome. In the office or on VPN, traffic reaches Azure through the VPN or ExpressRoute gateway. The VPN must route the private endpoint subnet and the resolver IP through the tunnel."],
|
|
1535
|
+
cdns: ["Corporate DNS", "Add conditional forwarders for openai.azure.com, cognitiveservices.azure.com and services.ai.azure.com to the DNS Private Resolver inbound IP. Forward the public zone names, not the privatelink names."],
|
|
1536
|
+
fw: ["Azure Firewall / NVA", "Every outbound flow passes here. Allow TCP 443 to the private endpoint IP, plus FQDN rules for your work systems and apps. Setup downloads are only needed if you don't pre-build the image."],
|
|
1537
|
+
gw: ["VPN / ExpressRoute gateway", "Carries office and VPN traffic into the hub. Make sure the private endpoint subnet and resolver IP are advertised to on-premises (BGP or static routes)."],
|
|
1538
|
+
dnsr: ["DNS Private Resolver", "Its inbound endpoint lets corporate DNS and AVD resolve the Foundry hostname to the private endpoint IP. It is linked to the private DNS zones."],
|
|
1539
|
+
pe: ["Private endpoint", "Gives the Foundry resource a private IP in the AI spoke (group ID: account). Put an NSG on the subnet that allows TCP 443 only from the AVD subnet and the office and VPN ranges; private endpoint network policies must be enabled for the NSG to apply."],
|
|
1540
|
+
apim: ["API Management (optional)", "Use this only if Azure Policy forces key access off on Foundry. ForgeQE calls the gateway with a subscription key; the gateway calls Foundry with its managed identity. Deploy it in internal VNet mode."],
|
|
1541
|
+
pdz: ["Private DNS zones", "privatelink.openai.azure.com, privatelink.cognitiveservices.azure.com and privatelink.services.ai.azure.com, holding the private endpoint's A records and linked to the hub VNet."],
|
|
1542
|
+
fdy: ["Foundry model deployment", "The only place prompts, page content and code excerpts go. Public network access is disabled, so it answers only through the private endpoint. ForgeQE authenticates with an API key."],
|
|
1543
|
+
ado: ["Azure DevOps · Entra ID", "Reached over HTTPS 443 through the firewall. Sign in with a personal access token, or with az login (Entra ID). Jira, Confluence and Git remotes follow the same pattern if you use them."],
|
|
1544
|
+
aut: ["Apps under test", "The sites Chrome opens. Allow them on the firewall or proxy, and list them in browser.allowed_hosts."],
|
|
1545
|
+
dl: ["Setup downloads", "Only during first-time setup or an upgrade. Bake ForgeQE, Chrome and its components into the AVD image and laptop package, and this path is never used."],
|
|
1546
|
+
nin: ["Inbound: none", "ForgeQE runs no server reachable from the network. No inbound NSG or firewall rule is needed for it."],
|
|
1547
|
+
loop: ["Loopback only", "The tool bridge, the agent runtime and Chrome's control port listen on 127.0.0.1 on random ephemeral ports. Endpoint protection must allow loopback traffic; nothing is reachable from other machines."],
|
|
1548
|
+
};
|
|
1549
|
+
// [id, source, sourceHandle, target, targetHandle, label, kind, extra]
|
|
1550
|
+
const AZ_EDGES = [
|
|
1551
|
+
["avd-fw", "avd", "r50s", "fw", "l50t", "UDR", "org"],
|
|
1552
|
+
["lap-gw", "lap", "r50s", "gw", "l50t", "VPN / ER", "org"],
|
|
1553
|
+
["gw-fw", "gw", "t50s", "fw", "b50t", "routed", "org"],
|
|
1554
|
+
["fw-pe", "fw", "r50s", "pe", "l50t", "TCP 443", "org"],
|
|
1555
|
+
["pe-fdy", "pe", "r50s", "fdy", "l50t", "Private Link", "org"],
|
|
1556
|
+
["fw-ado", "fw", "t50s", "ado", "b50t", "TCP 443", "org"],
|
|
1557
|
+
["fw-aut", "fw", "t75s", "aut", "b50t", "TCP 443", "org", { cy: 196 }],
|
|
1558
|
+
["fw-dl", "fw", "t25s", "dl", "b50t", "443 · setup only", "pub", { cy: 196 }],
|
|
1559
|
+
["lap-cdns", "lap", "b50s", "cdns", "t50t", "DNS 53", "local"],
|
|
1560
|
+
["cdns-dnsr", "cdns", "r50s", "dnsr", "l50t", "53", "local"],
|
|
1561
|
+
["avd-dnsr", "avd", "l50s", "dnsr", "b50t", "DNS 53", "local", { cx: -40 }],
|
|
1562
|
+
["dnsr-pdz", "dnsr", "r50s", "pdz", "l50t", "lookup", "local"],
|
|
1563
|
+
["apim-fdy", "apim", "r50s", "fdy", "l75t", "managed identity", "muted", { cx: 858 }],
|
|
1564
|
+
];
|
|
1565
|
+
const AZ_FLOWS = [
|
|
1566
|
+
{ t: "Model calls from AVD", d: "AVD session host → firewall → Foundry private endpoint, TCP 443. The browser engine and the agents both call the model this way.", e: ["avd-fw", "fw-pe", "pe-fdy"], w: "org" },
|
|
1567
|
+
{ t: "Model calls from laptops", d: "Laptop → VPN or ExpressRoute → firewall → private endpoint, TCP 443. The VPN must route the private endpoint subnet through the tunnel.", e: ["lap-gw", "gw-fw", "fw-pe", "pe-fdy"], w: "org" },
|
|
1568
|
+
{ t: "Name resolution on AVD", d: "The AVD VNet's DNS points at the resolver inbound IP, which answers from the private DNS zones, so the Foundry hostname resolves to its private IP.", e: ["avd-dnsr", "dnsr-pdz"], w: "local" },
|
|
1569
|
+
{ t: "Name resolution on laptops", d: "Corporate DNS forwards the Foundry domains to the resolver inbound IP over UDP/TCP 53.", e: ["lap-cdns", "cdns-dnsr", "dnsr-pdz"], w: "local" },
|
|
1570
|
+
{ t: "Work systems and sign-in", d: "Azure DevOps, Entra ID sign-in (az login) and any Jira, Confluence or Git remote, over TCP 443 through the firewall.", e: ["avd-fw", "lap-gw", "gw-fw", "fw-ado"], w: "org" },
|
|
1571
|
+
{ t: "Apps under test", d: "Chrome opens your internal sites over TCP 443 (or 80) through the firewall or proxy.", e: ["avd-fw", "lap-gw", "gw-fw", "fw-aut"], w: "org" },
|
|
1572
|
+
{ t: "Setup downloads", d: "Only when installing or upgrading. Not needed at all if ForgeQE and its components are baked into the image.", e: ["avd-fw", "fw-dl"], w: "pub" },
|
|
1573
|
+
{ t: "Optional gateway", d: "Only if key access must stay disabled on Foundry: ForgeQE → API Management (subscription key) → Foundry (managed identity).", e: ["apim-fdy"], w: "org" },
|
|
1574
|
+
];
|
|
1575
|
+
function buildAzEdges(pal, active) {
|
|
1576
|
+
return AZ_EDGES.map(([id, s, sh, t, th, label, kind, x]) => {
|
|
1577
|
+
const on = active ? active.has(id) : false;
|
|
1578
|
+
const mk = { type: MarkerType.ArrowClosed, color: pal[kind] || pal.local, width: 17, height: 17 };
|
|
1579
|
+
return {
|
|
1580
|
+
id, source: s, sourceHandle: sh, target: t, targetHandle: th, type: "flow", label, data: Object.assign({ bi: false }, x || {}),
|
|
1581
|
+
labelBgPadding: [4, 2], labelBgBorderRadius: 0, markerEnd: mk,
|
|
1582
|
+
className: "e-" + kind + (active ? (on ? " on" : " off") : ""), zIndex: on ? 10 : 0,
|
|
1583
|
+
};
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const DRAG_AZ = withExtents(AZ_NODES);
|
|
1587
|
+
|
|
1588
|
+
const AZ_RULES = [
|
|
1589
|
+
["AVD subnet; office and VPN ranges", "Foundry private endpoint IP", "TCP 443", "`<resource>.openai.azure.com` (resolves to the private IP)", "Model calls from the browser engine and the agents", ["org", "Required"]],
|
|
1590
|
+
["AVD subnet; corporate DNS servers", "DNS Private Resolver inbound IP (or firewall DNS proxy)", "UDP/TCP 53", "`openai.azure.com`, `cognitiveservices.azure.com`, `services.ai.azure.com`", "Resolve Foundry to its private IP", ["org", "Required"]],
|
|
1591
|
+
["AVD subnet; office and VPN ranges", "Apps under test", "TCP 443 (80 if HTTP)", "Your staging and production hosts", "Browser runs", ["org", "Required"]],
|
|
1592
|
+
["Loopback on each machine", "127.0.0.1", "TCP, ephemeral ports", "None: never leaves the machine", "Tool bridge, agents, Chrome control; allow in endpoint protection", ["local", "Required"]],
|
|
1593
|
+
["AVD subnet; office and VPN ranges", "Azure DevOps", "TCP 443", "`dev.azure.com`, `*.dev.azure.com`, `*.visualstudio.com`, `*.vssps.visualstudio.com`", "Test cases, bugs, pull requests, repos", ["org", "If used"]],
|
|
1594
|
+
["AVD subnet; office and VPN ranges", "Entra ID", "TCP 443", "`login.microsoftonline.com`, `management.azure.com`", "az login for Azure DevOps (not needed with a PAT)", ["org", "If used"]],
|
|
1595
|
+
["AVD subnet; office and VPN ranges", "Atlassian", "TCP 443", "`<site>.atlassian.net`, `api.atlassian.com`", "Jira, Confluence, Xray", ["org", "If used"]],
|
|
1596
|
+
["AVD subnet; office and VPN ranges", "Git remote", "TCP 443 (or 22)", "Your Azure Repos or GitHub host", "Clone; push a branch only when asked", ["org", "If used"]],
|
|
1597
|
+
["AVD subnet; office and VPN ranges", "Teams / SMTP", "TCP 443 / 587", "`*.webhook.office.com`, your SMTP relay", "Monitoring alerts", ["org", "If used"]],
|
|
1598
|
+
["Image build or first run", "GitHub releases", "TCP 443", "`github.com`, `objects.githubusercontent.com`, `release-assets.githubusercontent.com`, `api.github.com`, `astral.sh`, `bun.sh`", "Runtime components and Python builds, checksum-verified", ["pub", "Setup only"]],
|
|
1599
|
+
["Image build or first run", "Python packages", "TCP 443", "`pypi.org`, `files.pythonhosted.org`", "Browser engine libraries", ["pub", "Setup only"]],
|
|
1600
|
+
["Image build or first run", "npm registry", "TCP 443", "`registry.npmjs.org`", "ForgeQE install, the Azure DevOps connector, add-ons on first use", ["pub", "Setup only"]],
|
|
1601
|
+
["Image build or first run", "Chromium download", "TCP 443", "`cdn.playwright.dev`, `playwright.download.prss.microsoft.com`", "Only if Google Chrome is not installed", ["pub", "Setup only"]],
|
|
1602
|
+
["Image build or first run", "Chrome Web Store", "TCP 443", "`clients2.google.com`, `clients2.googleusercontent.com`", "Ad-block and cookie-banner extensions, once; optional", ["pub", "Optional"]],
|
|
1603
|
+
["Any", "Public model price list", "TCP 443", "`models.dev`, `raw.githubusercontent.com`", "Cost display only; set `FORGEQE_PRICES=off` and block", ["block", "Block"]],
|
|
1604
|
+
["Network", "ForgeQE machines", "None", "None", "ForgeQE needs no inbound rule", ["block", "No rule"]],
|
|
1605
|
+
];
|
|
1606
|
+
const AZ_DNS = [
|
|
1607
|
+
["Private DNS zone", "`privatelink.openai.azure.com`", "A record for the Foundry resource → private endpoint IP; link to the hub VNet"],
|
|
1608
|
+
["Private DNS zone", "`privatelink.cognitiveservices.azure.com`", "Same private endpoint; created with the endpoint's DNS zone group"],
|
|
1609
|
+
["Private DNS zone", "`privatelink.services.ai.azure.com`", "Same private endpoint; Foundry's AI services hostname"],
|
|
1610
|
+
["Conditional forwarder", "`openai.azure.com` → resolver inbound IP", "On corporate DNS, so laptops resolve the private IP"],
|
|
1611
|
+
["Conditional forwarder", "`cognitiveservices.azure.com` → resolver inbound IP", "On corporate DNS"],
|
|
1612
|
+
["Conditional forwarder", "`services.ai.azure.com` → resolver inbound IP", "On corporate DNS"],
|
|
1613
|
+
["VNet DNS setting", "AVD spoke VNet → resolver inbound IP (or firewall DNS proxy)", "Session hosts resolve through Azure, never public DNS"],
|
|
1614
|
+
];
|
|
1615
|
+
const AZ_POLICY = [
|
|
1616
|
+
["Azure AI Services resources should have key access disabled", ["block", "Conflicts"], "ForgeQE authenticates to the model with an API key. Either exempt this one Foundry resource, or put API Management in front and let it call Foundry with a managed identity."],
|
|
1617
|
+
["Azure AI Services resources should restrict network access", ["org", "Compatible"], "Public network access disabled; only the private endpoint is used."],
|
|
1618
|
+
["Azure AI Services should use private link", ["org", "Compatible"], "Uses the private endpoint in the AI spoke."],
|
|
1619
|
+
["Allowed locations / data residency", ["org", "Compatible"], "Choose a Standard (regional) or Data Zone deployment, not Global, if prompts must stay in a region or geography."],
|
|
1620
|
+
["Diagnostic settings should be enabled", ["org", "Compatible"], "No effect on ForgeQE. Turn on request logs for your own audit."],
|
|
1621
|
+
["Deny public IP / internet egress", ["org", "Compatible"], "ForgeQE needs no public IP. Pre-build the image so session hosts need no internet at all."],
|
|
1622
|
+
["Endpoint protection blocks loopback ports", ["pub", "Check"], "ForgeQE's local processes talk over 127.0.0.1 on random ports. Allow loopback for ForgeQE, Chrome and Python."],
|
|
1623
|
+
["TLS inspection on the proxy", ["pub", "Check"], "Supply the corporate root CA through NODE_EXTRA_CA_CERTS, SSL_CERT_FILE and REQUESTS_CA_BUNDLE, or exclude the Foundry hostname from inspection."],
|
|
1624
|
+
];
|
|
1625
|
+
const AZ_CHECK = [
|
|
1626
|
+
h(Fragment, null, h("code", null, "nslookup <resource>.openai.azure.com"), " returns a private IP (10.x / 172.16–31.x / 192.168.x) on AVD and on a laptop"),
|
|
1627
|
+
h(Fragment, null, h("code", null, "Test-NetConnection <resource>.openai.azure.com -Port 443"), " succeeds from AVD and from a laptop on VPN"),
|
|
1628
|
+
"From outside the company network, the same hostname is refused: public access is disabled",
|
|
1629
|
+
h(Fragment, null, h("code", null, "forgeqe models"), " lists only the Foundry profile, and its key variable is set"),
|
|
1630
|
+
h(Fragment, null, h("code", null, "forgeqe run --direct \"Read the homepage\" --url https://<internal-app>"), " passes on AVD and on a laptop"),
|
|
1631
|
+
h(Fragment, null, h("code", null, "netstat -ano | findstr LISTENING"), " during a run shows ForgeQE only on 127.0.0.1"),
|
|
1632
|
+
"Firewall logs show no denied flows from ForgeQE machines other than the blocked price list",
|
|
1633
|
+
];
|
|
1634
|
+
|
|
1635
|
+
function Azure() {
|
|
1636
|
+
const dark = useDark();
|
|
1637
|
+
const pal = dark ? PALETTE.dark : PALETTE.light;
|
|
1638
|
+
const [sub, setSub] = useState(0);
|
|
1639
|
+
const [flow, setFlow] = useState(-1);
|
|
1640
|
+
const [info, setInfo] = useState(null);
|
|
1641
|
+
const [done, setDone] = useState(() => AZ_CHECK.map(() => false));
|
|
1642
|
+
const [pos, onNodesChange, resetLayout] = useDragLayout();
|
|
1643
|
+
const cur = flow >= 0 ? AZ_FLOWS[flow] : null;
|
|
1644
|
+
const activeE = cur ? new Set(cur.e) : null;
|
|
1645
|
+
const activeN = cur ? new Set(cur.e.flatMap((id) => { const e = AZ_EDGES.find((x) => x[0] === id); return [e[1], e[3]]; })) : null;
|
|
1646
|
+
const nodes = useMemo(() => DRAG_AZ.map((n) => n.type === "zone" ? place(n, pos) :
|
|
1647
|
+
Object.assign({}, place(n, pos), { className: activeN ? (activeN.has(n.id) ? "hl" : "dim") : "" })), [flow, pos]);
|
|
1648
|
+
const edges = useMemo(() => buildAzEdges(pal, activeE), [flow, dark]);
|
|
1649
|
+
const subs = [["az-net", "Network diagram", AZ_FLOWS.length + " flows"], ["az-fw", "Firewall & ports", AZ_RULES.length + " rules"],
|
|
1650
|
+
["az-dns", "DNS & private endpoint", AZ_DNS.length + " records"], ["az-pol", "Policies & rollout", AZ_POLICY.length + " policies"]];
|
|
1651
|
+
const onKey = (e) => {
|
|
1652
|
+
let n = null;
|
|
1653
|
+
if (e.key === "ArrowRight") n = (sub + 1) % subs.length;
|
|
1654
|
+
else if (e.key === "ArrowLeft") n = (sub + subs.length - 1) % subs.length;
|
|
1655
|
+
if (n !== null) { setSub(n); const b = document.getElementById("sub-" + subs[n][0]); b && b.focus(); }
|
|
1656
|
+
};
|
|
1657
|
+
const head = (cols) => h("thead", null, h("tr", null, h("th", { "aria-label": "Row" }, "#"), cols.map((x) => h("th", { key: x }, x))));
|
|
1658
|
+
const count = done.filter(Boolean).length;
|
|
1659
|
+
|
|
1660
|
+
const net = h("div", { className: "split fill" },
|
|
1661
|
+
h("figure", { className: "fig", style: { margin: 0 } },
|
|
1662
|
+
h("header", null,
|
|
1663
|
+
h("span", { className: "mono" }, "Fig. 06 · Azure network"),
|
|
1664
|
+
h("div", { className: "key" },
|
|
1665
|
+
h("span", null, h("i", { style: { borderTopColor: pal.org, borderTopStyle: "dotted", borderTopWidth: "3px" } }), "HTTPS 443"),
|
|
1666
|
+
h("span", null, h("i", { style: { borderTopColor: pal.local, borderTopStyle: "dotted", borderTopWidth: "3px" } }), "DNS 53"),
|
|
1667
|
+
h("span", null, h("i", { style: { borderTopColor: pal.pub, borderTopStyle: "dotted", borderTopWidth: "3px" } }), "Setup only"),
|
|
1668
|
+
h("span", null, h("i", { style: { borderTopColor: pal.muted, borderTopStyle: "dotted", borderTopWidth: "3px" } }), "Optional"))),
|
|
1669
|
+
h("div", { className: "canvas" },
|
|
1670
|
+
h(ReactFlowProvider, null, h(FitCanvas, {
|
|
1671
|
+
nodes, edges, plain: true, onNodesChange,
|
|
1672
|
+
onNodeClick: (_, n) => AZ_INFO[n.id] && setInfo([REF[n.id]].concat(AZ_INFO[n.id])),
|
|
1673
|
+
onPaneClick: () => setInfo(null),
|
|
1674
|
+
})),
|
|
1675
|
+
resetBtn(pos, resetLayout),
|
|
1676
|
+
info ? h("div", { className: "info", role: "status" },
|
|
1677
|
+
h("button", { onClick: () => setInfo(null), "aria-label": "Close" }, "×"),
|
|
1678
|
+
h("span", { className: "mono" }, info[0]),
|
|
1679
|
+
h("b", null, info[1]), h("span", { className: "muted" }, info[2])) : null)),
|
|
1680
|
+
h("aside", { className: "side" },
|
|
1681
|
+
h("section", { className: "sec" },
|
|
1682
|
+
h("span", { className: "mono" }, "Flows"),
|
|
1683
|
+
h("h3", null, "Follow each connection"),
|
|
1684
|
+
h("p", { className: "hintline" }, "Arrows point from the side that opens the connection. Pick a flow to trace it; click any box for details."),
|
|
1685
|
+
h("ol", { className: "scn" }, AZ_FLOWS.map((f, i) =>
|
|
1686
|
+
h("li", { key: f.t },
|
|
1687
|
+
h("button", { "aria-pressed": i === flow, onClick: () => setFlow(i === flow ? -1 : i) },
|
|
1688
|
+
h("span", { className: "n" }, "F" + (i + 1)),
|
|
1689
|
+
h("span", null, f.t),
|
|
1690
|
+
h("span", { className: "c" }, f.e.length + " hops"))))),
|
|
1691
|
+
h("div", { className: "ctrl" }, h("button", { className: "btn", onClick: () => setFlow(-1) }, "Show all"))),
|
|
1692
|
+
cur ? h("section", { className: "sec" },
|
|
1693
|
+
h("span", { className: "mono" }, "F" + (flow + 1)),
|
|
1694
|
+
h("h3", null, cur.t),
|
|
1695
|
+
h("p", { className: "muted", style: { fontSize: "12.5px" } }, cur.d),
|
|
1696
|
+
h("span", { className: "where " + cur.w, style: { marginTop: "8px", display: "inline-block" } }, { org: "Inside your network", local: "DNS inside your network", pub: "Setup only" }[cur.w])) : null));
|
|
1697
|
+
|
|
1698
|
+
const fwTab = h("div", { className: "wrapc" },
|
|
1699
|
+
h("div", { className: "tw" }, h("table", null,
|
|
1700
|
+
head(["Source", "Destination", "Port", "FQDN / target", "Why", "Status"]),
|
|
1701
|
+
h("tbody", null, AZ_RULES.map((r, i) => h("tr", { key: i },
|
|
1702
|
+
h("td", { className: "rn" }, "R" + String(i + 1).padStart(2, "0")), h("td", { className: "nm" }, r[0]), h("td", null, r[1]),
|
|
1703
|
+
h("td", null, h("code", null, r[2])), h("td", null, rich(r[3])), h("td", null, rich(r[4])), h("td", null, tag(r[5]))))))),
|
|
1704
|
+
h("p", { className: "tnote" }, h("span", { className: "mono" }, "Note"),
|
|
1705
|
+
"Rules R01–R04 are all a pre-built AVD image needs. Allow R05–R09 only for the systems you use. R10–R14 are for building the image or a laptop's first install; with a pre-built image they can stay closed."));
|
|
1706
|
+
|
|
1707
|
+
const dnsTab = h("div", { className: "wrapc" },
|
|
1708
|
+
h("div", { className: "tw" }, h("table", null,
|
|
1709
|
+
head(["Type", "Name", "Purpose"]),
|
|
1710
|
+
h("tbody", null, AZ_DNS.map((r, i) => h("tr", { key: i },
|
|
1711
|
+
h("td", { className: "rn" }, "N" + String(i + 1).padStart(2, "0")), h("td", { className: "nm" }, r[0]), h("td", null, rich(r[1])), h("td", null, rich(r[2]))))))),
|
|
1712
|
+
h("div", { className: "grid2" },
|
|
1713
|
+
h("section", { className: "spec enter" },
|
|
1714
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Spec 06.1"), h("h3", null, "Foundry and private endpoint")),
|
|
1715
|
+
h("p", { className: "ds" }, "Settings on the Azure side. The private endpoint lives in its own subnet in the AI spoke."),
|
|
1716
|
+
h(Sheet, { file: "Foundry resource + networking", kind: "AZURE", hi: /Disabled|account|Enabled|443|false/, text:
|
|
1717
|
+
`# Foundry (Azure AI services) resource
|
|
1718
|
+
publicNetworkAccess : Disabled
|
|
1719
|
+
networkAcls.defaultAction : Deny
|
|
1720
|
+
disableLocalAuth : false # API key auth (or use APIM)
|
|
1721
|
+
model deployment type : Standard or DataZone
|
|
1722
|
+
|
|
1723
|
+
# Private endpoint
|
|
1724
|
+
subnet : snet-private-endpoints (AI spoke)
|
|
1725
|
+
groupId : account
|
|
1726
|
+
privateEndpointNetworkPolicies : Enabled # so the NSG applies
|
|
1727
|
+
DNS zone group : the three privatelink zones
|
|
1728
|
+
|
|
1729
|
+
# NSG on snet-private-endpoints
|
|
1730
|
+
allow TCP 443 from AVD subnet, office and VPN ranges
|
|
1731
|
+
deny everything else inbound` })),
|
|
1732
|
+
h("section", { className: "spec enter" },
|
|
1733
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Spec 06.2"), h("h3", null, "ForgeQE on AVD and laptops")),
|
|
1734
|
+
h("p", { className: "ds" }, "Use the resource's Azure OpenAI endpoint hostname. Keep the Foundry domains and loopback out of the proxy."),
|
|
1735
|
+
h(Sheet, { file: "llms.json + environment", kind: "JSON / ENV", hi: /endpoint|NO_PROXY|FORGEQE_PRICES|CA_|CERT/, text:
|
|
1736
|
+
`{ "default": "foundry",
|
|
1737
|
+
"models": { "foundry": {
|
|
1738
|
+
"provider": "azure",
|
|
1739
|
+
"model": "gpt-4o", "deployment": "gpt-4o",
|
|
1740
|
+
"endpoint": "https://<resource>.openai.azure.com/",
|
|
1741
|
+
"api_key_env": "WP_AZURE_API_KEY",
|
|
1742
|
+
"deployment_type": "data_zone" } } }
|
|
1743
|
+
|
|
1744
|
+
HTTPS_PROXY=http://proxy.corp.example:8080
|
|
1745
|
+
NO_PROXY=localhost,127.0.0.1,::1,.openai.azure.com,.cognitiveservices.azure.com,.services.ai.azure.com
|
|
1746
|
+
NODE_EXTRA_CA_CERTS=C:\\certs\\corp-root.pem
|
|
1747
|
+
SSL_CERT_FILE=C:\\certs\\corp-root.pem
|
|
1748
|
+
REQUESTS_CA_BUNDLE=C:\\certs\\corp-root.pem
|
|
1749
|
+
FORGEQE_PRICES=off` }))));
|
|
1750
|
+
|
|
1751
|
+
const polTab = h("div", { className: "wrapc" },
|
|
1752
|
+
h("div", { className: "tw" }, h("table", null,
|
|
1753
|
+
head(["Policy or control", "Effect on ForgeQE", "What to do"]),
|
|
1754
|
+
h("tbody", null, AZ_POLICY.map((r, i) => h("tr", { key: i },
|
|
1755
|
+
h("td", { className: "rn" }, "P" + String(i + 1).padStart(2, "0")), h("td", { className: "nm" }, r[0]), h("td", null, tag(r[1])), h("td", null, r[2])))))),
|
|
1756
|
+
h("div", { className: "grid2" },
|
|
1757
|
+
h("section", { className: "spec enter" },
|
|
1758
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Rollout"), h("h3", null, "AVD golden image and laptops")),
|
|
1759
|
+
h("ol", { className: "plain" }, [
|
|
1760
|
+
"Install Google Chrome and ForgeQE in the image, then run forgeqe setup once so every component is in place.",
|
|
1761
|
+
"Point FORGEQE_BUN, FORGEQE_PYTHON and code.runtime at machine-wide installs, so session hosts never download anything.",
|
|
1762
|
+
"On multi-session hosts keep FORGEQE_HOME per user (the default, in the FSLogix profile): it holds saved logins and logs.",
|
|
1763
|
+
"Deliver the model key per user or per team from Key Vault into WP_AZURE_API_KEY. Never bake keys into the image.",
|
|
1764
|
+
"Set the proxy, NO_PROXY and CA variables machine-wide on AVD and through Intune on laptops.",
|
|
1765
|
+
"On laptops, make the VPN route the private endpoint subnet and resolver IP, and use corporate DNS with the forwarders.",
|
|
1766
|
+
"Set browser.allowed_hosts to your internal app domains in the shared forgeqe.yaml.",
|
|
1767
|
+
].map((s, i) => h("li", { key: i }, h("span", { className: "n" }, "S" + (i + 1)), h("span", null, s))))),
|
|
1768
|
+
h("section", { className: "spec enter" },
|
|
1769
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Pre-flight"), h("h3", null, "Check before go-live"),
|
|
1770
|
+
h("span", { className: "prog mono", "aria-live": "polite" }, count + " / " + AZ_CHECK.length,
|
|
1771
|
+
h("i", { "aria-hidden": "true" }, h("b", { style: { width: (count / AZ_CHECK.length * 100) + "%" } })))),
|
|
1772
|
+
h("ul", { className: "vchk" }, AZ_CHECK.map((v, i) =>
|
|
1773
|
+
h("li", { key: i, className: done[i] ? "ck" : "" },
|
|
1774
|
+
h("label", null,
|
|
1775
|
+
h("input", { type: "checkbox", checked: done[i], onChange: () => setDone((d) => d.map((x, j) => j === i ? !x : x)) }),
|
|
1776
|
+
h("span", null, v))))))),
|
|
1777
|
+
h("section", { className: "notice", "aria-labelledby": "az-gtk" },
|
|
1778
|
+
h("div", { className: "hd" }, h("span", { className: "mono" }, "Notice"), h("h3", { id: "az-gtk" }, "Good to know")),
|
|
1779
|
+
h("ul", null,
|
|
1780
|
+
h("li", null, h("b", null, "Key-based model access."), " ForgeQE uses an API key for Foundry today, not Entra ID tokens. Plan the key-access policy (P01) first."),
|
|
1781
|
+
h("li", null, h("b", null, "Use the Azure OpenAI hostname."), " Point the profile at https://<resource>.openai.azure.com/, not a Foundry project URL."),
|
|
1782
|
+
h("li", null, h("b", null, "Forward public zone names."), " Corporate DNS forwards openai.azure.com, not privatelink.openai.azure.com."),
|
|
1783
|
+
h("li", null, h("b", null, "Loopback must stay direct."), " Keep localhost and 127.0.0.1 in NO_PROXY, or ForgeQE's own processes can't talk to each other."),
|
|
1784
|
+
h("li", null, h("b", null, "Chrome uses the system proxy."), " Make sure your apps under test are reachable from a normal Chrome on AVD first."),
|
|
1785
|
+
h("li", null, h("b", null, "No inbound rules."), " If a security review asks which ports ForgeQE listens on: none on the network, only 127.0.0.1."))));
|
|
1786
|
+
|
|
1787
|
+
return h(Fragment, null,
|
|
1788
|
+
h(ChapterHead, { n: "06", title: "Azure deployment", desc: "Models from Azure AI Foundry on a private network, used from AVD and org laptops. Every flow, port and policy." }),
|
|
1789
|
+
h("div", { className: "seg", role: "tablist", "aria-label": "Azure views", onKeyDown: onKey, style: { flex: "none" } },
|
|
1790
|
+
subs.map(([k, l], i) => h("button", {
|
|
1791
|
+
key: k, id: "sub-" + k, role: "tab", "aria-selected": sub === i, "aria-controls": "az-panel", tabIndex: sub === i ? 0 : -1, onClick: () => setSub(i),
|
|
1792
|
+
}, h("span", { className: "mono" }, "6." + (i + 1)), l)),
|
|
1793
|
+
h("span", { className: "count mono" }, subs[sub][2])),
|
|
1794
|
+
h("div", { id: "az-panel", role: "tabpanel", "aria-labelledby": "sub-" + subs[sub][0], className: sub === 0 ? "fill" : "", style: sub === 0 ? { display: "flex", flexDirection: "column" } : null },
|
|
1795
|
+
[net, fwTab, dnsTab, polTab][sub]));
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/* ---------- shell ---------- */
|
|
1799
|
+
const TABS = [
|
|
1800
|
+
["overview", "Overview", "The five commitments and three zones"],
|
|
1801
|
+
["architecture", "Architecture & data flow", "Trace each hop across the perimeter"],
|
|
1802
|
+
["agents", "How the agents work", "Three local agents, step by step"],
|
|
1803
|
+
["data", "Data & connections", "Data inventory and outbound list"],
|
|
1804
|
+
["controls", "Controls & checks", "Configure, then verify it yourself"],
|
|
1805
|
+
["azure", "Azure deployment", "Foundry, AVD and laptops on a private network"],
|
|
1806
|
+
];
|
|
1807
|
+
const MARK = h("img", { src: "data:image/png;base64,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", width: 26, height: 26, alt: "" });
|
|
1808
|
+
const SUN = h("svg", { viewBox: "0 0 16 16", "aria-hidden": "true" }, h("circle", { cx: 8, cy: 8, r: 3, fill: "none", stroke: "currentColor", strokeWidth: 1.4 }),
|
|
1809
|
+
h("path", { d: "M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6 13 13M3 13l1.4-1.4M11.6 4.4 13 3", stroke: "currentColor", strokeWidth: 1.4 }));
|
|
1810
|
+
const MOON = h("svg", { viewBox: "0 0 16 16", "aria-hidden": "true" }, h("path", { d: "M13 10.2A5.5 5.5 0 0 1 5.8 3a5.5 5.5 0 1 0 7.2 7.2Z", fill: "none", stroke: "currentColor", strokeWidth: 1.4 }));
|
|
1811
|
+
|
|
1812
|
+
function App() {
|
|
1813
|
+
const dark = useDark();
|
|
1814
|
+
const init = () => { const t = (location.hash || "").slice(1); return TABS.some(([k]) => k === t) ? t : "overview"; };
|
|
1815
|
+
const [tab, setTab] = useState(init);
|
|
1816
|
+
const panelRef = useRef(null);
|
|
1817
|
+
useEffect(() => {
|
|
1818
|
+
const on = () => setTab(init());
|
|
1819
|
+
window.addEventListener("hashchange", on);
|
|
1820
|
+
return () => window.removeEventListener("hashchange", on);
|
|
1821
|
+
}, []);
|
|
1822
|
+
useEffect(() => { panelRef.current && (panelRef.current.scrollTop = 0); const b = document.getElementById("tab-" + tab); b && b.scrollIntoView && b.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [tab]);
|
|
1823
|
+
const go = (k) => { setTab(k); try { history.replaceState(null, "", "#" + k); } catch (e) {} };
|
|
1824
|
+
const onKey = (e) => {
|
|
1825
|
+
const i = TABS.findIndex(([k]) => k === tab);
|
|
1826
|
+
let n = null;
|
|
1827
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") n = TABS[(i + 1) % TABS.length][0];
|
|
1828
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") n = TABS[(i + TABS.length - 1) % TABS.length][0];
|
|
1829
|
+
else if (e.key === "Home") n = TABS[0][0];
|
|
1830
|
+
else if (e.key === "End") n = TABS[TABS.length - 1][0];
|
|
1831
|
+
if (n) {
|
|
1832
|
+
e.preventDefault();
|
|
1833
|
+
go(n);
|
|
1834
|
+
const b = document.getElementById("tab-" + n); b && b.focus();
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
const toggle = () => {
|
|
1838
|
+
const next = dark ? "light" : "dark";
|
|
1839
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
1840
|
+
try { localStorage.setItem("wp-theme", next); } catch (e) {}
|
|
1841
|
+
};
|
|
1842
|
+
const body = { overview: h(Overview, { go }), architecture: h(Architecture), agents: h(Agents), data: h(Data), controls: h(Controls_), azure: h(Azure) }[tab];
|
|
1843
|
+
const pal = dark ? PALETTE.dark : PALETTE.light;
|
|
1844
|
+
return h(Fragment, null,
|
|
1845
|
+
h("header", { className: "mast" },
|
|
1846
|
+
h("div", { className: "mark" }, MARK, h("b", null, "ForgeQE")),
|
|
1847
|
+
h("span", { className: "sep", "aria-hidden": "true" }),
|
|
1848
|
+
h("div", { className: "ttl" },
|
|
1849
|
+
h("h1", null, "Data security dossier"),
|
|
1850
|
+
h("span", { className: "sub" }, "How it works, where data goes, and how to keep it in your organisation")),
|
|
1851
|
+
h("div", { className: "right" },
|
|
1852
|
+
h("div", { className: "status mono" }, h("i", { className: "pulse", "aria-hidden": "true" }), h("span", { className: "lt" }, "Model traffic"), h("span", null, "→ ", h("em", null, "your tenant only"))),
|
|
1853
|
+
h("button", { className: "theme", onClick: toggle, "aria-label": dark ? "Switch to light theme" : "Switch to dark theme", title: dark ? "Light theme" : "Dark theme" }, dark ? SUN : MOON))),
|
|
1854
|
+
h("nav", { className: "rail", "aria-label": "Chapters" },
|
|
1855
|
+
h("div", { className: "cap mono" }, "Contents"),
|
|
1856
|
+
h("div", { className: "chapters", role: "tablist", "aria-label": "Sections", "aria-orientation": "vertical", onKeyDown: onKey },
|
|
1857
|
+
TABS.map(([k, l, d], i) => h("button", {
|
|
1858
|
+
key: k, id: "tab-" + k, role: "tab", "aria-selected": tab === k, "aria-controls": "panel", tabIndex: tab === k ? 0 : -1, onClick: () => go(k),
|
|
1859
|
+
}, h("span", { className: "n" }, String(i + 1).padStart(2, "0")), h("span", { className: "t" }, l), h("span", { className: "d" }, d)))),
|
|
1860
|
+
h("div", { className: "foot" },
|
|
1861
|
+
h("span", { className: "mono" }, "Key"),
|
|
1862
|
+
KEY.map(([k, l]) => h("span", { key: k, className: "k" }, h("i", { style: { borderTopColor: pal[k], borderTopStyle: "dotted", borderTopWidth: "3px" } }), l)))),
|
|
1863
|
+
h("main", { id: "panel", ref: panelRef, className: "panel", role: "tabpanel", "aria-labelledby": "tab-" + tab, tabIndex: -1 }, body));
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
ReactDOM.createRoot(document.getElementById("app")).render(h(App));
|
|
1867
|
+
})();
|
|
1868
|
+
</script>
|
|
1869
|
+
</body>
|
|
1870
|
+
</html>
|