Commit Graph

14 Commits

Author SHA1 Message Date
a1e91f776d Compare definitions by meaning, and stop miscounting what was recorded
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
Two problems found by deploying the previous commits to production.

FIRST: the rewrite guard compared raw Markdown, so it refused a publish over
formatting. The authoring UI re-serialises a definition when somebody saves it
— writing `webSearch: false` where the hand-authored file omitted the key, and
ordering the frontmatter its own way — and the parser reads absent and false
identically (agent.go: `data["webSearch"] == true`). A definition nobody
meaningfully changed stopped a deploy. Refusing a change that is not a change
is still a bug, even though it fails safe.

definition.SameAgent and SameSkill compare the parsed definition instead, and
are deliberately conservative, because the two ways of being wrong are not
equally bad. A false difference blocks a deploy: visible, recoverable. A false
SAMENESS lets a changed agent overwrite an approved version silently, which is
the thing versioning exists to prevent. So:

  - The body is compared verbatim. Agent.Body carries `json:"-"`, so a
    comparison that only marshalled the struct would call a completely
    rewritten system prompt "unchanged". There is a test that fails loudly on
    exactly that, because it is the mistake this design invites.
  - List ORDER stays significant. loader.go resolves Skills in order and that
    order reaches prompt assembly, so two definitions listing the same skills
    differently are still different. A deploy that only reorders still has to
    raise its version. That is a limit, recorded in a test rather than left to
    be discovered: loosening it needs somebody to decide skill order cannot
    matter, which is not a decision to bury in a comparison function.

What it absorbs is exactly what the round trip produces: frontmatter key order,
whitespace, and a defaulted value written out in full.

SECOND: importagents reported "9 agent version(s) recorded" on a run that
recorded nothing. The counter incremented on every successful Snapshot call,
and Snapshot returns nil for the idempotent no-op as well as for a real insert.
The skill counter was already honest; the agent one was not. snapshotAgent now
distinguishes recorded / conflict / already-present, and only the first counts.
Verified locally: 1 on the run that added activity-agent v2, 0 on the re-run,
where it previously said 9. A number that says nine every time is one nobody
checks on the day it matters.

Full suite green against PostgreSQL, only TestLive* skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-29 11:02:22 +05:30
Suriyakumarvijayanayagam
5ab16b836a Publish activity-agent as v2; the change to it was real, not formatting
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
Correcting the previous commit's reasoning. It claimed the difference between
this file and what production published was inert — a defaulted `webSearch:
false` and a reordered skills list. That was based on comparing the file
against the LIVE row in agent_definitions. The live row was the wrong thing to
compare against: Snapshot compares against the stored v1 in
definition_versions, and those two had diverged.

Read from production directly, v1 as published carries two skills:

    skills:
      - anomaly-detection
      - operational-risk

and the live row carries three, with activity-analysis added. So a skill was
added to this agent after v1 was published, in place, without the version being
raised — the exact silent rewrite this branch exists to stop. It is a genuine
change of behaviour: an agent with a third skill answers differently from one
with two.

So the version is raised rather than the file being bent to match. v1 keeps
what was approved; the current definition, which is what production has been
serving, becomes v2. The earlier alignment of field order and `webSearch:
false` is kept — that part WAS serialisation, and matching it keeps future
imports quiet.

Production has three agents with version rows at all, so two others may hold
the same kind of drift. They did not conflict on this import, which means their
live rows still match what was published; it does not mean nobody edited them.

The image ships agents/, so this file only reaches production on the next
image build. Any build from main at or after this commit carries it; a build
from an older tree will fail the import again, by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 20:14:43 +05:30
Suriyakumarvijayanayagam
04b11a079b Align activity-agent.md with the version production actually published
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
The first run of the new importagents against production refused, which is the
guard working rather than a fault: version 1 of activity-agent was already
published and said something different from the file this repository ships.

