Enforce numeric bounds in tool schemas #237

Merged
lost-rob0t merged 7 commits from fix/234-numeric-schema-bounds into main 2026-08-27 02:15:47 +00:00
lost-rob0t commented 2026-08-26 09:22:41 +00:00 (Migrated from github.com)

Fixes #234.

RAGE slice

TDD-first from exact canonical main 267697bef10a3fffff7c093e1435ece770e7444b. Current exact candidate head is bdc8bd25faacb7c6f056f88aa18c6c536ef700f8.

The bug was that rlm_tool accepted declarative numeric schema bounds but only checked primitive integer/number type at invocation. This made real core schemas such as minimum:1, maximum:32 and #175's exclusiveMinimum:0 provider-visible but unenforced by the canonical tool boundary.

TDD contract

The first branch commit (86bbc93d866e2e409206617ac5670a70197c334b) pinned the behavior before implementation. Negative cases assert the expected structured rejection while the suite stays green; no xfail, skip, swallowed error, caller-side workaround, or CI weakening is used.

Focused contracts cover:

  • exclusiveMinimum:0 rejects zero and negatives before handler execution;
  • positive values pass;
  • integer minimum/maximum boundaries are exact;
  • exclusiveMaximum rejects the exact boundary;
  • malformed non-numeric bound metadata fails at registration;
  • contradictory/empty numeric intervals fail at registration;
  • nested argument paths remain visible in the schema error detail;
  • result schemas enforce the same bound semantics before returning success;
  • invalid arguments never call the trusted handler.

Analyze / research

Live source showed this is not an exclusiveMinimum-only feature gap: rlm_prompt_compiler already publishes the real search_tools limit schema with minimum:1 and maximum:32. The smallest coherent dialect is therefore the four numeric bound keywords already represented by current contracts:

  • minimum
  • maximum
  • exclusiveMinimum
  • exclusiveMaximum

No matching downstream implementation/ownership was found in lost-rob0t/a0-symbolics or lost-rob0t/agentProlog; this is generic runtime behavior.

Design / realization

The fix stays inside the existing validate_schema_definition/1 + validate_schema_value/3 path:

  • validate bound metadata for integer|number during registration;
  • reject contradictory lower/upper intervals before registration succeeds;
  • enforce the same four bounds for argument and result values;
  • preserve the existing tool_error{phase:schema,kind:schema_validation_failed,...} envelope and nested path;
  • preserve validation before capability/authority/effect/handler execution;
  • keep integer values valid for type:number as before.

No second schema subsystem, provider validator, authority path, effect path, scheduler, or downstream product logic was added.

Adversarial review

The first realization used a code-owned constructed comparison goal. Although the comparator was not model-controlled, that introduced unnecessary meta-call surface. It was replaced with closed explicit predicates for each supported bound, so no bound key or comparator becomes callable data.

A later adversarial pass found one unrelated metadata_context/2 no-op/singleton contaminating the branch. The cleanup was built as a detached Git object, inspected before the branch moved, then fast-forwarded as bdc8bd25faacb7c6f056f88aa18c6c536ef700f8. That cleanup commit changes exactly one hunk: it restores the canonical session metadata clause from !, Context = session(Session). to !. The complete candidate diff is now scope-clean and changes only:

  • prolog/rlm_tool.pl
  • test/rlm_tool_test.pl
  • docs/tools.md

docs/tools.md also corrects a stale ordering statement: the executable runtime intentionally validates arguments before capability authorization, and existing tests already enforce that behavior.

Exact-head verification

The exact candidate head bdc8bd25faacb7c6f056f88aa18c6c536ef700f8 was tested through synthetic merge commit b70bace37df7854178bddf076001301b840d5705, which merges that exact head into unchanged canonical main 267697bef10a3fffff7c093e1435ece770e7444b.

All returned required workflows are green on this head:

  • canonical CI: success;
  • deterministic PlUnit: 78 suites / 895 discovered / 895 passed / 0 failed / 0 timeout / 0 blocked / 0 fixme;
  • deterministic benchmark/conformance: 16/16 passed;
  • deterministic deep-recursion experiment: 15/15 passed;
  • credential-free CLI/trace and fresh-process graph/artifact restart checks: passed;
  • credential-backed REAL OpenRouter: passed;
  • pinned Paid OpenRouter: passed;
  • Nix flake: passed;
  • clean SWI pack install: passed;
  • Tree-sitter FFI: passed;
  • whitespace checks: passed.

Current main has not moved from the tested base. Current PR review submissions and unresolved review threads are empty. Any changed candidate head invalidates this evidence.

Non-goals

  • no #175/#221 timeout-policy rewrite;
  • no JSON Schema implementation project;
  • no provider-specific validation layer;
  • no Agent Zero/AgentProlog product code;
  • no CI weakening.
