mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
fix(providers): D21 — validateProvider enforces maxSpawnTimeMs contract field (round-2 F1)
cold-audit catch from 2026-05-24
Round-2 cold-audit Finding 1 (P2 contract drift). ADR 0002 Amendment 1
(D11, commit f659e29) added `maxSpawnTimeMs` to the Provider contract
v1.0 hints set alongside requiresTTY/concurrentSpawnSafe/maxConcurrent.
But `lib/providers/base.mjs` validateProvider was never updated —
ProviderHints typedef listed only the original 3 fields, and the
validator's error message + per-field checks ignored maxSpawnTimeMs
entirely. A plugin that omitted the field would still pass validation;
each plugin's `?? 600_000` runtime default masked the omission at the
spawn site. Round-1 cold audit + D11 diff review both missed this.
Changes (lib/providers/base.mjs +6 / -1, test-features.mjs +36):
1. ProviderHints typedef extended:
`@property {number} [maxSpawnTimeMs] - optional integer milliseconds, default 600000`
The `[name]` JSDoc marker correctly indicates optional.
2. Hints existence error message updated to include the new field:
`'hints must be an object with { requiresTTY, concurrentSpawnSafe, maxConcurrent, maxSpawnTimeMs }'`
3. Per-field validation added inside the hints-object branch:
```
if (p.hints.maxSpawnTimeMs !== undefined) {
if (typeof !== 'number' || !Number.isInteger(...) || <= 0) push error
}
```
The `!== undefined` outer guard correctly short-circuits omission to
"valid" (the field is optional per ADR). The inner check rejects:
negative, zero, non-integer (catches floats, NaN, Infinity since
Number.isInteger handles all non-finite cases), and non-number types.
4. test-features.mjs Suite 4: 6 new tests covering all rejection axes
+ both positive-path cases (omitted-is-valid, positive-integer-is-valid):
- rejects negative
- rejects zero
- rejects non-integer (100.5)
- rejects non-number ('600' string)
- accepts omitted (optional field)
- accepts positive integer (60000)
Tests: 328 → 334 (+6). All three shipped plugins (anthropic / codex /
mistral) declare maxSpawnTimeMs: 600_000 (positive integer) and continue
to pass validation. Suite 16 spawn-timeout tests confirm the existing
positive integer flows through to the runtime enforcement loop unchanged.
Authority:
- ADR 0002 § Amendments § Amendment 1 (D11, 2026-05-23) — establishes
maxSpawnTimeMs as part of the v1.0 hints contract
https://github.com/dtzp555-max/olp/blob/main/docs/adr/0002-plugin-architecture.md
- ADR 0004 § Trigger taxonomy — Hard triggers bullet 4 — the contract
field's use site (per-plugin spawn-timeout enforcement → SPAWN_TIMEOUT
hard trigger)
- CC 开发铁律 v1.6 § 10.x — Round-2 Cold Audit caught this
Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent
of drafter): APPROVE. Verified ADR Amendment 1 wording matches validator
behavior; verified all 4 rejection axes plus 2 positive axes covered
by tests; verified existing plugin declarations continue to pass via
334/334 (Suite 16 spawn-timeout suite included). No blocking issues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -521,6 +521,42 @@ describe('Provider contract validation', () => {
|
||||
assert.ok(r.errors.some(e => e.includes('maxConcurrent')));
|
||||
});
|
||||
|
||||
it('validateProvider rejects negative maxSpawnTimeMs', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: -1 } }));
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs')));
|
||||
});
|
||||
|
||||
it('validateProvider rejects zero maxSpawnTimeMs', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 0 } }));
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs')));
|
||||
});
|
||||
|
||||
it('validateProvider rejects non-integer maxSpawnTimeMs (e.g., 100.5)', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 100.5 } }));
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs')));
|
||||
});
|
||||
|
||||
it('validateProvider rejects non-number maxSpawnTimeMs (e.g., \'600\')', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: '600' } }));
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs')));
|
||||
});
|
||||
|
||||
it('validateProvider accepts omitted maxSpawnTimeMs (optional field)', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4 } }));
|
||||
assert.equal(r.valid, true);
|
||||
assert.deepEqual(r.errors, []);
|
||||
});
|
||||
|
||||
it('validateProvider accepts positive integer maxSpawnTimeMs (e.g., 60000)', () => {
|
||||
const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 60000 } }));
|
||||
assert.equal(r.valid, true);
|
||||
assert.deepEqual(r.errors, []);
|
||||
});
|
||||
|
||||
it('rejects non-object input', () => {
|
||||
const r = validateProvider(null);
|
||||
assert.equal(r.valid, false);
|
||||
|
||||
Reference in New Issue
Block a user