The difference is inert. Production's copy adds `webSearch: false`, which is
exactly what the parser defaults to when the key is absent (agent.go:471 reads
`data["webSearch"] == true`), and lists the same three skills in a different
order. Both forms parse to the identical agent — 2 tools, 0 sources, 3 skills,
confirmed by running the importer's own dry-run over each.

What happened is a round trip: somebody edited this agent in the UI, the editor
re-serialised it, and that serialisation is what got snapshotted as v1. The
hand-authored file was never the published artefact for this one.

So the file is updated to match rather than the version being bumped. Bumping
would publish a v2 that differs from v1 only in field order and a defaulted
key, which is noise in a history whose whole purpose is to say what changed.

This unblocks the import. It does not address the underlying awkwardness: the
comparison is textual, so any future UI edit that reformats without changing
meaning will block a deploy the same way. Comparing the PARSED definition
instead would fix that properly and is the right follow-up — it needs a
decision about what counts as semantically equal (skill order, starter order)
and should not be rushed in behind a deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 20:01:48 +05:30
Suriyakumarvijayanayagam
0cda877cd6 Version skills too, numbered by the server rather than by their author
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
repo.KindSkill existed with nothing writing it. Migration 000010 says "agents
and skills version identically", the table has always accepted kind='skill',
and no path on either side ever recorded one — so an edit to a skill left no
record of what it used to say. Agents name their skills and the runtime refuses
to load one whose skill is missing, so a skill changing under a pinned agent is
the same class of problem the last two commits fixed, one layer down.

Skills are numbered differently, and not by preference. An agent's frontmatter
carries `version:`, so its author decides when a change is a new version and can
be refused for rewriting an old one. definition.Skill has no such field, the
skill_definitions table has no such column, and the vocabulary is active |
inactive rather than draft | published. Giving skills an authored version would
mean a migration, a parser change on BOTH sides of the conformance test in
internal/definition — which replays a capture of the real frontend module graph
— and an edit to all 23 shipped skills. That is a feature, not this fix.

So the server assigns it: one after whatever was last published. This is not an
invention. repo.VersionsRepo.LatestVersion was written for exactly this and
says so — "the next published version has to follow what was actually published
rather than what somebody wrote in the frontmatter" — and had no callers
outside its own test.

Because the author never names a version, there is nothing to refuse: an edit
is always a new version. What needs care instead is the opposite — a save that
changed nothing must NOT be one, or every deploy would add a version to all 23
skills and the number would stop meaning anything. Each publish is compared
against the last recorded copy first. Inactive skills are not recorded at all;
inactive is this vocabulary's draft.

Verified against a live stack:

  - first import over 23 unversioned skills: 23 skill version(s) recorded
  - second import, files unchanged: 0 recorded, total still 23
  - one skill edited: 1 recorded, that skill at v1, v2; v1 still holds the
    original text and v2 the edit

The test fails without the change — "after create: 0 version(s), want 1" — and
covers the three behaviours that matter: an edit versions, an identical save
does not, and an inactive skill is not recorded.

Both races noted on the agent path apply here as well: two simultaneous edits
can compute the same next number, and the loser's snapshot is dropped rather
than failing the author's save.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 19:34:06 +05:30
Suriyakumarvijayanayagam
6b3dda8e5a Record versions when importagents publishes, and refuse a silent rewrite
The previous commit closed this hole on the authoring path. This is the other
half, and the larger one: every organization agent is published by this
command, so until now none of them were versioned at all. definition_versions
was empty on a fully deployed system, and each deploy rewrote v1 in place with
whatever the files happened to say.

Versions are now recorded through the same transaction as the definitions, so
the history and the row it describes cannot disagree — either both land or
neither does. repo.VersionsRepo.Snapshot is what refuses a spec whose content
changed without its `version:` being raised, and that refusal now stops the
import rather than being absent.

Every offending spec is collected instead of the first being returned, matching
how the parse errors above it already behave: an operator who forgot to bump
three files should see three. That is safe here because the refusal comes from
comparing a row this code read, not from a failed statement — the INSERT is ON
CONFLICT DO NOTHING, so the transaction stays healthy and the remaining specs
can still be checked.

