- Honor the canonical Knowledge API URL and fail closed with typed errors when
paged or exact project-resource responses omit or malform their envelopes,
preventing false
ok: trueresults and undefined-resource crashes.
- Add package-owned guarded CLI helpers for create, update, bounded query, and
exact readback. Opaque descriptors cross the child-process boundary only
through the runtime-owned anonymous process IPC channel; private titles,
bodies, tags, selectors, and result proofs stay out of argv, stdin,
environment variables, files, stdout, and stderr. The IPC transport works
across Bun and Node parents on Linux, macOS, and Windows without Linux
/procassumptions or a write-before-connection race. - Reject public/body-bearing guarded CLI input, regular-file descriptors, malformed or oversized private frames, replay conflicts, and expired descriptors without echoing private content. Preserve FCAME-1 authority binding, compare-and-swap/create-if-absent semantics, immutable receipts, digest-only proof, exact readback, and same-operation duplicate evidence.
- Republish the same reviewed bounded-query product code through the isolated exact-clone provenance path so the registry can record the landed commit.
- Reject command-shaped unknown lookups before they can fall through to the AI-backed prompt path, while preserving the explicit documented prompt forms.
- Prevent unsupported
knowledge showlookups by stored full ID, short ID, or custom item ID from entering prompt or generation paths. - Add producer-bounded list and ranked-search semantics, private guarded query descriptors, mixed-version compatibility, and the live private-query acceptance command.
- Add bounded exact binding-state readback and receipt-backed guarded adoption for legacy Knowledge rows. Adoption requires the full ID, expected version, and raw-content SHA-256; changes only the FCAME-1 binding/provenance fields; replays deterministically; and can be rolled back only from its immutable adoption receipt while the adopted row still matches.
- Preserve ordinary SQLite, hosted PostgreSQL/API, guarded-write, versioning,
and CLI behavior, including hosted deployments whose
tenant_idcolumn is UUID rather than text.
- Ship the hosted guarded-write authority fix from #78: the authority trigger now casts hosted tenant IDs to UUID before comparing them, preserving guarded write enforcement on hosted Postgres tenants whose signed authority identifier reaches the trigger as text.
- Fix the FCAME-1 guarded writer so
createKnowledgeGuardedWriter({ env })authenticates guarded HTTP requests with the API endpoint and credential from the supplied env, rather than allowing ambient profile, override, or disk credential tiers to outrank that caller-supplied transport env.
- Align the public declaration surface with the runtime export contract by
keeping
materializeKnowledgePrivateInputinternal while preserving the supportedcreateKnowledgePrivateInputDescriptorand guarded-writer API.
- Explain recovery from the obsolete
HASNA_KNOWLEDGE_STORAGE_MODE=cloudsetting: unset it to preserve the default SQLite backend, or set the mode explicitly tosqliteorpostgreswhen that backend is intended; removed placement modes remain invalid.
- Add the package-owned FCAME-1 production writer. Private Knowledge payloads enter through a metadata-only, in-memory descriptor and travel directly in the authenticated HTTP body; the guarded path has no CLI, argv, stdin, plaintext-temp-file, local JSON/SQLite, or raw-store fallback.
- Bind every mutation to explicit authority classification/id, signed tenant, logical scope, parent, stable operation/step IDs, a deterministic key, and create-if-absent or compare-and-swap semantics.
- Add transactional operation claims and immutable terminal receipts, bounded exact reconciliation, terminal-completeness assertions, exact full-ID readback, and same-operation replay proof/refusal.
- Add immutable ordered workflow manifests with deterministic forward-repair or
accepted-receipt-scoped compensation keys. Knowledge-authority steps are
enforced in order, and an exact terminal recovery closes only its accepted
prefix while leaving accepted completeness false. Cross-authority terminal
completeness fails closed as
external_authority_receipt_verifier_required:<classification>:<authority id>until the external package supplies a verifiable receipt path; this package never certifies an Instructions mutation it cannot verify. - Keep legacy
/v1/notesand local item-store behavior compatible. Guarded routes are additive and remain unavailable until the server has explicitHASNA_KNOWLEDGE_AUTHORITY_CLASSIFICATIONandHASNA_KNOWLEDGE_AUTHORITY_IDconfiguration.
- Added focused tests for every runtime export in
src/store.ts, including legacy-store collision handling, malformed inputs, atomic persistence, lock cleanup/reentrancy, and ID boundaries. - Added direct tests for
src/conflict-agent.tscovering complete and limited evidence, durable fake-run telemetry, missing provider credentials, and an unknown conflict. The source resolver was not duplicated here because its successful resolution, revision/citation evidence, ACL denials, and raw-byte boundary are already exercised directly in the existing database and open-files fixture suites.
BREAKING for the fleet flip. HASNA_KNOWLEDGE_API_URL +
HASNA_KNOWLEDGE_API_KEY no longer select the cloud backend on their own. A flip
that writes only those two variables now leaves the CLI reading the on-box store;
it must also write HASNA_KNOWLEDGE_STORAGE_MODE=cloud. Nothing about reaching
the cloud got harder — it has to be asked for.
Why the old behaviour had to go, measured on this machine: both variables were
exported in a login shell, the tmux server carried them, so every pane inherited
them and bun test reported 99 failures instead of 1 — 64 of them the same
"cloud API flip is active" refusal, and the rest of the suite operating against
the live store while believing it was isolated. The symptom named neither the
cause nor the store. With this change the same suite, with those variables still
exported and nothing else neutralised, is 309 pass / 2 skip / 1 fail across 312
tests, the one failure being the pre-existing context pack and proposal context commands case, which is red on main too. Before the two redirect tests
below were added it was 307 / 2 / 1 across 310, and the four CI jobs that reached
the test step reported that same 307 / 1 / 310 with none of those variables set —
so the fix makes a polluted local run equivalent to a clean CI run rather than
merely quieter.
- Request-boundary guard (
src/net-guard.ts). WhileNODE_ENV=test, an outbound request from this package whose target is not loopback is refused before a socket is opened; refusals never name the target host. Verified against the real configured endpoint under explicit cloud mode with 0 connect() syscalls, 0 AF_INET/AF_INET6 sockets and 0 connects to :443, and positive-controlled by the same command with the guard disarmed, which made 4 connects to :443. This is the primary control, not the environment clearing: a preload orbeforeAllthat clears the selector vars is defeated by a later file's module-scope assignment, by onebun testprocess sharing one preload, and bybunfig.tomlresolving from the cwd — each of which produced a green run with live writes in the siblingmementosfix. Loopback is allowed on purpose so hermetic transport tests stay real, and that allowance is not a springboard: while armed the guard follows redirects itself and checks every hop, becausefetchfollows a 3xx internally and an internal hop never returns through afetchImpl. A 127.0.0.1 server answering 302 with an off-boxLocationreached a public host — measured at 4 connects to :443 and an HTTP 200 — through a request the guard had already approved; the same probe now makes 0 connects and raises the guard's own host-withholding refusal. - Explicit mode selection (
src/knowledge-mode.ts). The first mode key that carries a value wins and returns, soKNOWLEDGE_MODE=localis authoritative on a machine whose shell exports a URL and a key; with no mode key the answer islocal. Callers hand@hasna/contractsa mode-pinned environment, in both directions, so its own presence-inference (resolveClientTransport: url + key ⇒ cloud) can no longer pick a backend behind this package. Two layers were inferring; both are closed. knowledge modereports the resolved backend, the env var that selected it, pointer vars that are present but ignored, and whether the outbound guard is armed. It reads the environment only — no store open, no config read, no request — verified byte-identical file trees in an isolatedHOMEand cwd across six invocations. Env var names only, never values.- Test isolation, as defence in depth:
tests/cli.test.tsno longer hands the ambient pointer vars to spawned CLI children.auth whoamireportsauthenticated: truefrom the mere presence of an API key, so the hosted-auth contract test had been measuring the developer's shell instead of the temp auth dir it created.
Wording-only corrections to claims landed by #34/#35. No behaviour change: the
only non-comment source edit is one added list --help line, plus one added test
case (six expect() calls, 860 -> 866 in tests/cli.test.ts); the removal
semantics, exit codes and messages are untouched.
- The
untagwhole-value-versus-split exclusivity is per raw-tvalue, not "one shape per run". Four sites said per run (README.mdtwice,src/cli.tstwice); repeated-tonuntagstill accumulates, so on an item carrying both shapesuntag -t "a,b,c" -t aremoves 2 and-t "a,b,c" -t a -t b -t cremoves 4 in a single run. The re-run contract itself is true and unchanged (removed: 1, thenremoved: 3, then exit 1) — only its stated reason was wrong, and the reason is what a reader would reimplement from. (Repeated-tonlistnarrows instead; that has not changed.) - Corrected the justification for quoting names in the partial-miss
untagmessage, at three sites (README.md,src/cli.ts,tests/cli.test.ts). The cited case —(not found: p, q)colliding with one tag literally namedp, q— is unreachable: a comma-bearing value only enters the removal set via the whole-value branch, which requires the tag to be stored, so it is found by definition and never appears innot_found. Because every entry is comma-free the", "join is injective, so a plain space is a legibility problem, not a collision. The load-bearing reachable case is whitespacetrim()does not strip:-t $'p\nq'yieldsnot_found: ["p\nq"], which joined raw would break the single-line message in two. Now covered by a test assertion. - Documented an undocumented flag precedence on
list: when both--archivedand--include-archivedare passed,--archivedwins (archived items only) in either order. Documented, not changed. - Made the consequence of a split-only
list -tconditional instead of universal, at three sites (README.md,src/cli.ts,tests/cli.test.ts). All three said the defect "returns a DIFFERENT item ... attotal: 1and exit 0" outright. It only does that when the corpus also holds an item carrying the three names separately; with the glued item alone it returnstotal: 0at exit 0. Measured by removing the whole-value branch from thelistpredicate and running both corpora:total: 0/ no ids with the glued item alone,total: 1/ the other item's id once a split-shape item exists. Both outcomes are silent, so the point stands — but which one occurs is a property of the corpus, not of the query, and a swap has to be constructed rather than assumed. - Corrected the
verify:generatedcitation under "inventory paths block fix".scripts/verify-generated-artifacts.mjsonly (a)git diff --exit-codesbin/knowledge-mcp.jsanddist, and (b) greps four generated files for two stale Windows-path patterns. It never compares a bundle against its source, so its exit 0 says the untouched artifacts are untouched — measured: it exits 0 withsrc/cli.tsdiverged frombin/knowledge.js, and exits 0 even withbin/knowledge.jscorrupted outright. To check bundle/source sync, rebuild the bundle to a temp outfile with thebuildscript's command and compare against the committed file after per-line trailing-whitespace stripping (seescripts/strip-generated-trailing-whitespace.mjs), and read the lockfile caveat below first.
Two pre-existing problems were found while verifying this and are not fixed here, because both need their own change and review:
bun run verify:generatedis red onmain, independently of this entry. The script rebuilds first, and the rebuild does not reproduce the committedbin/knowledge-mcp.jsordist/index.js:package.jsonis inlined into the bundles and gained afilesentry in #33 that the committed mcp bundle predates, and the zod codegen indist/index.jshas drifted.bin/knowledge.jsis not affected (it is--minifyed). Onlybun scripts/verify-generated-artifacts.mjson its own exits 0.- There is no committed lockfile, so
bun install --frozen-lockfileexits 0 while pinning nothing.@hasna/eventsis bundled intobin/knowledge.js(it is not in the--externallist) and is declared^0.1.3, so the rebuild-and-compare check below is dependency-state-dependent: 0.1.14 reproduces the committed bundle, 0.1.13 does not. Checknode_modules/@hasna/events/package.jsonbefore concluding a bundle is out of sync.
Release-only bump. No source change: this ships work already merged to main
that no published artifact carried.
The headline is the -t/--tag silent data-loss fix from #34
(fix(cli): stop silently dropping repeated -t tags on add/update/upsert,
merged 2026-07-27T19:39:04Z). That fix landed three days after 0.2.91 was
published (2026-07-24T15:52:10Z) and package.json was never bumped, so main
and the registry both reported 0.2.91 while behaving differently — main
stored all five tags, the published artifact stored only the last one — and
npm publish could not ship the fix at all, because that version already
existed. Measured against installed 0.2.91:
knowledge add … -t convention -t naming -t repos -t github -t proposed
stored ["proposed"] at exit 0. Because tags are the retrieval surface, such an
entry is invisible to every --tag query, which is indistinguishable from never
having been written.
Since the version number was identical, nothing signalled the difference. The
same gap held back 31 other commits merged since the 0.2.91 publish,
including the request-boundary guard, the platform-agnostic redaction fix, the
ok_untag truthful-removal fix and the generated-artifact checks described under
"Unreleased" above; they ship here too.
- Bump
0.2.91→0.2.92and regenerate the shipped bundles, which embed the version string. The bundle diff is provably the version string alone:bin/knowledge.jsandbin/knowledge-mcp.jsare byte-identical to their predecessors once0.2.92is reverted to0.2.91, andverify:generatedreports all 6 bundles rebuilding byte-identically.
Harden the local JSON item-store against lock corruption and add a sanctioned
recovery path for merging a legacy app-folder workspace into a populated
canonical workspace. Composes with the safe legacy global-store import (0.2.90):
withLock remains reentrant and the hardened lock replaces the previous
check-then-write acquisition used by that import.
- Replace the check-then-write JSON store lock with exclusive
open(..., 'wx')creation, owner metadata (owner-only release), PID-aware conservative stale-lock quarantine (stale locks are renamed aside, not blindly deleted, behind a dedicated breaker lock), non-busyAtomics.waitretry sleep, and fsynced temp-file writes with atomic rename (writeFileAtomic). - Add
knowledge storage merge-legacy-path(CLI + service + SDK): dry-run by default with current/legacy/duplicate/stranded/conflict/expected/final item counts, refuses conflicting duplicate IDs orshort_idcollisions, snapshots the legacy workspace before writing, merges only non-conflicting legacy items under a lock, and is idempotent on rerun. - Avoid opening SQLite (WAL locks) while summarizing a workspace that is about to
be moved during
migrate-legacy-path.
Knowledge private-ref lint/redaction hardening (rescoped from PR #18, originally
authored against the pre-reconcile 0.2.78 line). The parts of the original change
that re-introduced the client-side Postgres sync engine and legacy local-JSON
item writes were dropped, since main already removed those forbidden
DSN-on-client paths and unified item CRUD behind the Store abstraction. Kept only
the additive, non-regressive security hardening:
- Add
src/private-ref.ts: lint (assertNoPrivateRefs) and redaction (redactPrivateRefs) for private.hasnapaths,file://URIs, raw DB/export refs,cloud.env, and database URLs. - Apply private-ref lint on source/manifest/app-wiki ingestion and redaction on sync-bundle export/import (including embedded artifact bytes).
- Block forbidden Knowledge workspace artifacts (
cloud.env, pre-cloud DB/JSON backups,migration-exports/) instorage validate; makestorage validateexit non-zero on failure. Gate sync export/import on a valid storage contract. - Document runtime-env/secret-ref handling in the storage contract
(
secret_handling) and record DB URL rotation as blocked without live secret authority. No live secret mutation performed. - Make local stores, workspaces, artifacts, backups, and exports owner-only (0700 dirs / 0600 files) where Knowledge writes them.
- Bump the MCP stdio test timeout to 10s to reduce flakiness on slower hosts.
Fix knowledge inventory --json reporting the wrong paths block in
self_hosted/cloud (api) mode, where it disagreed with knowledge paths.
- fix(knowledge): the
inventorypathsblock now reflects the real on-box workspace layout (json_store_path=workspace.jsonStorePath,json_store_exists/knowledge_db_existsvia read-onlyexistsSync), matchingknowledge paths. PreviouslyitemOnlyInventory()echoed the cloud transport URL asjson_store_pathand hardcodedknowledge_db_exists: false. The cloud item-corpus source location is still surfaced vialegacy_store.path. - test(knowledge):
tests/cloud-inventory.test.tsnow assertsinventory.pathsequalsservice.paths()for all four path fields, that the/v1URL never appears in the paths block, and that it is reported onlegacy_store.path. - Rebuilt generated bundles so shipped artifacts carry the fix.
verify:generatedpassing is not evidence that a rebuilt bundle matches its source — see "documentation corrections to the multi-tag work" for what that check does and does not prove, and for the check that establishes bundle/source sync.
Top-priority correctness fix: the hosted (cloud) notes list returned materially different, near-empty results versus local. Brings cloud search to parity with the local SQLite FTS behavior shipped in Stage 1 (#29).
- Replaced the
title/content ILIKE '%q%'+ORDER BY created_at DESCcloud path (NoteRepo.list,src/serve.ts) with a weightedtsvectorgenerated column (title = A, content = B) + GIN index (src/db/pg-migrations.ts), queried viawebsearch_to_tsquery('english', …)and ranked byts_rank_cd(created_at as a deterministic tiebreak). Fixes the "cloud returns nothing" bug where multi-term / word-order-varying queries matched no substring and results were ordered by recency rather than relevance. - Postgres migrations are appended to
PG_MIGRATIONS(index-derived ids, never inserted mid-array) and are idempotent. - Added an in-process Postgres (
@electric-sql/pglite, devDependency) parity suite (tests/search-pg-parity.test.ts) running the realNoteRepoagainst the real migrations, asserting word-order independence, relevance-over-recency, phrase adjacency,totalreflecting the FTS predicate, and sqlite-vs-pg equivalence over the shared corpus.
Fix the unsafe legacy global-store migration. ensureStore previously copied
~/.open-knowledge/db.json verbatim over the canonical ~/.hasna/knowledge/db.json
on first global use, which could clobber an existing canonical store.
- Replace the raw first-use copy with a safe merge: canonical records win on
id/short_idcollisions, the legacy file is treated as a read-only source (never moved/rewritten/deleted), invalid records are counted and skipped. - Add
knowledge storage import-legacy [--dry-run] [--scope global] [--json]for explicit preview/import with import reports (underruns/) and pre-import backups (underexports/) when an existing canonical store changes. The command is global-only and rejects other scopes. - Make
withLockreentrant within a single process so an import invoked while the caller already holds the canonical store lock does not self-deadlock. - Add focused CLI tests: dry-run preview, project-scope rejection, existing-canonical merge/ no-overwrite/idempotent re-run, and reentrant-lock import.
Harden public npm package contents so internal docs never ship. The published
package previously included the entire docs/ and scripts/ trees via broad
files entries, which packed docs/canonical-secrets-bootstrap-2026-06-08.md
(internal secret-path topology and account references) into the public tarball.
- Replace the broad
docsandscriptsentries inpackage.jsonfileswith an explicit allowlist of public guides and dev scripts; the internal secrets-bootstrap runbook is now excluded from the package. - Add
scripts/validate-public-package.mjs(npm run release:pack:check), a fail-closed check that diffsnpm pack --dry-runagainst the allowlist and rejects any unreviewed or forbidden docs/scripts path. Wired intoprepublishOnly. - Add
tests/package-release.test.ts(bun run test:package) asserting the allowlist and the packed manifest. - Document the allowlist policy in
README.mdandSECURITY.md.
Security/hygiene: stop shipping the internal infra host knowledge.hasna.xyz as the
default hosted API URL in the published package. The default now resolves to the public
product domain https://knowledge.md.
DEFAULT_KNOWLEDGE_API_URL(src/auth.ts),defaultKnowledgeConfig()hosted default (src/workspace.ts), thenormalizeModealias (src/service.ts), and doc comments insrc/cli.ts/src/cloud-store.tsnow useknowledge.mdinstead of the internal host.- Propagated to README,
docs/examples,docs/migration, andtests/cloud-store.test.ts. - Rebuilt
dist/andbin/(shipped artifacts) so the leaked default is gone from what installs actually run, not just source. - Known residual (out of scope, needs a
@hasna/contractsfix): when hosted mode is set with a key but no URL,defaultCloudBaseUrl()in@hasna/contractsstill templateshttps://<app>.hasna.xyz.createClientTransportexposes no base-URL override, so this repo cannot close that path alone. Documented explicitly intests/cloud-store.test.ts.
Reconcile main with the published npm line (npm/knowledge/v0.2.86), which had
diverged: the deployed runtime carried a Store-unification + cloud-routing refactor
that never landed on main, while main carried two CLI fixes the published line
lacked. This release re-converges both histories.
- Merge the published release tag
npm/knowledge/v0.2.86intomain(merge commit, preserving full ancestry so futuremerge-base --is-ancestorchecks pass). Brings in:- refactor(store): unify knowledge-item CRUD behind one Store (LocalStore + ApiStore) (1506111)
- refactor(knowledge): remove dead raw-fetch RemoteKnowledgeClient, make registry descriptor truthful (2b6bd21)
- fix(knowledge): close residual cloud-mode routing gaps for catalog commands (2d235a9)
- fix(knowledge): route SDK item CRUD + inventory through the unified Store in all 3 modes (92c3fcc)
- fix(knowledge): close split-brain read + drop dead API client and client DSN surface (5213a51)
- fix(knowledge): stop context-pack hang, repair cloud project-panel, drop dead remote command (8daa0ea)
- Retains the two
main-only CLI fixes on top of the refactor:- fix(cli): don't leak internal Error stack on usage/validation errors (#23)
- fix(cli): make
<sub> --helpprint per-command usage (#24)
- Version bumped to 0.2.87 (strictly above published 0.2.86) so npm and
mainreconverge on publish.