opencode-codex-security 0.0.0-stage → 0.1.0
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/CONTRIBUTING.md +15 -0
- package/DISCLOSURE +19 -0
- package/LICENSE +202 -0
- package/README.md +103 -2
- package/SECURITY.md +22 -0
- package/dist/gui-experimental.cjs +17 -0
- package/docs/GLOBAL-CONFIG.md +64 -0
- package/docs/MULTI-SCAN.md +62 -0
- package/docs/PUBLISHING.md +71 -0
- package/docs/RELEASING.md +53 -0
- package/docs/SDK.md +36 -0
- package/fixtures/vulnerable/docs/ARCHITECTURE.md +2 -0
- package/fixtures/vulnerable/src/accounts.js +4 -0
- package/fixtures/vulnerable/src/diagnostics.js +5 -0
- package/fixtures/vulnerable/src/download.js +5 -0
- package/fixtures/vulnerable/src/profile.js +4 -0
- package/package.json +79 -4
- package/src/core/codex-adapter.ts +430 -0
- package/src/core/credential-guard.ts +83 -0
- package/src/core/planned-scan.ts +316 -0
- package/src/core/report-safety.ts +41 -0
- package/src/core/result-summary.ts +512 -0
- package/src/core/run-history.ts +107 -0
- package/src/core/run-telemetry.ts +359 -0
- package/src/core/scan-plan.ts +182 -0
- package/src/core/scan-service.mjs +129 -0
- package/src/core/scanner.mjs +129 -0
- package/src/core/sdk-settings.ts +123 -0
- package/src/core/security-operations.ts +598 -0
- package/src/gui.tsx +102 -0
- package/src/operation-tools.ts +247 -0
- package/src/plan-view.ts +47 -0
- package/src/plugin.ts +346 -0
- package/src/rpc.ts +124 -0
- package/src/scan-display.ts +284 -0
- package/src/security-operations-panel.tsx +551 -0
- package/src/security-panel.tsx +1003 -0
- package/src/security-styles.ts +10 -0
- package/src/security-tui-operations.tsx +343 -0
- package/src/state-order.ts +21 -0
- package/src/tui.tsx +630 -0
- package/src/types.ts +84 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, For, Show, on, onCleanup } from 'solid-js';
|
|
2
|
+
import { createStore, reconcile } from 'solid-js/store';
|
|
3
|
+
import type {
|
|
4
|
+
SecurityOperationInputs,
|
|
5
|
+
SecurityOperationState,
|
|
6
|
+
SecurityOperationApproval,
|
|
7
|
+
} from './core/security-operations';
|
|
8
|
+
import { elapsedLabel, phaseLabel, projectName, splitPaths, userError } from './scan-display';
|
|
9
|
+
|
|
10
|
+
export interface OperationPort {
|
|
11
|
+
capabilities?: { policy: boolean; validation: boolean; questionAnswers: boolean };
|
|
12
|
+
list(): Promise<unknown>;
|
|
13
|
+
prepare(input: SecurityOperationInputs): Promise<unknown>;
|
|
14
|
+
start(input: SecurityOperationApproval): Promise<unknown>;
|
|
15
|
+
answer(input: { operationId: string; questionId: string; answer: string }): Promise<unknown>;
|
|
16
|
+
cancel(input: { operationId: string }): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface ValidationSeed {
|
|
19
|
+
repository: string;
|
|
20
|
+
finding: string;
|
|
21
|
+
}
|
|
22
|
+
export function SecurityOperationsPanel(props: {
|
|
23
|
+
kind: 'policy' | 'validation';
|
|
24
|
+
directory: string;
|
|
25
|
+
port: OperationPort;
|
|
26
|
+
seed?: ValidationSeed;
|
|
27
|
+
}) {
|
|
28
|
+
const [data, setData] = createStore<{ operations: SecurityOperationState[] }>({ operations: [] });
|
|
29
|
+
const [selected, select] = createSignal<string>();
|
|
30
|
+
const [form, setForm] = createSignal(true);
|
|
31
|
+
const [repository, setRepository] = createSignal(props.seed?.repository ?? props.directory);
|
|
32
|
+
const [path, setPath] = createSignal('');
|
|
33
|
+
const [documents, setDocuments] = createSignal('');
|
|
34
|
+
const [finding, setFinding] = createSignal(props.seed?.finding ?? '');
|
|
35
|
+
const [seeded, setSeeded] = createSignal(!!props.seed);
|
|
36
|
+
const [answer, setAnswer] = createSignal('');
|
|
37
|
+
const [uncapped, setUncapped] = createSignal(false);
|
|
38
|
+
const [busy, setBusy] = createSignal(false);
|
|
39
|
+
const [error, setError] = createSignal('');
|
|
40
|
+
const [now, setNow] = createSignal(Date.now());
|
|
41
|
+
const [loaded, setLoaded] = createSignal(false);
|
|
42
|
+
let stopped = false,
|
|
43
|
+
polling = false,
|
|
44
|
+
contextRevision = 0;
|
|
45
|
+
const resetForm = (seed?: ValidationSeed) => {
|
|
46
|
+
contextRevision++;
|
|
47
|
+
setRepository(seed?.repository ?? props.directory);
|
|
48
|
+
setSeeded(!!seed);
|
|
49
|
+
setFinding(seed?.finding ?? '');
|
|
50
|
+
setPath('');
|
|
51
|
+
setDocuments('');
|
|
52
|
+
setAnswer('');
|
|
53
|
+
setUncapped(false);
|
|
54
|
+
setError('');
|
|
55
|
+
select(undefined);
|
|
56
|
+
setForm(true);
|
|
57
|
+
};
|
|
58
|
+
createEffect(
|
|
59
|
+
on(
|
|
60
|
+
() => [props.kind, props.directory, props.seed] as const,
|
|
61
|
+
() => resetForm(props.seed),
|
|
62
|
+
{ defer: true },
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
const operations = createMemo(() => data.operations.filter((op) => op.kind === props.kind));
|
|
66
|
+
const current = createMemo(
|
|
67
|
+
() =>
|
|
68
|
+
operations().find((op) => op.operationId === selected()) ??
|
|
69
|
+
operations().find((op) => op.status === 'awaiting_answers') ??
|
|
70
|
+
operations()[0],
|
|
71
|
+
);
|
|
72
|
+
const label = () => (props.kind === 'policy' ? 'Policy draft' : 'Finding validation');
|
|
73
|
+
createEffect(() => {
|
|
74
|
+
void current()?.question?.id;
|
|
75
|
+
setAnswer('');
|
|
76
|
+
});
|
|
77
|
+
createEffect(() => {
|
|
78
|
+
void current()?.operationId;
|
|
79
|
+
setUncapped(false);
|
|
80
|
+
});
|
|
81
|
+
const refresh = async () => {
|
|
82
|
+
if (stopped || polling) return;
|
|
83
|
+
polling = true;
|
|
84
|
+
try {
|
|
85
|
+
const response = await props.port.list();
|
|
86
|
+
const next = Array.isArray(response)
|
|
87
|
+
? response
|
|
88
|
+
: (response as { operations?: SecurityOperationState[] })?.operations;
|
|
89
|
+
if (!Array.isArray(next)) throw new Error('Could not read security operations.');
|
|
90
|
+
if (!stopped) {
|
|
91
|
+
setData('operations', reconcile(next, { key: 'operationId', merge: true }));
|
|
92
|
+
if (!loaded()) {
|
|
93
|
+
setLoaded(true);
|
|
94
|
+
if (!props.seed && next.some((op) => op.kind === props.kind)) setForm(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (!stopped) setError(userError(e));
|
|
99
|
+
} finally {
|
|
100
|
+
polling = false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const perform = async (action: () => Promise<unknown>) => {
|
|
104
|
+
if (busy() || stopped) return;
|
|
105
|
+
setBusy(true);
|
|
106
|
+
setError('');
|
|
107
|
+
try {
|
|
108
|
+
await action();
|
|
109
|
+
if (!stopped) await refresh();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (!stopped) setError(userError(e));
|
|
112
|
+
} finally {
|
|
113
|
+
if (!stopped) setBusy(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const prepare = (event: SubmitEvent) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
const preparingRevision = contextRevision;
|
|
119
|
+
void perform(async () => {
|
|
120
|
+
const op = (await props.port.prepare({
|
|
121
|
+
kind: props.kind,
|
|
122
|
+
repository: repository().trim() || props.directory,
|
|
123
|
+
contextFiles: splitPaths(documents()),
|
|
124
|
+
...(props.kind === 'policy'
|
|
125
|
+
? path().trim()
|
|
126
|
+
? { path: path().trim() }
|
|
127
|
+
: {}
|
|
128
|
+
: { finding: finding() }),
|
|
129
|
+
})) as SecurityOperationState;
|
|
130
|
+
if (!stopped && preparingRevision === contextRevision) {
|
|
131
|
+
select(op.operationId);
|
|
132
|
+
setData(
|
|
133
|
+
'operations',
|
|
134
|
+
reconcile(
|
|
135
|
+
[op, ...data.operations.filter((item) => item.operationId !== op.operationId)],
|
|
136
|
+
{ key: 'operationId', merge: true },
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
setForm(false);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const start = (op: SecurityOperationState) =>
|
|
144
|
+
void perform(() =>
|
|
145
|
+
props.port.start({
|
|
146
|
+
operationId: op.operationId,
|
|
147
|
+
approveOpenAIInference: true,
|
|
148
|
+
...(op.kind === 'validation' ? { approveUncappedValidation: true as const } : {}),
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
void refresh();
|
|
152
|
+
const timer = setInterval(() => {
|
|
153
|
+
setNow(Date.now());
|
|
154
|
+
void refresh();
|
|
155
|
+
}, 1500);
|
|
156
|
+
onCleanup(() => {
|
|
157
|
+
stopped = true;
|
|
158
|
+
clearInterval(timer);
|
|
159
|
+
});
|
|
160
|
+
return (
|
|
161
|
+
<section aria-label={label()}>
|
|
162
|
+
<div class="gs-section-heading">
|
|
163
|
+
<h3>{label()}</h3>
|
|
164
|
+
<Show when={!form()}>
|
|
165
|
+
<button class="gs-button" disabled={busy()} onClick={() => resetForm()}>
|
|
166
|
+
New {props.kind === 'policy' ? 'draft' : 'validation'}
|
|
167
|
+
</button>
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
<p class="gs-muted gs-small">
|
|
171
|
+
{props.kind === 'policy'
|
|
172
|
+
? 'Build security guidance from the repository, with questions when context is missing. Draft only: existing files stay unchanged.'
|
|
173
|
+
: 'Assess a finding against the source. This does not run an exploit or prove runtime reproduction.'}
|
|
174
|
+
</p>
|
|
175
|
+
<Show when={error()}>
|
|
176
|
+
<p class="gs-notice gs-error" role="alert">
|
|
177
|
+
{error()}
|
|
178
|
+
</p>
|
|
179
|
+
</Show>
|
|
180
|
+
<Show when={form()}>
|
|
181
|
+
<form class="gs-form" onSubmit={prepare}>
|
|
182
|
+
<label>
|
|
183
|
+
Target repository
|
|
184
|
+
<input
|
|
185
|
+
aria-label="Operation repository"
|
|
186
|
+
value={repository()}
|
|
187
|
+
required
|
|
188
|
+
disabled={busy()}
|
|
189
|
+
onInput={(event) => {
|
|
190
|
+
setRepository(event.currentTarget.value);
|
|
191
|
+
setPath('');
|
|
192
|
+
setDocuments('');
|
|
193
|
+
setFinding('');
|
|
194
|
+
setSeeded(false);
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
<span class="gs-help">
|
|
198
|
+
{seeded()
|
|
199
|
+
? 'Starts from the selected finding’s repository. Check the target before review.'
|
|
200
|
+
: 'Defaults to this workspace. Changing it affects only this operation.'}
|
|
201
|
+
</span>
|
|
202
|
+
</label>
|
|
203
|
+
<Show
|
|
204
|
+
when={props.kind === 'policy'}
|
|
205
|
+
fallback={
|
|
206
|
+
<label>
|
|
207
|
+
Finding to assess
|
|
208
|
+
<textarea
|
|
209
|
+
aria-label="Finding to assess"
|
|
210
|
+
value={finding()}
|
|
211
|
+
required
|
|
212
|
+
maxLength={16000}
|
|
213
|
+
disabled={busy()}
|
|
214
|
+
onInput={(event) => setFinding(event.currentTarget.value)}
|
|
215
|
+
placeholder="Describe the suspected issue, location, and evidence."
|
|
216
|
+
/>
|
|
217
|
+
<span class="gs-help">Use finding text or JSON. Do not include credentials.</span>
|
|
218
|
+
</label>
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
<label>
|
|
222
|
+
Component directory <span class="gs-muted">Optional</span>
|
|
223
|
+
<input
|
|
224
|
+
aria-label="Policy component directory"
|
|
225
|
+
value={path()}
|
|
226
|
+
disabled={busy()}
|
|
227
|
+
onInput={(event) => setPath(event.currentTarget.value)}
|
|
228
|
+
placeholder="src/api"
|
|
229
|
+
/>
|
|
230
|
+
<span class="gs-help">
|
|
231
|
+
Relative to the repository. Leave blank for the whole project.
|
|
232
|
+
</span>
|
|
233
|
+
</label>
|
|
234
|
+
</Show>
|
|
235
|
+
<details>
|
|
236
|
+
<summary>Supporting documents</summary>
|
|
237
|
+
<label>
|
|
238
|
+
Context files
|
|
239
|
+
<textarea
|
|
240
|
+
aria-label="Operation context files"
|
|
241
|
+
value={documents()}
|
|
242
|
+
disabled={busy()}
|
|
243
|
+
onInput={(event) => setDocuments(event.currentTarget.value)}
|
|
244
|
+
placeholder="docs/architecture.md"
|
|
245
|
+
/>
|
|
246
|
+
<span class="gs-help">
|
|
247
|
+
One repository-relative text document per line. Do not include secrets.
|
|
248
|
+
</span>
|
|
249
|
+
</label>
|
|
250
|
+
</details>
|
|
251
|
+
<div class="gs-actions">
|
|
252
|
+
<button class="gs-button gs-primary" disabled={busy()} type="submit">
|
|
253
|
+
{busy()
|
|
254
|
+
? 'Preparing…'
|
|
255
|
+
: 'Review ' + (props.kind === 'policy' ? 'policy draft' : 'validation')}
|
|
256
|
+
</button>
|
|
257
|
+
<Show when={operations().length}>
|
|
258
|
+
<button
|
|
259
|
+
class="gs-button"
|
|
260
|
+
type="button"
|
|
261
|
+
disabled={busy()}
|
|
262
|
+
onClick={() => setForm(false)}
|
|
263
|
+
>
|
|
264
|
+
Back
|
|
265
|
+
</button>
|
|
266
|
+
</Show>
|
|
267
|
+
</div>
|
|
268
|
+
<p class="gs-muted gs-small">Preparation makes no model calls.</p>
|
|
269
|
+
</form>
|
|
270
|
+
</Show>
|
|
271
|
+
<Show when={operations().length > 1}>
|
|
272
|
+
<ul class="gs-run-list" aria-label="Recent operations">
|
|
273
|
+
<For each={operations()}>
|
|
274
|
+
{(op) => (
|
|
275
|
+
<li>
|
|
276
|
+
<button
|
|
277
|
+
class="gs-run-button"
|
|
278
|
+
aria-pressed={current()?.operationId === op.operationId}
|
|
279
|
+
onClick={() => {
|
|
280
|
+
select(op.operationId);
|
|
281
|
+
setForm(false);
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
<div class="gs-run-top">
|
|
285
|
+
<span class="gs-run-title">
|
|
286
|
+
{projectName(op.repository)}
|
|
287
|
+
{op.plan.path ? ' / ' + op.plan.path : ''}
|
|
288
|
+
</span>
|
|
289
|
+
<span class="gs-status" data-status={op.status}>
|
|
290
|
+
{op.status === 'awaiting_answers'
|
|
291
|
+
? 'Needs your answer'
|
|
292
|
+
: phaseLabel(op.status)}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="gs-run-target">Target: {op.repository}</div>
|
|
296
|
+
<div class="gs-run-meta">
|
|
297
|
+
{new Date(op.createdAt).toLocaleTimeString([], {
|
|
298
|
+
hour: '2-digit',
|
|
299
|
+
minute: '2-digit',
|
|
300
|
+
})}{' '}
|
|
301
|
+
· {op.operationId.slice(0, 8)}
|
|
302
|
+
</div>
|
|
303
|
+
</button>
|
|
304
|
+
</li>
|
|
305
|
+
)}
|
|
306
|
+
</For>
|
|
307
|
+
</ul>
|
|
308
|
+
</Show>
|
|
309
|
+
<Show when={!form() && current()}>
|
|
310
|
+
{(op) => (
|
|
311
|
+
<article class="gs-detail" aria-label="Selected operation">
|
|
312
|
+
<div class="gs-detail-title">
|
|
313
|
+
<h3>{phaseLabel(op().phase)}</h3>
|
|
314
|
+
<p>
|
|
315
|
+
Target: {op().repository}
|
|
316
|
+
{op().plan.path ? ' / ' + op().plan.path : ''}
|
|
317
|
+
</p>
|
|
318
|
+
</div>
|
|
319
|
+
<Show when={op().status === 'prepared'}>
|
|
320
|
+
<div class="gs-review">
|
|
321
|
+
<h3>
|
|
322
|
+
Review {props.kind === 'policy' ? 'policy generation' : 'source validation'}
|
|
323
|
+
</h3>
|
|
324
|
+
<dl>
|
|
325
|
+
<dt>Repository</dt>
|
|
326
|
+
<dd>{op().repository}</dd>
|
|
327
|
+
<Show when={op().plan.path}>
|
|
328
|
+
<dt>Component</dt>
|
|
329
|
+
<dd>{op().plan.path}</dd>
|
|
330
|
+
</Show>
|
|
331
|
+
<Show when={op().plan.contextFiles.length}>
|
|
332
|
+
<dt>Supporting documents</dt>
|
|
333
|
+
<dd>
|
|
334
|
+
{op()
|
|
335
|
+
.plan.contextFiles.map((file) => file.path)
|
|
336
|
+
.join(', ')}
|
|
337
|
+
</dd>
|
|
338
|
+
</Show>
|
|
339
|
+
</dl>
|
|
340
|
+
<Show when={op().kind === 'validation'}>
|
|
341
|
+
<details class="gs-advanced">
|
|
342
|
+
<summary>Finding to assess</summary>
|
|
343
|
+
<pre>{op().plan.finding}</pre>
|
|
344
|
+
</details>
|
|
345
|
+
</Show>
|
|
346
|
+
<div class="gs-approval">
|
|
347
|
+
<Show
|
|
348
|
+
when={op().plan.mock}
|
|
349
|
+
fallback={
|
|
350
|
+
<>
|
|
351
|
+
<p>
|
|
352
|
+
Repository source{op().kind === 'validation' ? ', the finding,' : ''} and
|
|
353
|
+
selected context will be sent to OpenAI.
|
|
354
|
+
</p>
|
|
355
|
+
<p>
|
|
356
|
+
<strong>{op().plan.costNotice}</strong>
|
|
357
|
+
</p>
|
|
358
|
+
<p>
|
|
359
|
+
{op().kind === 'policy'
|
|
360
|
+
? 'A policy draft will be returned for review. No files will be applied.'
|
|
361
|
+
: 'This is static source assessment, not runtime reproduction. No patch will be applied.'}
|
|
362
|
+
</p>
|
|
363
|
+
</>
|
|
364
|
+
}
|
|
365
|
+
>
|
|
366
|
+
<p>Mock operation only. No model calls or source uploads.</p>
|
|
367
|
+
</Show>
|
|
368
|
+
</div>
|
|
369
|
+
<Show when={op().kind === 'validation' && !op().plan.mock}>
|
|
370
|
+
<label class="gs-check">
|
|
371
|
+
<input
|
|
372
|
+
type="checkbox"
|
|
373
|
+
checked={uncapped()}
|
|
374
|
+
onChange={(e) => setUncapped(e.currentTarget.checked)}
|
|
375
|
+
/>{' '}
|
|
376
|
+
I approve validation without a cost ceiling.
|
|
377
|
+
</label>
|
|
378
|
+
</Show>
|
|
379
|
+
<details class="gs-advanced">
|
|
380
|
+
<summary>Model settings</summary>
|
|
381
|
+
<p>
|
|
382
|
+
{op().plan.settings.codex.model} · {op().plan.settings.codex.reasoningEffort}{' '}
|
|
383
|
+
reasoning · Daybreak {op().plan.settings.codex.daybreak ? 'on' : 'off'}
|
|
384
|
+
</p>
|
|
385
|
+
<p class="gs-muted">
|
|
386
|
+
Account and model access are not verified. {op().plan.globalConfigFile}
|
|
387
|
+
</p>
|
|
388
|
+
</details>
|
|
389
|
+
<Show when={Date.parse(op().plan.expiresAt) <= now()}>
|
|
390
|
+
<p class="gs-error">This preview expired. Prepare a new one before starting.</p>
|
|
391
|
+
</Show>
|
|
392
|
+
<div class="gs-actions">
|
|
393
|
+
<button
|
|
394
|
+
class="gs-button gs-primary"
|
|
395
|
+
disabled={
|
|
396
|
+
busy() ||
|
|
397
|
+
Date.parse(op().plan.expiresAt) <= now() ||
|
|
398
|
+
(op().kind === 'validation' && !op().plan.mock && !uncapped())
|
|
399
|
+
}
|
|
400
|
+
onClick={() => start(op())}
|
|
401
|
+
>
|
|
402
|
+
{busy()
|
|
403
|
+
? 'Starting…'
|
|
404
|
+
: op().plan.mock
|
|
405
|
+
? 'Start mock operation'
|
|
406
|
+
: 'Approve and start'}
|
|
407
|
+
</button>
|
|
408
|
+
<button
|
|
409
|
+
class="gs-button"
|
|
410
|
+
disabled={busy()}
|
|
411
|
+
onClick={() => {
|
|
412
|
+
setRepository(op().repository);
|
|
413
|
+
setPath(op().plan.path ?? '');
|
|
414
|
+
setDocuments(
|
|
415
|
+
op()
|
|
416
|
+
.plan.contextFiles.map((file) => file.path)
|
|
417
|
+
.join('\n'),
|
|
418
|
+
);
|
|
419
|
+
setFinding(op().plan.finding ?? '');
|
|
420
|
+
setForm(true);
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
Edit request
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</Show>
|
|
428
|
+
<Show when={op().status === 'running' || op().status === 'awaiting_answers'}>
|
|
429
|
+
<div class="gs-facts">
|
|
430
|
+
<span>{elapsedLabel(op().startedAt, null, now())}</span>
|
|
431
|
+
<Show when={op().estimatedCostUsd !== undefined}>
|
|
432
|
+
<span>Estimated usage USD {(op().estimatedCostUsd ?? 0).toFixed(2)}</span>
|
|
433
|
+
</Show>
|
|
434
|
+
</div>
|
|
435
|
+
</Show>
|
|
436
|
+
<Show when={op().question}>
|
|
437
|
+
{(question) => (
|
|
438
|
+
<form
|
|
439
|
+
class="gs-form gs-questions"
|
|
440
|
+
onSubmit={(e) => {
|
|
441
|
+
e.preventDefault();
|
|
442
|
+
const q = question();
|
|
443
|
+
void perform(() =>
|
|
444
|
+
props.port.answer({
|
|
445
|
+
operationId: op().operationId,
|
|
446
|
+
questionId: q.id,
|
|
447
|
+
answer: answer(),
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
<h3>Your input is needed</h3>
|
|
453
|
+
<ol>
|
|
454
|
+
<For each={question().questions}>{(text) => <li>{text}</li>}</For>
|
|
455
|
+
</ol>
|
|
456
|
+
<label>
|
|
457
|
+
Your answer
|
|
458
|
+
<textarea
|
|
459
|
+
aria-label="Answer security questions"
|
|
460
|
+
maxLength={8000}
|
|
461
|
+
required
|
|
462
|
+
value={answer()}
|
|
463
|
+
disabled={busy()}
|
|
464
|
+
onInput={(e) => setAnswer(e.currentTarget.value)}
|
|
465
|
+
/>
|
|
466
|
+
<span class="gs-help">
|
|
467
|
+
Answer each question, or say what is unknown. Your answer goes to OpenAI for
|
|
468
|
+
this operation; do not include secrets.
|
|
469
|
+
</span>
|
|
470
|
+
</label>
|
|
471
|
+
<button
|
|
472
|
+
class="gs-button gs-primary"
|
|
473
|
+
disabled={busy() || !answer().trim()}
|
|
474
|
+
type="submit"
|
|
475
|
+
>
|
|
476
|
+
Send answer and continue
|
|
477
|
+
</button>
|
|
478
|
+
</form>
|
|
479
|
+
)}
|
|
480
|
+
</Show>
|
|
481
|
+
<Show when={op().status === 'running' || op().status === 'awaiting_answers'}>
|
|
482
|
+
<button
|
|
483
|
+
class="gs-button"
|
|
484
|
+
disabled={busy()}
|
|
485
|
+
onClick={() =>
|
|
486
|
+
void perform(() => props.port.cancel({ operationId: op().operationId }))
|
|
487
|
+
}
|
|
488
|
+
>
|
|
489
|
+
Cancel this operation
|
|
490
|
+
</button>
|
|
491
|
+
</Show>
|
|
492
|
+
<Show when={op().error}>
|
|
493
|
+
<p class="gs-notice gs-error" role="alert">
|
|
494
|
+
{op().error}
|
|
495
|
+
</p>
|
|
496
|
+
</Show>
|
|
497
|
+
<Show when={op().status === 'cancelled'}>
|
|
498
|
+
<p class="gs-notice">Cancelled. No completed assessment is available.</p>
|
|
499
|
+
</Show>
|
|
500
|
+
<Show when={op().result}>
|
|
501
|
+
{(result) => (
|
|
502
|
+
<section class="gs-operation-result">
|
|
503
|
+
<h3>{result().kind === 'policy' ? 'Policy draft' : 'Source assessment'}</h3>
|
|
504
|
+
<Show
|
|
505
|
+
when={result().kind === 'policy'}
|
|
506
|
+
fallback={
|
|
507
|
+
<p>
|
|
508
|
+
<strong>
|
|
509
|
+
{phaseLabel((result() as { disposition: string }).disposition)}
|
|
510
|
+
</strong>{' '}
|
|
511
|
+
· Static review, not runtime reproduction.
|
|
512
|
+
</p>
|
|
513
|
+
}
|
|
514
|
+
>
|
|
515
|
+
<p class="gs-muted gs-small">
|
|
516
|
+
Draft only. Review it before applying. No repository files were changed.
|
|
517
|
+
</p>
|
|
518
|
+
</Show>
|
|
519
|
+
<pre>
|
|
520
|
+
{result().kind === 'policy'
|
|
521
|
+
? (result() as { content: string }).content
|
|
522
|
+
: (result() as { report: string }).report}
|
|
523
|
+
</pre>
|
|
524
|
+
<Show when={result().kind === 'policy'}>
|
|
525
|
+
<For each={(result() as { reviewNotes: string[] }).reviewNotes}>
|
|
526
|
+
{(note) => <p class="gs-muted gs-small">{note}</p>}
|
|
527
|
+
</For>
|
|
528
|
+
</Show>
|
|
529
|
+
<Show when={result().truncated}>
|
|
530
|
+
<p class="gs-notice">
|
|
531
|
+
This preview is truncated. Read the full local SDK artifact before relying on
|
|
532
|
+
it.
|
|
533
|
+
</p>
|
|
534
|
+
</Show>
|
|
535
|
+
</section>
|
|
536
|
+
)}
|
|
537
|
+
</Show>
|
|
538
|
+
<details class="gs-advanced">
|
|
539
|
+
<summary>Operation details</summary>
|
|
540
|
+
<p class="gs-id">{op().operationId}</p>
|
|
541
|
+
<p class="gs-muted">
|
|
542
|
+
{op().answeredQuestionCount} questions answered. Operation history is kept for this
|
|
543
|
+
server session.
|
|
544
|
+
</p>
|
|
545
|
+
</details>
|
|
546
|
+
</article>
|
|
547
|
+
)}
|
|
548
|
+
</Show>
|
|
549
|
+
</section>
|
|
550
|
+
);
|
|
551
|
+
}
|