Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,48 @@ input, not authenticated proof of its claimed source evidence. Redacted or
ambiguous records cannot yield a predicate direction. The Markdown report
shows up to eight relevant comparisons; JSON retains the full list.

An observed guard can also carry `source_behavior`, from the narrower
`sdk_boolean_function/v1` profile. This inspects the **entire** selected tool
function, including statements after a return and both arms of every branch,
and a single source-level `Agent(name="…", tools=[tool])` (or `tools=[]`).
Its module contains only the exact SDK imports, the one sibling guard import,
the decorated function and that literal Agent assignment, in definition order.
The guard module contains only the selected pure Boolean function. Every
function parameter is Boolean; expressions are Boolean literals, parameters,
`not`, `and`, `or`, and the already-resolved guard call. Statements are `if`
and `return`; an implicit return is recorded as `none`. There are no opaque
operation leaves. External calls, free/configuration variables, mutation,
additional functions or Agent configuration, handoffs and unresolved imports
leave the source model unresolved, even in apparently unreachable code.

`source_behavior.returns[i]` is `true`, `false` or `none` for input mask `i`
over **all** sorted tool parameters, not just the parameters used by the first
guard. `configuration_reads: none` means the closed model admitted no such
read; it is absent as a claim when the model is unresolved. The literal
`binding` records the source Agent symbol/name and whether its tools list
contains the selected function. This is source membership, not a claim that
the Agent is a deployed entry point or that the installed SDK executes it.

The comparison keeps four axes separate: all return values (`returns`), the
set of inputs returning true (`true_domain`), literal tool membership
(`binding`), and the true-return inputs reachable through that membership
(`bound_true_domain`). A narrowed first guard can still have a widened whole
function true domain; unchanged returns can accompany added membership.
Changing `False` to `None` changes returns even when the true domain is equal.
A changed Agent identity leaves the bound relation unresolved. Missing old
models, ambiguous capability identities and redacted evidence never establish
equality. The model and its inputs are carried in each side's existing guard
record and verifier-bound artifacts; it introduces no separate authority.

**A true return is not approval.** The model does not prove a caller-controlled
Boolean came from an approver, establish an action's effect/authority or
attribute an existing policy predicate to a change. The outer capability
dependency coverage remains `incomplete` and finding exclusion remains false.
Neither a narrowed true domain nor equal source models is a safe verdict.
Joining complete source evidence to actual capability/policy dependencies and
the fixed-history evaluation remains work in #557/#515/#563. This SDK profile
does not cover #515's TypeScript MongoDB acceptance case.