The header comment claimed "it does not create versions" as a deliberate
omission, deferring immutability to Phase 3. Phase 3 shipped; the comment is
updated rather than left to describe a decision that has been reversed.

Verified against a live stack:

  - first run over nine unversioned agents: 9 version(s) recorded
  - second run, files unchanged: still 9, not 18 — republishing is a no-op
  - a spec edited without a bump: refused by name, exit 1, and the live row
    did NOT contain the edit; the whole transaction rolled back
  - the same spec with version: 2: exit 0, v1 and v2 both in history, live
    row at v2

Not addressed, and visible while testing this: the command does not enforce
monotonicity. A file whose version is LOWERED still overwrites the live row,
because the upsert writes whatever the frontmatter says. History is unharmed —
the older version is already recorded and matches — but the deployed
definition silently goes backwards. That wants its own change.

cmd/importagents still has no test files, which predates this. The refusal
itself is covered by repo/versions_test.go; what is untested here is the
collecting and rollback around it, and run() opens its own pool from config,
so making it testable is a refactor rather than an addition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 19:24:56 +05:30
Suriyakumarvijayanayagam
80ba57ace3 Refuse an edit that would rewrite an already-published version
§3 says a published version is immutable and editing publishes a new one.
The machinery for that was all present — an append-only definition_versions
table, a trigger, and repo.VersionsRepo.Snapshot, which already refuses to
store a version number whose content differs from what is stored.

Nothing acted on that refusal. snapshotIfPublished's error was discarded at
both call sites (`_ = s.snapshotIfPublished(...)`), and deliberately so: the
comment there explains that losing an author's work to protect a record of it
is the wrong trade. That is right for a recording failure and wrong for
exactly one case. A conflict is not the history failing to record; it is the
invariant firing.

The effect was silent. Editing a published agent without raising the
frontmatter version answered 200: the live row took the new text, the history
kept the old, and two different definitions were both called v1. Because
runtime.LoadAgentVersion resolves a pin by returning the CURRENT definition
whenever the pinned number equals the current one, a conversation pinned to v1
then ran the rewritten instructions while the audit trail showed the
originals. Verified against a live stack before the fix: PATCH answered 200,
agent_definitions held "SILENTLY CHANGED" and definition_versions still held
the published text, both labelled v2.

So the conflict is now detected before anything is written, where refusing
costs the author nothing but a version bump. The post-write snapshot keeps its
original contract for every other kind of failure, and republishing a version
unchanged stays the no-op it was. Drafts are untouched: they carry no promise,
and are still rewritten in place.

Not addressed here, and each its own change:

  - cmd/importagents never creates versions at all (documented at main.go:10),
    so the nine file-published organization agents are outside this entirely
    and every deploy still mutates v1 in place.
  - skill definitions never snapshot, so KindSkill exists with nothing writing
    it. Fixing that changes skill authoring behaviour and wants its own pass.
  - a concurrent publish of one version number with differing content can still
    pass this check and be caught by the unique index afterwards, where it is
    swallowed as before. That is the pre-existing behaviour, narrowed rather
    than removed.

Tests: the new case fails without the fix — the live row takes the rewritten
text at version 1 — and passes with it. Full suite green against PostgreSQL,
with only TestLive* skipped, which is what CI allows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 18:28:20 +05:30
Suriyakumarvijayanayagam
f48b5606df Make the local-db overlay actually start, and pass the model credential through
The overlay had never been run against a fresh volume. Two faults, the first
hiding the second:

  - postgres:16-alpine ships libssl but not the openssl CLI, so the first-boot
    certificate generation exited 127 in a restart loop. It failed invisibly:
    the 2>/dev/null on the openssl line swallowed sh's "not found" as well, so
    `docker logs` was completely empty. openssl is now installed on the boot
    that generates the certificate, inside the same guard, so a restart still
    needs no network.

  - the certificate was written into /var/lib/postgresql/data BEFORE initdb
    ran, and initdb refuses to initialise a directory that is not empty. That
    made a fresh volume unstartable regardless of the first fault. The
    certificate now lives in its own volume, which keeps it persistent — the
    reason it was put in the data directory — without touching the cluster's.