Fixes #234. ## RAGE slice TDD-first from exact canonical `main` `267697bef10a3fffff7c093e1435ece770e7444b`. Current exact candidate head is `bdc8bd25faacb7c6f056f88aa18c6c536ef700f8`. The bug was that `rlm_tool` accepted declarative numeric schema bounds but only checked primitive `integer/number` type at invocation. This made real core schemas such as `minimum:1, maximum:32` and #175's `exclusiveMinimum:0` provider-visible but unenforced by the canonical tool boundary. ## TDD contract The first branch commit (`86bbc93d866e2e409206617ac5670a70197c334b`) pinned the behavior before implementation. Negative cases assert the expected structured rejection while the suite stays green; no xfail, skip, swallowed error, caller-side workaround, or CI weakening is used. Focused contracts cover: - `exclusiveMinimum:0` rejects zero and negatives before handler execution; - positive values pass; - integer `minimum`/`maximum` boundaries are exact; - `exclusiveMaximum` rejects the exact boundary; - malformed non-numeric bound metadata fails at registration; - contradictory/empty numeric intervals fail at registration; - nested argument paths remain visible in the schema error detail; - result schemas enforce the same bound semantics before returning success; - invalid arguments never call the trusted handler. ## Analyze / research Live source showed this is not an `exclusiveMinimum`-only feature gap: `rlm_prompt_compiler` already publishes the real `search_tools` limit schema with `minimum:1` and `maximum:32`. The smallest coherent dialect is therefore the four numeric bound keywords already represented by current contracts: - `minimum` - `maximum` - `exclusiveMinimum` - `exclusiveMaximum` No matching downstream implementation/ownership was found in `lost-rob0t/a0-symbolics` or `lost-rob0t/agentProlog`; this is generic runtime behavior. ## Design / realization The fix stays inside the existing `validate_schema_definition/1` + `validate_schema_value/3` path: - validate bound metadata for `integer|number` during registration; - reject contradictory lower/upper intervals before registration succeeds; - enforce the same four bounds for argument and result values; - preserve the existing `tool_error{phase:schema,kind:schema_validation_failed,...}` envelope and nested path; - preserve validation before capability/authority/effect/handler execution; - keep integer values valid for `type:number` as before. No second schema subsystem, provider validator, authority path, effect path, scheduler, or downstream product logic was added. ## Adversarial review The first realization used a code-owned constructed comparison goal. Although the comparator was not model-controlled, that introduced unnecessary meta-call surface. It was replaced with closed explicit predicates for each supported bound, so no bound key or comparator becomes callable data. A later adversarial pass found one unrelated `metadata_context/2` no-op/singleton contaminating the branch. The cleanup was built as a detached Git object, inspected before the branch moved, then fast-forwarded as `bdc8bd25faacb7c6f056f88aa18c6c536ef700f8`. That cleanup commit changes exactly one hunk: it restores the canonical session metadata clause from `!, Context = session(Session).` to `!.` The complete candidate diff is now scope-clean and changes only: - `prolog/rlm_tool.pl` - `test/rlm_tool_test.pl` - `docs/tools.md` `docs/tools.md` also corrects a stale ordering statement: the executable runtime intentionally validates arguments before capability authorization, and existing tests already enforce that behavior. ## Exact-head verification The exact candidate head `bdc8bd25faacb7c6f056f88aa18c6c536ef700f8` was tested through synthetic merge commit `b70bace37df7854178bddf076001301b840d5705`, which merges that exact head into unchanged canonical `main` `267697bef10a3fffff7c093e1435ece770e7444b`. All returned required workflows are green on this head: - canonical CI: success; - deterministic PlUnit: **78 suites / 895 discovered / 895 passed / 0 failed / 0 timeout / 0 blocked / 0 fixme**; - deterministic benchmark/conformance: 16/16 passed; - deterministic deep-recursion experiment: 15/15 passed; - credential-free CLI/trace and fresh-process graph/artifact restart checks: passed; - credential-backed REAL OpenRouter: passed; - pinned Paid OpenRouter: passed; - Nix flake: passed; - clean SWI pack install: passed; - Tree-sitter FFI: passed; - whitespace checks: passed. Current `main` has not moved from the tested base. Current PR review submissions and unresolved review threads are empty. Any changed candidate head invalidates this evidence. ## Non-goals - no #175/#221 timeout-policy rewrite; - no JSON Schema implementation project; - no provider-specific validation layer; - no Agent Zero/AgentProlog product code; - no CI weakening.
lost-rob0t commented 2026-08-26 10:06:11 +00:00 (Migrated from github.com)

RAGE adversarial review HOLD on exact head 07b552e2c0eb47faf9c9c48635cc1879f99b5a8d.