Verification binds the reader's captured dependency bytes and named absent
import candidates in `verification-plan.json` under
`inputs.options.dependency_inputs`. A current-control read checks
Expand Down
186 changes: 186 additions & 0 deletions docs/report-schema.v0.43.json
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,170 @@
"title": "BindingSurfaceDiff",
"type": "object"
},
"BooleanSourceBehavior": {
"additionalProperties": false,
"description": "A closed source-function model, never deployed behavior or policy evidence.",
"properties": {
"binding": {
"anyOf": [
{
"$ref": "#/$defs/BooleanSourceBinding"
},
{
"type": "null"
}
],
"default": null
},
"configuration_reads": {
"anyOf": [
{
"const": "none",
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Configuration Reads"
},
"parameters": {
"items": {
"type": "string"
},
"title": "Parameters",
"type": "array"
},
"reader_profile": {
"const": "sdk_boolean_function/v1",
"default": "sdk_boolean_function/v1",
"title": "Reader Profile",
"type": "string"
},
"reason": {
"title": "Reason",
"type": "string"
},
"returns": {
"items": {
"enum": [
"true",
"false",
"none"
],
"type": "string"
},
"title": "Returns",
"type": "array"
},
"status": {
"enum": [
"observed",
"unresolved",
"redacted"
],
"title": "Status",
"type": "string"
}
},
"required": [
"status",
"reason"
],
"title": "BooleanSourceBehavior",
"type": "object"
},
"BooleanSourceBinding": {
"additionalProperties": false,
"properties": {
"agent_name": {
"title": "Agent Name",
"type": "string"
},
"agent_symbol": {
"title": "Agent Symbol",
"type": "string"
},
"tool_bound": {
"title": "Tool Bound",
"type": "boolean"
}
},
"required": [
"agent_symbol",
"agent_name",
"tool_bound"
],
"title": "BooleanSourceBinding",
"type": "object"
},
"BooleanSourceComparison": {
"additionalProperties": false,
"properties": {
"binding": {
"default": "unresolved",
"enum": [
"unchanged",
"added",
"removed",
"changed",
"unresolved"
],
"title": "Binding",
"type": "string"
},
"bound_true_domain": {
"default": "unresolved",
"enum": [
"unchanged",
"widened",
"narrowed",
"changed",
"unresolved"
],
"title": "Bound True Domain",
"type": "string"
},
"finding_exclusion_eligible": {
"const": false,
"default": false,
"title": "Finding Exclusion Eligible",
"type": "boolean"
},
"reason": {
"title": "Reason",
"type": "string"
},
"returns": {
"default": "unresolved",
"enum": [
"unchanged",
"changed",
"unresolved"
],
"title": "Returns",
"type": "string"
},
"true_domain": {
"default": "unresolved",
"enum": [
"unchanged",
"widened",
"narrowed",
"changed",
"unresolved"
],
"title": "True Domain",
"type": "string"
}
},
"required": [
"reason"
],
"title": "BooleanSourceComparison",
"type": "object"
},
"CapabilityChangeBlock": {
"additionalProperties": false,
"description": "The diff-derived capability delta, grouped by direction.\n\nReviewer-facing projection over ``action_surface_diff`` /\n``tool_surface_diff`` (roadmap \u00a77.1). Four member lists \u2014\n``added`` / ``removed`` / ``broadened`` / ``narrowed`` \u2014 plus\n``enabled`` (mirrors the surface-diff enabled flag: ``False`` when no\nbase is available, so the block is a stable empty shape rather than\nabsent). Never gates on its own.",
Expand Down Expand Up @@ -4870,6 +5034,17 @@
"title": "Reason",
"type": "string"
},
"source_behavior": {
"anyOf": [
{
"$ref": "#/$defs/BooleanSourceComparison"
},
{
"type": "null"
}
],
"default": null
},
"tool_id": {
"anyOf": [
{
Expand Down Expand Up @@ -5003,6 +5178,17 @@
"title": "Reason",
"type": "string"
},
"source_behavior": {
"anyOf": [
{
"$ref": "#/$defs/BooleanSourceBehavior"
},
{
"type": "null"
}
],
"default": null
},
"source_id": {
"title": "Source Id",
"type": "string"
Expand Down
6 changes: 6 additions & 0 deletions src/agents_shipgate/cli/scan/sanitization.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,12 @@ def _public_tool_surfaces(
public.status = "redacted"
public.reason = "guard_evidence_redacted"
public.allowed_inputs = []
if public.source_behavior is not None:
public.source_behavior.status = "redacted"
public.source_behavior.reason = "source_behavior_evidence_redacted"
public.source_behavior.returns = []
public.source_behavior.binding = None
public.source_behavior.configuration_reads = None
if diffs.diff_reference_error:
public_tool_surface_diff = disabled_tool_surface_diff(
redact_data(
Expand Down
91 changes: 91 additions & 0 deletions src/agents_shipgate/core/guard_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from agents_shipgate.core.domain import LoadedToolSource, Tool
from agents_shipgate.schemas.guard_dependencies import (
BooleanSourceComparison,
GuardDependencyComparison,
GuardDependencyEvidence,
)
Expand Down Expand Up @@ -60,6 +61,91 @@ def _valid_predicate(row: GuardDependencyEvidence) -> bool:
)


def _valid_source(row: GuardDependencyEvidence) -> bool:
source = row.source_behavior
return bool(
_valid_predicate(row)
and source is not None
and source.status == "observed"
and source.configuration_reads == "none"
and source.binding is not None
and type(source.binding.tool_bound) is bool
and len(source.parameters) <= 8
and source.parameters == sorted(set(source.parameters))
and set(row.parameters) <= set(source.parameters)
and len(source.returns) == 1 << len(source.parameters)
and all(value in {"true", "false", "none"} for value in source.returns)
)


def _domain_direction(before: set[int], after: set[int]) -> str:
return (
"unchanged"
if before == after
else "widened"
if before < after
else "narrowed"
if after < before
else "changed"
)


def _compare_source(
old: GuardDependencyEvidence | None, new: GuardDependencyEvidence | None
) -> BooleanSourceComparison:
result = BooleanSourceComparison(reason="base_or_head_source_model_unavailable")
if old is None or new is None or not _valid_source(old) or not _valid_source(new):
return result
before, after = old.source_behavior, new.source_behavior
if (
old.tool_id,
old.source_id,
old.tool_path,
old.tool_symbol,
old.guard_path,
old.guard_symbol,
before.parameters,
) != (
new.tool_id,
new.source_id,
new.tool_path,
new.tool_symbol,
new.guard_path,
new.guard_symbol,
after.parameters,
):
result.reason = "source_subject_or_parameter_domain_changed"
return result
result.returns = "unchanged" if before.returns == after.returns else "changed"
previous = {i for i, value in enumerate(before.returns) if value == "true"}
following = {i for i, value in enumerate(after.returns) if value == "true"}
result.true_domain = _domain_direction(previous, following)
if (before.binding.agent_symbol, before.binding.agent_name) != (
after.binding.agent_symbol,
after.binding.agent_name,
):
result.binding = "changed"
result.reason = "agent_source_identity_changed; bound relation is unresolved"
return result
result.binding = (
"unchanged"
if before.binding.tool_bound == after.binding.tool_bound
else "added"
if after.binding.tool_bound
else "removed"
)
result.bound_true_domain = _domain_direction(
previous if before.binding.tool_bound else set(),
following if after.binding.tool_bound else set(),
)
result.reason = (
"Compare the closed Boolean source function and its literal Agent tool membership only. "
"A true return is not approval, authority or an action effect; deployed wiring and "
"finding-predicate attribution remain unproved. No finding is excluded."
)
return result


def compare_guard_dependencies(
current: list[GuardDependencyEvidence], base: list[GuardDependencyEvidence]
) -> list[GuardDependencyComparison]:
Expand Down Expand Up @@ -121,6 +207,11 @@ def compare_guard_dependencies(
reason=reason,
before=old,
after=new,
source_behavior=(
_compare_source(old, new)
if any(row.source_behavior is not None for row in old_rows + new_rows)
else None
),
)
)
return comparisons
Loading
Loading