Skip to content

Commit 011f3d9

Browse files
committed
fix(apply): accept safe page-model options
1 parent c20f798 commit 011f3d9

7 files changed

Lines changed: 144 additions & 49 deletions

batch/codex-apply-one.prompt.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ dispatch, or any other denial is terminal.
186186
Write required narrative answers in the candidate's plain, specific voice
187187
using only `cv.md`, the report, and the facts file. If a required field
188188
cannot be answered truthfully, stop before Submit.
189-
11. Build one immutable actions array matching the installed Geometra 1.65.6
189+
11. Build one immutable actions array matching the installed Geometra 1.65.7
190190
schema exactly. Do not use obsolete `valuesByLabel`, nested `target`, or
191191
`labelOrText` properties. Use this shape:
192192

@@ -279,7 +279,7 @@ dispatch, or any other denial is terminal.
279279
12. Execute one logical `geometra_run_actions` transaction using the owned
280280
session ID, `softTimeoutMs: 45000`, `output: "full"`,
281281
`includeSteps: true`, and the immutable actions array. Do not use the
282-
compact batched fill path: Geometra 1.65.6 can abort the whole grouped text
282+
compact batched fill path: Geometra 1.65.7 can abort the whole grouped text
283283
action there without identifying the field, while the sequential in-call
284284
path is stable and preserves exact-once submission. If it returns a soft
285285
pause with `resumeFromIndex`, continuation is allowed only with the same

bin/geometra-mcp-launcher.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
55
import { dirname, join, resolve } from 'node:path';
66
import { fileURLToPath } from 'node:url';
77

8-
const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.65.6';
8+
const DEFAULT_FALLBACK_PACKAGE = '@geometra/mcp@1.65.7';
99
const RESOLVE_ONLY_FLAG = '--job-forge-resolve-target';
1010
const DEFAULT_LOG_RELATIVE_PATH = '.jobforge-mcp/geometra-mcp.jsonl';
1111
const DEFAULT_HEARTBEAT_MS = 15_000;

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "job-forge",
3-
"version": "2.14.70",
3+
"version": "2.14.71",
44
"description": "AI-powered job search pipeline for Codex and OpenCode",
55
"type": "module",
66
"bin": {
@@ -219,7 +219,7 @@
219219
"@agent-pattern-labs/iso-score": "^0.1.2",
220220
"@agent-pattern-labs/iso-timeline": "^0.1.2",
221221
"@agent-pattern-labs/iso-trace": "^0.5.2",
222-
"@geometra/mcp": "1.65.6",
222+
"@geometra/mcp": "1.65.7",
223223
"@modelcontextprotocol/sdk": "1.29.0",
224224
"@razroo/gmail-mcp": "1.8.1",
225225
"playwright": "1.61.1",

scripts/apply-worker-guard.mjs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ const RECOVERABLE_CONTROL_DENIAL = 'Tool update_plan is outside the live-applica
103103
const RECOVERABLE_PRE_SUBMIT_QUERY_DENIAL = 'Geometra query is limited to one exact zero-form Application-tab discovery or bounded post-submit evidence.';
104104
const RECOVERABLE_PRE_SCHEMA_RUN_ACTIONS_DENIAL = 'Geometra run_actions requires successful parsed connect, page-model, and form-schema responses.';
105105
const RECOVERABLE_PRE_NAVIGATION_FORM_SCHEMA_DENIAL = 'Geometra form schema requires parsed page-model form evidence or one proven application-navigation click.';
106+
const PAGE_MODEL_INPUT_DENIAL_CATEGORY = 'geometra-page-model-input';
107+
const PAGE_MODEL_INPUT_KEYS = [
108+
'sessionId',
109+
'maxPrimaryActions',
110+
'maxSectionsPerKind',
111+
'includeScreenshot',
112+
'blockDetection',
113+
'blockedSitePolicy',
114+
'manualHandoff',
115+
];
106116
const PATH_VALUE_OPTIONS = new Set([
107117
'--input',
108118
'--output',
@@ -239,12 +249,21 @@ export function evaluateApplyWorkerToolCall(input, currentState = initialGuardSt
239249
decision = deny(`Tool ${toolName || '(unknown)'} is outside the live-application allowlist.`);
240250
}
241251

242-
state.events.push({ tool: toolName || '(unknown)', decision: decision.allow ? 'allow' : 'deny' });
252+
const event = { tool: toolName || '(unknown)', decision: decision.allow ? 'allow' : 'deny' };
253+
if (!decision.allow && decision.denialCategory) {
254+
event.denialCategory = decision.denialCategory;
255+
event.inputKeys = [...decision.inputKeys];
256+
}
257+
state.events.push(event);
243258
if (!decision.allow) {
244259
const denial = {
245260
tool: toolName || '(unknown)',
246261
reason: decision.reason,
247262
eventIndex: state.events.length - 1,
263+
...(decision.denialCategory ? {
264+
denialCategory: decision.denialCategory,
265+
inputKeys: [...decision.inputKeys],
266+
} : {}),
248267
};
249268
if (isRecoverableGuardDenial(toolName, toolInput, decision, state)) {
250269
state.recoverableDenials.push(denial);
@@ -422,11 +441,8 @@ function evaluateGeometra(tool, args, state) {
422441
if (!session.allow) return session;
423442
if (tool === 'geometra_page_model') {
424443
if (state.pageModelCalls !== 0) return deny('Only one Geometra page-model call is allowed before submission.');
425-
if (args.blockDetection !== true
426-
|| args.blockedSitePolicy !== 'manual-handoff'
427-
|| !hasOnlyKeys(args, ['sessionId', 'blockDetection', 'blockedSitePolicy'])) {
428-
return deny('Geometra page model requires only the owned session with block detection and manual handoff enforced.');
429-
}
444+
const inputDecision = evaluatePageModelInput(args);
445+
if (!inputDecision.allow) return inputDecision;
430446
state.pageModelBlockDetectionEnforced = true;
431447
state.pageModelCalls += 1;
432448
}
@@ -1335,6 +1351,50 @@ function hasOnlyKeys(value, allowedKeys) {
13351351
return Object.keys(value || {}).every((key) => allowed.has(key));
13361352
}
13371353

1354+
function evaluatePageModelInput(args) {
1355+
if (!hasOnlyKeys(args, PAGE_MODEL_INPUT_KEYS)) {
1356+
return denyPageModelInput('Geometra page model contains fields outside the supported safe schema.', args);
1357+
}
1358+
if (args.blockDetection !== true) {
1359+
return denyPageModelInput('Geometra page model requires blockDetection=true.', args);
1360+
}
1361+
if (args.blockedSitePolicy !== 'manual-handoff') {
1362+
return denyPageModelInput('Geometra page model requires blockedSitePolicy="manual-handoff".', args);
1363+
}
1364+
if (args.includeScreenshot !== undefined && args.includeScreenshot !== false) {
1365+
return denyPageModelInput('Geometra page model screenshots must be omitted or disabled.', args);
1366+
}
1367+
if (args.manualHandoff !== undefined && typeof args.manualHandoff !== 'boolean') {
1368+
return denyPageModelInput('Geometra page model manualHandoff must be boolean when provided.', args);
1369+
}
1370+
if (args.maxPrimaryActions !== undefined
1371+
&& (!Number.isInteger(args.maxPrimaryActions)
1372+
|| args.maxPrimaryActions < 1
1373+
|| args.maxPrimaryActions > 12)) {
1374+
return denyPageModelInput('Geometra page model maxPrimaryActions must be an integer from 1 through 12.', args);
1375+
}
1376+
if (args.maxSectionsPerKind !== undefined
1377+
&& (!Number.isInteger(args.maxSectionsPerKind)
1378+
|| args.maxSectionsPerKind < 1
1379+
|| args.maxSectionsPerKind > 16)) {
1380+
return denyPageModelInput('Geometra page model maxSectionsPerKind must be an integer from 1 through 16.', args);
1381+
}
1382+
return allow();
1383+
}
1384+
1385+
function denyPageModelInput(reason, args) {
1386+
return deny(reason, {
1387+
denialCategory: PAGE_MODEL_INPUT_DENIAL_CATEGORY,
1388+
inputKeys: safeDiagnosticInputKeys(args),
1389+
});
1390+
}
1391+
1392+
function safeDiagnosticInputKeys(value) {
1393+
return [...new Set(Object.keys(value || {}).map((key) => (
1394+
/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key) ? key : '[redacted-key]'
1395+
)))].sort();
1396+
}
1397+
13381398
function jobIdFromUrl(value) {
13391399
try {
13401400
return new URL(value).pathname.split('/').filter(Boolean).at(-1) || '';
@@ -1797,8 +1857,8 @@ function allow() {
17971857
return { allow: true, reason: null };
17981858
}
17991859

1800-
function deny(reason) {
1801-
return { allow: false, reason };
1860+
function deny(reason, diagnostics = {}) {
1861+
return { allow: false, reason, ...diagnostics };
18021862
}
18031863

18041864
function parseStatePath(argv) {

test/apply-worker-guard.test.mjs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -416,29 +416,65 @@ test('malformed connect responses retain independent block evidence', () => {
416416
}
417417
});
418418

419-
test('page-model discovery requires the exact block-detecting request', () => {
419+
test('page-model discovery accepts the safe Geometra schema and expanded defaults', () => {
420420
for (const toolInput of [
421-
{ sessionId: 'session-1' },
422-
{ ...pageModelInput, blockDetection: false },
423-
{ ...pageModelInput, blockedSitePolicy: 'continue' },
424-
{ ...pageModelInput, includeScreenshot: false },
421+
pageModelInput,
422+
{
423+
...pageModelInput,
424+
maxPrimaryActions: 6,
425+
maxSectionsPerKind: 8,
426+
includeScreenshot: false,
427+
manualHandoff: false,
428+
},
429+
{
430+
...pageModelInput,
431+
maxPrimaryActions: 12,
432+
maxSectionsPerKind: 16,
433+
includeScreenshot: false,
434+
manualHandoff: true,
435+
},
425436
]) {
426437
const result = evaluateApplyWorkerToolCall({
427438
tool_name: 'mcp__geometra__geometra_page_model',
428439
tool_input: toolInput,
429440
}, successfulConnect(initialGuardState(JOB_URL, PROJECT_ROOT)));
430-
assert.equal(result.allow, false, JSON.stringify(toolInput));
431-
assert.equal(result.state.pageModelCalls, 0, JSON.stringify(toolInput));
432-
assert.equal(result.state.pageModelBlockDetectionEnforced, false, JSON.stringify(toolInput));
441+
assert.equal(result.allow, true, result.reason);
442+
assert.equal(result.state.pageModelCalls, 1);
443+
assert.equal(result.state.pageModelBlockDetectionEnforced, true);
433444
}
445+
});
434446

435-
const allowed = evaluateApplyWorkerToolCall({
436-
tool_name: 'mcp__geometra__geometra_page_model',
437-
tool_input: pageModelInput,
438-
}, successfulConnect(initialGuardState(JOB_URL, PROJECT_ROOT)));
439-
assert.equal(allowed.allow, true, allowed.reason);
440-
assert.equal(allowed.state.pageModelCalls, 1);
441-
assert.equal(allowed.state.pageModelBlockDetectionEnforced, true);
447+
test('page-model discovery rejects unsafe schema values with value-free diagnostics', () => {
448+
for (const [label, toolInput] of [
449+
['missing block detection', { sessionId: 'session-1', blockedSitePolicy: 'manual-handoff' }],
450+
['disabled block detection', { ...pageModelInput, blockDetection: false }],
451+
['wrong blocked-site policy', { ...pageModelInput, blockedSitePolicy: 'continue' }],
452+
['enabled screenshot', { ...pageModelInput, includeScreenshot: true }],
453+
['non-boolean screenshot', { ...pageModelInput, includeScreenshot: 'false' }],
454+
['non-boolean handoff', { ...pageModelInput, manualHandoff: 'false' }],
455+
['primary cap below range', { ...pageModelInput, maxPrimaryActions: 0 }],
456+
['primary cap above range', { ...pageModelInput, maxPrimaryActions: 13 }],
457+
['primary cap non-integer', { ...pageModelInput, maxPrimaryActions: 1.5 }],
458+
['section cap below range', { ...pageModelInput, maxSectionsPerKind: 0 }],
459+
['section cap above range', { ...pageModelInput, maxSectionsPerKind: 17 }],
460+
['section cap non-integer', { ...pageModelInput, maxSectionsPerKind: 1.5 }],
461+
['unknown key', { ...pageModelInput, unsupportedOption: 'sensitive-value' }],
462+
]) {
463+
const result = evaluateApplyWorkerToolCall({
464+
tool_name: 'mcp__geometra__geometra_page_model',
465+
tool_input: toolInput,
466+
}, successfulConnect(initialGuardState(JOB_URL, PROJECT_ROOT)));
467+
const expectedKeys = Object.keys(toolInput).sort();
468+
assert.equal(result.allow, false, label);
469+
assert.equal(result.state.pageModelCalls, 0, label);
470+
assert.equal(result.state.pageModelBlockDetectionEnforced, false, label);
471+
assert.equal(result.state.violations.length, 1, label);
472+
assert.equal(result.state.violations[0].denialCategory, 'geometra-page-model-input', label);
473+
assert.deepEqual(result.state.violations[0].inputKeys, expectedKeys, label);
474+
assert.equal(result.state.events.at(-1).denialCategory, 'geometra-page-model-input', label);
475+
assert.deepEqual(result.state.events.at(-1).inputKeys, expectedKeys, label);
476+
assert.equal(JSON.stringify(result.state).includes('sensitive-value'), false, label);
477+
}
442478
});
443479

444480
test('page-model and form-schema capabilities unlock only after successful structured responses', () => {

test/geometra-release-chain.test.mjs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
77
import test from 'node:test';
88

99
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10-
const GEOMETRA_VERSION = '1.65.6';
11-
1210
async function readJson(path) {
1311
return JSON.parse(await readFile(path, 'utf8'));
1412
}
@@ -20,15 +18,16 @@ test('JobForge pins and resolves the released Geometra dependency chain', async
2018
const installedProxy = await readJson(join(ROOT, 'node_modules/@geometra/proxy/package.json'));
2119
const launcher = await readFile(join(ROOT, 'bin/geometra-mcp-launcher.mjs'), 'utf8');
2220
const prompt = await readFile(join(ROOT, 'batch/codex-apply-one.prompt.md'), 'utf8');
21+
const geometraVersion = pkg.dependencies['@geometra/mcp'];
2322

24-
assert.equal(pkg.dependencies['@geometra/mcp'], GEOMETRA_VERSION);
25-
assert.equal(lock.packages[''].dependencies['@geometra/mcp'], GEOMETRA_VERSION);
26-
assert.equal(lock.packages['node_modules/@geometra/mcp'].version, GEOMETRA_VERSION);
27-
assert.equal(lock.packages['node_modules/@geometra/proxy'].version, GEOMETRA_VERSION);
28-
assert.equal(installedMcp.version, GEOMETRA_VERSION);
29-
assert.equal(installedProxy.version, GEOMETRA_VERSION);
30-
assert.ok(launcher.includes(`@geometra/mcp@${GEOMETRA_VERSION}`));
31-
assert.ok(prompt.includes(`installed Geometra ${GEOMETRA_VERSION}`));
23+
assert.match(geometraVersion, /^\d+\.\d+\.\d+$/);
24+
assert.equal(lock.packages[''].dependencies['@geometra/mcp'], geometraVersion);
25+
assert.equal(lock.packages['node_modules/@geometra/mcp'].version, geometraVersion);
26+
assert.equal(lock.packages['node_modules/@geometra/proxy'].version, geometraVersion);
27+
assert.equal(installedMcp.version, geometraVersion);
28+
assert.equal(installedProxy.version, geometraVersion);
29+
assert.ok(launcher.includes(`@geometra/mcp@${geometraVersion}`));
30+
assert.ok(prompt.includes(`installed Geometra ${geometraVersion}`));
3231

3332
const result = spawnSync(
3433
process.execPath,
@@ -43,7 +42,7 @@ test('JobForge pins and resolves the released Geometra dependency chain', async
4342
assert.equal(target.resolvedPath, siblingPath);
4443
} else {
4544
assert.equal(target.source, 'npm-package');
46-
assert.equal(target.packageSpec, `@geometra/mcp@${GEOMETRA_VERSION}`);
45+
assert.equal(target.packageSpec, `@geometra/mcp@${geometraVersion}`);
4746
}
4847
});
4948

0 commit comments

Comments
 (0)