Skip to content

Feature/vector log retention - #3

Merged
vybe merged 3 commits into
mainfrom
feature/vector-log-retention
Jan 7, 2026
Merged

Feature/vector log retention#3
vybe merged 3 commits into
mainfrom
feature/vector-log-retention

Conversation

@oleksandr-korin

Copy link
Copy Markdown
Contributor

No description provided.

oleksandr-korin and others added 3 commits January 5, 2026 18:22
Add automated log retention, rotation, and archival system for Vector logs
to address SOC2/ISO27001 compliance requirements.

## Problem
Vector logging (introduced in 0ec3a7f) captures all container logs but lacked:
- Log rotation (files grow indefinitely)
- Retention policy (no automated cleanup)
- Archival capabilities (manual clearing loses history)
- Long-term storage (no S3/GCS integration)

This creates compliance gaps for SOC2/ISO27001 which require:
- Defined retention policies
- Secure archival of audit trails
- Prevention of log data loss

## Solution
### 1. Daily Log Rotation
Vector now writes to date-stamped files instead of single growing files:
- platform-2026-01-05.json (not platform.json)
- agents-2026-01-05.json (not agents.json)

### 2. Automated Archival Service
Backend APScheduler runs nightly (default: 3 AM UTC) to:
- Find log files older than retention period (default: 90 days)
- Compress with gzip (achieves ~90% size reduction)
- Verify integrity with SHA256 checksums
- Upload to S3-compatible storage (optional)
- Delete originals after successful archive

### 3. Admin API Endpoints
- GET /api/logs/stats - Log file statistics and sizes
- GET /api/logs/retention - Current retention configuration
- PUT /api/logs/retention - Update retention (runtime)
- POST /api/logs/archive - Manually trigger archival
- GET /api/logs/health - Service health check

### 4. S3-Compatible Storage
Optional integration with:
- AWS S3
- MinIO (self-hosted)
- Cloudflare R2
- Any S3-compatible service

Gracefully disabled by default - works without S3 configuration.

## Configuration
Add to .env (all optional, have sensible defaults):
```bash
LOG_RETENTION_DAYS=90        # Days to keep logs
LOG_ARCHIVE_ENABLED=true     # Enable automated archival
LOG_CLEANUP_HOUR=3           # Hour (UTC) to run nightly job
LOG_S3_ENABLED=false         # Upload archives to S3
LOG_S3_BUCKET=               # S3 bucket name
LOG_S3_ACCESS_KEY=           # S3 access key
LOG_S3_SECRET_KEY=           # S3 secret key
LOG_S3_ENDPOINT=             # Custom endpoint (for MinIO, R2)
LOG_S3_REGION=us-east-1      # AWS region
```

## Files Changed
New:
- src/backend/services/log_archive_service.py (313 lines)
- src/backend/services/s3_storage.py (177 lines)
- src/backend/routers/logs.py (177 lines)
- tests/test_log_archive.py (629 lines, 27 tests)

Modified:
- config/vector.yaml - Date-based file paths
- docker-compose.yml - Env vars + trinity-archives volume
- docker/backend/Dockerfile - Added boto3 dependency
- src/backend/main.py - Router + scheduler integration
- tests/requirements-test.txt - Test dependencies
- docs/memory/changelog.md - Detailed changelog entry
- docs/memory/feature-flows/vector-logging.md - Retention docs

## Testing
All 27 tests passing:
- 5 authentication tests
- 7 configuration tests
- 3 compression/integrity tests
- 3 manual archive tests
- 2 S3 integration tests
- 2 integration workflow tests
- 2 validation tests
- 3 error handling tests

## Impact
- No breaking changes to existing Vector setup
- Disk space stabilizes after retention period
- Compliance-ready audit trail with secure archival
- Production-ready with comprehensive test coverage

## Compliance
Addresses SOC2 CC7.2 (System Monitoring) and ISO27001 A.12.4.1
(Event Logging) requirements for log retention and archival.

🤖 Generated with Claude Sonnet 4.5
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add uplot npm package to fix import error in SparklineChart.vue component.
This dependency is required for the telemetry sparkline charts feature.

Error fixed:
- Failed to resolve import "uplot" from "src/components/SparklineChart.vue"

Changes:
- src/frontend/package-lock.json - Added uplot package and dependencies
- tests/test_log_archive.py - Whitespace cleanup
Architectural change following feedback to maintain full data sovereignty.
All logs and archives now stay within operator-controlled infrastructure.

Changes:
- Created pluggable ArchiveStorage interface with LocalArchiveStorage
- Removed external S3/boto3 dependency entirely
- Fixed scheduler restart bug in retention update endpoint
- Updated docs with sovereign backup strategies (NAS, rsync, volumes)
- All 19 API tests passing

Breaking: S3 configuration no longer supported (by design)
Non-breaking: Archives still stored in trinity-archives volume

