Commit Graph

8 Commits

Author SHA1 Message Date
7d04b2f0b5 Answer a subject access request and an erasure over HTTP
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 8s
memory.Held and memory.Forget were written the day the store was and had no
route, so the honest answer to "show me what you hold about this candidate" was
"a developer runs a query". That is not a compliance posture; it is a promise
with no mechanism, and a subject access request has a statutory clock.

  GET    /api/v1/memories?subject=candidate&id=…   what is held
  DELETE /api/v1/memories?subject=candidate&id=…   erase it

Each memory comes back with its author, so "an agent inferred this" and "a
recruiter wrote this" stay distinguishable — a response that flattened them
would be misleading in the one place it matters most.

THE AUTHORISATION IS THE WHOLE DESIGN, AND THE FIRST VERSION WAS WRONG. Gating
a read of candidate memories on `list` of job-applications looked right and was
not: a talent may list applications because every other read path scopes them
to their OWN rows, and this route has no row scoping — so the check passed and
the response would have carried the whole organisation's memories about
everybody. A test caught it before it shipped.

A personal memory now requires `delete` on the record it concerns, for reading
as much as for erasing. Deleting somebody's application is an administrative
capability and nothing scopes it to self, which makes it the honest proxy for
"may act on other people's records here". No new permission, and it cannot
drift from the record's own policy. Workspace facts have no personal subject
and stay at `list`.

Erasing workspace memories refuses without an explicit id: "erase everything"
is a plausible thing to want and a catastrophic thing to do by a mistyped query
string, so it is not reachable by omission. An erasure is logged at Info with
who asked and when — the row itself is redacted, so the log is the durable
record that it happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:35:18 +05:30
e90bc33d0f Memory hygiene: a relevance floor, no duplicates, and expiry that actually deletes
Some checks failed
CI / test (push) Failing after 4m40s
CI / fixture (push) Failing after 8s
Three things that decide whether memory improves with use or rots with it.

A RELEVANCE FLOOR. Recall returned its top five whatever they scored, so a run
about shift cover was handed five memories about certifications simply because
nothing better existed — and the block tells the model these are things the
workspace remembered, so it reads them as pertinent. Embeddings are
unit-normalised, so knowledge_dot is cosine, and 0.30 is where text is usually
about something else. A judgement rather than a measurement, and the honest way
to tune it is to watch what gets carried on real questions.

NO DUPLICATES. The same standing preference comes up in conversation after
conversation, and each run that hears it has no idea the last one wrote it
down. Five recall slots spent on one fact restated five ways is the normal
failure, not a rare one. A write with the same normalised text, in the same org
and about the same subject, pushes the existing memory's expiry out instead of
adding a row — matched on the same sentence rather than a similar one, because
collapsing two genuinely different facts is the worse error.

EXPIRY THAT DELETES. expires_at was set and filtered on read, and nothing ever
removed anything: the row was invisible and still retained. "We keep it ninety
days" has to be true of the table, not only of the query. Prune is batched, and
a redaction is kept for a thirty-day grace period so an erasure stays provable
shortly afterwards.

It runs in the maintenance sweeper that already exists rather than a second
scheduler — same ticker, same cancellation, same failure isolation. That forced
one honest change: Maintenance() used to be nil without OAuth, on the reasoning
that there was nothing to sweep. There is now, and a retention promise enforced
only when an unrelated feature happens to be enabled is not a promise. The test
that asserted the old behaviour now asserts the new one and says why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 20:29:28 +05:30
f2aa3b3ad8 mcp connection
Some checks failed
CI / fixture (push) Has been cancelled
CI / test (push) Has been cancelled
2026-09-22 10:58:02 +05:30
4e1f746b22 update the archive options
Some checks failed
CI / test (push) Failing after 4m40s
CI / fixture (push) Failing after 7s
2026-09-10 19:30:46 +05:30
f7df96c973 agent build 2026-08-28 12:21:44 +05:30
b6f8655909 aravind changes 2026-08-25 16:37:05 +05:30
Suriyakumarvijayanayagam
954ba9076f Add CORS credentials, transactional endpoints, and container deployment
CORS
  cors.go never set Access-Control-Allow-Credentials, so the
  cookie-authenticated API was unreadable from any cross-origin frontend:
  the server answered correctly and the browser blocked the page from
  reading it. Set for allowlisted origins on both the preflight and the
  actual response. Three tests added.

  HTTP_COOKIE_SAMESITE (lax|none|strict, default lax) is new. CORS is only
  half of what a cross-origin browser call needs; SameSite is judged on
  registrable domain, so a frontend on an unrelated domain gets perfect CORS
  headers and still no cookie. "none" is the only value that survives that,
  and validate() refuses it without the Secure flag.

  The "*" rejection now explains itself: browsers refuse Allow-Origin "*"
  together with credentials, so it would break every authenticated call
  rather than loosen anything.

Transactional endpoints (api-contract.md 12.1)
  POST /api/v1/job-applications/{id}/hire
  POST /api/v1/job-postings/{id}/assignments

  Replaces two client-side loops that wrote several records with no
  transaction and no rollback. Each is now one endpoint and one transaction,
  built over repo.Repo so org scoping, derived columns, type casts and error
  translation are not re-derived. Authorization reuses the existing policy
  table rather than adding a parallel one: a workflow is exactly as
  privileged as the writes it performs. 13 tests, including both rollback
  paths.

Bug fix in the repository layer
  repo.bindValue handled int64/int/float64/string but not int32, which is
  what pgx returns for a PostgreSQL `int` column. Nothing previously read a
  record and wrote one of its fields elsewhere, so it never surfaced; the
  hire flow does exactly that and failed with "ai_score must be a number".
  Both KindInt and KindFloat now accept the widths pgx actually produces.

Deployment
  infrastructure/Dockerfile.api  multi-stage, cross-compiling (BUILDPLATFORM
    + GOARCH) so linux/amd64 builds from arm64 are compiled rather than
    emulated. Alpine runtime, non-root uid 10001, 22.1 MB. Ships api, seed,
    setpassword and migrate, plus the migrations, so a Kubernetes
    initContainer can apply the schema from the same image and tag as the
    API. HEALTHCHECK keys on status code, not body, so a "degraded" instance
    is not pulled from rotation during a migration window.

  infrastructure/docker-compose.yml  migrations run to completion before the
    API starts. Assumes a managed PostgreSQL; the local-db overlay adds one
    with TLS enabled so APP_ENV=production is met rather than dodged.

  scripts/drop_public_tables.go  the one-off used to clear an unrelated
    schema from krowdb on 2026-08-24, kept for the record. Build-tagged
    ignore and gated on CONFIRM_DROP=yes.

Verified against PostgreSQL: 16/16 new tests pass, and the image was built,
run and exercised end to end (login, CORS preflight, authenticated reads,
transaction rollback).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmQiGq73Uyfq7J4yR8Vxxw
2026-08-25 11:33:01 +05:30
7d12ebef3d first commit 2026-08-24 13:06:29 +05:30