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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
# Changelog

## Unreleased — the backend is chosen explicitly, and test egress is refused

**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`). While `NODE_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 or `beforeAll` that clears the selector vars is defeated by a later
file's module-scope assignment, by one `bun test` process sharing one preload,
and by `bunfig.toml` resolving from the cwd — each of which produced a green
run with live writes in the sibling `mementos` fix. 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, because `fetch` follows a 3xx internally and an internal hop never returns
through a `fetchImpl`. A 127.0.0.1 server answering 302 with an off-box
`Location` reached 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, so `KNOWLEDGE_MODE=local` is authoritative on
a machine whose shell exports a URL and a key; with no mode key the answer is
`local`. Callers hand `@hasna/contracts` a 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 mode`** reports 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 isolated `HOME` and cwd
across six invocations. Env var **names** only, never values.
- Test isolation, as defence in depth: `tests/cli.test.ts` no longer hands the
ambient pointer vars to spawned CLI children. `auth whoami` reports
`authenticated: true` from 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.

## Unreleased — documentation corrections to the multi-tag work

Wording-only corrections to claims landed by #34/#35. No behaviour change: the
Expand Down
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,31 @@ knowledge paths [--scope global|project|local] [--verbose] [--json]
Show compact resolved app paths by default. Use `--verbose` or `--json` for
every path and the loaded config.

### mode
```bash
knowledge mode [--json]
```
Report which backend this process would use — `local` (the on-box store) or
`cloud` (the HTTP `/v1` API) — and which environment variable selected it.

Selection is **explicit only**. Set `HASNA_KNOWLEDGE_STORAGE_MODE=local|cloud`
(aliases `HASNA_KNOWLEDGE_MODE`, `KNOWLEDGE_STORAGE_MODE`, `KNOWLEDGE_MODE`, in
that precedence order; `self_hosted` is accepted as a deprecated spelling of
`cloud`). Setting `HASNA_KNOWLEDGE_API_URL` / `HASNA_KNOWLEDGE_API_KEY` alone
does **not** switch backends — those are pointers saying where the cloud is and
how to authenticate, and `mode` reports them as present-but-ignored so a machine
that has them exported in its shell is not silently reading a different store
than a machine that does not.

The command reads the environment only: no store is opened, no config file is
read, and no request is made, so it answers correctly on a machine with no
config and no network. Environment variable **names** are printed, never values.

While `NODE_ENV=test`, every non-loopback outbound request from this package is
refused before a socket is opened; `mode` reports that as
`network_guard_active`. Point the API URL at `127.0.0.1` for a hermetic
cloud-transport test.

### storage
```bash
knowledge storage status [--scope project] [--json]
Expand Down Expand Up @@ -706,8 +731,10 @@ the remote repo root or remote `.hasna/knowledge` path.
`knowledge sync` owns knowledge semantics and conflict visibility for
peer/machine catalog transfer. It is distinct from `db storage status`, which is
a read-only local catalog inspector. (The legacy `db storage sync`/`push`/`pull`
Postgres-DSN commands were removed; cross-machine sharing uses the cloud API
flip — `HASNA_KNOWLEDGE_API_URL` + `HASNA_KNOWLEDGE_API_KEY` — instead.)
Postgres-DSN commands were removed; cross-machine sharing uses cloud mode —
`HASNA_KNOWLEDGE_STORAGE_MODE=cloud` plus `HASNA_KNOWLEDGE_API_URL` +
`HASNA_KNOWLEDGE_API_KEY` — instead. The mode var is required: the two pointer
vars on their own do not switch backends. See [`mode`](#mode).)

### setup / auth / remote
```bash
Expand Down Expand Up @@ -741,10 +768,12 @@ Initialize or inspect the versioned SQLite catalog at
durable table list, and local sync history. It is read-only. The legacy
`push`/`pull`/`sync` Postgres commands and the client `HASNA_KNOWLEDGE_DATABASE_URL`
DSN were removed — a raw database DSN is never distributed to clients. To share
knowledge across machines, use the cloud API flip instead: set
`HASNA_KNOWLEDGE_API_URL` + `HASNA_KNOWLEDGE_API_KEY` so every read/write routes
through the self-hosted HTTP API. The optional mode env vars are
`HASNA_KNOWLEDGE_STORAGE_MODE` / `KNOWLEDGE_STORAGE_MODE` (`local` or `cloud`).
knowledge across machines, use cloud mode instead: set
`HASNA_KNOWLEDGE_STORAGE_MODE=cloud` **and** `HASNA_KNOWLEDGE_API_URL` +
`HASNA_KNOWLEDGE_API_KEY` so every read/write routes through the HTTP API. The
mode var is **not** optional and the pointer vars are **not** a substitute for
it: presence of a URL and a key never switches backends on its own. Run
[`knowledge mode`](#mode) to see what the current environment resolves to.
The durable table list excludes local derived FTS indexes such as `chunks_fts`.

### wiki
Expand Down
Loading
Loading