Files:
+ src/backend/services/archive_storage.py (new storage abstraction)
- src/backend/services/s3_storage.py (deleted)
M docker-compose.yml (removed S3 vars, added LOG_ARCHIVE_PATH)
M docker/backend/Dockerfile (removed boto3)
M src/backend/routers/logs.py (fixed responses + scheduler)
M src/backend/services/log_archive_service.py (uses interface)
M tests/requirements-test.txt (removed boto3)
M tests/test_log_archive.py (removed S3 tests)
@vybe
vybe merged commit 3a9563a into main Jan 7, 2026
vybe added a commit that referenced this pull request Jan 7, 2026
- Add Phase 13-14 requirements for scalability & process orchestration
- Document simplified role model (Executor/Monitor/Informed)
- Add human approval steps concept for business processes
- Enhance demo-analyst and demo-researcher templates
- Improve create-demo-agent-fleet command
- Fix replay mode activity & context simulation in network.js
- Merge conflict resolution for PR #3 and #4 changelog entries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
pavshulin pushed a commit that referenced this pull request Mar 31, 2026
- requirements.md: Added SLACK-FILES requirement (§15.1b-iii)
- feature-flows/slack-file-sharing.md: Full flow document
- feature-flows.md: Index updated with new flow
- task_execution_service.py: Move start_time before try block (review item #3)
- message_router.py: Sanitize session_id in upload path (review item #4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vybe added a commit that referenced this pull request Mar 31, 2026
…container (#222)

* feat: Bidirectional file sharing — Slack to agent (inbound) (#222)

Enable Slack users to upload files that agents can process. Images are
embedded as base64 data URIs for Claude vision. Text files (CSV, JSON,
TXT, etc.) are copied into per-session container directories via Docker
put_archive API.

Security:
- Filename sanitization (path traversal prevention, hidden file rejection)
- Per-user file upload rate limiting (5 files/min)
- Size caps: 5MB per image, 10MB per file, 10MB total inline images
- Max 10 files per message
- Unsupported formats rejected with user message (PDF, archives, video, audio)
- Per-session upload dirs cleaned up after execution (all exit paths)

Architecture:
- FileAttachment model + files field on NormalizedMessage (channel-agnostic)
- adapter.download_file() — each channel implements its own download auth
- files:read OAuth scope added for Slack workspace installs
- container_put_archive() async wrapper in docker_utils
- Task execution logging: timeout, HTTP status, elapsed time

Tests: 36 unit tests covering sanitization, type routing, size limits,
       extraction, rate limiting, session directories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: Add requirements, feature flow for Slack file sharing (#222)

- requirements.md: Added SLACK-FILES requirement (§15.1b-iii)
- feature-flows/slack-file-sharing.md: Full flow document
- feature-flows.md: Index updated with new flow
- task_execution_service.py: Move start_time before try block (review item #3)
- message_router.py: Sanitize session_id in upload path (review item #4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by:  Pavlo <pash@pashs-MBP.home>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: vybe <me@evyborov.com>
vybe added a commit that referenced this pull request Apr 21, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
trinity-ability pushed a commit that referenced this pull request Aug 21, 2026
…e surface that never rendered (ent#429)

Slices 3+4 of ent#364. Most of the rendering shipped in #2300; auditing the
acceptance criteria against the code left three things genuinely undone, one of
them a promise the feature has been making falsely since it landed.

**AC #2 — nothing homeless.** `workspace_session_id` was READ by the ask
projection and written by nothing, so `chat_id` was always null and an ask
raised by a scheduled run belonged nowhere. It is now resolved at RAISE time
(`client_portal.service.ensure_thread_for_ask`), reusing the client's existing
thread with that agent and opening one only if they have never chatted — the
same `_resolve_session_id(..., None)` a first client turn takes, so an ask lands
IN the conversation rather than beside it. Render-time resolution would not be
an attachment: it is a guess repeated per view, with nothing durable to audit.

**And it is platform-written, which nothing enforced.** The projection's
docstring already promised "platform-written context only" while the clamp
happily passed an agent-authored `workspace_session_id` straight through —
`chat_id` is where the Workspace SENDS a reader, so an agent could choose that
destination. Stripped unconditionally now, before the real one is written, and
stripped whether or not an addressee resolves.

Three ordering details, each load-bearing:

  * the addressee is resolved BEFORE the context block, because the thread id is
    written INTO context and doing it after would add bytes the size cap had
    already signed off on;
  * the context dict is REBUILT, not popped — `out = dict(req)` is a shallow
    copy, so popping would reach into the caller's own dict and the clamp's
    contract is that it never mutates the request;
  * the thread id survives the oversize-context marker, or an agent-controlled
    oversize context would be the one way to produce a homeless ask.

Fail-SOFT, deliberately the opposite direction to `_validated_addressee` beside
it: that fails closed because it is an authorization decision, this one only
decides where a link points, and losing the whole question over a missing link
is the worse trade.

**AC #1 — the inline-in-chat surface has never rendered.** `PortalConversation`
passes `props.agent`, the agent OBJECT, to `asksForAgent`, which compares it
against the string `agent_name`. It matched nothing, and the wrapper is
`v-if="agentAsks.length"`, so the third of ent#364's three surfaces silently did
not exist. Verified with a throwaway probe before believing it:

    asksForAgent('scout')          -> 1 ask
    asksForAgent({name: 'scout'})  -> 0 asks

Pinned against the SOURCE, because this repo's vitest runs in `node` with no
component-mount harness — crude, but it is the difference between catching this
class again and not.

**AC #3 — expiry now says WHEN.** The row said "This expired before it was
answered" with no time, and the #1142 sweep DELETES terminal rows, so between
lapsing and being swept this is the only evidence the question was ever asked.
An hour ago and last March are different situations. `expiredLabel` lives in
`portalUtils` with `now` injected, per this file's rule that a sentence composed
inside a component is a sentence no test can reach; it degrades to the bare
wording on a missing, garbled or future timestamp, because a wrong time is worse
than no time — the reader would act on it.

Plus `askThreadLink`: "Open the conversation", shown only when the ask names a
thread the reader is not already in. Additive — it never hides an ask from the
thread being read. Narrowing the inline surface to only its own thread is the
open question, not this.

All Workspace work in the OSS repo, per the standing rule.

Backend slice: 687 passed, 1 skipped. Frontend: 52 files / 1097 tests, tokens
OK, build OK. 6 of the 9 new backend cases fail without this change; the other 3
are regression guards for properties it could have broken.

Related to ent#429.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
trinity-ability pushed a commit that referenced this pull request Aug 21, 2026
…e surface that never rendered (ent#429)

Slices 3+4 of ent#364. Most of the rendering shipped in #2300; auditing the
acceptance criteria against the code left three things genuinely undone, one of
them a promise the feature has been making falsely since it landed.

**AC #2 — nothing homeless.** `workspace_session_id` was READ by the ask
projection and written by nothing, so `chat_id` was always null and an ask
raised by a scheduled run belonged nowhere. It is now resolved at RAISE time
(`client_portal.service.ensure_thread_for_ask`), reusing the client's existing
thread with that agent and opening one only if they have never chatted — the
same `_resolve_session_id(..., None)` a first client turn takes, so an ask lands
IN the conversation rather than beside it. Render-time resolution would not be
an attachment: it is a guess repeated per view, with nothing durable to audit.

**And it is platform-written, which nothing enforced.** The projection's
docstring already promised "platform-written context only" while the clamp
happily passed an agent-authored `workspace_session_id` straight through —
`chat_id` is where the Workspace SENDS a reader, so an agent could choose that
destination. Stripped unconditionally now, before the real one is written, and
stripped whether or not an addressee resolves.

Three ordering details, each load-bearing:

  * the addressee is resolved BEFORE the context block, because the thread id is
    written INTO context and doing it after would add bytes the size cap had
    already signed off on;
  * the context dict is REBUILT, not popped — `out = dict(req)` is a shallow
    copy, so popping would reach into the caller's own dict and the clamp's
    contract is that it never mutates the request;
  * the thread id survives the oversize-context marker, or an agent-controlled
    oversize context would be the one way to produce a homeless ask.

Fail-SOFT, deliberately the opposite direction to `_validated_addressee` beside
it: that fails closed because it is an authorization decision, this one only
decides where a link points, and losing the whole question over a missing link
is the worse trade.

**AC #1 — the inline-in-chat surface has never rendered.** `PortalConversation`
passes `props.agent`, the agent OBJECT, to `asksForAgent`, which compares it
against the string `agent_name`. It matched nothing, and the wrapper is
`v-if="agentAsks.length"`, so the third of ent#364's three surfaces silently did
not exist. Verified with a throwaway probe before believing it:

    asksForAgent('scout')          -> 1 ask
    asksForAgent({name: 'scout'})  -> 0 asks

Pinned against the SOURCE, because this repo's vitest runs in `node` with no
component-mount harness — crude, but it is the difference between catching this
class again and not.

**AC #3 — expiry now says WHEN.** The row said "This expired before it was
answered" with no time, and the #1142 sweep DELETES terminal rows, so between
lapsing and being swept this is the only evidence the question was ever asked.
An hour ago and last March are different situations. `expiredLabel` lives in
`portalUtils` with `now` injected, per this file's rule that a sentence composed
inside a component is a sentence no test can reach; it degrades to the bare
wording on a missing, garbled or future timestamp, because a wrong time is worse
than no time — the reader would act on it.

Plus `askThreadLink`: "Open the conversation", shown only when the ask names a
thread the reader is not already in. Additive — it never hides an ask from the
thread being read. Narrowing the inline surface to only its own thread is the
open question, not this.

All Workspace work in the OSS repo, per the standing rule.

Backend slice: 687 passed, 1 skipped. Frontend: 52 files / 1097 tests, tokens
OK, build OK. 6 of the 9 new backend cases fail without this change; the other 3
are regression guards for properties it could have broken.

Related to ent#429.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants