Commit graph

13 commits

Author SHA1 Message Date
le king fu
59b963220d feat(agent): daily pull of the encrypted PostgreSQL backups
Some checks failed
CI / verification (pull_request) Failing after 28s
POSIX sh agent run by a systemd user timer (Persistent=true, retries on
failure): rsync over the classic sshd on 100.86.30.21:2222 with a pinned
host key, .age files only, 30-day rotation that fails loudly, and a
report to POST /backups/pull when the ingest token is configured (sent
through a header file, never argv). Never decrypts.

Refs maximus/la-compagnie-maximus#220

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:18:17 -04:00
le king fu
cf72292967 feat(agent): one-shot backfill of the Claude usage history from local transcripts
agent/claude-usage/backfill-history.js runs by hand from the repo (never
from the cron install, the agent is unchanged). It rebuilds the closed days
with the agent's own aggregate() on past windows and writes { days } to the
--out file, which the server merges with `claude-usage-history.js --import`
(an already archived date wins).

Each 30-day window keeps only its 28 inner days and the next steps back 28
days, so a request straddling a window boundary is deduplicated by the
window that keeps it and counted once. The first window stops at D-2. The
walk is bounded by --max-windows (default 24) rather than guessed from file
mtimes, which say nothing of the oldest line a resumed session holds.

It never reads the credentials file, opens no connection, writes only the
--out file (mode 600, never overwritten) and prints counts only.

Refs #55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:44:45 -04:00
le king fu
f694677a1a test(agent): pin ENOENT as a read error below the root, note the subagent-copy limit
All checks were successful
CI / verification (pull_request) Successful in 29s
CI / verification (push) Successful in 30s
Review of #56: making ENOENT an empty scan on every directory, not only the
root, left the suite green. A vanished subdirectory is now a pinned read
error. The header also names the other known limit of the midnight fix:
the mtime threshold may skip a subagent transcript holding the only copy
of a straddling request's earlier line.

Refs #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:53:27 -04:00
le king fu
ca33f10b85 docs: document the read-error freeze rule and count the #54 cases
All checks were successful
CI / verification (pull_request) Successful in 31s
Refs #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:30:20 -04:00
le king fu
123b3c3c8c fix(agent): no double count across midnight, no freeze on a read error, bounded limits read
- aggregate: frozen days are dropped after deduplication, not before. A
  request straddling midnight whose best line sat on a day frozen since
  was counted again on the open day. On equal output the earliest line now
  wins (it was the first one read), so the choice no longer depends on
  which files a beat reads.
- aggregate: unreadable directories, failed stats (ENOENT included) and
  stream errors are counted in `readErrors`; a missing root is still an
  empty scan. The beat then freezes no new day (nextState freeze: false),
  still pushes, and logs `read_errors=N` with no path.
- limits: the response is read chunk by chunk and counted in bytes; past
  256 KiB the stream is cancelled instead of being read in full and
  measured in characters.
- state: a test now covers the chmodSync of a leftover temp file.

Each fix was checked by mutation (13 mutations, 13 red).