The numeric-bound implementation and exact-head gates are green, but the branch currently contains an unrelated no-op/singleton change in metadata_context/2:

metadata_context(Current, session(Session)) :-
    ...,
    !,
    Context = session(Session).

Context is not the clause output; the second argument is already session(Session). Canonical main ends that clause at !. This line does not belong to #234, introduces a stray singleton variable, and should be removed before promotion. Keep the PR draft until the diff is scope-clean, then rerun the full exact-head gate because any cleanup commit invalidates the current SHA evidence.

No weakening of the numeric tests: the failure contracts should continue to assert structured rejection while CI stays green.

RAGE adversarial review HOLD on exact head `07b552e2c0eb47faf9c9c48635cc1879f99b5a8d`. The numeric-bound implementation and exact-head gates are green, but the branch currently contains an unrelated no-op/singleton change in `metadata_context/2`: ```prolog metadata_context(Current, session(Session)) :- ..., !, Context = session(Session). ``` `Context` is not the clause output; the second argument is already `session(Session)`. Canonical `main` ends that clause at `!.` This line does not belong to #234, introduces a stray singleton variable, and should be removed before promotion. Keep the PR draft until the diff is scope-clean, then rerun the full exact-head gate because any cleanup commit invalidates the current SHA evidence. No weakening of the numeric tests: the failure contracts should continue to assert structured rejection while CI stays green.
lost-rob0t commented 2026-08-26 11:08:29 +00:00 (Migrated from github.com)

RAGE recovery update on exact head 33a76a820aa86187b86a88fefbde604a847a3605: the failed whole-file cleanup attempt was fully reverted as a fast-forward restore. 07b552e2... -> 33a76a82... has zero net file changes, and the restored exact head is green across CI, Paid OpenRouter, Nix flake, Clean SWI pack install, and Tree-sitter FFI. The prior adversarial HOLD still stands because the unrelated Context = session(Session) no-op remains in metadata_context/2. Do not promote/merge until that line is removed without changing the numeric-bound contract, followed by a fresh exact-head gate.

RAGE recovery update on exact head `33a76a820aa86187b86a88fefbde604a847a3605`: the failed whole-file cleanup attempt was fully reverted as a fast-forward restore. `07b552e2...` -> `33a76a82...` has **zero net file changes**, and the restored exact head is green across CI, Paid OpenRouter, Nix flake, Clean SWI pack install, and Tree-sitter FFI. The prior adversarial HOLD still stands because the unrelated `Context = session(Session)` no-op remains in `metadata_context/2`. Do not promote/merge until that line is removed without changing the numeric-bound contract, followed by a fresh exact-head gate.
lost-rob0t commented 2026-08-26 12:18:35 +00:00 (Migrated from github.com)

RAGE adversarial HOLD resolved on exact head bdc8bd25faacb7c6f056f88aa18c6c536ef700f8.

The unrelated metadata_context/2 contaminant is gone. The cleanup commit was constructed as a detached Git object, inspected before the branch moved, and its entire diff is one hunk restoring the canonical session clause from !, Context = session(Session). to !. The complete PR remains limited to prolog/rlm_tool.pl, test/rlm_tool_test.pl, and docs/tools.md.

Fresh exact-head verification is green on the synthetic merge ref into unchanged main 267697bef10a3fffff7c093e1435ece770e7444b: 895/895 deterministic PlUnit tests, benchmark/conformance, deep recursion, CLI/trace, restart checks, credential-backed REAL OpenRouter, pinned Paid OpenRouter, Nix flake, clean SWI pack install, Tree-sitter FFI, and whitespace all passed.

Adversarial decision: GO from the engineering side. The negative numeric-bound contracts continue to assert structured rejection while the suite stays green.

RAGE adversarial HOLD resolved on exact head `bdc8bd25faacb7c6f056f88aa18c6c536ef700f8`. The unrelated `metadata_context/2` contaminant is gone. The cleanup commit was constructed as a detached Git object, inspected before the branch moved, and its entire diff is one hunk restoring the canonical session clause from `!, Context = session(Session).` to `!.` The complete PR remains limited to `prolog/rlm_tool.pl`, `test/rlm_tool_test.pl`, and `docs/tools.md`. Fresh exact-head verification is green on the synthetic merge ref into unchanged `main` `267697bef10a3fffff7c093e1435ece770e7444b`: 895/895 deterministic PlUnit tests, benchmark/conformance, deep recursion, CLI/trace, restart checks, credential-backed REAL OpenRouter, pinned Paid OpenRouter, Nix flake, clean SWI pack install, Tree-sitter FFI, and whitespace all passed. Adversarial decision: **GO** from the engineering side. The negative numeric-bound contracts continue to assert structured rejection while the suite stays green.
Sign in to join this conversation.
No description provided.