feat(unified): follow the PRO-1618 unified API contract on unified databases - #38
SohamRatnaparkhi wants to merge 5 commits into
Conversation
…tabases A database is split (knowledge + memory corpora, type on every call) or unified (one corpus; type is never sent). The CLI now reads the layout once per command from GET /databases details[].type and branches on that, never on a request flag. Split databases keep every request and rendering exactly as before; on a unified database: - query is a raw JSON POST /query with no type, parsed as the four-key body (chunks, graph, relations, llm_prompt). New --llm prints llm_prompt verbatim on stdout; --output json prints the body verbatim; --follow-forceful-relations/--no-follow-forceful-relations is forwarded. The renderer detects the shape by the body, not the layout. - ingest is a raw JSON POST /context/ingest with the context list and one item of exactly one --text or --conversation-file, plus --context-id, --title, --enrich/--no-enrich, --instructions, --happened-at, --attributes, --custom-attributes, --category, --forceful-relation, --acl and --upsert/--no-upsert. Files, --kind, --user-name and --markdown are refused there; the unified-only options are refused on split. - list, delete, relations, subgraph and inspect never send type; an explicit --kind is refused rather than dropped. - database create --type split|unified, and database list shows the type. The pinned SDK cannot be relied on for the unified values, so these calls go over the wrapper's raw v2 path with the same headers, envelope unwrap and error translation. Conformance gains the ingest-unified-json vector and the runner records raw calls the same way as SDK calls. Supersedes #30. Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d853be5 to
57747bd
Compare
|
OpenHack SummarySecurity review of feat(unified): follow the PRO-1618 unified API contract on unified databases. 10 changed files; 1 finding at or above the low reporting threshold. Confidence Score: 3/5Review the findings below before merging. Security merge-readiness rubric: 1 = critical, 2 = high, 3 = medium, 4 = low, 5 = no reportable findings. This score reflects scan findings, not a guarantee of correctness or complete coverage. Files Needing Attention: src/hydradb_cli/commands/_impl.py Important Files Changed
Prompt To Fix With AIReview the findings for https://github.com/hydra-db/hydradb-cli/pull/38 at commit 0e2c33a8377ad0cf741bfbad49e5a28ac68c4be5. Verify each finding against the current code before fixing it. Preserve unrelated changes and run focused regression tests. ### Issue 1: [P3] Terminal escape-sequence injection through raw unified query prompt output Vulnerability type: CWE-150: Improper Neutralization of Escape, Meta, or Control Sequences src/hydradb_cli/commands/_impl.py:380 This pull request adds a raw-output mode for unified query results. In the `llm` branch, the CLI emits the server-built prompt verbatim with `typer.echo`. The prompt is assembled from content stored through the new unified ingest flow, so an authenticated contributor can store terminal escape sequences. A different user who runs `hydradb query ... --llm` interactively can receive those bytes in their terminal, where OSC/CSI sequences may alter display, create misleading hyperlinks or overwrite the clipboard in supporting emulators. The existing structured rendering protects API data through Rich text/table handling, but this new branch bypasses it. Piped output is not the demonstrated execution context; the issue requires interactive terminal output. Recommendation: Preserve verbatim prompt bytes for non-interactive/piped `--llm` use, but sanitize terminal control sequences whenever output is directed to an interactive terminal. Strip CSI, OSC, and other escape sequences (including BEL and C1 variants), or render the prompt as non-markup plain text through a no-wrap terminal renderer. Add tests with OSC 8, OSC 52, CSI cursor-control, BEL, and C1 sequences for TTY output, while retaining a test that confirms the documented piped output behavior. Last reviewed commit: 0e2c33a · View review on OpenHack
|
…st (PRO-1618) A failed layout probe read as split, so an auth or network failure sent the split request shape to a database that may be unified. The probe now propagates; only a successful response that lacks the database's layout metadata still answers split. _is_unified routes the error through the same api/network handlers as every other call. A unified ingest that came back with failed_count > 0 or a row carrying an error status printed the details and exited 0. The command now exits nonzero after printing the server's per-item errors. Signed-off-by: SohamRatnaparkhi <soham.ratnaparkhi@gmail.com>
…d queries (PRO-1618)
The unified /query body renamed its declared-relation list from `relations`
to `forceful_relations` (same item shape), and every graph path now carries
`origin`: `query_path` or `chunk_relation`.
- The renderer reads `forceful_relations` only, as a list; there is no
fallback to `relations`. The section is titled "Forceful relations",
labelled R1.. as llm_prompt labels them, and hidden when the list is empty.
- Shape detection also accepts a `forceful_relations` array and tells a split
body apart by type: a split body carries `graph` ({paths}) and
`forceful_relations` ({declared, inferred}) as objects.
- Graph paths are grouped by origin, the way the split body kept query_paths
and chunk_relations apart. Query-path hops cite the returned chunk they
came from; a chunk relation lists the chunk it hangs under, matched by
relation.chunk_id against chunks[] and forceful_relations[].chunk. A path
with no known origin gets a group of its own. P labels stay positions in
graph[], as llm_prompt numbers them.
- A unified /query meta no longer has tenant_id, sub_tenant_id or
source_type. The unified path only ever read meta.request_id; a test now
pins that in every output mode.
- Fixture: forceful_relations, both origins, and the llm_prompt heading
"=== FORCEFUL RELATIONS ===" with its guide line.
- Drops two em dashes and a stale failed-probe comment left by earlier
commits on this branch.
Signed-off-by: SohamRatnaparkhi <soham@hydradb.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…(PRO-1618)
The final unified /query chunk shape: chunks[].enrichment is a plain string
(it was {text, kind}) and chunks[].enrichment_kind is a new sibling field
carrying the declared context_category. Either can be absent, and a kind is
present even when there is no enrichment. forceful_relations[].chunk has the
same shape. llm_prompt is now a markdown document; the old
=== CONTEXT === / === GRAPH === layout is gone.
- The chunk panel reads enrichment as a string and enrichment_kind beside it,
shows a kind with no enrichment, and ignores the old object shape.
- tests/golden/query_unified.json is now a real envelope rendered by the
server's own handler test; the tests read its data body and keep handing
the CLI their own request_id.
- README and CHANGELOG describe the new fields and the markdown prompt.
Split databases are unchanged.
Signed-off-by: SohamRatnaparkhi <soham@hydradb.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
05a311f to
58ed716
Compare
The unified answer is not compacted anywhere. The readable view trimmed a chunk's content and enrichment to 300 characters and a forceful chunk's content to 120; all three now print whole. --llm already printed llm_prompt verbatim; --output json is unchanged. Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| print_json(body) | ||
| return | ||
| if llm: | ||
| typer.echo(body.get("llm_prompt") or "") |
There was a problem hiding this comment.
Terminal escape-sequence injection through raw unified query prompt output
Vulnerability type: CWE-150: Improper Neutralization of Escape, Meta, or Control Sequences
This pull request adds a raw-output mode for unified query results. In the llm branch, the CLI emits the server-built prompt verbatim with typer.echo. The prompt is assembled from content stored through the new unified ingest flow, so an authenticated contributor can store terminal escape sequences. A different user who runs hydradb query ... --llm interactively can receive those bytes in their terminal, where OSC/CSI sequences may alter display, create misleading hyperlinks or overwrite the clipboard in supporting emulators. The existing structured rendering protects API data through Rich text/table handling, but this new branch bypasses it. Piped output is not the demonstrated execution context; the issue requires interactive terminal output.
Location: src/hydradb_cli/commands/_impl.py:380
Recommendation:
Preserve verbatim prompt bytes for non-interactive/piped --llm use, but sanitize terminal control sequences whenever output is directed to an interactive terminal. Strip CSI, OSC, and other escape sequences (including BEL and C1 variants), or render the prompt as non-markup plain text through a no-wrap terminal renderer. Add tests with OSC 8, OSC 52, CSI cursor-control, BEL, and C1 sequences for TTY output, while retaining a test that confirms the documented piped output behavior.
Prompt To Fix With AI
Review the findings for https://github.com/hydra-db/hydradb-cli/pull/38 at commit 0e2c33a8377ad0cf741bfbad49e5a28ac68c4be5. Verify each finding against the current code before fixing it. Preserve unrelated changes and run focused regression tests. ### Issue 1: [P3] Terminal escape-sequence injection through raw unified query prompt output Vulnerability type: CWE-150: Improper Neutralization of Escape, Meta, or Control Sequences src/hydradb_cli/commands/_impl.py:380 This pull request adds a raw-output mode for unified query results. In the `llm` branch, the CLI emits the server-built prompt verbatim with `typer.echo`. The prompt is assembled from content stored through the new unified ingest flow, so an authenticated contributor can store terminal escape sequences. A different user who runs `hydradb query ... --llm` interactively can receive those bytes in their terminal, where OSC/CSI sequences may alter display, create misleading hyperlinks or overwrite the clipboard in supporting emulators. The existing structured rendering protects API data through Rich text/table handling, but this new branch bypasses it. Piped output is not the demonstrated execution context; the issue requires interactive terminal output. Recommendation: Preserve verbatim prompt bytes for non-interactive/piped `--llm` use, but sanitize terminal control sequences whenever output is directed to an interactive terminal. Strip CSI, OSC, and other escape sequences (including BEL and C1 variants), or render the prompt as non-markup plain text through a no-wrap terminal renderer. Add tests with OSC 8, OSC 52, CSI cursor-control, BEL, and C1 sequences for TTY output, while retaining a test that confirms the documented piped output behavior.
TIP: Reply
@openhack-agentor@openhack-agent fix thisto fix this finding. To ask a question, mention@openhack-agentfollowed by your question.
Summary
Brings the CLI onto the PRO-1618 unified API contract. A database is either
split(knowledge + memory corpora,typeon every call) orunified(one corpus). The CLI now reads the layout once per command fromGET /databasesdetails[].typeand branches on that, never on a request flag.Supersedes #30 (
soham/pro-1618-unified), which conflicts withmain. This branch starts fromorigin/mainand ports the still-relevant parts of #30 (layout detection,databases.layouts()/layout(),--type split|unifiedondatabase create, the Type column ondatabase list, file uploads refused on unified), then applies the contract. #30 is left open for the author to close.Split databases: unchanged
Every existing request and rendering on a split database is byte-for-byte what it was: the SDK multipart ingest with
type, the SDK query, thedeletedefault ofknowledge, the split query renderer againsttests/golden/query.json. The only addition visible on a split database is oneGET /databasesprobe per command, and a clear refusal of the new unified-only flags.Unified databases
hydradb query: raw JSONPOST /querywith notype; the four-key body (chunks,graph,forceful_relations,llm_prompt) is parsed and rendered as chunk panels (context_id,score,content,enrichmentas a plain string withenrichment_kind, the declared category, beside it, temporal facts; either enrichment field can be absent, a kind with no enrichment is still shown, and the old{text, kind}object is not read), graph tables grouped bygraph[].origin(query paths, whose hops cite the returned chunk they came from, apart from chunk relation paths, each listed under the chunk it hangs under byrelation.chunk_id; a path with no known origin gets its own group) and a "Forceful relations" table (R1..,via.from->via.to; eachchunkhas the same shape as achunks[]item), hidden when the list is empty.relations(the old root key) is not read. New--llmprintsllm_prompt, a markdown document (# Query results,## Results,## Forceful relations,## Related facts,## Temporal facts,## Sources), verbatim on stdout (feedback hint on stderr) so it can be piped into a model call.--output jsonprints the body verbatim, nothing added; the request id is lifted from the envelopemetaforhydradb feedback.--follow-forceful-relations/--no-follow-forceful-relationsis forwarded. The renderer detects the shape by the body (llm_prompt, orgraph/forceful_relationsas arrays, vschunk_content/graph_context; a split body carriesgraphandforceful_relationsas objects), so a unified body that reaches the split path is still rendered as what it is. Onlymeta.request_idis read from a unified response's meta, which no longer carriestenant_id,sub_tenant_idorsource_type.hydradb ingest: raw JSONPOST /context/ingestwith thecontextlist and one item of exactly one--textor--conversation-file(JSON list of{role, content, name?}), plus--context-id,--title,--enrich/--no-enrich,--instructions,--happened-at(YYYY-MM-DD),--attributesand--custom-attributes(JSON objects),--category, repeatable--forceful-relationand--acl,--upsert/--no-upsert. Everything is validated locally and named by turn or flag before a round trip. Files,--kind,--user-nameand--markdownare refused with a message. The 202'sresults[].source_idis rendered as the item's context id.list,delete,relations,subgraph,inspect: never sendtype; an explicit--kindis refused rather than silently dropped. The deprecated aliases that hard-code a kind (recall full,memories add,knowledge upload, ...) are refused on a unified database with the same message.database create --type split|unified,database listwith a Type column.The pinned SDK (
hydradb-sdk>=2.1.4,<3) cannot be relied on for the unified values, so every unified request (anddatabase create --type) goes over the wrapper's raw v2 path with the same headers, envelope unwrap and error translation as the SDK path.databases.list()stays on the SDK path: the SDK model already carriesdetails[]withextra="allow".One judgement call to flag:
--context(additional_context) is still forwarded on a unified query when the user passes it. The contract's field list does not name it, but it is an existing v2 request field the server binds on the same request struct and nothing rejects it on unified.Files
src/hydradb_cli/hydra/client.py:LAYOUT_*constants,databases.create(layout=)over the raw path,databases.layouts()/layout()memoised probe,context.query_unified()returning(body, request_id),context.ingest_context(),_raw_post_with_meta().src/hydradb_cli/commands/_impl.py: layout helpers, shape-detecting renderer dispatch and the unified renderer,--llmoutput,do_ingest_unified+build_context_item+ conversation/JSON/date validation, kind refusal on unified for list/delete/relations/subgraph, file refusal,database create --type, Type column.src/hydradb_cli/commands/canonical.py:--llm,--follow-forceful-relations, the unified ingest options and dispatch,delete --kindoptional,database create --type, help text.tests/test_unified.py(new, 72 tests): layout probe, wrapper query/ingest bodies (exact JSON), CLI query rendering/JSON verbatim/--llm, split renderer unchanged againsttests/golden/query.json, unified ingest items and refusals, split ingest unchanged, list/delete/relations/subgraph/inspect, database create/list, an end-to-end verbatim JSON run through the real wrapper, graph grouping by origin, no fallback torelations,enrichmentas a string withenrichment_kindbeside it (and no reading of the old object), and a pin that onlymeta.request_idis read on the unified path.tests/golden/query_unified.json(new): a real unified/queryenvelope rendered by the server's own handler test (hydradb-application PRO-1618):forceful_relations[], bothgraph[].originvalues,enrichmentwithenrichment_kind, a start-only temporal fact and the markdownllm_prompt. The tests read itsdatabody and hand the CLI their own request id.conformance/vectors.json: newingest-unified-jsonvector (JSON body,contextlist,typeforbidden).conformance/conftest.py: rawhttpxcalls routed to the recorder.conformance/test_conformance.py: unified dispatch. The master copy of the vectors that lives with the shared CONTRACT.md should gain the same vector.README.md,CHANGELOG.md: unified sections and the new options.Verification
pytest -q: 427 passed.ruff check .andruff format --check .: clean.Added lines scanned for em dashes: none.
Nothing is compacted. The readable view prints a unified chunk's content and enrichment, and a forceful chunk's content, whole (no 300/120 character trims).
--llmprintsllm_promptverbatim and--output jsonis the body verbatim.🤖 Generated with Claude Code