Refs #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:29:29 -04:00
le king fu
7f01e6afe5 docs(agent): document the claude-usage agent install and its guarantees
Resolves #50

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:26:15 -04:00
le king fu
e274ca7ed5 feat(agent): add the claude-usage agent: plan limits, local cache, push
limits.js reads the Claude Code OAuth credentials read-only and calls the
pinned Anthropic usage endpoint once (redirect refused, 10 s timeout, no
retry, never a refresh); detail codes come from a fixed enum only.
state.js freezes every Toronto day before yesterday, empty ones included,
prunes past 35 days, keeps the last good limits, and writes atomically in
mode 600. push-claude-usage.js writes the cache before pushing, refuses a
non-https URL (exit 1) and a body over 64 KiB (exit 2, nothing sent), and
prints no token. run-claude-usage.sh takes a non-blocking flock so a beat
that overlaps the previous one exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:26:15 -04:00
le king fu
c888f7c633 fix(agent): take the scan threshold from the open days, key maps without prototype
Some checks are pending
CI / verification (pull_request) Successful in 28s
CI / verification (push) Waiting to run
The threshold came from the most recent frozen key: a frozen day in the
future, or a gap in the frozen run, pushed it past open days whose files
were then never read, and the agent would have frozen them empty for good.
It is now the oldest open day of the window (the same day as before for the
agent's contiguous frozen prefix).

Project, model and day maps are now Object.create(null): a project named
__proto__ or constructor wrote NaN counters onto Object.prototype and
dropped out of the top.

Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:25:43 -04:00
le king fu
5cb9182911 fix(agent): collapse doubled slashes in cwd before project mapping
All checks were successful
CI / verification (pull_request) Successful in 26s
Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:17:56 -04:00
le king fu
7d55e30ffd feat(agent): aggregate Claude usage from local transcripts
Pure, dependency-free module for the claude-usage agent. It walks
~/.claude/projects/**/*.jsonl (subagent transcripts included), keeps
assistant lines with message.usage, drops <synthetic> and truncated
lines, deduplicates on (message.id, requestId) across all files at the
largest output_tokens, and buckets by America/Toronto day computed as
strings only (DST-safe).

Returns { openDays, days, projects, claudeCodeVersion }: the recomputed
non-frozen days, the merged 30-day series (at most 12 models per day,
the rest folded into "other"), the top 8 projects by work tokens plus
"(autres)", and the version of the newest line. Files whose mtime day
is before the day after the most recent frozen day are never opened.

Resolves #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:17:56 -04:00
le king fu
523d90bf7c fix(hosts): require 3 loadAvg values and sweep orphan temp files (#25)
All checks were successful
CI / verification (pull_request) Successful in 21s
CI / verification (push) Successful in 20s
Follow-ups from the review of the workstation monitoring stack (#18-#23):

- loadAvg must hold exactly 3 finite numbers (1/5/15-min averages). The
  admin card joins them, so a shorter array rendered a partial line and a
  longer one was silently truncated; both are now a 400, and a stored
  snapshot off that format degrades to neverSeen on read.
- sweepStaleTempFiles() runs at startup, before listen, and removes the
  `.<id>.<pid>.<ms>.tmp` files a process killed mid-write leaves behind,
  once older than an hour. Exact name format only, regular files only
  (lstat), best effort: a missing HOSTS_DIR stays silent, other failures
  are logged and never stop the server.
- Document that cleanString keeps control characters on purpose (safety
  rests on the consumer escaping at render time), and that frozenRequest
  does not freeze `new Date()`, which stamps receivedAt on POST.

Fixes #25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:30:07 -04:00
le king fu
e181a9691c fix(review): close the three gaps found in stack review
Follow-up to the /pr-review pass on PRs #18-#22. Three findings, none of
which changed behaviour, all of which weakened a guarantee the stack was
supposed to provide.

1. auth.test.js carried "any new route must be added here" but /hosts was
   never added when #13 introduced it, so no test asserted GET /hosts -> 401
   on a missing header. The drift class the file exists to catch slipped on
   its first outing. ROUTES now covers /hosts, and a dedicated block pins
   both directions of the token separation: a read token cannot write, an
   ingest token cannot read.

2. ingestOversized() folded any transport error into 413, so the four
   oversized-body tests would have stayed green if the server had stopped
   writing the status and merely killed the socket - on the one path where
   "a status, not a dead socket" is the whole client contract. Transport
   errors are now surfaced instead of swallowed.

3. HOST_AGENT_TIMEOUT_MS was read by push-metrics.js but never exported by
   run-push.sh, so setting it in the documented env file did nothing. Now
   exported and documented.

Also drops a claim from agent/README.md that the review proved false: the
ingest/read token split buys no containment on the ThinkPad, which already
stores HEALTH_TOKEN in cleartext for defenseur-auto. Losing that laptop
compromises both, so they rotate together.
2026-08-16 14:24:43 -04:00
le king fu
e24f5b6f00 feat(agent): push workstation metrics to /hosts/<id> from cron
Zero-dependency local agent: collect one snapshot through the shared
metrics.js, POST it once to /hosts/<id>, exit. Cron runs it every five
minutes. Nothing is queued and nothing is replayed — a heartbeat from five
minutes ago describes a machine that no longer exists, so a failed push is
logged and dropped rather than spooled.

run-push.sh sources ~/.config/maximus-host-agent.env (mode 600), refuses to
start when HOSTS_API_URL, HOSTS_INGEST_TOKEN or HOST_ID is missing, and never
enables shell tracing: the script is meant to be piped into `logger`, where
`set -x` would echo the ingest token into /var/log/syslog and journald for
good. Same reason the failure path reports only err.code and the HTTP status —
never an error object, request options, headers, or a response body. The cost
is accepted: a 400 says the payload was rejected, not why.

50 tests, including two that spawn the real process against a failing server
and scan its actual stdout and stderr for any five-character fragment of the
token. The payload is pinned against the server's own sanitizeSnapshot(), and
one case pushes a real collectMetrics() snapshot through the real ingestion
handler, so a drift between agent and server turns a test red instead of
producing a 400 at 3 a.m. on the ThinkPad.

agent/ stays out of the Docker image: the COPY line is untouched, and a test
pins that it copies files one by one and never names the directory.

Installing on a workstation — frozen copy, env file, crontab line, dry run,
and why two machines must never share a HOST_ID — is documented in
agent/README.md. The install itself belongs to the commissioning issue.

Resolves #15
2026-08-16 12:21:59 -04:00