icoa-cli 2.19.58 → 2.19.60
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/commands/ai4ctf.js +27 -0
- package/dist/commands/ctf4ai-demo.js +26 -0
- package/dist/commands/exam.js +23 -1
- package/dist/repl.js +32 -4
- package/package.json +1 -1
package/dist/commands/ai4ctf.js
CHANGED
|
@@ -300,6 +300,33 @@ export function registerAi4ctfCommand(program) {
|
|
|
300
300
|
.description('Chat with your AI teammate')
|
|
301
301
|
.action(async () => {
|
|
302
302
|
logCommand('ai4ctf');
|
|
303
|
+
// Block mid-exam: ai4ctf starts a scripted demo challenge, which would
|
|
304
|
+
// interrupt a real exam session. Redirect to the actual exam AI tool.
|
|
305
|
+
const { getRealExamState } = await import('../lib/exam-state.js');
|
|
306
|
+
const realExam = getRealExamState();
|
|
307
|
+
if (realExam) {
|
|
308
|
+
const currentQ = realExam._lastQ || 1;
|
|
309
|
+
const inAi4ctfRange = currentQ >= 31 && currentQ <= 38;
|
|
310
|
+
console.log();
|
|
311
|
+
console.log(chalk.yellow(' ⚠ You are in a real exam — ai4ctf demo is blocked here.'));
|
|
312
|
+
console.log();
|
|
313
|
+
if (inAi4ctfRange) {
|
|
314
|
+
console.log(chalk.white(` You are on Q${currentQ} (AI4CTF section).`));
|
|
315
|
+
console.log(chalk.white(' To ask the AI about this question:'));
|
|
316
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan(`hint "your question"`));
|
|
317
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`hint "how do I decrypt AES-CBC in Python?"`));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.log(chalk.white(` You are on Q${currentQ}. The AI4CTF section is Q31–38.`));
|
|
321
|
+
console.log(chalk.gray(' Jump there: ') + chalk.bold.cyan('exam q 31'));
|
|
322
|
+
console.log(chalk.gray(' Then ask the AI with: ') + chalk.white('hint "your question"'));
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
console.log(chalk.gray(' ai4ctf as a chat command is demo-only. In the real exam,'));
|
|
326
|
+
console.log(chalk.gray(' `hint` is the AI interface — 25K AI4CTF + 25K CTF4AI tokens.'));
|
|
327
|
+
console.log();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
303
330
|
const config = getConfig();
|
|
304
331
|
const modelName = config.geminiModel || 'gemma-4-31b-it';
|
|
305
332
|
// Demo challenge context
|
|
@@ -170,6 +170,32 @@ export function registerCtf4aiDemoCommand(program) {
|
|
|
170
170
|
.description('CTF4AI Demo — Prompt injection challenge')
|
|
171
171
|
.action(async () => {
|
|
172
172
|
logCommand('ctf4ai');
|
|
173
|
+
// Block mid-exam: ctf4ai starts a scripted koala demo, would derail
|
|
174
|
+
// a real exam session and burn AI tokens against the wrong budget.
|
|
175
|
+
const { getRealExamState } = await import('../lib/exam-state.js');
|
|
176
|
+
const realExam = getRealExamState();
|
|
177
|
+
if (realExam) {
|
|
178
|
+
const currentQ = realExam._lastQ || 1;
|
|
179
|
+
const inCtf4aiRange = currentQ >= 39;
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.yellow(' ⚠ You are in a real exam — ctf4ai demo is blocked here.'));
|
|
182
|
+
console.log();
|
|
183
|
+
if (inCtf4aiRange) {
|
|
184
|
+
console.log(chalk.white(` You are on Q${currentQ} (CTF4AI section).`));
|
|
185
|
+
console.log(chalk.white(' The question scenario itself is the AI target — read Q39/Q40'));
|
|
186
|
+
console.log(chalk.white(' carefully. To ask the AI assistant for help:'));
|
|
187
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan(`hint "your question"`));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(chalk.white(` You are on Q${currentQ}. The CTF4AI section is Q39–40.`));
|
|
191
|
+
console.log(chalk.gray(' Jump there: ') + chalk.bold.cyan('exam q 39'));
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(chalk.gray(' ctf4ai as a standalone command is demo-only. In the real exam,'));
|
|
195
|
+
console.log(chalk.gray(' each Q39/Q40 scenario contains its own AI target to attack.'));
|
|
196
|
+
console.log();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
173
199
|
if (ctf4aiActive) {
|
|
174
200
|
console.log(chalk.gray(` ${t('ctf4aiAlready')}`));
|
|
175
201
|
return;
|
package/dist/commands/exam.js
CHANGED
|
@@ -326,6 +326,21 @@ function printQuestionProgress(current, total, answered) {
|
|
|
326
326
|
const pct = Math.round((current / total) * 100);
|
|
327
327
|
console.log();
|
|
328
328
|
console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
|
|
329
|
+
// Time remaining — colour-coded countdown so contestants always see where
|
|
330
|
+
// they stand. Authoritative clock lives on the server (confirmedAt +
|
|
331
|
+
// duration enforced at submit time); this is a display helper.
|
|
332
|
+
const deadline = getExamDeadline();
|
|
333
|
+
if (deadline) {
|
|
334
|
+
const remainingSec = Math.max(0, Math.round((deadline.getTime() - Date.now()) / 1000));
|
|
335
|
+
const mm = Math.floor(remainingSec / 60);
|
|
336
|
+
const ss = remainingSec % 60;
|
|
337
|
+
const timeStr = `${mm}:${String(ss).padStart(2, '0')}`;
|
|
338
|
+
const color = remainingSec <= 60 ? chalk.red.bold
|
|
339
|
+
: remainingSec <= 300 ? chalk.red
|
|
340
|
+
: remainingSec <= 600 ? chalk.yellow
|
|
341
|
+
: chalk.gray;
|
|
342
|
+
console.log(` ${chalk.gray('⏱ Time remaining:')} ${color(timeStr)} ${chalk.gray('(server-authoritative)')}`);
|
|
343
|
+
}
|
|
329
344
|
}
|
|
330
345
|
// Help budget per exam.md §3: 10 base + 5 hidden bonus (unlocked via `more help`).
|
|
331
346
|
// Demo still uses the lighter 5 + 3 set via _helpMax overrides at demo start.
|
|
@@ -967,12 +982,19 @@ export function registerExamCommand(program) {
|
|
|
967
982
|
const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
968
983
|
let c;
|
|
969
984
|
if (isPractical) {
|
|
970
|
-
// Accept flag format: ICOA{...} or any string
|
|
971
985
|
c = choice.trim();
|
|
972
986
|
if (!c) {
|
|
973
987
|
printError('Please provide your flag: exam answer <n> ICOA{your_flag}');
|
|
974
988
|
return;
|
|
975
989
|
}
|
|
990
|
+
// Reject letter-only answers on practical questions — almost certainly
|
|
991
|
+
// a user who typed `A` thinking MCQ was still in play. Submitting 'A'
|
|
992
|
+
// as the flag is a footgun that would waste an attempt silently.
|
|
993
|
+
if (/^[A-Da-d]$/.test(c)) {
|
|
994
|
+
printError(`Q${num} is a practical question — answer with a flag, not a letter.`);
|
|
995
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${num} ICOA{your_flag}`));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
976
998
|
}
|
|
977
999
|
else {
|
|
978
1000
|
c = choice.toUpperCase();
|
package/dist/repl.js
CHANGED
|
@@ -597,14 +597,32 @@ export async function startRepl(program, resumeMode) {
|
|
|
597
597
|
return;
|
|
598
598
|
}
|
|
599
599
|
// ─── Quick exam answer shortcuts ───
|
|
600
|
-
// "A" / "B" / "C" / "D" → answer current question
|
|
601
|
-
// "2 C" / "5 A" → answer specific question
|
|
600
|
+
// "A" / "B" / "C" / "D" → answer current question (MCQ only)
|
|
601
|
+
// "2 C" / "5 A" → answer specific question (MCQ only)
|
|
602
|
+
// Practical questions (Q31-40) require flag format (ICOA{...}) — single
|
|
603
|
+
// letters on those would be silently accepted as the wrong flag answer,
|
|
604
|
+
// which is a footgun. Block and nudge the user toward the flag syntax.
|
|
602
605
|
const examState = getExamState();
|
|
603
606
|
if (examState) {
|
|
604
607
|
const upper = input.toUpperCase().trim();
|
|
605
|
-
//
|
|
608
|
+
// Helper: is question N practical (no A/B/C/D options)?
|
|
609
|
+
const isPracticalQ = (n) => {
|
|
610
|
+
const q = examState.questions.find((qq) => qq.number === n);
|
|
611
|
+
if (!q)
|
|
612
|
+
return false;
|
|
613
|
+
return q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
614
|
+
};
|
|
615
|
+
// Single letter: A, B, C, D → answer current question (only if MCQ)
|
|
606
616
|
if (/^[ABCD]$/.test(upper)) {
|
|
607
617
|
const currentQ = examState._lastQ || 1;
|
|
618
|
+
if (isPracticalQ(currentQ)) {
|
|
619
|
+
console.log();
|
|
620
|
+
console.log(chalk.yellow(` Q${currentQ} is a practical question — answer with a flag, not a letter.`));
|
|
621
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${currentQ} ICOA{your_flag}`));
|
|
622
|
+
console.log();
|
|
623
|
+
rl.prompt();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
608
626
|
processing = true;
|
|
609
627
|
try {
|
|
610
628
|
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
@@ -614,9 +632,18 @@ export async function startRepl(program, resumeMode) {
|
|
|
614
632
|
rl.prompt();
|
|
615
633
|
return;
|
|
616
634
|
}
|
|
617
|
-
// "N X" pattern: e.g. "2 C", "15 A"
|
|
635
|
+
// "N X" pattern: e.g. "2 C", "15 A" (MCQ only — same protection)
|
|
618
636
|
const match = upper.match(/^(\d+)\s+([ABCD])$/);
|
|
619
637
|
if (match) {
|
|
638
|
+
const targetQ = parseInt(match[1], 10);
|
|
639
|
+
if (isPracticalQ(targetQ)) {
|
|
640
|
+
console.log();
|
|
641
|
+
console.log(chalk.yellow(` Q${targetQ} is a practical question — answer with a flag, not a letter.`));
|
|
642
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${targetQ} ICOA{your_flag}`));
|
|
643
|
+
console.log();
|
|
644
|
+
rl.prompt();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
620
647
|
processing = true;
|
|
621
648
|
try {
|
|
622
649
|
await program.parseAsync(['node', 'icoa', 'exam', 'answer', match[1], match[2]]);
|
|
@@ -667,6 +694,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
667
694
|
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
|
|
668
695
|
'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
|
|
669
696
|
'exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'logout', 'ctf4ai',
|
|
697
|
+
'mark', 'unmark', 'review', 'submit',
|
|
670
698
|
];
|
|
671
699
|
if (!knownCommands.includes(cmd)) {
|
|
672
700
|
// Block dangerous commands
|