Unified databases (PRO-1618): adopt the client contract (no type, context[] body, four-key query, llm_prompt verbatim) - #13
Conversation
On a unified database the plugin now speaks the contract exactly, and a split database keeps every byte it had. Query: no `type` is sent; `follow_forceful_relations` goes with the request (config `followForcefulRelations`, default true); the four-key response (chunks, graph, relations, llm_prompt) is recognised by shape and parsed into its own normalised form; the injected <hydradb-context> block is the server's llm_prompt verbatim, citation labels included, and the `query` text output renders the structured fields in the same labelling. This also replaces the undefined `buildSection` call the unified render path had. Ingest: the JSON body's list key is `context` (never `items`), with the request-level enrich/upsert/instructions defaults and the contract's item fields (context_id, title, enrich, upsert, instructions, happened_at as YYYY-MM-DD, attributes, custom_attributes). `is_markdown` and `user_name` are not item fields in the contract, so a text item carries them inside custom_attributes rather than as invented fields. The 202 is parsed: results[].source_id is the context id, surfaced by `ingest --note` and `ingest --session --json`. Other endpoints: list, relations and delete send no `type` on a unified database; database create keeps `type: "unified"`. Tests: shared fixtures for both response shapes; the unified query wire and parse, the injected llm_prompt, the exact unified ingest body for text and conversation items, the 202 parser, no-`type` list/relations/delete; a `query-unified` --json golden; and two whole-text split goldens cut from the pre-contract code so split output is pinned byte-for-byte. Docs: hydradb-api-info/unified-databases.md rewritten to the contract (request, item fields, 202, four-key response), docs/usage.md, README, config examples, conformance/README.md and the query/search/auto-recall/ hydradb-context/last-recall/setup skills. Signed-off-by: SohamRatnaparkhi <soham.ratnaparkhi@gmail.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
This comment has been minimized.
This comment has been minimized.
…llowForcefulRelations in doctor Three findings from the first review. A unified ingest answers 202 and names refused items in results[] with status "failed". Returning that quietly let the workspace sync record the file as synced and skip it for ever, and let turn capture and session upsert advance their hashes for content the server never stored. addItems now raises, the way a split database's 4xx does, naming the context ids and reasons with the parsed 202 attached; the sync leaves the file untracked and sends it again next run (pinned by test 12c). The structured unified render carries each chunk's temporal facts next to its content and enrichment, for chunks[] and relations[] alike, so the query text output and the no-llm_prompt fallback keep the dated claim. doctor's text output prints followForcefulRelations, which its --json already carried. Signed-off-by: SohamRatnaparkhi <soham.ratnaparkhi@gmail.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OpenHack SummarySecurity review of Unified databases (PRO-1618): adopt the client contract (no type, context[] body, four-key query, llm_prompt verbatim). 25 changed files; 1 finding at or above the low reporting threshold. Confidence Score: 4/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: scripts/lib/hydra-client.mjs Important Files Changed
Prompt To Fix With AIReview the findings for https://github.com/hydra-db/hydradb-claude-code/pull/13 at commit 58699e12f08a8ec0ecfdc04aa64f168221670063. Verify each finding against the current code before fixing it. Preserve unrelated changes and run focused regression tests. ### Issue 1: [P4] Unsanitized unified-ingest response data is echoed to terminal and JSON output Vulnerability type: CWE-116: Improper Encoding or Escaping of Output scripts/lib/hydra-client.mjs:687 For unified ingestion, the new parser copies `results[].source_id` directly into `contextIds`, and the new CLI paths expose those values. `/hydradb:ingest --note` appends the first context ID to stdout; `ingest --session --json` returns all context IDs; and failed 202 responses are converted into errors containing server-provided error text. Unlike recall normalization, these values are not passed through `redactSecrets` or `trimText`, and terminal output is not protected against ANSI/OSC control sequences. A malicious or compromised backend can therefore inject deceptive terminal output or bypass the plugin's redaction layer. The pre-existing raw HTTP error-body path is not treated as part of this regression; the new regression is the successful 202 and newly surfaced per-item response data. Recommendation: Treat every unified-ingest response field as untrusted. In `parseUnifiedIngestResponse`, apply `redactSecrets` and bounded `trimText` to context IDs, titles, statuses, error codes, error text, and the response message, using appropriate caps. Before writing any server-derived value to terminal stdout or stderr, remove ANSI/OSC and other terminal control sequences or use a terminal-safe escaping helper. Apply equivalent bounded sanitization before placing values in session JSON, while preserving structured error data separately if needed. Add tests covering successful `source_id`, failed-item `error`, and session JSON values containing control sequences and secret-shaped tokens. Last reviewed commit: 58699e1 · View review on OpenHack
|
…vised contract (PRO-1618)
The unified /query body renamed its root key `relations` to
`forceful_relations` and added `graph[].origin`. On unified databases only
(split untouched, both split goldens unchanged):
- Shape detection now requires a `forceful_relations` array beside `graph`
and `llm_prompt`. There is no fallback to `relations`: recallUnified refuses
any other body with a named error, so it lands in the recall's errors
instead of reaching readers without the bucket.
- The normalized result exposes `forcefulRelations[]` (was `relations[]`),
`graph[].origin` ("query_path" | "chunk_relation", omitted otherwise), and
last-recall reports `unifiedForcefulRelationCount`.
- The structured rendering uses the server's `=== FORCEFUL RELATIONS ===`
heading and guide line; the fixture llm_prompt does too.
- The unified envelope meta (no tenant_id, sub_tenant_id, source_type) is
pinned by a wire test; nothing on the unified path reads meta.
- Fixtures, wire tests (27), the query-unified golden, the unified-databases
reference, usage docs, README and the query/search/auto-recall/last-recall
skills follow the new names.
Signed-off-by: SohamRatnaparkhi <soham@hydradb.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (PRO-1618)
The unified /query chunk shape is final: `enrichment` is a plain string
(was `{text, kind}`) and `enrichment_kind` is a new sibling carrying the
declared context_category, on chunks[] and on forceful_relations[].chunk
alike. llm_prompt is markdown; the `=== CONTEXT === / === GRAPH ===`
layout is gone. On unified databases only (split untouched, both split
goldens unchanged):
- normalizeUnifiedChunk redacts and keeps the enrichment string as
`enrichment` and the kind as `enrichmentKind`; each is omitted when
empty, and the kind is kept when a chunk has no enrichment. The old
object form is not read.
- The structured render (query text output, and the fallback when a
server sends no llm_prompt) follows the server's markdown layout:
`## Results` with `### n.`, `**Relevance:**` / `**Category:**`,
`**Enrichment:**`, `**Temporal:**`; `## Forceful relations` with the
guide line and `### Rn.` / `**Linked from:**`; `## Related facts` with
`[Pn]` paths.
- The fixture carries the new fields and a markdown llm_prompt, and a new
wire test reads the real envelope from the server's handler test
(conformance/unified-query-envelope.json) end to end (28 wire tests).
- The unified-databases reference shows that real envelope; usage docs,
README and the query/search/auto-recall/hydradb-context skills describe
the string enrichment, enrichmentKind and the markdown citation numbers.
Signed-off-by: SohamRatnaparkhi <soham@hydradb.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Product decision: remove compaction from the query response in llm_prompt/context. On a unified database the injected context and the query output now carry the server's llm_prompt whole, and the structured rendering of unified chunks never trims content, enrichment or temporal. Split databases are unchanged (split goldens and tests pass as before). - buildHydraContextBlock injects the unified section whole. The maxContextChars budget now applies to the split MEMORY/KNOWLEDGE sections only, so a split block is byte-identical. - The structured unified render (query text output, and the fallback when a server sends no llm_prompt) drops truncateText(content, 700), truncateText(enrichment, 280) and truncateText(temporal, 280). - normalizeUnifiedResponse keeps every field whole: chunk content and enrichment (were capped at 1200), temporal facts and path summaries (400), graph triplet names, predicates, contexts and temporal details (120/80/180/80), context and chunk ids and via ids. The node, relation and triplet sanitisers take the trim function as an argument, so the split lane keeps its caps. Secret redaction still applies everywhere. - A new conformance test pins it: a 20k-char llm_prompt passes a 7000 maxContextChars budget whole (redaction still on), long content, enrichment, temporal, path summaries and triplets come through the normaliser, the fallback and the structured form uncut, and a split block is still capped. - README and usage docs say the budget is split-only and note Claude Code's own 10,000-character additionalContext cap (over it, Claude Code saves the block to a file and shows a 2,000-character preview). Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-1618) parseUnifiedIngestResponse copied results[].source_id, title, status, error, error_code and message straight from the server, and some of it is printed (the context id after `ingest --note`, refusal reasons in the raised error) or returned as JSON (`ingest --session --json`). Each is now redacted, stripped of terminal control sequences (CSI, OSC and other C0/C1 characters) and bounded. New stripControlChars in sanitize.mjs; test 12b covers escape sequences and a secret-shaped refusal reason. Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…2 (PRO-1618) A control character inside a key split it into fragments the secret patterns did not match, and stripping afterwards joined it back into an unredacted key. ingestResponseText now strips first, then redacts, then bounds. Test 12b covers a key split by a BEL character. Signed-off-by: SohamRatnaparkhi <soham@hydradb.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stacked on #11. Brings the plugin onto the PRO-1618 unified API contract (the hydradb-application #1625 query response plus the stacked ingest-contract PR). Everything below applies to a unified database only; a split database keeps every byte it had, and that is now pinned by goldens cut from the pre-contract code.
Unified: what changed
Query (
POST /query)typeis sent (absent is the unified default;knowledge/memoryare 400).follow_forceful_relationsgoes with the request, from the new config knobfollowForcefulRelations(defaulttrue, envHYDRADB_FOLLOW_FORCEFUL_RELATIONS).chunks,graph,forceful_relations,llm_prompt) is recognised by shape (graphandforceful_relationsarrays and a stringllm_prompt), never by a flag, and parsed intounified.chunks[](contextId,chunkId,score,content,enrichment(a plain string),enrichmentKind(the declaredcontext_category, kept even when there is no enrichment),temporal[]),unified.graph[](origin,pathSummary,triplets),unified.forcefulRelations[](via{from,to},chunkwith the same fields) andunified.llmPrompt. A body that still names the bucketrelationsis not read as unified (no fallback);recallUnifiedrefuses it with a named error that lands in the recall's errors.<hydradb-context>block isllm_promptverbatim: markdown (## Resultswith### 1.,## Forceful relationswith### R1.,## Related factswith[P1],## Temporal facts,## Sources), numbered for citation as[1],[R1],[P1]. It is injected whole: only secret redaction applies, never truncation or summarising, and themaxContextCharsbudget applies to split databases only (see "No compaction" below). This also removes the undefinedbuildSectioncall that Unified databases (PRO-1618): layout-aware recall and ingest, items[] body #11 left on the unified render path.querytext output renders the structured fields in the server's markdown layout:## Resultswith### 1.per chunk (**Relevance:**,**Category:**, content,**Enrichment:**,**Temporal:**),## Forceful relationswith the server's guide line and### R1./**Linked from:**, and## Related factswith[P1]paths;last-recallreportsunifiedCount,unifiedGraphPathCount,unifiedForcefulRelationCount.Ingest (
POST /context/ingest; every write: turn capture, session upsert,ingest --note,ingest --session, workspace sync)context(neveritems), plus request-levelenrich/upsert/instructionswhen set.textorconversation,context_id,title,enrich,instructions(wascustom_instructions),happened_atasYYYY-MM-DD(cut from the file mtime),attributes,custom_attributes.is_markdownanduser_nameare not item fields in the contract, so a text item carries them insidecustom_attributesinstead of as invented top-level fields; a conversation's attribution stays the per-turnname. Note: the server build I checked still accepts both at top level, so this is a contract-strictness choice and is a one-line flip inmemoryToItemif you would rather send them.results[].source_idis the context id):ingest --noteprints it,ingest --session --jsonreturnscontextIds. A 202 that names a refused item (status: "failed") raises, the way a split database's 4xx does, so the workspace sync never records the file as synced and turn/session capture never advance their hashes for content the server did not store.Other endpoints
POST /context/list,GET /context/relations,DELETE /contextsend notypeon a unified database.POST /databaseskeepstype: "unified".Split: unchanged
The multipart ingest,
typeon every call, the legacy normaliser and the MEMORY/KNOWLEDGE block are untouched.conformance/golden/split-normalized.golden.jsonandsplit-context-block.golden.txtwere generated from the PR #11 code before any edit, and the final tree reproduces them byte-for-byte (the check regenerates and diffs them).Files
scripts/lib/hydra/index.mjs: notypeon unified query/list/relations/delete;contextlist key;follow_forceful_relations; request-level ingest defaults.scripts/lib/hydra-client.mjs:isUnifiedQueryResponse,normalizeUnifiedResponse,EMPTY_UNIFIED_RECALL,parseUnifiedIngestResponse,toHappenedAt;memoryToItemon the contract's names;addItemssendscontext, returns the parsed 202 and raises on refused items.scripts/lib/context-format.mjs:buildUnifiedContextString(llm_prompt verbatim) andbuildUnifiedStructuredString(markdown: content, enrichment, category, temporal, forceful relations, graph); replaces the undefinedbuildSection.scripts/plugin.mjs: forceful-relations pass-through, unified fallback in split mode, unified counts inlast-recall, context ids from the 202, structured text render,followForcefulRelationsindoctor.scripts/lib/config.mjs,config.example.json,.hydradb-plugin.json.example:followForcefulRelations.conformance/fixtures.mjs(new): the split and unified query fixtures.conformance/unified-query-envelope.json(new): the real unified/queryenvelope from the server's handler test, read end to end by wire test 10e.conformance/tests.mjs,scripts/check.mjs: tests 10/10a/10b/10c/10d/10e/11/12/12b/12c/13x/14 rewritten or added (20 to 28 wire tests), unified and split goldens (3 to 6).conformance/golden/:query-unified.shape.json(new),split-*.golden.*(new),doctor.shape.json(addsfollowForcefulRelations).hydradb-api-info/unified-databases.md(rewritten to the contract),docs/usage.md,README.md,conformance/README.md, skillsquery,search,auto-recall,hydradb-context,last-recall,setup.Checks
npm run check: 7 scripts, 6 JSON files, 16 conformance vectors, 29 HTTP-level wire tests, 6 goldens. Pass.npm run pack:dry-run: pass.query,query --json,ingest --note,user-prompt-submitthenlast-recall,stop,ingest --session --json): zero calls carriedtype, all three ingests were JSONcontext[]bodies, the injected block containedllm_promptverbatim.fab86ba): a 202 that refuses an item now raises instead of returning (test 12c: the workspace sync leaves the file untracked, so it is retried next run); the structured unified render includes each chunk's temporal facts;doctortext output printsfollowForcefulRelations.27a0f3d): response root keyrelationsis nowforceful_relations(plugin nameforcefulRelations),graph[].origin(query_path|chunk_relation) is carried, the forceful-relations guide line is carried, and a wire test pins the unified envelope meta (notenant_id/sub_tenant_id/source_type; nothing on the unified path reads meta).58699e1):chunks[].enrichmentis a plain string (was{text, kind}) andenrichment_kindis its sibling, onforceful_relations[].chunktoo; the plugin exposes them asenrichmentandenrichmentKind.llm_promptis markdown, so the fixture prompt, the structured render, the docs and the skills moved off the old=== CONTEXT === / === GRAPH ===layout. Thequery-unifiedshape golden now listsenrichmentandenrichmentKind(on chunks and forceful-relation chunks); both split goldens are unchanged.715472e, product decision "remove compaction from query response in llm_prompt/context"): the unifiedllm_promptis injected whole, pastmaxContextChars(the budget now applies only to the split MEMORY/KNOWLEDGE sections, so a split block is byte-identical); the structured render dropstruncateTexton content (700), enrichment (280) and temporal (280); the unified normaliser drops its length caps on content and enrichment (1200), temporal and path summaries (400), graph triplet names/predicates/contexts/temporal details, and context/chunk/via ids. Secret redaction is kept. New wire test 10d-2 pins it with a 20k-characterllm_promptand long fields (29 wire tests); both split goldens are unchanged.9e85ec0, OpenHack P4 finding):parseUnifiedIngestResponsenow redacts, strips terminal control sequences from, and bounds every server-provided string (context ids, title, status, error, error_code, message) beforeingest --noteprints it,ingest --session --jsonreturns it, or a refusal error carries it; commit45b5ce6(Greptile finding) strips control characters before redacting, so a key split by one cannot slip past the secret patterns. Test 12b covers both.Host size limit
Claude Code caps a hook's
additionalContextat 10,000 characters, with no setting to raise it. Over the cap, Claude Code saves the block to a file in the session directory and shows Claude the file path plus a preview of the first 2,000 characters. The plugin does not truncate to fit; a unified block that long reaches Claude through that file.docs/usage.mdsays so.Not touched: the shared four-repo
CONTRACT.md(the PRO-1298 client contract; its header forbids editing one repo's copy alone) and thehydradb-api-info/*pages that document split-only v1 endpoints.🤖 Generated with Claude Code