@ycodium-ai/plugin-redteam-model 0.0.0-stage → 0.2.2942

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1041 @@
1
+ // src/index.ts
2
+ import { definePlugin } from "@ycodium-ai/plugin-api";
3
+
4
+ // src/catalog.ts
5
+ var PLUGIN_ID = "redteam-model";
6
+ var PHASES = ["authorization", "scope", "evidence", "report"];
7
+ var MODES = [
8
+ { id: "redteam", group: "general" },
9
+ { id: "pentest", group: "professional" },
10
+ { id: "code-audit", group: "professional" },
11
+ { id: "binary-analysis", group: "professional" },
12
+ { id: "attack-defense", group: "professional" },
13
+ { id: "av-evasion", group: "professional" },
14
+ { id: "incident-response", group: "professional" },
15
+ { id: "cloud-security", group: "professional" },
16
+ { id: "ctf-solver", group: "professional" },
17
+ { id: "asset-mapping", group: "professional" }
18
+ ];
19
+ var MODE_COPY = {
20
+ redteam: {
21
+ displayName: "Researcher",
22
+ detail: "General session. Gates are optional."
23
+ },
24
+ pentest: {
25
+ displayName: "Assessment",
26
+ detail: "Authorized review with the four gates required."
27
+ },
28
+ "code-audit": {
29
+ displayName: "Code review",
30
+ detail: "Source review with the four gates required."
31
+ },
32
+ "binary-analysis": {
33
+ displayName: "Binary review",
34
+ detail: "Binary review with the four gates required."
35
+ },
36
+ "attack-defense": {
37
+ displayName: "Exercise",
38
+ detail: "Authorized exercise with the four gates required."
39
+ },
40
+ "av-evasion": {
41
+ displayName: "Detection review",
42
+ detail: "Detection review. No evasion steps, payloads, or refusal bypass."
43
+ },
44
+ "incident-response": {
45
+ displayName: "Incident review",
46
+ detail: "Incident review with the four gates required."
47
+ },
48
+ "cloud-security": {
49
+ displayName: "Cloud review",
50
+ detail: "Cloud review with the four gates required."
51
+ },
52
+ "ctf-solver": {
53
+ displayName: "Puzzle workspace",
54
+ detail: "Puzzle workspace with the four gates required."
55
+ },
56
+ "asset-mapping": {
57
+ displayName: "Asset inventory",
58
+ detail: "Inventory workflow with the four gates required."
59
+ }
60
+ };
61
+ function modeById(id) {
62
+ const row = MODES.find((mode) => mode.id === id);
63
+ if (row === void 0) return null;
64
+ const copy = MODE_COPY[row.id];
65
+ return { id: row.id, group: row.group, ...copy };
66
+ }
67
+ function listModes() {
68
+ return MODES.map((mode) => modeById(mode.id)).filter((mode) => mode !== null);
69
+ }
70
+ var MODULES = [
71
+ { id: "stage-gate", plane: "host", availability: "active" },
72
+ { id: "route-boost", plane: "host", availability: "active" },
73
+ { id: "sec-enforce", plane: "host", availability: "active" },
74
+ { id: "refusal-guard", plane: "host", availability: "withheld" },
75
+ { id: "trace-vault", plane: "host", availability: "active" },
76
+ { id: "auto-advance", plane: "host", availability: "active" },
77
+ { id: "product-subagents", plane: "host", availability: "withheld" },
78
+ { id: "mcp-studio", plane: "host", availability: "record" },
79
+ { id: "results", plane: "host", availability: "active" },
80
+ { id: "hunter", plane: "host", availability: "withheld" },
81
+ { id: "campaign-memory", plane: "host", availability: "active" },
82
+ { id: "mode-group", plane: "host", availability: "active" },
83
+ { id: "session-pulse", plane: "host", availability: "active" },
84
+ { id: "coverage", plane: "host", availability: "active" },
85
+ { id: "scanner-tools", plane: "preset", availability: "withheld" },
86
+ { id: "semgrep-audit", plane: "preset", availability: "withheld" },
87
+ { id: "shell-desk", plane: "host", availability: "withheld" }
88
+ ];
89
+ function moduleById(id) {
90
+ return MODULES.find((module) => module.id === id) ?? null;
91
+ }
92
+ var PROJECTION_NAME = "board";
93
+ var WIRE = {
94
+ gates: `${PLUGIN_ID}_gates_list`,
95
+ goal: `${PLUGIN_ID}_operation_goal`,
96
+ progress: `${PLUGIN_ID}_operation_progress`,
97
+ publish: `${PLUGIN_ID}_publish`,
98
+ probe: `${PLUGIN_ID}_probe`,
99
+ note: `${PLUGIN_ID}_ledger_note`
100
+ };
101
+ var REMINDER_TEXT = "Continue the open workflow gates: authorization, scope, evidence, and report. If authorization is not recorded, stop and ask for it.";
102
+
103
+ // src/model.ts
104
+ var MAX_OPERATIONS = 50;
105
+ var MAX_DENIALS = 12;
106
+ var MAX_NOTES = 20;
107
+ var MAX_TEXT = 500;
108
+ function emptyModules() {
109
+ return Object.fromEntries(MODULES.map((module) => [module.id, "absent"]));
110
+ }
111
+ function emptyManager() {
112
+ return {
113
+ version: 1,
114
+ deployed: false,
115
+ modules: emptyModules(),
116
+ operations: [],
117
+ probeDenial: true,
118
+ lastDenial: null,
119
+ packageName: "",
120
+ directoryPath: "",
121
+ uninstallId: ""
122
+ };
123
+ }
124
+ function emptySession(modeId = null) {
125
+ return {
126
+ modeId,
127
+ phase: "authorization",
128
+ goal: "",
129
+ done: 0,
130
+ total: PHASES.length,
131
+ reportOpen: false,
132
+ authorizationRecorded: false,
133
+ denials: [],
134
+ notes: []
135
+ };
136
+ }
137
+ function clip(value) {
138
+ const text2 = value.trim();
139
+ return text2.length > MAX_TEXT ? text2.slice(0, MAX_TEXT) : text2;
140
+ }
141
+ function settleInterrupted(state, at) {
142
+ if (!state.operations.some((operation) => operation.status === "running")) return state;
143
+ return {
144
+ ...state,
145
+ operations: state.operations.map(
146
+ (operation) => operation.status === "running" ? { ...operation, status: "failed", detail: "interrupted", at } : operation
147
+ )
148
+ };
149
+ }
150
+ function push(state, record) {
151
+ return { ...state, operations: [record, ...state.operations].slice(0, MAX_OPERATIONS) };
152
+ }
153
+ function finishOperation(state, id, status, detail) {
154
+ return {
155
+ ...state,
156
+ operations: state.operations.map(
157
+ (operation) => operation.id === id ? { ...operation, status, detail: clip(detail) } : operation
158
+ )
159
+ };
160
+ }
161
+ function beginOperation(state, record) {
162
+ return push(state, record);
163
+ }
164
+ function setDeployed(state, deployed) {
165
+ return { ...state, deployed };
166
+ }
167
+ function installModule(state, id) {
168
+ const module = moduleById(id);
169
+ if (module === null) return { state, error: "unknown-module" };
170
+ if (module.availability === "withheld") return { state, error: "withheld" };
171
+ return {
172
+ state: { ...state, modules: { ...state.modules, [module.id]: "installed" } },
173
+ error: null
174
+ };
175
+ }
176
+ function updateModule(state, id) {
177
+ if (state.modules[id] !== "installed") return { state, error: "not-installed" };
178
+ return installModule(state, id);
179
+ }
180
+ function uninstallModule(state, id) {
181
+ if (moduleById(id) === null) return { state, error: "unknown-module" };
182
+ return { state: { ...state, modules: { ...state.modules, [id]: "absent" } }, error: null };
183
+ }
184
+ function installActiveModules(state) {
185
+ const modules = { ...state.modules };
186
+ const withheld = [];
187
+ for (const module of MODULES) {
188
+ if (module.availability === "withheld") {
189
+ withheld.push(module.id);
190
+ continue;
191
+ }
192
+ modules[module.id] = "installed";
193
+ }
194
+ const detail = withheld.length === 0 ? "installed" : `withheld:${withheld.join(",")}`;
195
+ return { state: { ...state, modules }, detail };
196
+ }
197
+ function toBoard(state) {
198
+ return {
199
+ deployed: state.deployed,
200
+ probeDenial: state.probeDenial,
201
+ packageName: state.packageName,
202
+ directoryPath: state.directoryPath,
203
+ uninstallId: state.uninstallId,
204
+ lastDenial: state.lastDenial,
205
+ operations: state.operations,
206
+ modules: MODULES.map((module) => ({
207
+ id: module.id,
208
+ plane: module.plane,
209
+ availability: module.availability,
210
+ installed: state.modules[module.id] === "installed"
211
+ })),
212
+ modes: listModes()
213
+ };
214
+ }
215
+ function rememberDenial(state, denial) {
216
+ return { ...state, lastDenial: denial };
217
+ }
218
+ function patchManagerFields(state, patch) {
219
+ return {
220
+ ...state,
221
+ ...patch.probeDenial === void 0 ? {} : { probeDenial: patch.probeDenial },
222
+ ...patch.packageName === void 0 ? {} : { packageName: clip(patch.packageName) },
223
+ ...patch.directoryPath === void 0 ? {} : { directoryPath: clip(patch.directoryPath) },
224
+ ...patch.uninstallId === void 0 ? {} : { uninstallId: clip(patch.uninstallId) }
225
+ };
226
+ }
227
+ function isPhase(value) {
228
+ return PHASES.includes(value);
229
+ }
230
+ function withMode(session, modeId) {
231
+ return session.modeId === modeId ? session : { ...session, modeId };
232
+ }
233
+ function setPhase(session, phase) {
234
+ const index = PHASES.indexOf(phase);
235
+ const done = phase === "report" && session.reportOpen ? PHASES.length : index;
236
+ return { ...session, phase, done, total: Math.max(session.total, PHASES.length) };
237
+ }
238
+ function setGoal(session, goal) {
239
+ return { ...session, goal: clip(goal) };
240
+ }
241
+ function setProgress(session, done, total) {
242
+ const safeTotal = Number.isFinite(total) ? Math.max(0, Math.min(999, Math.trunc(total))) : session.total;
243
+ const safeDone = Number.isFinite(done) ? Math.max(0, Math.min(safeTotal, Math.trunc(done))) : session.done;
244
+ return { ...session, done: safeDone, total: safeTotal };
245
+ }
246
+ function recordAuthorization(session) {
247
+ return { ...session, authorizationRecorded: true };
248
+ }
249
+ function openReport(session) {
250
+ if (!session.authorizationRecorded) return { session, error: "authorization-missing" };
251
+ return {
252
+ session: {
253
+ ...session,
254
+ reportOpen: true,
255
+ phase: "report",
256
+ done: Math.max(session.done, session.total)
257
+ },
258
+ error: null
259
+ };
260
+ }
261
+ function pushDenial(session, denial) {
262
+ return { ...session, denials: [denial, ...session.denials].slice(0, MAX_DENIALS) };
263
+ }
264
+ function pushNote(session, note) {
265
+ const text2 = clip(note);
266
+ if (text2.length === 0) return session;
267
+ return { ...session, notes: [text2, ...session.notes].slice(0, MAX_NOTES) };
268
+ }
269
+ function isRecord(value) {
270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
271
+ }
272
+ function readFlag(value) {
273
+ return value === "installed" ? "installed" : "absent";
274
+ }
275
+ function parseManager(value) {
276
+ if (!isRecord(value)) return emptyManager();
277
+ const modules = emptyModules();
278
+ if (isRecord(value["modules"])) {
279
+ for (const module of MODULES) modules[module.id] = readFlag(value["modules"][module.id]);
280
+ }
281
+ const operations = Array.isArray(value["operations"]) ? value["operations"].flatMap((entry) => {
282
+ if (!isRecord(entry)) return [];
283
+ const status = entry["status"];
284
+ if (status !== "running" && status !== "succeeded" && status !== "failed") return [];
285
+ if (typeof entry["id"] !== "string" || typeof entry["kind"] !== "string") return [];
286
+ return [
287
+ {
288
+ id: entry["id"],
289
+ kind: entry["kind"],
290
+ status,
291
+ detail: typeof entry["detail"] === "string" ? entry["detail"] : "",
292
+ at: typeof entry["at"] === "string" ? entry["at"] : ""
293
+ }
294
+ ];
295
+ }) : [];
296
+ const base = emptyManager();
297
+ return {
298
+ ...base,
299
+ deployed: value["deployed"] === true,
300
+ modules,
301
+ operations: operations.slice(0, MAX_OPERATIONS),
302
+ probeDenial: value["probeDenial"] !== false,
303
+ lastDenial: parseDenial(value["lastDenial"]),
304
+ packageName: typeof value["packageName"] === "string" ? value["packageName"] : "",
305
+ directoryPath: typeof value["directoryPath"] === "string" ? value["directoryPath"] : "",
306
+ uninstallId: typeof value["uninstallId"] === "string" ? value["uninstallId"] : ""
307
+ };
308
+ }
309
+ function parseDenial(value) {
310
+ if (!isRecord(value)) return null;
311
+ if (typeof value["tool"] !== "string" || typeof value["reason"] !== "string") return null;
312
+ return {
313
+ tool: value["tool"],
314
+ code: typeof value["code"] === "string" ? value["code"] : "denied",
315
+ reason: value["reason"],
316
+ at: typeof value["at"] === "string" ? value["at"] : ""
317
+ };
318
+ }
319
+ function parseSession(value) {
320
+ if (!isRecord(value)) return emptySession();
321
+ const phase = typeof value["phase"] === "string" && isPhase(value["phase"]) ? value["phase"] : "authorization";
322
+ const denials = Array.isArray(value["denials"]) ? value["denials"].flatMap((entry) => {
323
+ const denial = parseDenial(entry);
324
+ return denial === null ? [] : [denial];
325
+ }) : [];
326
+ const notes = Array.isArray(value["notes"]) ? value["notes"].filter((entry) => typeof entry === "string").slice(0, MAX_NOTES) : [];
327
+ return {
328
+ modeId: typeof value["modeId"] === "string" ? value["modeId"] : null,
329
+ phase,
330
+ goal: typeof value["goal"] === "string" ? value["goal"] : "",
331
+ done: typeof value["done"] === "number" ? value["done"] : 0,
332
+ total: typeof value["total"] === "number" ? value["total"] : PHASES.length,
333
+ reportOpen: value["reportOpen"] === true,
334
+ authorizationRecorded: value["authorizationRecorded"] === true,
335
+ denials: denials.slice(0, MAX_DENIALS),
336
+ notes
337
+ };
338
+ }
339
+
340
+ // src/commands.ts
341
+ var objectSchema = { type: "object", properties: {} };
342
+ function readString(value) {
343
+ return typeof value === "string" ? value : "";
344
+ }
345
+ function registerCommands(ctx, runtime) {
346
+ const emit = /* @__PURE__ */ new Set();
347
+ const bump = () => {
348
+ for (const listener of emit) listener();
349
+ };
350
+ runtime.subscribe(bump);
351
+ ctx.command({
352
+ name: "state.get",
353
+ description: "Read the mode manager board.",
354
+ scope: "read",
355
+ payloadSchema: objectSchema,
356
+ handle: async () => ({ board: await runtime.board() })
357
+ });
358
+ ctx.command({
359
+ name: "session.get",
360
+ description: "Read one session's gates, denials, and notes.",
361
+ scope: "read",
362
+ payloadSchema: {
363
+ type: "object",
364
+ properties: { threadId: { type: "string" } }
365
+ },
366
+ handle: async (payload) => ({
367
+ session: await runtime.readSession(readString(payload["threadId"]) || null)
368
+ })
369
+ });
370
+ ctx.stream({
371
+ name: "live",
372
+ description: "Follow the mode manager board.",
373
+ scope: "read",
374
+ buffer: { latest: 1 },
375
+ payloadSchema: objectSchema,
376
+ handle: async (_payload, send, signal) => {
377
+ const push2 = async () => {
378
+ send({ board: await runtime.board() });
379
+ };
380
+ await push2();
381
+ const listener = () => {
382
+ void push2();
383
+ };
384
+ emit.add(listener);
385
+ try {
386
+ await new Promise((resolve) => {
387
+ signal.addEventListener("abort", () => resolve(), { once: true });
388
+ });
389
+ } finally {
390
+ emit.delete(listener);
391
+ }
392
+ }
393
+ });
394
+ ctx.command({
395
+ name: "deploy",
396
+ description: "Register the ten mode presets.",
397
+ scope: "operate",
398
+ payloadSchema: objectSchema,
399
+ handle: async () => {
400
+ const board = await runtime.deploy();
401
+ bump();
402
+ return { board };
403
+ }
404
+ });
405
+ ctx.command({
406
+ name: "remove",
407
+ description: "Unregister the mode presets.",
408
+ scope: "operate",
409
+ payloadSchema: objectSchema,
410
+ handle: async () => ({ board: await runtime.removeModes() })
411
+ });
412
+ ctx.command({
413
+ name: "module.install",
414
+ description: "Install one runtime module. Withheld modules fail.",
415
+ scope: "operate",
416
+ payloadSchema: {
417
+ type: "object",
418
+ properties: { id: { type: "string" } },
419
+ required: ["id"]
420
+ },
421
+ handle: async (payload) => ({ board: await runtime.install(readString(payload["id"])) })
422
+ });
423
+ ctx.command({
424
+ name: "module.update",
425
+ description: "Update one installed module.",
426
+ scope: "operate",
427
+ payloadSchema: {
428
+ type: "object",
429
+ properties: { id: { type: "string" } },
430
+ required: ["id"]
431
+ },
432
+ handle: async (payload) => ({ board: await runtime.update(readString(payload["id"])) })
433
+ });
434
+ ctx.command({
435
+ name: "module.uninstall",
436
+ description: "Mark one runtime module absent.",
437
+ scope: "operate",
438
+ payloadSchema: {
439
+ type: "object",
440
+ properties: { id: { type: "string" } },
441
+ required: ["id"]
442
+ },
443
+ handle: async (payload) => ({ board: await runtime.uninstall(readString(payload["id"])) })
444
+ });
445
+ ctx.command({
446
+ name: "repair",
447
+ description: "Install every module this package is allowed to run.",
448
+ scope: "operate",
449
+ payloadSchema: objectSchema,
450
+ handle: async () => ({ board: await runtime.repair() })
451
+ });
452
+ ctx.command({
453
+ name: "fields.set",
454
+ description: "Store the probe hold and the package install fields.",
455
+ scope: "operate",
456
+ payloadSchema: {
457
+ type: "object",
458
+ properties: {
459
+ probeDenial: { type: "boolean" },
460
+ packageName: { type: "string" },
461
+ directoryPath: { type: "string" },
462
+ uninstallId: { type: "string" }
463
+ }
464
+ },
465
+ handle: async (payload) => ({
466
+ board: await runtime.setFields({
467
+ ...typeof payload["probeDenial"] === "boolean" ? { probeDenial: payload["probeDenial"] } : {},
468
+ ...typeof payload["packageName"] === "string" ? { packageName: payload["packageName"] } : {},
469
+ ...typeof payload["directoryPath"] === "string" ? { directoryPath: payload["directoryPath"] } : {},
470
+ ...typeof payload["uninstallId"] === "string" ? { uninstallId: payload["uninstallId"] } : {}
471
+ })
472
+ })
473
+ });
474
+ ctx.command({
475
+ name: "authorization.record",
476
+ description: "Record that authorization was captured for this session.",
477
+ scope: "operate",
478
+ payloadSchema: {
479
+ type: "object",
480
+ properties: { threadId: { type: "string" } },
481
+ required: ["threadId"]
482
+ },
483
+ handle: async (payload) => ({
484
+ session: await runtime.markAuthorization(readString(payload["threadId"]))
485
+ })
486
+ });
487
+ ctx.command({
488
+ name: "report.open",
489
+ description: "Open the report gate after authorization is recorded.",
490
+ scope: "operate",
491
+ payloadSchema: {
492
+ type: "object",
493
+ properties: { threadId: { type: "string" } },
494
+ required: ["threadId"]
495
+ },
496
+ handle: async (payload) => runtime.markReport(readString(payload["threadId"]))
497
+ });
498
+ ctx.command({
499
+ name: "phase.set",
500
+ description: "Move the session phase rail.",
501
+ scope: "operate",
502
+ payloadSchema: {
503
+ type: "object",
504
+ properties: { threadId: { type: "string" }, phase: { type: "string" } },
505
+ required: ["threadId", "phase"]
506
+ },
507
+ handle: async (payload) => {
508
+ const phase = readString(payload["phase"]);
509
+ return {
510
+ session: await runtime.markPhase(
511
+ readString(payload["threadId"]),
512
+ isPhase(phase) ? phase : ""
513
+ )
514
+ };
515
+ }
516
+ });
517
+ ctx.command({
518
+ name: "reminder",
519
+ description: "Send the gate reminder as a user turn. Requires the auto-advance module.",
520
+ scope: "operate",
521
+ payloadSchema: {
522
+ type: "object",
523
+ properties: { threadId: { type: "string" } },
524
+ required: ["threadId"]
525
+ },
526
+ handle: async (payload) => runtime.remind(readString(payload["threadId"]))
527
+ });
528
+ }
529
+
530
+ // src/runtime.ts
531
+ import * as NodeCrypto from "node:crypto";
532
+
533
+ // src/guard.ts
534
+ var PROBE_REASON = "Probe is held by the mode manager. Turn the hold off to let this harmless call run.";
535
+ var REPORT_REASON = "The report gate is closed. Record authorization and open the report gate before publishing.";
536
+ var PATH_REASON = "The report gate is closed for paths under reports/.";
537
+ function guardDecision(input) {
538
+ if (modeById(input.session.modeId) === null) return { decision: "allow" };
539
+ if (input.manager.probeDenial && isNamed(input.name, "probe", WIRE.probe)) {
540
+ return { decision: "deny", reason: PROBE_REASON, code: "probe-held" };
541
+ }
542
+ const mode = modeById(input.session.modeId);
543
+ const enforced = input.manager.modules["sec-enforce"] === "installed" && mode?.group === "professional";
544
+ if (!enforced || input.session.reportOpen) return { decision: "allow" };
545
+ if (isNamed(input.name, "publish", WIRE.publish)) {
546
+ return { decision: "deny", reason: REPORT_REASON, code: "report-closed" };
547
+ }
548
+ if (mentionsReportPath(input.args)) {
549
+ return { decision: "deny", reason: PATH_REASON, code: "report-path" };
550
+ }
551
+ return { decision: "allow" };
552
+ }
553
+ function isNamed(name, short, wire) {
554
+ return name === short || name === wire;
555
+ }
556
+ function mentionsReportPath(value) {
557
+ if (typeof value === "string") {
558
+ const text2 = value.trim();
559
+ return text2 === "reports" || text2.startsWith("reports/") || text2.startsWith("reports\\");
560
+ }
561
+ if (Array.isArray(value)) return value.some((entry) => mentionsReportPath(entry));
562
+ if (typeof value === "object" && value !== null) {
563
+ return Object.values(value).some((entry) => entry !== void 0 && mentionsReportPath(entry));
564
+ }
565
+ return false;
566
+ }
567
+
568
+ // src/prompt.ts
569
+ var SECTION_TEXT = "Mode manager is active. A session without one of its presets gets no mode envelope. Model connection stays on the executor. Gates, when a preset is selected, are authorization, scope, evidence, and report.";
570
+ function prefixFor(mode) {
571
+ const hold = mode.id === "av-evasion" ? " This preset does not include evasion steps, payloads, or refusal bypass." : "";
572
+ return `Mode ${mode.id} (${mode.displayName}, ${mode.group}). Follow authorization, scope, evidence, and report. Do not treat the preset as permission.${hold}`;
573
+ }
574
+ function sessionPrompt(session, routeBoost) {
575
+ if (modeById(session.modeId) === null) return null;
576
+ const envelope = envelopeFor(session, routeBoost);
577
+ return envelope === null ? SECTION_TEXT : `${SECTION_TEXT}
578
+ ${envelope}`;
579
+ }
580
+ function envelopeFor(session, routeBoost) {
581
+ if (!routeBoost) return null;
582
+ const mode = modeById(session.modeId);
583
+ if (mode === null) return null;
584
+ const report = session.reportOpen ? "open" : "closed";
585
+ return `[mode-manager] mode=${mode.id} group=${mode.group} phase=${session.phase} gates=authorization,scope,evidence,report report=${report} done=${session.done}/${session.total}`;
586
+ }
587
+
588
+ // src/store.ts
589
+ import * as NodeFSP from "node:fs/promises";
590
+ import * as NodePath from "node:path";
591
+ function emptySnapshot(at) {
592
+ return { manager: settleInterrupted(emptyManager(), at), sessions: {}, modes: {} };
593
+ }
594
+ function parseSnapshot(value, at) {
595
+ if (typeof value !== "object" || value === null) return emptySnapshot(at);
596
+ const record = value;
597
+ const sessions = {};
598
+ if (typeof record["sessions"] === "object" && record["sessions"] !== null) {
599
+ for (const [id, session] of Object.entries(record["sessions"])) {
600
+ sessions[id] = parseSession(session);
601
+ }
602
+ }
603
+ const modes = {};
604
+ if (typeof record["modes"] === "object" && record["modes"] !== null) {
605
+ for (const [id, mode] of Object.entries(record["modes"])) {
606
+ modes[id] = typeof mode === "string" ? mode : null;
607
+ }
608
+ }
609
+ return {
610
+ manager: settleInterrupted(parseManager(record["manager"]), at),
611
+ sessions,
612
+ modes
613
+ };
614
+ }
615
+ function openManagerDisk(dataDir, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
616
+ const file = NodePath.join(dataDir, "manager.json");
617
+ let chain = Promise.resolve();
618
+ const read = async () => {
619
+ try {
620
+ const raw = await NodeFSP.readFile(file, "utf8");
621
+ return parseSnapshot(JSON.parse(raw), now());
622
+ } catch {
623
+ return emptySnapshot(now());
624
+ }
625
+ };
626
+ return {
627
+ read,
628
+ update: (change) => {
629
+ const run = chain.then(async () => {
630
+ const current = await read();
631
+ const next = change(current);
632
+ await NodeFSP.mkdir(dataDir, { recursive: true });
633
+ const temporary = `${file}.${process.pid}.tmp`;
634
+ await NodeFSP.writeFile(temporary, JSON.stringify(next), "utf8");
635
+ await NodeFSP.rename(temporary, file);
636
+ return next;
637
+ });
638
+ chain = run.then(
639
+ () => void 0,
640
+ () => void 0
641
+ );
642
+ return run;
643
+ }
644
+ };
645
+ }
646
+
647
+ // src/runtime.ts
648
+ function sessionValue(session) {
649
+ return {
650
+ modeId: session.modeId,
651
+ phase: session.phase,
652
+ goal: session.goal,
653
+ done: session.done,
654
+ total: session.total,
655
+ reportOpen: session.reportOpen,
656
+ authorizationRecorded: session.authorizationRecorded,
657
+ denials: session.denials.map((denial) => ({ ...denial })),
658
+ notes: [...session.notes]
659
+ };
660
+ }
661
+ function isRecord2(value) {
662
+ return typeof value === "object" && value !== null;
663
+ }
664
+ function openRuntime(ctx, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
665
+ const disk = openManagerDisk(ctx.paths.dataDir, now);
666
+ let current = { manager: emptyManager(), sessions: {}, modes: {} };
667
+ let presetDisposers = [];
668
+ const listeners = /* @__PURE__ */ new Set();
669
+ const notify = () => {
670
+ for (const listener of listeners) listener();
671
+ };
672
+ const syncPresets = (deployed) => {
673
+ for (const dispose of presetDisposers) dispose();
674
+ presetDisposers = [];
675
+ if (!deployed) return [];
676
+ const skipped = [];
677
+ for (const mode of listModes()) {
678
+ try {
679
+ presetDisposers.push(
680
+ ctx.agentPresets.register({
681
+ id: mode.id,
682
+ displayName: mode.displayName,
683
+ description: mode.detail,
684
+ promptPrefix: prefixFor(mode)
685
+ })
686
+ );
687
+ } catch (error) {
688
+ const message = error instanceof Error ? error.message : "";
689
+ if (!message.includes("already registered")) throw error;
690
+ skipped.push(mode.id);
691
+ }
692
+ }
693
+ return skipped;
694
+ };
695
+ const project = async (threadId, session) => {
696
+ await ctx.sessionProjections.set({
697
+ name: PROJECTION_NAME,
698
+ threadId,
699
+ value: sessionValue(session)
700
+ });
701
+ };
702
+ const ready = (async () => {
703
+ const snap = await disk.read();
704
+ current = await disk.update(() => snap);
705
+ syncPresets(current.manager.deployed);
706
+ notify();
707
+ })();
708
+ const update = async (change) => {
709
+ await ready;
710
+ current = await disk.update(change);
711
+ notify();
712
+ return current;
713
+ };
714
+ const sessionFor = (threadId) => {
715
+ if (threadId === null || threadId.length === 0) return emptySession();
716
+ return current.sessions[threadId] ?? emptySession(current.modes[threadId] ?? null);
717
+ };
718
+ const saveSession = async (threadId, session) => {
719
+ await update((snap) => ({ ...snap, sessions: { ...snap.sessions, [threadId]: session } }));
720
+ await project(threadId, session);
721
+ };
722
+ const logged = async (kind, patch) => {
723
+ const id = NodeCrypto.randomUUID();
724
+ const at = now();
725
+ await update((snap) => ({
726
+ ...snap,
727
+ manager: beginOperation(snap.manager, { id, kind, status: "running", detail: "", at })
728
+ }));
729
+ try {
730
+ await update((snap) => {
731
+ const result = patch(snap.manager);
732
+ return {
733
+ ...snap,
734
+ manager: finishOperation(
735
+ result.state,
736
+ id,
737
+ result.error === null ? "succeeded" : "failed",
738
+ result.error ?? result.detail ?? "ok"
739
+ )
740
+ };
741
+ });
742
+ const finished = current.manager.operations.find((operation) => operation.id === id);
743
+ const skipped = syncPresets(current.manager.deployed);
744
+ if (skipped.length > 0 && finished?.status === "succeeded") {
745
+ await update((snap) => ({
746
+ ...snap,
747
+ manager: finishOperation(
748
+ snap.manager,
749
+ id,
750
+ "succeeded",
751
+ `deployed; skipped ${skipped.join(",")}`
752
+ )
753
+ }));
754
+ }
755
+ } catch (error) {
756
+ const detail = error instanceof Error ? error.message : "failed";
757
+ await update((snap) => ({
758
+ ...snap,
759
+ manager: finishOperation(snap.manager, id, "failed", detail)
760
+ }));
761
+ }
762
+ return toBoard(current.manager);
763
+ };
764
+ const runtime = {
765
+ ready: () => ready,
766
+ moduleInstalled: (id) => current.manager.modules[id] === "installed",
767
+ sessionFor,
768
+ save: async (threadId, session) => {
769
+ if (threadId === null || threadId.length === 0) return;
770
+ await saveSession(threadId, session);
771
+ },
772
+ board: async () => {
773
+ await ready;
774
+ return toBoard(current.manager);
775
+ },
776
+ readSession: async (threadId) => {
777
+ await ready;
778
+ return sessionFor(threadId);
779
+ },
780
+ contextFor: async (threadId) => {
781
+ await ready;
782
+ return sessionPrompt(
783
+ sessionFor(threadId),
784
+ current.manager.modules["route-boost"] === "installed"
785
+ );
786
+ },
787
+ guard: async (input) => {
788
+ await ready;
789
+ const outcome = guardDecision({
790
+ name: input.name,
791
+ args: input.args,
792
+ manager: current.manager,
793
+ session: sessionFor(input.threadId)
794
+ });
795
+ if (outcome.decision === "allow") return { decision: "allow" };
796
+ const denial = {
797
+ tool: input.name,
798
+ code: outcome.code ?? "denied",
799
+ reason: outcome.reason,
800
+ at: now()
801
+ };
802
+ const threadId = input.threadId;
803
+ await update((snap) => {
804
+ const next = threadId === null || threadId.length === 0 ? null : pushDenial(
805
+ snap.sessions[threadId] ?? emptySession(snap.modes[threadId] ?? null),
806
+ denial
807
+ );
808
+ return {
809
+ ...snap,
810
+ manager: rememberDenial(snap.manager, denial),
811
+ sessions: next === null || threadId === null ? snap.sessions : { ...snap.sessions, [threadId]: next }
812
+ };
813
+ });
814
+ if (threadId !== null && threadId.length > 0 && current.sessions[threadId] !== void 0) {
815
+ await project(threadId, current.sessions[threadId]);
816
+ }
817
+ return { decision: "deny", reason: outcome.reason };
818
+ },
819
+ rememberCreated: async (event) => {
820
+ if (event.type !== "thread.created" || event.aggregateId.length === 0) return;
821
+ const payload = isRecord2(event.payload) ? event.payload : {};
822
+ const raw = payload["presetId"];
823
+ const presetId = typeof raw === "string" && modeById(raw) !== null ? raw : null;
824
+ const threadId = event.aggregateId;
825
+ await update((snap) => ({
826
+ ...snap,
827
+ modes: { ...snap.modes, [threadId]: presetId },
828
+ sessions: {
829
+ ...snap.sessions,
830
+ [threadId]: withMode(snap.sessions[threadId] ?? emptySession(presetId), presetId)
831
+ }
832
+ }));
833
+ const saved = current.sessions[threadId];
834
+ if (saved !== void 0) await project(threadId, saved);
835
+ },
836
+ subscribe: (listener) => {
837
+ listeners.add(listener);
838
+ return () => {
839
+ listeners.delete(listener);
840
+ };
841
+ },
842
+ deploy: () => logged("deploy-modes", (manager) => ({
843
+ state: setDeployed(manager, true),
844
+ error: null,
845
+ detail: "deployed"
846
+ })),
847
+ removeModes: () => logged("remove-modes", (manager) => ({
848
+ state: setDeployed(manager, false),
849
+ error: null,
850
+ detail: "removed"
851
+ })),
852
+ install: (id) => logged("install", (manager) => installModule(manager, id)),
853
+ update: (id) => logged("update", (manager) => updateModule(manager, id)),
854
+ uninstall: (id) => logged("uninstall", (manager) => uninstallModule(manager, id)),
855
+ repair: () => logged("repair", (manager) => {
856
+ const repaired = installActiveModules(manager);
857
+ return { state: repaired.state, error: null, detail: repaired.detail };
858
+ }),
859
+ setFields: async (patch) => {
860
+ await update((snap) => ({ ...snap, manager: patchManagerFields(snap.manager, patch) }));
861
+ return toBoard(current.manager);
862
+ },
863
+ markAuthorization: async (threadId) => {
864
+ await ready;
865
+ const session = recordAuthorization(sessionFor(threadId));
866
+ if (threadId.length > 0) await saveSession(threadId, session);
867
+ return session;
868
+ },
869
+ markReport: async (threadId) => {
870
+ await ready;
871
+ const opened = openReport(sessionFor(threadId));
872
+ if (opened.error === null && threadId.length > 0) await saveSession(threadId, opened.session);
873
+ return opened;
874
+ },
875
+ markPhase: async (threadId, phase) => {
876
+ await ready;
877
+ const session = sessionFor(threadId);
878
+ if (phase !== "authorization" && phase !== "scope" && phase !== "evidence" && phase !== "report") {
879
+ return session;
880
+ }
881
+ const next = setPhase(session, phase);
882
+ if (threadId.length > 0) await saveSession(threadId, next);
883
+ return next;
884
+ },
885
+ remind: async (threadId) => {
886
+ await ready;
887
+ if (current.manager.modules["auto-advance"] !== "installed") {
888
+ return { ok: false, error: "auto-advance is not installed" };
889
+ }
890
+ if (threadId.length === 0) return { ok: false, error: "thread-missing" };
891
+ const execution = await ctx.projections.getThreadExecution(threadId);
892
+ if (execution === null) return { ok: false, error: "thread-missing" };
893
+ await ctx.commands.dispatch(reminderCommand(threadId, execution, now()));
894
+ return { ok: true };
895
+ }
896
+ };
897
+ return runtime;
898
+ }
899
+ function reminderCommand(threadId, execution, at) {
900
+ return {
901
+ type: "thread.turn.start",
902
+ commandId: NodeCrypto.randomUUID(),
903
+ threadId,
904
+ message: {
905
+ messageId: NodeCrypto.randomUUID(),
906
+ role: "user",
907
+ text: REMINDER_TEXT,
908
+ attachments: []
909
+ },
910
+ runtimeMode: execution.runtimeMode,
911
+ interactionMode: execution.interactionMode,
912
+ createdAt: at,
913
+ ...execution.modelSelection == null ? {} : { modelSelection: execution.modelSelection }
914
+ };
915
+ }
916
+
917
+ // src/tools.ts
918
+ var objectSchema2 = (properties, required = []) => ({
919
+ type: "object",
920
+ properties,
921
+ ...required.length === 0 ? {} : { required: [...required] }
922
+ });
923
+ function text(value, isError = false) {
924
+ return isError ? { text: value, isError: true } : { text: value };
925
+ }
926
+ function readString2(value) {
927
+ return typeof value === "string" ? value : "";
928
+ }
929
+ function readNumber(value) {
930
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.NaN;
931
+ }
932
+ function createTools(host) {
933
+ return [
934
+ {
935
+ name: "gates_list",
936
+ description: "List the four workflow gates for the selected mode: authorization, scope, evidence, and report. Does not describe a procedure.",
937
+ inputSchema: objectSchema2({}),
938
+ handle: async (_input, invocation) => {
939
+ await host.ready();
940
+ if (!host.moduleInstalled("stage-gate")) return text("stage-gate is not installed.", true);
941
+ const session = host.sessionFor(invocation.threadId);
942
+ const mode = modeById(session.modeId);
943
+ const forced = mode?.group === "professional";
944
+ return text(
945
+ `mode=${mode?.id ?? "none"} forced=${forced ? "yes" : "no"} gates=${PHASES.join(",")} phase=${session.phase}`
946
+ );
947
+ }
948
+ },
949
+ {
950
+ name: "operation_goal",
951
+ description: "Store a short goal for this session. The text is a label, not a procedure.",
952
+ inputSchema: objectSchema2({ goal: { type: "string", description: "Short goal label." } }, [
953
+ "goal"
954
+ ]),
955
+ handle: async (input, invocation) => {
956
+ await host.ready();
957
+ if (!host.moduleInstalled("results")) return text("results is not installed.", true);
958
+ const session = setGoal(host.sessionFor(invocation.threadId), readString2(input["goal"]));
959
+ await host.save(invocation.threadId, session);
960
+ return text(`goal=${session.goal}`);
961
+ }
962
+ },
963
+ {
964
+ name: "operation_progress",
965
+ description: "Store done and total counts for the session progress chip.",
966
+ inputSchema: objectSchema2(
967
+ {
968
+ done: { type: "number", description: "Completed count." },
969
+ total: { type: "number", description: "Total count." }
970
+ },
971
+ ["done", "total"]
972
+ ),
973
+ handle: async (input, invocation) => {
974
+ await host.ready();
975
+ if (!host.moduleInstalled("results")) return text("results is not installed.", true);
976
+ const session = setProgress(
977
+ host.sessionFor(invocation.threadId),
978
+ readNumber(input["done"]),
979
+ readNumber(input["total"])
980
+ );
981
+ await host.save(invocation.threadId, session);
982
+ return text(`progress=${session.done}/${session.total}`);
983
+ }
984
+ },
985
+ {
986
+ name: "publish",
987
+ description: "Open the report gate after authorization is recorded. The guard can refuse this call before it runs.",
988
+ inputSchema: objectSchema2({}),
989
+ handle: async (_input, invocation) => {
990
+ await host.ready();
991
+ const current = host.sessionFor(invocation.threadId);
992
+ const opened = openReport(recordAuthorization(current));
993
+ if (opened.error !== null) return text(opened.error, true);
994
+ await host.save(invocation.threadId, opened.session);
995
+ return text("report gate open");
996
+ }
997
+ },
998
+ {
999
+ name: "probe",
1000
+ description: "Harmless call used to show a guard refusal. It does not touch the workspace.",
1001
+ inputSchema: objectSchema2({}),
1002
+ handle: async () => text("probe ran")
1003
+ },
1004
+ {
1005
+ name: "ledger_note",
1006
+ description: "Append a short note to this session ledger.",
1007
+ inputSchema: objectSchema2({ note: { type: "string", description: "Short note." } }, ["note"]),
1008
+ handle: async (input, invocation) => {
1009
+ await host.ready();
1010
+ if (!host.moduleInstalled("trace-vault"))
1011
+ return text("trace-vault is not installed.", true);
1012
+ const session = pushNote(host.sessionFor(invocation.threadId), readString2(input["note"]));
1013
+ await host.save(invocation.threadId, session);
1014
+ return text("note stored");
1015
+ }
1016
+ }
1017
+ ];
1018
+ }
1019
+
1020
+ // src/index.ts
1021
+ var redteamModelPlugin = definePlugin({
1022
+ id: "redteam-model",
1023
+ apiVersion: 4,
1024
+ activate(ctx) {
1025
+ const runtime = openRuntime(ctx);
1026
+ ctx.sessionProjections.register({ name: PROJECTION_NAME });
1027
+ ctx.systemPrompt.context((turn) => runtime.contextFor(turn.threadId));
1028
+ ctx.tools.guard((input) => runtime.guard(input));
1029
+ ctx.toolkit({ name: "modes", tools: createTools(runtime) });
1030
+ ctx.reactor({
1031
+ name: "remember-preset",
1032
+ events: ["thread.created"],
1033
+ handle: (event) => runtime.rememberCreated(event)
1034
+ });
1035
+ registerCommands(ctx, runtime);
1036
+ }
1037
+ });
1038
+ var index_default = redteamModelPlugin;
1039
+ export {
1040
+ index_default as default
1041
+ };