Separately, docker-compose.yml did not pass ANTHROPIC_API_KEY to the api
container, so a compose deployment could never register the agent run routes:
POST /agents/{id}/runs answered 404 and /version reported two endpoints fewer.
The model and embedder variables are now passed through, all defaulting to
empty so a deployment without them behaves exactly as it did.

Verified on a fresh volume: 56/56 verify-deploy checks against the resulting
stack, including a live agent run and 34 chunks embedded through Ollama.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-28 17:12:34 +05:30
d190fc8ee9 Preserve CLAUDE.md and add a handover document
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
Neither survived a machine change. CLAUDE.md sat in the directory ABOVE both
repositories, which is not a git repository at all, so the governing document
for the project existed on exactly one laptop. It is now in this repository;
place a copy at the parent level on a new machine, where it covers both.

docs/handover.md records what CLAUDE.md does not: what was decided and why,
what is deployed and how to verify it, and the conventions that produce
confident wrong numbers rather than errors — a score of 0 meaning "not rated",
screened_at being vestigial, shift data anchored to today.

Written because Claude Code's own memory is per-machine and keyed to the
absolute path of the checkout: it does not sync, and a different path on a new
machine reads a different folder. A file in the repository travels with the
code and is useful to a person besides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186JgqQUCDS8ZwGmyw3ymWu
2026-08-28 13:56:50 +05:30
a222dcd3e4 Add evals for every shipped agent, a policy corpus, and CI
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
§9 says no agent ships without evals. Eight of the nine had none: the two
other suites in evals/ are harness fixtures rather than agents in the
registry, so the rule was being met by one agent in nine.

Evals — 40 new cases, five per agent, every one carrying mustNotLeak:

  - the agent is loaded from its real spec in agents/*.md rather than
    written out again in Go. A hand-copied agent tests the copy: it keeps
    passing after somebody edits the spec, which is the moment it most
    needed to fail.
  - callNamed calls the tool a case names. toolThenAnswer always called
    tools[0], so seven of positions-agent's eight tools were unreachable,
    and a boundary nothing calls is a boundary nothing tests.
  - seedWorkspace fills BOTH tenants. A leak test against an empty second
    tenant cannot fail.

Verified by breaking workersByScore's org predicate: six cases across four
agents fail with LEAKED "RIVAL".

Knowledge — six policy documents, taking the corpus from 2 to 8 (34
chunks). Three restricted to admin and employer, five tenant-wide. They
cover what the tools cannot: a tool reports how many shifts went unworked,
a policy says what cover costs inside 24 hours.

corpus_test.go treats those documents as product rather than fixtures. The
first version was tautological — it read audience: from a file and checked
that file's audience was enforced, so opening a restricted document passed.
mustNotBeTenantWide now holds that judgement apart from the files, with the
reason recorded for each.

CI — the checks this repository already had, made unskippable. testutil
calls t.Skipf on an unreachable database, so a dead service container would
produce a green build over a suite that ran almost nothing. Simulated: go
test exits 0 with 74 tests skipped, including every tenant-isolation test.
The guard exits 1 and names them, while still allowing TestLive* to skip
without a model key.

This CI tests; it does not deploy. The README's claim that migrations are
run by CI against the target database remains aspirational.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186JgqQUCDS8ZwGmyw3ymWu
2026-08-28 13:52: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
cadea4bd92 Merge pull request 'Add CORS credentials, transactional endpoints, and container deployment' (#1) from feat/cors-transactions-docker into main
Reviewed-on: #1
2026-08-25 06:04:14 +00:00
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