Add strict read-only source and database investigation tools
This commit is contained in:
1 parent
798925d25d
commit
66c76a9930
14 files changed
+2069
-2
No files matched your search
@@ -22,8 +22,9 @@ python -m venv .venv
|
||||
The lifecycle foundation supports one-pass inbox processing, resumable package
|
||||
checkpoints and a deterministic generic CaseBuilder. An async LLM gateway and CaseInterpreter
|
||||
provide interpretation and bounded initial hypothesis generation, with durable
|
||||
per-hypothesis budget accounting. Investigation tools, diagnosis,
|
||||
Change Requests and a continuous analysis worker remain future milestones.
|
||||
per-hypothesis budget accounting. Read-only source/database snapshot queries are
|
||||
available. Diagnosis, Change Requests and a continuous analysis worker remain
|
||||
future milestones.
|
||||
|
||||
Expected-behavior resolution and the read-only provider contract: [expected-behavior-v1.md](docs/expected-behavior-v1.md).
|
||||
|
||||
@@ -39,3 +40,6 @@ rules: [budget-manager-v1.md](docs/budget-manager-v1.md), with [budget configura
|
||||
Case-local asynchronous test requests, confirmed feedback and budget-preserving
|
||||
event-driven resume: [async-tests-v1.md](docs/async-tests-v1.md). Web UI and external
|
||||
bot/client adapters are future integrations.
|
||||
|
||||
Read-only packaged source/database queries and the strict tool allowlist:
|
||||
[investigation-tools-v1.md](docs/investigation-tools-v1.md).
|
||||
@@ -0,0 +1,148 @@
|
||||
# Investigation tools v1
|
||||
|
||||
This milestone provides the read-only Investigation Tool Router and initial
|
||||
source/database queries. The architecture is unchanged. No shell, arbitrary SQL,
|
||||
live database access, source edits, web UI, bot/client steering, EvidenceStore or
|
||||
autonomous Investigation Loop is introduced.
|
||||
|
||||
## API and controller boundary
|
||||
|
||||
```python
|
||||
from viano_bug_analysis.tools import InvestigationToolRouter
|
||||
|
||||
router = InvestigationToolRouter()
|
||||
result = router.query(package_path, {
|
||||
"format": "viano-investigation-request", "format_version": 1,
|
||||
"case_id": case_id, "package_id": package_id, "analysis_revision": revision,
|
||||
"requests": [{
|
||||
"request_id": "Q0001", "hypothesis_id": "H1", "action_id": "query-0001",
|
||||
"operation": "source.find_text",
|
||||
"arguments": {"source_refs": [source_ref], "text": "activate",
|
||||
"case_sensitive": True, "offset": 0, "limit": 20},
|
||||
}],
|
||||
})
|
||||
```
|
||||
|
||||
`request_schema()` exposes the bundled strict `investigation-request-v1` schema
|
||||
for future prompting. Unknown operations/fields, invalid arguments, duplicate
|
||||
request IDs and repeated hypothesis/action pairs are rejected. The router checks
|
||||
manifest identity/revision, known initial hypothesis IDs, active generation or
|
||||
investigation phase, and a fresh canonical case. Blocked cases cannot query.
|
||||
|
||||
The entire batch is validated and preflighted before any query handler executes.
|
||||
Preflight includes declared roles, bounded source reads, encoding, ranges,
|
||||
database structure and known columns. A rejected batch returns no partial result.
|
||||
Preflight itself necessarily reads the selected snapshots; it has no writes or
|
||||
external effects. Runtime errors are controlled `ToolError` codes without raw
|
||||
source data in diagnostic text.
|
||||
|
||||
Tools read only the ZIP snapshots declared by `case.json` in `source_context` and
|
||||
`database_context`. Each read verifies its existing case hash and size. Arbitrary
|
||||
package members, unknown/unclassified attachments, absolute server paths and
|
||||
manifest pointers are not alternative query targets. Server path strings inside
|
||||
wowsrc data remain metadata. Nothing is extracted or executed.
|
||||
|
||||
The router returns a versioned `investigation-result-v1` document with manifest
|
||||
identity, source package generation, canonical case ref/hash, batch hash,
|
||||
per-request hash and typed operation-specific results. It does not update the
|
||||
manifest, write checkpoints, charge/refund credits or persist evidence.
|
||||
The future trusted controller must reserve budget **before** dispatch, persist
|
||||
the request and result, verify that the case generation has not changed, and
|
||||
apply BudgetManager result/settlement rules. Retries of these deterministic
|
||||
snapshot queries have no tool side effects; billing idempotence remains in the
|
||||
controller/BudgetManager. Future controller hypothesis eligibility, reservations
|
||||
and updates require their own full validation; a known birth ID alone does not
|
||||
authorize spending against a terminal account.
|
||||
|
||||
## Initial allowlist
|
||||
|
||||
| Operation | Arguments and behavior |
|
||||
| --- | --- |
|
||||
| `source.find_text` | Explicit `source_refs`, literal `text`, `case_sensitive`, `offset`, `limit`; matches lines, not regex |
|
||||
| `source.find_symbol` | Explicit `source_refs`, one identifier `symbol`, `offset`, `limit`; token-boundary occurrences |
|
||||
| `source.read_context` | `source_ref`, inclusive 1-based `start_line` and `end_line` |
|
||||
| `database.describe` | `source_ref` selecting a verified schema/snapshot descriptor |
|
||||
| `database.lookup` | `source_ref`, nonempty `filters` map of exact scalar equality, projection `columns`, `offset`, `limit` |
|
||||
| `database.find_references` | `source_ref`, explicit `search_columns`, scalar `value`, projection `columns`, `offset`, `limit` |
|
||||
|
||||
Source results include actual source SHA-256, 1-based line numbers and line text.
|
||||
Symbol matches can be uses, declarations, definitions, comments or strings: this
|
||||
is not a language parser or verified definition/registration resolver. Text
|
||||
search is literal and may explicitly ignore case. Source encoding is UTF-8 with
|
||||
optional BOM; binary/NUL or undecodable sources reject the batch.
|
||||
|
||||
Database results include original source SHA-256 and an actual JSON Pointer to
|
||||
each original row (for example `/rows/2` or `/2`). Projection never changes the
|
||||
pointer. Equality does not coerce booleans, strings or numeric types; `true`, `1`,
|
||||
`"1"` and `1.0` remain different typed values. Reference search only finds equal
|
||||
values in specified columns; it does not assert foreign-key relationships.
|
||||
SQL-like text in a scalar value is ordinary data, never executable SQL.
|
||||
|
||||
All searches expose checked sources, total matches, `truncated` and a stable
|
||||
`next_offset`. Sorting follows requested file order and original row/line order.
|
||||
Individual source lines have separate `text_truncated` markers. Limits reject
|
||||
excessive input/output rather than quietly dropping queried files. A no-match
|
||||
result applies only to the selected packaged snapshot; it proves neither absence
|
||||
from an entire repository nor absence from the current server/database.
|
||||
|
||||
## Database snapshot contract and profile adapter
|
||||
|
||||
The neutral `database-snapshot-v1` format is:
|
||||
|
||||
```json
|
||||
{
|
||||
"format": "viano-database-snapshot", "format_version": 1,
|
||||
"table": "switches",
|
||||
"columns": [{"name": "id", "type": "integer"}],
|
||||
"rows": [{"id": 42}], "coverage": "partial"
|
||||
}
|
||||
```
|
||||
|
||||
Coverage is explicit: `partial`, `complete_snapshot`, `unknown` or `schema_only`.
|
||||
Even a complete snapshot is a packaged point-in-time claim, not a live query.
|
||||
Duplicate column names, rows containing undeclared columns and schema-only
|
||||
descriptors containing rows are rejected. Raw arrays of row objects can be
|
||||
queried without inventing table identity or schema: their coverage is `unknown`,
|
||||
and columns must occur in stored rows. An empty raw array cannot validate a
|
||||
column name; querying unknown columns rejects. A schema-only descriptor may have
|
||||
known columns and no rows, but that absence is not evidence of a missing DB row.
|
||||
|
||||
wowsrc's native `{table, columns:[{column_name, column_type, ...}], ...}` descriptors
|
||||
are normalized explicitly in `profiles/vcore/db_tools.py`:
|
||||
|
||||
```python
|
||||
from viano_bug_analysis.profiles.vcore.db_tools import normalize_schema_descriptor
|
||||
from viano_bug_analysis.tools.database_query import DatabaseQueryTool
|
||||
|
||||
router = InvestigationToolRouter(
|
||||
database_tool=DatabaseQueryTool(normalize_schema_descriptor),
|
||||
)
|
||||
```
|
||||
|
||||
This mapping reports `schema_only`. It does not infer table ownership from file
|
||||
names, numeric IDs, ambiguous artifact rankings or `schema_table_matches`. Other
|
||||
projects can supply their own trusted descriptor adapter or use the neutral
|
||||
format. Live DB connectors, symbol parsers, registration discovery, timeline/log
|
||||
queries and actual external research providers are later extensions.
|
||||
|
||||
## Limits and asynchronous testing
|
||||
|
||||
`ToolLimits` provides adjustable operational defaults, not permanent v1 format
|
||||
caps: request 64 KiB, batch 16 requests, 64 distinct selected files, 8 MiB per file,
|
||||
32 MiB combined selected bytes, 100,000 database rows, 100 returned matches per
|
||||
query, 200 context lines, 4,096 characters per returned source line and 2 MiB per
|
||||
result batch. Existing package and CaseBuilder limits apply separately when
|
||||
validating the package and canonical case inventory.
|
||||
|
||||
Asynchronous human/automated tests remain in the separate controller handoff
|
||||
described in [async-tests-v1.md](async-tests-v1.md). They are deliberately not
|
||||
executed by this snapshot query router. The future Investigation Loop can combine
|
||||
both interfaces: fast read-only query results and persisted external test waits.
|
||||
No deadline, waiting state or budget preservation behavior changes here.
|
||||
|
||||
Tests cover generic snapshots, allowlist and path safety, whole-batch preflight,
|
||||
type distinctions, pagination, provenance, input/output limits, blocked/stale
|
||||
contexts and byte-for-byte read-only behavior. The actual BUG-20 regression reads
|
||||
three lines of packaged source, its quest table schema using the Vcore adapter,
|
||||
and the actual stored quest row through an explicit ID filter;
|
||||
it does not inject a known fix or claim to test model reasoning.
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Explicit wowsrc schema descriptor mapping; no guessed tables or live SQL."""
|
||||
|
||||
from viano_bug_analysis.tools.config import ToolError
|
||||
|
||||
|
||||
def normalize_schema_descriptor(document):
|
||||
if isinstance(document, dict) and document.get("format") == "viano-database-snapshot":
|
||||
return document
|
||||
if (not isinstance(document, dict) or not isinstance(document.get("table"), str)
|
||||
or not isinstance(document.get("columns"), list)):
|
||||
raise ToolError("TOOL_DATABASE_DESCRIPTOR")
|
||||
columns = []
|
||||
for column in document["columns"]:
|
||||
if (not isinstance(column, dict) or not isinstance(column.get("column_name"), str)
|
||||
or not isinstance(column.get("column_type"), str)):
|
||||
raise ToolError("TOOL_DATABASE_DESCRIPTOR")
|
||||
columns.append({"name": column["column_name"], "type": column["column_type"]})
|
||||
return {"format": "viano-database-snapshot", "format_version": 1,
|
||||
"table": document["table"], "columns": columns, "rows": [], "coverage": "schema_only"}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format",
|
||||
"format_version",
|
||||
"table",
|
||||
"columns",
|
||||
"rows",
|
||||
"coverage"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"const": "viano-database-snapshot"
|
||||
},
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"enum": [
|
||||
"partial",
|
||||
"complete_snapshot",
|
||||
"unknown",
|
||||
"schema_only"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format",
|
||||
"format_version",
|
||||
"case_id",
|
||||
"package_id",
|
||||
"analysis_revision",
|
||||
"requests"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"const": "viano-investigation-request"
|
||||
},
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"case_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"analysis_revision": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"requests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.find_text"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_refs",
|
||||
"text",
|
||||
"case_sensitive",
|
||||
"offset",
|
||||
"limit"
|
||||
],
|
||||
"properties": {
|
||||
"source_refs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.find_symbol"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_refs",
|
||||
"symbol",
|
||||
"offset",
|
||||
"limit"
|
||||
],
|
||||
"properties": {
|
||||
"source_refs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.read_context"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"start_line",
|
||||
"end_line"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"start_line": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"end_line": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.describe"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.lookup"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"filters",
|
||||
"columns",
|
||||
"offset",
|
||||
"limit"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"arguments"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"pattern": "^H[1-9][0-9]*$"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.find_references"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"search_columns",
|
||||
"value",
|
||||
"columns",
|
||||
"offset",
|
||||
"limit"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"search_columns": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format",
|
||||
"format_version",
|
||||
"case_id",
|
||||
"package_id",
|
||||
"analysis_revision",
|
||||
"package_generation",
|
||||
"case_ref",
|
||||
"case_sha256",
|
||||
"batch_sha256",
|
||||
"results"
|
||||
],
|
||||
"properties": {
|
||||
"format": {
|
||||
"const": "viano-investigation-result"
|
||||
},
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"case_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"package_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"analysis_revision": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"package_generation": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"case_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"case_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"batch_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.find_text"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matches",
|
||||
"total_matches",
|
||||
"truncated",
|
||||
"next_offset",
|
||||
"checked_sources",
|
||||
"scope",
|
||||
"interpretation"
|
||||
],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"line",
|
||||
"text",
|
||||
"text_truncated"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"text_truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_matches": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_source_snapshot"
|
||||
},
|
||||
"interpretation": {
|
||||
"const": "text_occurrences_not_verified_definitions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.find_symbol"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matches",
|
||||
"total_matches",
|
||||
"truncated",
|
||||
"next_offset",
|
||||
"checked_sources",
|
||||
"scope",
|
||||
"interpretation"
|
||||
],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"line",
|
||||
"text",
|
||||
"text_truncated"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"text_truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_matches": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_source_snapshot"
|
||||
},
|
||||
"interpretation": {
|
||||
"const": "text_occurrences_not_verified_definitions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "source.read_context"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matches",
|
||||
"total_matches",
|
||||
"truncated",
|
||||
"next_offset",
|
||||
"checked_sources",
|
||||
"scope",
|
||||
"interpretation"
|
||||
],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"line",
|
||||
"text",
|
||||
"text_truncated"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"text_truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_matches": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_source_snapshot"
|
||||
},
|
||||
"interpretation": {
|
||||
"const": "text_occurrences_not_verified_definitions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.describe"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"table",
|
||||
"columns",
|
||||
"coverage",
|
||||
"checked_sources",
|
||||
"scope"
|
||||
],
|
||||
"properties": {
|
||||
"table": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"enum": [
|
||||
"partial",
|
||||
"complete_snapshot",
|
||||
"unknown",
|
||||
"schema_only"
|
||||
]
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_database_snapshot"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.lookup"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matches",
|
||||
"total_matches",
|
||||
"truncated",
|
||||
"next_offset",
|
||||
"checked_sources",
|
||||
"coverage",
|
||||
"scope",
|
||||
"interpretation"
|
||||
],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"row",
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"json_pointer"
|
||||
],
|
||||
"properties": {
|
||||
"row": {
|
||||
"type": "object"
|
||||
},
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"json_pointer": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_matches": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"enum": [
|
||||
"partial",
|
||||
"complete_snapshot",
|
||||
"unknown",
|
||||
"schema_only"
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_database_snapshot"
|
||||
},
|
||||
"interpretation": {
|
||||
"const": "explicit_column_matches_not_inferred_relationships"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"request_id",
|
||||
"hypothesis_id",
|
||||
"action_id",
|
||||
"operation",
|
||||
"request_sha256",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"hypothesis_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\S"
|
||||
},
|
||||
"operation": {
|
||||
"const": "database.find_references"
|
||||
},
|
||||
"request_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matches",
|
||||
"total_matches",
|
||||
"truncated",
|
||||
"next_offset",
|
||||
"checked_sources",
|
||||
"coverage",
|
||||
"scope",
|
||||
"interpretation"
|
||||
],
|
||||
"properties": {
|
||||
"matches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"row",
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"json_pointer"
|
||||
],
|
||||
"properties": {
|
||||
"row": {
|
||||
"type": "object"
|
||||
},
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"json_pointer": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_matches": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"checked_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"source_ref",
|
||||
"sha256",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"enum": [
|
||||
"partial",
|
||||
"complete_snapshot",
|
||||
"unknown",
|
||||
"schema_only"
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"const": "packaged_database_snapshot"
|
||||
},
|
||||
"interpretation": {
|
||||
"const": "explicit_column_matches_not_inferred_relationships"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Read-only, structured investigation queries over declared case snapshots."""
|
||||
|
||||
from .config import ToolError, ToolLimits
|
||||
from .router import InvestigationToolRouter
|
||||
|
||||
__all__ = ["ToolError", "ToolLimits", "InvestigationToolRouter"]
|
||||
@@ -0,0 +1,27 @@
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
super().__init__(code)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolLimits:
|
||||
"""Operational limits, independent of the versioned request/result format."""
|
||||
|
||||
max_request_bytes: int = 65536
|
||||
max_batch_requests: int = 16
|
||||
max_files: int = 64
|
||||
max_file_bytes: int = 8 * 1024 * 1024
|
||||
max_total_bytes: int = 32 * 1024 * 1024
|
||||
max_rows: int = 100000
|
||||
max_matches: int = 100
|
||||
max_context_lines: int = 200
|
||||
max_line_characters: int = 4096
|
||||
max_result_bytes: int = 2 * 1024 * 1024
|
||||
|
||||
def __post_init__(self):
|
||||
if any(type(value) is not int or value <= 0 for value in asdict(self).values()):
|
||||
raise ToolError("TOOL_LIMIT_CONFIG")
|
||||
@@ -0,0 +1,62 @@
|
||||
import hashlib
|
||||
|
||||
from viano_bug_analysis.case.assembler import CaseAssembler, CaseBuilderLimits
|
||||
from viano_bug_analysis.case.contracts import read_json, require_file
|
||||
from viano_bug_analysis.package.manifest import parse_manifest
|
||||
|
||||
from .config import ToolError
|
||||
|
||||
|
||||
class QueryContext:
|
||||
def __init__(self, reader, limits, case_limits=None):
|
||||
self.reader, self.limits = reader, limits
|
||||
self.used_bytes = 0
|
||||
self.cache = {}
|
||||
manifest = reader.manifest
|
||||
self.case_ref = manifest["artifacts"].get("case")
|
||||
self.case = read_json(reader, self.case_ref, "case", (case_limits or CaseBuilderLimits()).max_case_bytes)
|
||||
if any(self.case["case"][field] != manifest[field] for field in ("case_id", "package_id", "analysis_revision")):
|
||||
raise ToolError("TOOL_CASE_IDENTITY")
|
||||
current = CaseAssembler(case_limits).assemble(reader)
|
||||
if current["inputs"]["request_hash"] != self.case["inputs"]["request_hash"]:
|
||||
raise ToolError("TOOL_STALE_CASE")
|
||||
if any(current[field] != self.case[field] for field in ("source_context", "database_context")):
|
||||
raise ToolError("TOOL_SOURCE_CONFLICT")
|
||||
self.sources = {}
|
||||
for role in ("source", "database"):
|
||||
records = self.case[role + "_context"]["refs"]
|
||||
if len({record["path"] for record in records}) != len(records):
|
||||
raise ToolError("TOOL_SOURCE_CONFLICT")
|
||||
self.sources[role] = {record["path"]: record for record in records}
|
||||
|
||||
def check_ref(self, role, ref):
|
||||
if ref not in self.sources[role]:
|
||||
raise ToolError("TOOL_SOURCE_NOT_DECLARED")
|
||||
require_file(self.reader, ref)
|
||||
|
||||
def read(self, role, ref):
|
||||
self.check_ref(role, ref)
|
||||
if ref not in self.cache:
|
||||
if len(self.cache) >= self.limits.max_files:
|
||||
raise ToolError("TOOL_FILE_LIMIT")
|
||||
record = self.sources[role][ref]
|
||||
size = self.reader._archive.getinfo(ref).file_size
|
||||
if size > self.limits.max_file_bytes or self.used_bytes + size > self.limits.max_total_bytes:
|
||||
raise ToolError("TOOL_INPUT_LIMIT")
|
||||
with self.reader._archive.open(ref) as stream:
|
||||
data = self.reader.validator._read_bounded(stream, min(self.limits.max_file_bytes, self.reader.validator.limits.max_file_bytes))
|
||||
if len(data) != record["size"] or hashlib.sha256(data).hexdigest() != record["sha256"]:
|
||||
raise ToolError("TOOL_SOURCE_CHANGED")
|
||||
self.used_bytes += len(data)
|
||||
self.cache[ref] = data
|
||||
return self.cache[ref]
|
||||
|
||||
def json(self, ref):
|
||||
try:
|
||||
return parse_manifest(self.read("database", ref))
|
||||
except (ValueError, UnicodeError, RecursionError):
|
||||
raise ToolError("TOOL_DATABASE_JSON") from None
|
||||
|
||||
def provenance(self, role, ref):
|
||||
record = self.sources[role][ref]
|
||||
return {"source_ref": ref, "sha256": record["sha256"], "size": record["size"]}
|
||||
@@ -0,0 +1,80 @@
|
||||
from .config import ToolError
|
||||
|
||||
|
||||
class DatabaseQueryTool:
|
||||
"""Structured equality filters over packaged JSON, never a SQL connection."""
|
||||
|
||||
def __init__(self, descriptor_adapter=None):
|
||||
self.descriptor_adapter = descriptor_adapter
|
||||
|
||||
def prepare(self, context, request):
|
||||
args = request["arguments"]
|
||||
context.check_ref("database", args["source_ref"])
|
||||
if args.get("limit", 1) > context.limits.max_matches:
|
||||
raise ToolError("TOOL_MATCH_LIMIT")
|
||||
operation = request["operation"]
|
||||
document = context.json(args["source_ref"])
|
||||
if operation == "database.describe":
|
||||
if self.descriptor_adapter is not None:
|
||||
document = self.descriptor_adapter(document)
|
||||
if not isinstance(document, dict) or document.get("format") != "viano-database-snapshot" or document.get("format_version") != 1:
|
||||
raise ToolError("TOOL_DATABASE_DESCRIPTOR")
|
||||
from viano_bug_analysis.case.contracts import validate
|
||||
validate(document, "database-snapshot")
|
||||
self._check_snapshot(document, context.limits.max_rows)
|
||||
return document, [], "", document["coverage"]
|
||||
if isinstance(document, list):
|
||||
rows, pointer, coverage = document, "", "unknown"
|
||||
elif isinstance(document, dict) and document.get("format") == "viano-database-snapshot":
|
||||
from viano_bug_analysis.case.contracts import validate
|
||||
validate(document, "database-snapshot")
|
||||
self._check_snapshot(document, context.limits.max_rows)
|
||||
rows, pointer, coverage = document["rows"], "/rows", document["coverage"]
|
||||
else:
|
||||
raise ToolError("TOOL_DATABASE_ROWS")
|
||||
if len(rows) > context.limits.max_rows or any(not isinstance(row, dict) for row in rows):
|
||||
raise ToolError("TOOL_DATABASE_ROWS")
|
||||
columns = set().union(*(row.keys() for row in rows)) if rows else set()
|
||||
if isinstance(document, dict):
|
||||
columns |= {column["name"] for column in document["columns"]}
|
||||
requested = set(args.get("filters", {})) | set(args.get("columns", []))
|
||||
if operation == "database.find_references":
|
||||
requested |= set(args["search_columns"])
|
||||
if not requested <= columns:
|
||||
# An empty raw array supplies no verified column schema.
|
||||
raise ToolError("TOOL_DATABASE_COLUMN_UNKNOWN")
|
||||
return document, rows, pointer, coverage
|
||||
|
||||
@staticmethod
|
||||
def _check_snapshot(document, max_rows):
|
||||
names = [column["name"] for column in document["columns"]]
|
||||
if (len(set(names)) != len(names) or len(document["rows"]) > max_rows
|
||||
or any(not set(row) <= set(names) for row in document["rows"])
|
||||
or (document["coverage"] == "schema_only" and document["rows"])):
|
||||
raise ToolError("TOOL_DATABASE_DESCRIPTOR")
|
||||
|
||||
def execute(self, context, request):
|
||||
document, rows, pointer, coverage = self.prepare(context, request)
|
||||
args, operation = request["arguments"], request["operation"]
|
||||
if operation == "database.describe":
|
||||
return {"table": document["table"], "columns": document["columns"], "coverage": document["coverage"],
|
||||
"checked_sources": [context.provenance("database", args["source_ref"])], "scope": "packaged_database_snapshot"}
|
||||
def same(left, right):
|
||||
return type(left) is type(right) and left == right
|
||||
def predicate(row):
|
||||
if operation == "database.lookup":
|
||||
return all(name in row and same(row[name], value) for name, value in args["filters"].items())
|
||||
return any(name in row and same(row[name], args["value"]) for name in args["search_columns"])
|
||||
matches, total = [], 0
|
||||
for index, row in enumerate(rows):
|
||||
if not predicate(row):
|
||||
continue
|
||||
if args["offset"] <= total < args["offset"] + args["limit"]:
|
||||
value = {name: row[name] for name in args["columns"] if name in row} if args.get("columns") else row
|
||||
matches.append({"row": value, "source_ref": args["source_ref"], "sha256": context.sources["database"][args["source_ref"]]["sha256"],
|
||||
"json_pointer": f"{pointer}/{index}"})
|
||||
total += 1
|
||||
return {"matches": matches, "total_matches": total, "checked_sources": [context.provenance("database", args["source_ref"])],
|
||||
"truncated": total > args["offset"] + len(matches),
|
||||
"next_offset": args["offset"] + len(matches) if total > args["offset"] + len(matches) else None,
|
||||
"coverage": coverage, "scope": "packaged_database_snapshot", "interpretation": "explicit_column_matches_not_inferred_relationships"}
|
||||
@@ -0,0 +1,95 @@
|
||||
import copy
|
||||
import hashlib
|
||||
from importlib.resources import files
|
||||
import json
|
||||
|
||||
from viano_bug_analysis.case.contracts import read_json, validate
|
||||
from viano_bug_analysis.case.errors import CaseBuildError
|
||||
from viano_bug_analysis.lifecycle import State
|
||||
from viano_bug_analysis.lifecycle.storage import json_bytes
|
||||
from viano_bug_analysis.package import PackageReader, PackageReadError
|
||||
from viano_bug_analysis.package.validator import ARCHIVE_ERRORS
|
||||
|
||||
from .config import ToolError, ToolLimits
|
||||
from .context import QueryContext
|
||||
from .database_query import DatabaseQueryTool
|
||||
from .source_query import SourceQueryTool
|
||||
|
||||
|
||||
OPERATIONS = ("source.find_text", "source.find_symbol", "source.read_context",
|
||||
"database.describe", "database.lookup", "database.find_references")
|
||||
|
||||
|
||||
class InvestigationToolRouter:
|
||||
"""Validate a whole batch before dispatch; return results without case writes.
|
||||
|
||||
Budget reservation, result persistence, asynchronous handoffs and model calls
|
||||
remain responsibilities of the trusted controller, not investigation tools.
|
||||
"""
|
||||
|
||||
def __init__(self, limits=None, *, database_tool=None, case_limits=None):
|
||||
self.limits = limits or ToolLimits()
|
||||
self.case_limits = case_limits
|
||||
self.source = SourceQueryTool()
|
||||
self.database = database_tool or DatabaseQueryTool()
|
||||
|
||||
@staticmethod
|
||||
def request_schema():
|
||||
return json.loads(files("viano_bug_analysis.schemas").joinpath("investigation-request-v1.schema.json").read_text())
|
||||
|
||||
def query(self, package, batch):
|
||||
try:
|
||||
batch = copy.deepcopy(batch)
|
||||
if len(json_bytes(batch)) > self.limits.max_request_bytes:
|
||||
raise ToolError("TOOL_REQUEST_LIMIT")
|
||||
validate(batch, "investigation-request")
|
||||
requests = batch["requests"]
|
||||
if type(batch["analysis_revision"]) is not int or any(
|
||||
type(request["arguments"][field]) is not int
|
||||
for request in requests for field in ("offset", "limit", "start_line", "end_line")
|
||||
if field in request["arguments"]):
|
||||
raise ToolError("TOOL_REQUEST_INTEGER")
|
||||
if len(requests) > self.limits.max_batch_requests:
|
||||
raise ToolError("TOOL_BATCH_LIMIT")
|
||||
if (len({r["request_id"] for r in requests}) != len(requests)
|
||||
or len({(r["hypothesis_id"], r["action_id"]) for r in requests}) != len(requests)):
|
||||
raise ToolError("TOOL_DUPLICATE_REQUEST")
|
||||
with PackageReader(package) as reader:
|
||||
manifest = reader.manifest
|
||||
if any(batch[field] != manifest[field] for field in ("case_id", "package_id", "analysis_revision")):
|
||||
raise ToolError("TOOL_IDENTITY")
|
||||
if manifest["state"] not in {State.GENERATING_HYPOTHESES, State.INVESTIGATING_HYPOTHESES}:
|
||||
raise ToolError("TOOL_PHASE")
|
||||
context = QueryContext(reader, self.limits, self.case_limits)
|
||||
hypotheses = read_json(reader, manifest["artifacts"].get("hypotheses"), "hypotheses", self.limits.max_result_bytes)
|
||||
if any(hypotheses[field] != manifest[field] for field in ("case_id", "package_id", "analysis_revision")):
|
||||
raise ToolError("TOOL_HYPOTHESIS_IDENTITY")
|
||||
known = {record["id"] for record in hypotheses["hypotheses"]}
|
||||
prepared = []
|
||||
for request in requests:
|
||||
if request["hypothesis_id"] not in known:
|
||||
raise ToolError("TOOL_HYPOTHESIS_UNKNOWN")
|
||||
tool = self.source if request["operation"].startswith("source.") else self.database
|
||||
tool.prepare(context, request)
|
||||
prepared.append((tool, request))
|
||||
results = []
|
||||
for tool, request in prepared:
|
||||
result = {"request_id": request["request_id"], "hypothesis_id": request["hypothesis_id"],
|
||||
"action_id": request["action_id"], "operation": request["operation"],
|
||||
"request_sha256": hashlib.sha256(json_bytes(request)).hexdigest(),
|
||||
"data": tool.execute(context, request)}
|
||||
results.append(result)
|
||||
if len(json_bytes(results)) > self.limits.max_result_bytes:
|
||||
raise ToolError("TOOL_RESULT_LIMIT")
|
||||
document = {"format": "viano-investigation-result", "format_version": 1,
|
||||
**{field: manifest[field] for field in ("case_id", "package_id", "analysis_revision", "package_generation")},
|
||||
"case_ref": context.case_ref, "case_sha256": hashlib.sha256(json_bytes(context.case)).hexdigest(),
|
||||
"batch_sha256": hashlib.sha256(json_bytes(batch)).hexdigest(), "results": results}
|
||||
validate(document, "investigation-result")
|
||||
if len(json_bytes(document)) > self.limits.max_result_bytes:
|
||||
raise ToolError("TOOL_RESULT_LIMIT")
|
||||
return document
|
||||
except (CaseBuildError, PackageReadError) as error:
|
||||
raise ToolError(getattr(error, "code", "TOOL_PACKAGE")) from None
|
||||
except ARCHIVE_ERRORS + (ValueError, TypeError, KeyError, UnicodeError, RecursionError):
|
||||
raise ToolError("TOOL_INPUT") from None
|
||||
@@ -0,0 +1,67 @@
|
||||
import re
|
||||
from itertools import islice
|
||||
|
||||
from .config import ToolError
|
||||
|
||||
|
||||
class SourceQueryTool:
|
||||
def prepare(self, context, request):
|
||||
args, operation = request["arguments"], request["operation"]
|
||||
refs = args.get("source_refs", [args["source_ref"]] if "source_ref" in args else [])
|
||||
if len(refs) > context.limits.max_files:
|
||||
raise ToolError("TOOL_FILE_LIMIT")
|
||||
for ref in refs:
|
||||
context.check_ref("source", ref)
|
||||
try:
|
||||
text = context.read("source", ref).decode("utf-8-sig")
|
||||
except UnicodeError:
|
||||
raise ToolError("TOOL_SOURCE_ENCODING") from None
|
||||
if "\x00" in text:
|
||||
raise ToolError("TOOL_SOURCE_BINARY")
|
||||
if operation == "source.read_context" and args["end_line"] > len(text.splitlines()):
|
||||
raise ToolError("TOOL_CONTEXT_RANGE")
|
||||
if operation == "source.read_context":
|
||||
if args["end_line"] < args["start_line"] or args["end_line"] - args["start_line"] + 1 > context.limits.max_context_lines:
|
||||
raise ToolError("TOOL_CONTEXT_RANGE")
|
||||
elif args["limit"] > context.limits.max_matches:
|
||||
raise ToolError("TOOL_MATCH_LIMIT")
|
||||
return refs
|
||||
|
||||
def execute(self, context, request):
|
||||
args, operation = request["arguments"], request["operation"]
|
||||
refs = self.prepare(context, request)
|
||||
matches, checked = [], []
|
||||
total = 0
|
||||
offset = args.get("offset", 0)
|
||||
limit = args.get("limit", context.limits.max_context_lines)
|
||||
for ref in refs:
|
||||
try:
|
||||
text = context.read("source", ref).decode("utf-8-sig")
|
||||
except UnicodeError:
|
||||
raise ToolError("TOOL_SOURCE_ENCODING") from None
|
||||
if "\x00" in text:
|
||||
raise ToolError("TOOL_SOURCE_BINARY")
|
||||
lines = text.splitlines()
|
||||
checked.append(context.provenance("source", ref))
|
||||
if operation == "source.read_context":
|
||||
if args["start_line"] > len(lines) or args["end_line"] > len(lines):
|
||||
raise ToolError("TOOL_CONTEXT_RANGE")
|
||||
selected = islice(enumerate(lines, 1), args["start_line"]-1, args["end_line"])
|
||||
else:
|
||||
if operation == "source.find_symbol":
|
||||
pattern = re.compile(r"(?<![A-Za-z0-9_])" + re.escape(args["symbol"]) + r"(?![A-Za-z0-9_])")
|
||||
predicate = lambda line: pattern.search(line) is not None
|
||||
else:
|
||||
needle = args["text"] if args["case_sensitive"] else args["text"].casefold()
|
||||
predicate = lambda line: needle in (line if args["case_sensitive"] else line.casefold())
|
||||
selected = ((number, line) for number, line in enumerate(lines, 1) if predicate(line))
|
||||
for number, line in selected:
|
||||
if offset <= total < offset + limit:
|
||||
matches.append({"source_ref": ref, "sha256": context.sources["source"][ref]["sha256"],
|
||||
"line": number, "text": line[:context.limits.max_line_characters],
|
||||
"text_truncated": len(line) > context.limits.max_line_characters})
|
||||
total += 1
|
||||
return {"matches": matches, "checked_sources": checked, "total_matches": total,
|
||||
"truncated": total > offset + len(matches),
|
||||
"next_offset": offset + len(matches) if total > offset + len(matches) else None,
|
||||
"scope": "packaged_source_snapshot", "interpretation": "text_occurrences_not_verified_definitions"}
|
||||
@@ -93,4 +93,25 @@ def test_full_bug20_package_pair(tmp_path):
|
||||
if name != "manifest.json":
|
||||
assert before.read_bytes(name) == after.read_bytes(name)
|
||||
assert CaseBuilder(store).build(*identity).reused
|
||||
from viano_bug_analysis.tools import InvestigationToolRouter
|
||||
from viano_bug_analysis.tools.database_query import DatabaseQueryTool
|
||||
from viano_bug_analysis.profiles.vcore.db_tools import normalize_schema_descriptor
|
||||
code_ref = next(r["path"] for r in case["source_context"]["refs"] if r["path"].endswith(".cpp"))
|
||||
schema_ref = next(r["path"] for r in case["database_context"]["refs"] if r["path"].endswith("/database/schemas/quest_template.json"))
|
||||
rows_ref = next(r["path"] for r in case["database_context"]["refs"] if r["path"].endswith("/database/quest_template.json"))
|
||||
request = {"format":"viano-investigation-request","format_version":1,"case_id":identity[0],"package_id":identity[1],"analysis_revision":1,
|
||||
"requests":[{"request_id":"source-query","hypothesis_id":"H1","action_id":"source-query","operation":"source.read_context",
|
||||
"arguments":{"source_ref":code_ref,"start_line":1,"end_line":3}},
|
||||
{"request_id":"schema-query","hypothesis_id":"H2","action_id":"schema-query","operation":"database.describe",
|
||||
"arguments":{"source_ref":schema_ref}},
|
||||
{"request_id":"rows-query","hypothesis_id":"H3","action_id":"rows-query","operation":"database.lookup",
|
||||
"arguments":{"source_ref":rows_ref,"filters":{"ID":11117},"columns":["ID"],"offset":0,"limit":10}}]}
|
||||
before_query_hash = hashlib.sha256(accounted.package.read_bytes()).hexdigest()
|
||||
queried = InvestigationToolRouter(database_tool=DatabaseQueryTool(normalize_schema_descriptor)).query(accounted.package,request)
|
||||
assert len(queried["results"][0]["data"]["matches"]) == 3
|
||||
assert queried["results"][1]["data"]["table"] == "quest_template"
|
||||
assert queried["results"][1]["data"]["coverage"] == "schema_only"
|
||||
assert queried["results"][2]["data"]["matches"][0]["row"] == {"ID":11117}
|
||||
assert queried["results"][2]["data"]["coverage"] == "unknown"
|
||||
assert hashlib.sha256(accounted.package.read_bytes()).hexdigest() == before_query_hash
|
||||
assert [hashlib.sha256(path.read_bytes()).hexdigest() for path in (diag, src)] == original_hashes
|
||||
@@ -0,0 +1,189 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from importlib.resources import files
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from viano_bug_analysis.analysis import CaseInterpreter, HypothesisBuilder
|
||||
from viano_bug_analysis.case import CaseBuilder
|
||||
from viano_bug_analysis.lifecycle import State
|
||||
from viano_bug_analysis.package import PackageReader, PackageWriter
|
||||
from viano_bug_analysis.profiles.vcore.db_tools import normalize_schema_descriptor
|
||||
from viano_bug_analysis.tools import InvestigationToolRouter, ToolError, ToolLimits
|
||||
from viano_bug_analysis.tools.database_query import DatabaseQueryTool
|
||||
from test_case_builder import case_setup
|
||||
from test_case_interpreter import gateway, FixtureAdapter
|
||||
from test_hypothesis_builder import ProposalAdapter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tool_case(case_setup, tmp_path):
|
||||
store, _, _, _, _, old_index = case_setup
|
||||
index = copy.deepcopy(old_index)
|
||||
index["context"]["source"].append("tool/code.cpp")
|
||||
index["context"]["database"] += ["tool/db.json", "tool/schema.json", "tool/raw.json"]
|
||||
snapshot = {"format":"viano-database-snapshot","format_version":1,"table":"switches",
|
||||
"columns":[{"name":"id","type":"integer"},{"name":"enabled","type":"boolean"},{"name":"ref","type":"text"}],
|
||||
"rows":[{"id":1,"enabled":False,"ref":"A"},{"id":2,"enabled":True,"ref":"A"},{"id":3,"enabled":False,"ref":"B"}],"coverage":"partial"}
|
||||
schema = {"table":"switches","columns":[{"column_name":"id","column_type":"int(11)"}]}
|
||||
incoming = tmp_path / "tools-input.zip"
|
||||
PackageWriter().write(store.latest("case-A","stable-package"), incoming,
|
||||
additions={"tool/code.cpp":b"void activate() {\n activate();\n activate_more();\n}\n",
|
||||
"tool/db.json":json.dumps(snapshot).encode(),"tool/schema.json":json.dumps(schema).encode(),
|
||||
"tool/raw.json":b'[{"id":1},{"id":"1"},{"id":true}]',"tool/index.json":json.dumps(index).encode()},
|
||||
manifest_updates={"artifacts":{"case_inputs":"tool/index.json"}})
|
||||
store.intake(incoming)
|
||||
CaseBuilder(store).build("case-A","stable-package")
|
||||
asyncio.run(CaseInterpreter(store,gateway(FixtureAdapter())).interpret("case-A","stable-package"))
|
||||
result = asyncio.run(HypothesisBuilder(store,gateway(ProposalAdapter())).build("case-A","stable-package"))
|
||||
return store,result.package
|
||||
|
||||
|
||||
def batch(operation="source.find_text", arguments=None):
|
||||
args = arguments if arguments is not None else {"source_refs":["tool/code.cpp"],"text":"activate","case_sensitive":True,"offset":0,"limit":10}
|
||||
return {"format":"viano-investigation-request","format_version":1,"case_id":"case-A","package_id":"stable-package","analysis_revision":1,
|
||||
"requests":[{"request_id":"Q1","hypothesis_id":"H1","action_id":"A1","operation":operation,"arguments":args}]}
|
||||
|
||||
|
||||
def query(tool_case, operation="source.find_text", args=None, router=None):
|
||||
return (router or InvestigationToolRouter()).query(tool_case[1],batch(operation,args))["results"][0]["data"]
|
||||
|
||||
|
||||
def test_literal_search_provenance_pagination_and_no_writes(tool_case):
|
||||
original = hashlib.sha256(tool_case[1].read_bytes()).hexdigest()
|
||||
args = batch()["requests"][0]["arguments"]
|
||||
args["limit"] = 1
|
||||
first = query(tool_case,args=args)
|
||||
assert first["total_matches"] == 3 and first["truncated"] and first["next_offset"] == 1
|
||||
assert first["matches"][0]["line"] == 1
|
||||
with PackageReader(tool_case[1]) as reader:
|
||||
assert first["matches"][0]["sha256"] == hashlib.sha256(reader.read_bytes("tool/code.cpp")).hexdigest()
|
||||
args["offset"] = 2
|
||||
last = query(tool_case,args=args)
|
||||
assert last["matches"][0]["line"] == 3 and not last["truncated"]
|
||||
assert hashlib.sha256(tool_case[1].read_bytes()).hexdigest() == original
|
||||
assert tool_case[0].latest("case-A","stable-package") == tool_case[1]
|
||||
|
||||
|
||||
def test_symbol_occurrences_not_substrings_or_definitions(tool_case):
|
||||
result = query(tool_case,"source.find_symbol",{"source_refs":["tool/code.cpp"],"symbol":"activate","offset":0,"limit":10})
|
||||
assert [m["line"] for m in result["matches"]] == [1,2]
|
||||
assert result["interpretation"] == "text_occurrences_not_verified_definitions"
|
||||
|
||||
|
||||
def test_read_context_and_long_line_marking(tool_case):
|
||||
result = query(tool_case,"source.read_context",{"source_ref":"tool/code.cpp","start_line":2,"end_line":3},
|
||||
InvestigationToolRouter(ToolLimits(max_line_characters=4)))
|
||||
assert [m["line"] for m in result["matches"]] == [2,3]
|
||||
assert all(m["text_truncated"] for m in result["matches"])
|
||||
|
||||
|
||||
def test_database_structured_lookup_and_row_pointer(tool_case):
|
||||
result = query(tool_case,"database.lookup",{"source_ref":"tool/db.json","filters":{"enabled":False},"columns":["id"],"offset":0,"limit":1})
|
||||
assert result["matches"][0]["row"] == {"id":1}
|
||||
assert result["matches"][0]["json_pointer"] == "/rows/0"
|
||||
assert result["coverage"] == "partial" and result["next_offset"] == 1
|
||||
assert result["total_matches"] == 2
|
||||
|
||||
|
||||
def test_database_references_are_explicit_values_not_relationship_claims(tool_case):
|
||||
result = query(tool_case,"database.find_references",{"source_ref":"tool/db.json","search_columns":["ref"],"value":"A","columns":[],"offset":0,"limit":10})
|
||||
assert [m["row"]["id"] for m in result["matches"]] == [1,2]
|
||||
assert result["interpretation"] == "explicit_column_matches_not_inferred_relationships"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value,index", [(1,0),("1",1),(True,2)])
|
||||
def test_database_equality_does_not_coerce_boolean_string_or_number(tool_case,value,index):
|
||||
result = query(tool_case,"database.lookup",{"source_ref":"tool/raw.json","filters":{"id":value},"columns":[],"offset":0,"limit":10})
|
||||
assert result["total_matches"] == 1 and result["matches"][0]["json_pointer"] == f"/{index}"
|
||||
assert result["coverage"] == "unknown"
|
||||
|
||||
|
||||
def test_describe_generic_and_vcore_descriptor(tool_case):
|
||||
result = query(tool_case,"database.describe",{"source_ref":"tool/db.json"})
|
||||
assert result["table"] == "switches" and len(result["columns"]) == 3
|
||||
router = InvestigationToolRouter(database_tool=DatabaseQueryTool(normalize_schema_descriptor))
|
||||
result = query(tool_case,"database.describe",{"source_ref":"tool/schema.json"},router)
|
||||
assert result["coverage"] == "schema_only" and result["columns"] == [{"name":"id","type":"int(11)"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", [
|
||||
lambda b: b["requests"][0].update(operation="shell.exec"),
|
||||
lambda b: b["requests"][0].update(operation="testing.request_human"),
|
||||
lambda b: b["requests"][0]["arguments"].update(sql="DROP TABLE switches"),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["../escape"]),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["C:/server/code.cpp"]),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["tool/db.json"]),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["unknown/vendor"]),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["manifest.json"]),
|
||||
lambda b: b["requests"][0]["arguments"].update(source_refs=["tool/code.cpp","tool/code.cpp"]),
|
||||
lambda b: b.update(analysis_revision=2),
|
||||
lambda b: b["requests"][0].update(hypothesis_id="H99"),
|
||||
lambda b: b["requests"][0]["arguments"].update(limit=0),
|
||||
lambda b: b["requests"][0]["arguments"].update(limit=10.0),
|
||||
lambda b: b["requests"][0]["arguments"].update(text=""),
|
||||
])
|
||||
def test_unsafe_unsupported_and_wrongly_bound_requests_rejected(tool_case,change):
|
||||
request = batch()
|
||||
change(request)
|
||||
with pytest.raises(ToolError):
|
||||
InvestigationToolRouter().query(tool_case[1],request)
|
||||
|
||||
|
||||
def test_whole_batch_preflight_before_any_dispatch(tool_case,monkeypatch):
|
||||
router = InvestigationToolRouter()
|
||||
request = batch()
|
||||
invalid = batch("database.lookup",{"source_ref":"tool/db.json","filters":{"unknown":1},"columns":[],"offset":0,"limit":1})["requests"][0]
|
||||
invalid.update(request_id="Q2",action_id="A2")
|
||||
request["requests"].append(invalid)
|
||||
calls=[]
|
||||
monkeypatch.setattr(router.source,"execute",lambda *args: calls.append("dispatched"))
|
||||
with pytest.raises(ToolError,match="TOOL_DATABASE_COLUMN_UNKNOWN"):
|
||||
router.query(tool_case[1],request)
|
||||
assert not calls
|
||||
|
||||
|
||||
def test_search_empty_match_is_scoped_and_regex_is_literal(tool_case):
|
||||
args=batch()["requests"][0]["arguments"]
|
||||
args["text"]=".*"
|
||||
result=query(tool_case,args=args)
|
||||
assert result["matches"] == [] and result["scope"] == "packaged_source_snapshot"
|
||||
assert result["checked_sources"][0]["source_ref"] == "tool/code.cpp"
|
||||
|
||||
|
||||
def test_repeated_request_identity_rejected(tool_case):
|
||||
request=batch()
|
||||
request["requests"].append(copy.deepcopy(request["requests"][0]))
|
||||
with pytest.raises(ToolError,match="TOOL_DUPLICATE_REQUEST"):
|
||||
InvestigationToolRouter().query(tool_case[1],request)
|
||||
|
||||
|
||||
def test_context_ranges_and_operational_limits(tool_case):
|
||||
for start,end in [(0,1),(1,99),(3,2)]:
|
||||
with pytest.raises(ToolError):
|
||||
query(tool_case,"source.read_context",{"source_ref":"tool/code.cpp","start_line":start,"end_line":end})
|
||||
for limits in [ToolLimits(max_file_bytes=2),ToolLimits(max_total_bytes=2),ToolLimits(max_matches=1),ToolLimits(max_request_bytes=2),ToolLimits(max_result_bytes=2)]:
|
||||
with pytest.raises(ToolError):
|
||||
query(tool_case,router=InvestigationToolRouter(limits))
|
||||
|
||||
|
||||
def test_blocked_case_cannot_issue_queries(tool_case):
|
||||
store,_=tool_case
|
||||
path=store.advance("case-A","stable-package",State.BLOCKED,step_id="wait",reason="PROVIDER_UNAVAILABLE",retry_not_before="2099-01-01T00:00:00Z")
|
||||
with pytest.raises(ToolError,match="TOOL_PHASE"):
|
||||
InvestigationToolRouter().query(path,batch())
|
||||
|
||||
|
||||
def test_new_undeclared_input_invalidates_query_context(tool_case,tmp_path):
|
||||
changed=tmp_path/"changed.zip"
|
||||
PackageWriter().write(tool_case[1],changed,additions={"new/raw":b"evidence"})
|
||||
with pytest.raises(ToolError,match="TOOL_STALE_CASE"):
|
||||
InvestigationToolRouter().query(changed,batch())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["investigation-request","investigation-result","database-snapshot"])
|
||||
def test_new_schemas_valid(name):
|
||||
Draft202012Validator.check_schema(json.loads(files("viano_bug_analysis.schemas").joinpath(name+'-v1.schema.json').read_text()))
|
||||
Reference in new issue
Block a user