From ed1dfdb3a0486fcb8ffbbd006cb6a04aaf52f35b Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Mon, 31 Aug 2026 15:40:42 +0300 Subject: [PATCH 1/5] docs(specs): spec for agent files visible copy model and safe sync (CLEAN-50) Co-Authored-By: Claude Opus 4.7 --- .specify/feature.json | 2 +- .../checklists/requirements.md | 34 +++++ specs/008-agent-files-sync-safety/spec.md | 121 ++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 specs/008-agent-files-sync-safety/checklists/requirements.md create mode 100644 specs/008-agent-files-sync-safety/spec.md diff --git a/.specify/feature.json b/.specify/feature.json index 588ae7df..14159cb6 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/007-knowledge-workspaces-research" + "feature_directory": "specs/008-agent-files-sync-safety" } diff --git a/specs/008-agent-files-sync-safety/checklists/requirements.md b/specs/008-agent-files-sync-safety/checklists/requirements.md new file mode 100644 index 00000000..42daac9f --- /dev/null +++ b/specs/008-agent-files-sync-safety/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Agent Files — Visible Copy Model & Safe Sync + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-08-31 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain (Q1 → phased: constrain instructions now, tools in follow-up; Q2 → platform-recorded markers) +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification (Background cites verified runtime behavior deliberately — kept behavioral, no code refs) + +## Notes + +- All items pass (2026-08-31). Q1 resolved: constrain instructions in CLEAN-50, toolset extension deferred to follow-up ticket. Q2 resolved: platform-recorded take/send markers, agent clock excluded. diff --git a/specs/008-agent-files-sync-safety/spec.md b/specs/008-agent-files-sync-safety/spec.md new file mode 100644 index 00000000..cd7b1270 --- /dev/null +++ b/specs/008-agent-files-sync-safety/spec.md @@ -0,0 +1,121 @@ +# Feature Specification: Agent Files — Visible Copy Model & Safe Sync + +**Feature Branch**: `feat/CLEAN-50-agent-files-sync-safety` + +**Created**: 2026-08-31 + +**Status**: Draft + +**Jira**: [CLEAN-50](https://dreamvention.atlassian.net/browse/CLEAN-50) `[ADMIN]` + +**Input**: User description: "Возьми в работу CLEAN-50, а так же убедись — что именно пушит рантайм при Sync (все файлы или дельту)" + +## Background + +An agent's files exist as two copies: a shared copy (shown in the admin Files tab) and the running agent's own working copy (taken from the shared copy when the agent boots, sent back only when an operator triggers Sync). A Mazda dev-installation user edited SOUL.md through the admin agent and could not tell why the Files tab did not reflect the change — the model is technically correct but invisible to the operator. + +**Verified Sync behavior** (research, 2026-08-31): Sync sends back only a *delta*, not all files — the running agent tracks each file's local modification time and size from the moment it booted, and sends only files that changed locally since then. It also *removes* shared files that were deleted in the working copy (with an existing safeguard against removing everything at once). Crucially, it never checks whether the shared copy changed in the meantime. Consequences: + +- A file edited **only** in the shared copy (UI editor or admin-agent write) is left untouched by Sync — the "skip unchanged" rule protects it. +- A file edited **both** in the shared copy and in the agent's working copy (the SOUL.md case) is silently overwritten by the agent's version. +- A file deleted in the working copy is removed from the shared copy even if it was just updated there. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Sync warns before destroying newer shared edits (Priority: P1) + +An operator edits an agent file in the shared copy (via the UI editor or through the admin agent) while the agent is running. Later they press Sync. Instead of silently losing the edit, they see a warning listing the files whose shared copy is newer than what the running agent last took, including files Sync would remove, and must explicitly confirm before those files are overwritten or deleted. + +**Why this priority**: This is active data loss — "my edit disappeared" — and the direct cause of the user report. Everything else is visibility polish around this hazard. + +**Independent Test**: Edit a file in the Files tab while the agent is running and has also modified the same file, press Sync, observe the warning naming that file; confirm → file overwritten; cancel → shared edit intact. + +**Acceptance Scenarios**: + +1. **Given** a running agent whose working copy of a file changed AND whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** a warning lists that file as "will be overwritten, shared copy is newer" and Sync proceeds only after explicit confirmation. +2. **Given** a file deleted in the agent's working copy whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** the warning lists that file as "will be removed" and requires the same confirmation. +3. **Given** no conflicts (no shared-copy object newer than the agent's last take/send), **When** the operator triggers Sync, **Then** Sync runs immediately without an extra confirmation step. +4. **Given** a file edited only in the shared copy (agent's working copy untouched), **When** Sync runs, **Then** the file is not listed as a conflict and is not overwritten (existing delta behavior, must be preserved). + +--- + +### User Story 2 - The two-copy model is visible in the Files tab (Priority: P2) + +An operator opening the Files tab of a running agent can tell which copy they are looking at and that the running agent may hold newer content that will only appear after Sync. After a chat-driven change (agent wrote to its own files), the operator understands Sync is required to see it. + +**Why this priority**: Removes the confusion that triggered the report; without it operators cannot reason about what Sync will do even with the P1 guard in place. + +**Independent Test**: Open the Files tab of a Running agent — a hint explains the displayed copy is the shared one and the running agent may hold newer content, with Sync offered as the way to bring it in. For a stopped agent, no such hint appears. + +**Acceptance Scenarios**: + +1. **Given** an agent in Running state, **When** the operator opens the Files tab, **Then** a visible hint states the tab shows the shared copy, the running agent may hold newer content, and offers Sync. +2. **Given** an agent that is stopped, **When** the operator opens the Files tab, **Then** no stale-copy hint is shown (the shared copy is the only copy). +3. **Given** the Files tab is open, **When** the operator reads any file's details, **Then** the file's last-modified time of the shared copy is visible. + +--- + +### User Story 3 - Admin agent stops promising what it cannot do and surfaces restart (Priority: P3) + +An operator asks the admin agent to "create an agent and bind a knowledge base". Today the admin agent narrates doing both, though its toolset can do neither. After this feature, the admin agent's promises match its actual abilities, and when it writes an agent file it tells the operator a restart is required (or proposes the restart itself). + +**Why this priority**: Trust/expectation damage rather than data loss; independent of the Sync mechanics. + +**Independent Test**: Ask the admin agent to create an agent and bind a knowledge base; verify the reply either performs the action with a real tool or honestly states the limitation and points to the manual path. Ask it to write an agent file; verify the restart requirement is surfaced in the reply. + +**Acceptance Scenarios**: + +1. **Given** the admin agent chat, **When** the operator requests an action outside the toolset (create agent, bind knowledge base), **Then** the admin agent does not claim to perform it — it explains the limitation and points to the manual path (decided: instructions are constrained in this feature; real tools arrive in a follow-up ticket). +2. **Given** the admin agent writes an agent file, **When** it reports the result, **Then** the reply states a restart is required for the change to take effect and offers/suggests the restart action. + +--- + +### Edge Cases + +- Agent offline / not connected when Sync is pressed → keep today's behavior (Sync reports the agent is offline; no conflict check needed). +- Agent restarted after shared-copy edits → its working copy is fresh; Sync must show no false-positive warnings. +- File created only in the shared copy after the agent booted → Sync must not remove or overwrite it (verified: today it does not; behavior must be preserved). +- The agent's working copy lost all files → existing "refuse to delete everything" safeguard must remain in force. +- Clock skew between systems → conflict detection relies only on moments the platform itself records (agent start, last completed Sync) compared against shared-storage change times, with a small tolerance margin; the running agent's own clock is never consulted (decided: Q2). +- Warning shown, operator walks away, agent keeps writing → confirmation acts on the state at confirmation time; the warning list may be refreshed on confirm. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The platform MUST record, per agent and using its own clock domain, when the running agent last took files from the shared copy (agent start) and when the last Sync completed; the running agent's clock is not part of the comparison. +- **FR-002**: Before executing Sync, the system MUST identify shared-copy files that changed after the moment from FR-001 and that Sync would overwrite or remove. +- **FR-003**: When FR-002 finds at least one file at risk, the system MUST present the list to the operator and proceed only after explicit confirmation; cancelling MUST leave the shared copy untouched. +- **FR-004**: When FR-002 finds nothing at risk, Sync MUST proceed without additional friction. +- **FR-005**: Sync MUST continue to send only files actually changed in the agent's working copy and MUST NOT touch shared-copy files the agent never modified (preserve verified delta semantics). +- **FR-006**: The Files tab MUST tell the operator, for a running agent, that the displayed content is the shared copy and that the running agent may hold newer content, offering Sync as the remedy. +- **FR-007**: The Files tab MUST show each file's shared-copy last-modified time. +- **FR-008**: The admin agent MUST NOT claim to perform actions its toolset cannot perform: its instructions are constrained so that agent creation and knowledge-base binding requests get an honest limitation notice plus the manual path. Extending the toolset with those abilities is explicitly out of scope, tracked as a follow-up ticket. +- **FR-009**: After the admin agent writes an agent file, the operator-facing reply MUST state that a restart is required and offer the restart action. + +### Key Entities + +- **Shared copy**: The authoritative stored set of an agent's files; what the Files tab shows and what UI/admin-agent edits modify. Each file carries a last-modified time. +- **Working copy**: The running agent's private set of the same files, taken from the shared copy at boot, sent back (as a delta) only on Sync. +- **Sync operation**: Operator-triggered action that pushes the working-copy delta over the shared copy and removes files deleted in the working copy. +- **Sync markers (per agent)**: Times of last take (boot pull) and last send (successful Sync) — the reference points for conflict detection. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Zero silent overwrites: 100% of Sync executions that would overwrite or remove a shared file changed since the agent's last take/send are preceded by a warning naming that file. +- **SC-002**: An operator with no knowledge of the internals can, within 10 seconds of opening the Files tab of a running agent, answer "am I looking at the latest content?" (the hint provides the answer). +- **SC-003**: Conflict-free Sync keeps its current speed: no extra confirmation step when nothing is at risk. +- **SC-004**: Zero admin-agent replies that narrate performing agent creation or knowledge binding without a tool actually doing it. +- **SC-005**: "My edit disappeared" reports for agent files drop to zero after release. + +## Assumptions + +- The warn-and-confirm approach (list of at-risk files + explicit confirmation) is sufficient for v1; a full per-file diff view before overwrite is out of scope. +- Conflict detection compares shared-copy object change times against platform-recorded take/send markers (Q2 decision); content hashing is not required in v1. +- Rancher toolset extension (agent creation, knowledge-base binding) is deliberately deferred to a follow-up ticket (Q1 decision); this feature only aligns the admin agent's promises with its current abilities. +- The Files tab hint applies to the Running state only; Starting/Stopped agents show no hint. +- Admin console stays English-only (per repo i18n policy); no `app` console changes are in scope. +- CLEAN-48 knowledge-isolation work does not touch the files stack; no dependency. +- The existing "agent offline" Sync handling and the "refuse to delete everything" safeguard remain unchanged. From 1f369b1f22e5e6e66048e47729502f31c262399d Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Mon, 31 Aug 2026 15:58:58 +0300 Subject: [PATCH 2/5] docs(specs): implementation plan and design artifacts for safe sync (CLEAN-50) Co-Authored-By: Claude Opus 4.7 --- .../contracts/sync-api.md | 49 ++++++++++ .../008-agent-files-sync-safety/data-model.md | 47 +++++++++ specs/008-agent-files-sync-safety/plan.md | 95 +++++++++++++++++++ .../008-agent-files-sync-safety/quickstart.md | 48 ++++++++++ specs/008-agent-files-sync-safety/research.md | 57 +++++++++++ specs/008-agent-files-sync-safety/spec.md | 10 +- 6 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 specs/008-agent-files-sync-safety/contracts/sync-api.md create mode 100644 specs/008-agent-files-sync-safety/data-model.md create mode 100644 specs/008-agent-files-sync-safety/plan.md create mode 100644 specs/008-agent-files-sync-safety/quickstart.md create mode 100644 specs/008-agent-files-sync-safety/research.md diff --git a/specs/008-agent-files-sync-safety/contracts/sync-api.md b/specs/008-agent-files-sync-safety/contracts/sync-api.md new file mode 100644 index 00000000..027bcaaa --- /dev/null +++ b/specs/008-agent-files-sync-safety/contracts/sync-api.md @@ -0,0 +1,49 @@ +# Contract: Agent Files Sync API + +**Slice**: `api/src/slices/agent/file` | consumed by `admin` via generated `FilesService` (openapi-ts). +After DTO changes: regenerate `api` swagger (`bun run generate:swagger`) and `admin` client (`bun run build:api`). + +## POST /agents/:agentId/files/sync (changed) + +Request body (new, optional — absent body ≡ `{}`): + +```json +{ "confirm": false } +``` + +Responses: + +| Code | Body | When | +|------|------|------| +| 200 | `{ "agentOnline": true, "pushed": 3 }` | No at-risk files, or `confirm: true`. Side effect: pod pushes delta; `lastSyncAt` persisted on `sync_done`. | +| 200 | `{ "agentOnline": false }` | Agent socket not connected (unchanged behavior; no conflict check needed). | +| 409 | `{ "requiresConfirmation": true, "atRisk": [{ "path": "SOUL.md", "updatedAt": "2026-08-31T12:00:00Z" }], "baseline": "2026-08-31T09:15:00Z" }` | `baseline` exists, at-risk list non-empty, `confirm` not set. **No sync performed, no side effects.** | + +Rules: +- At-risk = S3 objects with `LastModified > baseline` (see [data-model.md](../data-model.md)). +- No baseline (both markers null) → skip check, behave as today. +- `confirm: true` bypasses the check entirely (list is NOT recomputed — operator accepted the risk shown). + +## GET /agents/:agentId (changed) + +Agent DTO gains nullable fields: + +```json +{ "lastPullAt": "2026-08-31T09:15:00Z", "lastSyncAt": null } +``` + +## Unchanged endpoints (relied upon) + +- `GET /agents/:agentId/files` — list; each node already carries `updatedAt` (S3 LastModified). UI starts displaying it. +- `PUT /agents/:agentId/files/content`, rancher `write_agent_file` — still write straight to S3; their writes are what makes S3 objects "newer than baseline". + +## UI contract (admin Files tab) + +- Agent `status === 'running'` → hint banner: shared copy is displayed; the running agent may hold newer content; Sync brings it in. Shows `lastPullAt`/`lastSyncAt` when present. English only. +- Sync click → `fileControllerSync({})`; on 409 → `useConfirmStore().ask()` listing `atRisk` paths + timestamps; confirm → `fileControllerSync({ confirm: true })`; cancel → no call. +- Stopped agent → no banner, no conflict flow (server returns no 409 anyway — markers reset relevance at next boot). + +## Rancher contract (P3) + +- `rancher/.agent/SOUL.md`: honesty constraints — never claim create-agent / knowledge-binding ability; state limitation + manual path (until CLEAN-51 lands). +- `write_agent_file` tool result text: append "Restart required for the change to take effect — offer restart_agent." (description already says it; result must too). diff --git a/specs/008-agent-files-sync-safety/data-model.md b/specs/008-agent-files-sync-safety/data-model.md new file mode 100644 index 00000000..3dbfa505 --- /dev/null +++ b/specs/008-agent-files-sync-safety/data-model.md @@ -0,0 +1,47 @@ +# Data Model: Agent Files — Visible Copy Model & Safe Sync + +**Spec**: [spec.md](./spec.md) | **Research**: [research.md](./research.md) + +## Agent (existing Prisma model — additions) + +File: `api/src/slices/agent/agent/agent.prisma` + +| Field | Type | Set when | Semantics | +|-------|------|----------|-----------| +| `lastPullAt` | `DateTime?` | Agent's bridle socket authenticates (`connected` event path: `bridle.gateway.ts` → `agentStatus.service.ts`) | Upper bound of the moment the pod pulled its working copy from S3 (pull happens at boot, moments before connect) | +| `lastSyncAt` | `DateTime?` | `sync_done` received (`bridle.gateway.ts:handleSyncResponse`) | Moment the last successful Sync push completed | + +Both nullable: agents deployed before this feature have neither until their next boot/sync. + +**Migration**: `prisma migrate dev --name agent-sync-markers` (additive, no backfill). + +## Derived values (not stored) + +| Value | Definition | +|-------|-----------| +| `baseline` | `max(lastSyncAt, lastPullAt - PULL_MARGIN)`; `PULL_MARGIN ≈ 60s` covers the boot-pull→connect window. If both markers are null → no baseline → conflict check is skipped (warn-free Sync, matches pre-feature behavior). | +| `atRisk[]` | S3 objects under the agent's prefix with `LastModified > baseline`. Fields per entry: `path`, `updatedAt` (S3 LastModified). | + +## DTO changes + +| DTO | Change | +|-----|--------| +| Agent response DTO (agent GET, feeds admin workspace) | + `lastPullAt`, `lastSyncAt` (nullable ISO strings) | +| Sync request body | + optional `confirm?: boolean` | +| Sync 409 response (new) | `{ requiresConfirmation: true, atRisk: [{path, updatedAt}], baseline }` | +| Sync 200 response | unchanged: `{ agentOnline, pushed }` | + +## State transitions + +``` +pod boots ──pull from S3──▶ socket connects ──▶ lastPullAt = now() +operator triggers Sync: + baseline exists AND atRisk non-empty AND !confirm ──▶ 409, no side effects + else ──▶ 'sync' → pod → 'sync_done' ──▶ lastSyncAt = now() +``` + +## Entities NOT changed + +- S3 file objects: no metadata additions; `LastModified` used as-is (already exposed as `updatedAt` in `file.gateway.ts`). +- Runtime manifest: untouched (delta semantics preserved per FR-005). +- `rancher/.agent/SOUL.md`: content-only edit (honesty constraints), no schema. diff --git a/specs/008-agent-files-sync-safety/plan.md b/specs/008-agent-files-sync-safety/plan.md new file mode 100644 index 00000000..40c34b48 --- /dev/null +++ b/specs/008-agent-files-sync-safety/plan.md @@ -0,0 +1,95 @@ +# Implementation Plan: Agent Files — Visible Copy Model & Safe Sync + +**Branch**: `feat/CLEAN-50-agent-files-sync-safety` | **Date**: 2026-08-31 | **Spec**: [spec.md](./spec.md) + +**Jira**: CLEAN-50 (parent) → CLEAN-52 (P1 sync guard, in progress), CLEAN-53 (P2 visibility), CLEAN-54 (P3 rancher honesty); CLEAN-51 (toolset extension, separate follow-up) + +**Input**: Feature specification from `/specs/008-agent-files-sync-safety/spec.md` + +## Summary + +Sync currently pushes the pod's delta (mtime+size manifest) over S3 and deletes pod-side orphans without ever checking S3 freshness — concurrent edits (SOUL.md case) are silently lost. Fix in three phases matching subtasks: (P1/CLEAN-52) platform records `lastPullAt`/`lastSyncAt` markers and the sync endpoint returns 409 with an at-risk file list requiring explicit confirmation; (P2/CLEAN-53) Files tab explains the S3-vs-pod copy model for running agents and shows per-file freshness; (P3/CLEAN-54) rancher SOUL.md constrained to its real abilities + `write_agent_file` result surfaces the restart requirement. No runtime-repo changes; delta semantics preserved. + +## Technical Context + +**Language/Version**: TypeScript (api: NestJS-style slices; admin: Nuxt 3 / Vue 3), Bun as package manager/runtime tooling + +**Primary Dependencies**: Prisma (api ORM), AWS SDK S3 (`ListObjectsV2`/`LastModified`), Socket.IO (bridle hub ↔ agent pods), openapi-ts (generated admin client), `useConfirmStore` modal (admin) + +**Storage**: Postgres via Prisma (`Agent` model gains `lastPullAt`, `lastSyncAt`); S3 as the shared file store (no changes to object layout) + +**Testing**: api — jest (`bun run test`); admin — no test runner (manual validation per [quickstart.md](./quickstart.md)) + +**Target Platform**: Linux server (api), browser (admin console); agent pods untouched + +**Project Type**: web service + admin SPA (monorepo slices) + +**Performance Goals**: conflict check adds ≤1 S3 list call on the sync path; zero extra calls on file browsing + +**Constraints**: no runtime-repo release; pod clock excluded from comparisons (Q2); admin UI English-only; legacy agents (null markers) must behave exactly as today + +**Scale/Scope**: ~3 api slices touched (agent/file, agent/agent, bridle) + 1 admin slice (agent/file) + rancher SOUL.md; dozens of agents per installation — per-agent S3 list is cheap + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +`.specify/memory/constitution.md` is an unfilled template — no ratified project principles. Gate passes vacuously. Applied repo rules instead: Jira CLEAN cycle (branch/commits/PR carry CLEAN-50), admin English-only i18n policy, OpenAPI regeneration flow for DTO changes. **Post-design re-check (2026-08-31): pass** — no violations introduced; Complexity Tracking empty. + +## Project Structure + +### Documentation (this feature) + +```text +specs/008-agent-files-sync-safety/ +├── plan.md # This file +├── research.md # Phase 0 — verified sync mechanics, decisions R1-R7 +├── data-model.md # Phase 1 — Agent marker fields, baseline/atRisk derivation +├── quickstart.md # Phase 1 — unit + E2E validation scenarios +├── contracts/ +│ └── sync-api.md # Phase 1 — sync 409 contract, agent DTO, UI/rancher contracts +└── tasks.md # Phase 2 (/speckit-tasks — not created yet) +``` + +### Source Code (repository root) + +```text +api/src/slices/ +├── agent/agent/ +│ ├── agent.prisma # + lastPullAt, lastSyncAt (migration: agent-sync-markers) +│ ├── data/agent.gateway.ts # + marker update methods +│ └── domain/agentStatus.service.ts # 'connected' event → set lastPullAt +├── agent/file/ +│ ├── file.controller.ts # sync endpoint: confirm body, 409 at-risk response +│ ├── domain/… # baseline + at-risk computation (new service logic, jest specs) +│ └── data/file.gateway.ts # reuse list/LastModified (no change expected) +├── bridle/data/bridle.gateway.ts # handleSyncResponse → set lastSyncAt +└── rancher/rancher.tool.ts # write_agent_file result += restart reminder + +admin/slices/agent/file/ +├── components/agentFile/Provider.vue # running-agent banner, 409 → confirm flow, per-file updatedAt +└── data/agentFile.gateway.ts # sync(confirm?) passthrough + +rancher/.agent/SOUL.md # honesty constraints (P3) + propagation step for deployed rancher + +api swagger + admin generated client # regenerate after DTO changes +``` + +**Structure Decision**: follow the existing slice layout; all changes land in already-owning slices (agent/file owns the sync contract, bridle owns socket moments, agent/agent owns persistence). No new slices, no shared abstractions. + +## Phase progression + +- **Phase 0** ([research.md](./research.md)): done — sync mechanics verified in runtime repo, markers/margin design (R2), single-endpoint 409 flow (R3), UI/rancher hooks (R4-R6). All Technical Context unknowns resolved; spec US1-scenario-4 adjusted to at-risk semantics (false positives acceptable, silent loss not). +- **Phase 1** ([data-model.md](./data-model.md), [contracts/sync-api.md](./contracts/sync-api.md), [quickstart.md](./quickstart.md)): done — additive Prisma fields, derived baseline/atRisk, endpoint contract, validation guide. +- **Phase 2**: `/speckit-tasks` — expected to group tasks by subtask: CLEAN-52 (migration → markers → guard → UI confirm) → CLEAN-53 (banner + timestamps) → CLEAN-54 (SOUL.md + tool result + propagation). + +## Risks & mitigations + +- **False positives right after boot** (S3 edits within the 60s pull margin) — cosmetic; wording says "may be overwritten". +- **AWS-vs-API clock skew** — NTP-level; affects warning accuracy only, never data (guard errs toward warning). +- **SOUL.md propagation** — deployed rancher agents hold the old prompt; tasks must include the reseed/update step (R6), else P3 ships dark. +- **Legacy agents** — null markers skip the check by design; first restart activates the guard. + +## Complexity Tracking + +*No constitution violations — table intentionally empty.* diff --git a/specs/008-agent-files-sync-safety/quickstart.md b/specs/008-agent-files-sync-safety/quickstart.md new file mode 100644 index 00000000..032062cd --- /dev/null +++ b/specs/008-agent-files-sync-safety/quickstart.md @@ -0,0 +1,48 @@ +# Quickstart Validation: Agent Files — Visible Copy Model & Safe Sync + +**Spec**: [spec.md](./spec.md) | **Contract**: [contracts/sync-api.md](./contracts/sync-api.md) + +## Prerequisites + +- `api` running with migrated DB (`cd api && bun run migrate && bun run dev`) +- `admin` running (`cd admin && bun run dev`), logged in as owner +- One deployable test agent (dev installation), its pod able to reach S3 +- After DTO changes: `cd api && bun run generate:swagger && cd ../admin && bun run build:api` + +## Unit tests (api, jest) + +```bash +cd api && bun run test +``` + +Must cover (new specs): baseline computation (`max(lastSyncAt, lastPullAt - margin)`, null cases), at-risk filtering by `LastModified`, 409 vs pass-through decision, `lastPullAt`/`lastSyncAt` persistence hooks. + +## E2E scenario 1 — conflict warning (US1, P1) + +1. Start/restart the test agent; wait for Running. **Expect**: agent DTO shows fresh `lastPullAt`. +2. In the Files tab, edit any file (e.g. `SOUL.md`) and save — this makes the S3 copy newer than baseline. +3. Press **Sync**. **Expect**: confirmation dialog listing that file with its timestamp; no sync happened yet (pushed count absent, file content unchanged). +4. Cancel. **Expect**: S3 edit intact (re-open file). +5. Press **Sync** again, confirm. **Expect**: sync runs, result shows pushed count, `lastSyncAt` updates. +6. Press **Sync** once more without editing anything. **Expect**: no dialog (FR-004), immediate sync. + +## E2E scenario 2 — no false alarm after restart (edge case) + +1. Edit a file in the Files tab while agent is Running. +2. Restart the agent; wait for Running (pod pulled fresh copy incl. the edit). +3. Press **Sync**. **Expect**: no confirmation dialog (baseline = new `lastPullAt`). + +## E2E scenario 3 — visibility hint (US2, P2) + +1. Open Files tab of a **Running** agent. **Expect**: banner explaining S3-copy-vs-pod model with Sync CTA and markers; per-file last-modified visible. +2. Stop the agent, reopen Files tab. **Expect**: no banner. + +## E2E scenario 4 — admin agent honesty (US3, P3) + +1. In rancher chat ask: "создай агента и привяжи базу знаний". **Expect**: honest limitation notice + manual path; no narrated fake success. +2. Ask it to update an agent file. **Expect**: reply mentions restart requirement and offers restart. +3. Verify deployed rancher got the new SOUL.md (propagation step from research R6). + +## Legacy agent check + +Agent deployed before the feature, never restarted/synced since: both markers null → Sync behaves exactly as before (no 409). Restart it once → markers appear. diff --git a/specs/008-agent-files-sync-safety/research.md b/specs/008-agent-files-sync-safety/research.md new file mode 100644 index 00000000..8085eb87 --- /dev/null +++ b/specs/008-agent-files-sync-safety/research.md @@ -0,0 +1,57 @@ +# Phase 0 Research: Agent Files — Visible Copy Model & Safe Sync + +**Date**: 2026-08-31 | **Spec**: [spec.md](./spec.md) + +## R1. Verified Sync mechanics (runtime repo) + +**Finding**: Sync pushes a *delta*, not all files. The runtime keeps an in-memory manifest `{mtimeMs, size}` per file, populated at boot-time pull; `pushIfChanged()` skips files whose local mtime+size are unchanged (`runtime/src/slices/bot/sync/data/s3-sync.gateway.ts:210-216`). An orphan sweep deletes S3 objects present in the manifest but missing locally, with a refuse-all-gone safeguard (`s3-sync.gateway.ts:275-295`). No S3 freshness check exists anywhere. Trigger: WebSocket `sync` event via bridle hub; pod replies `sync_done` with pushed count. + +**Implication**: files edited only in S3 survive Sync untouched; files changed on *both* sides are silently overwritten; pod-side deletions remove fresh S3 edits. + +## R2. Conflict-detection basis (Q2 decision: platform-recorded markers) + +**Decision**: Two new Prisma fields on `Agent`: `lastPullAt` (set when the agent's bridle socket authenticates — the boot pull happens moments before connect) and `lastSyncAt` (set when `sync_done` is received in `bridle.gateway.ts:handleSyncResponse`, `bridle.gateway.ts:317-332`). Baseline for comparison: `max(lastSyncAt, lastPullAt - margin)`; S3 objects with `LastModified > baseline` are *at risk*. Margin (~60s) applies only to `lastPullAt` to cover the pull→connect window; no margin on `lastSyncAt` (sync's own S3 writes complete before `sync_done`, margin would flag every just-pushed file). + +**Rationale**: no runtime-repo changes, no pod clock in the comparison (S3 `LastModified` = AWS clock, markers = API clock; NTP-level skew is acceptable for a warning). Changes stay in one repo/release. + +**Alternatives considered**: pod-reported delta (exact, but needs runtime release + clock skew); content hashes (skew-immune, but requires storing a manifest snapshot server-side and runtime changes). Both rejected for v1 per Q2. + +**Accepted limitation (spec adjusted)**: the API cannot know which files the pod actually changed, so the warning lists *at-risk* files ("S3 copy newer than what the pod holds — may be overwritten/removed if the pod also changed/deleted it"), including false positives for files edited only in S3. The P1 guarantee holds: nothing S3-newer is ever overwritten silently. Spec US1 scenario 4 updated accordingly. + +## R3. Where to hook the guard (API) + +**Decision**: keep a single endpoint. `POST /agents/:agentId/files/sync` (file.controller.ts:102-112) gains optional body `{ confirm?: boolean }`: +- compute at-risk list (S3 `ListObjectsV2` LastModified vs baseline — file.gateway.ts already exposes `updatedAt`); +- non-empty list + no `confirm` → **409** `{ requiresConfirmation: true, atRisk: [{path, updatedAt}], baseline }`, sync not executed; +- empty list or `confirm: true` → proceed as today, persist `lastSyncAt` on `sync_done`. + +**Rationale**: check is computed at the moment of sync (freshest list); no second round-trip on the happy path (FR-004); UI flow maps 1:1 to `useConfirmStore().ask()`. + +**Alternatives**: separate GET dry-run endpoint (extra round-trip, list can go stale between check and sync); rejected. + +## R4. Surfacing markers for the Files tab hint (P2) + +**Decision**: expose `lastPullAt` / `lastSyncAt` in the agent DTO (agent GET already feeds the workspace UI). Files tab hint for `status === 'running'` is static copy + the markers; per-file `updatedAt` is already returned by the list endpoint — display it. + +## R5. Admin UI building blocks + +- Confirmation modal: `useConfirmStore().ask()` (`admin/slices/common/stores/confirm.ts:15-44`), already used in `agentFile/Provider.vue:98-105` (discard) and `:195-201` (delete). Reuse for the 409 flow; file list goes into `description`. +- Sync trigger: `Provider.vue:63-90` → `agentFile.gateway.ts:76-80` → generated `FilesService.fileControllerSync`. 409 handling lands here. +- Admin is English-only (repo i18n policy) — no i18n work. + +## R6. Rancher admin-agent constraints (Q1 decision: instructions only) + +**Finding**: the admin agent's system prompt is `rancher/.agent/SOUL.md` (seeded by `rancher.service.ts` seed flow); toolset registered via `@Tool` decorators in `api/src/slices/rancher/rancher.tool.ts` (22 tools, no `create_agent`, no knowledge binding). `write_agent_file` tool description already says restart is required (`rancher.tool.ts:479`), but the *result* text does not remind the model to surface it. + +**Decision**: +1. SOUL.md: add an honesty section — never claim to create agents or bind knowledge bases; state the limitation + manual path (tools arrive with CLEAN-51). +2. `write_agent_file` tool *result*: append an explicit "Restart required — offer restart_agent" reminder so the model reliably surfaces it (FR-009). + +**Open concern for tasks phase**: SOUL.md is seeded — existing installations hold an older copy in S3/pod; the plan must include a propagation step (reseed/update path) or the change won't reach deployed rancher agents. + +## R7. Persistence & tooling + +- ORM: Prisma; Agent model at `api/src/slices/agent/agent/agent.prisma:8-44` (has `createdAt`, `updatedAt`, `firstDeployedAt`, `lastDeployStartedAt`, …). Schema change via `prisma migrate dev`. +- Status updates: `agent.gateway.ts:91-111` (`updateStatus`), driven by `agentStatus.service.ts` subscribing to bridle `agentEvents$` (`bridle.gateway.ts:75` emits `connected`) — `lastPullAt` hooks into the same path. +- Tests: api uses jest (`test: jest --passWithNoTests`); admin has no tests (manual validation via quickstart). +- OpenAPI: regenerate `api` swagger (`generate:swagger`) then `admin bun run build:api` (openapi-ts) after DTO changes. diff --git a/specs/008-agent-files-sync-safety/spec.md b/specs/008-agent-files-sync-safety/spec.md index cd7b1270..090a172a 100644 --- a/specs/008-agent-files-sync-safety/spec.md +++ b/specs/008-agent-files-sync-safety/spec.md @@ -32,10 +32,10 @@ An operator edits an agent file in the shared copy (via the UI editor or through **Acceptance Scenarios**: -1. **Given** a running agent whose working copy of a file changed AND whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** a warning lists that file as "will be overwritten, shared copy is newer" and Sync proceeds only after explicit confirmation. -2. **Given** a file deleted in the agent's working copy whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** the warning lists that file as "will be removed" and requires the same confirmation. -3. **Given** no conflicts (no shared-copy object newer than the agent's last take/send), **When** the operator triggers Sync, **Then** Sync runs immediately without an extra confirmation step. -4. **Given** a file edited only in the shared copy (agent's working copy untouched), **When** Sync runs, **Then** the file is not listed as a conflict and is not overwritten (existing delta behavior, must be preserved). +1. **Given** a running agent whose working copy of a file changed AND whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** a warning lists that file as at risk ("shared copy is newer — may be overwritten") and Sync proceeds only after explicit confirmation. +2. **Given** a file deleted in the agent's working copy whose shared copy was edited after the agent last took/sent files, **When** the operator triggers Sync, **Then** the warning covers it the same way (at risk of removal) and requires the same confirmation. +3. **Given** no shared-copy object newer than the agent's last take/send, **When** the operator triggers Sync, **Then** Sync runs immediately without an extra confirmation step. +4. **Given** a file edited only in the shared copy (agent's working copy untouched), **When** Sync runs after confirmation, **Then** the file is NOT overwritten (delta behavior preserved); it MAY appear in the at-risk warning, because the platform cannot see which files the agent changed — a false positive is acceptable, silent loss is not. --- @@ -84,7 +84,7 @@ An operator asks the admin agent to "create an agent and bind a knowledge base". ### Functional Requirements - **FR-001**: The platform MUST record, per agent and using its own clock domain, when the running agent last took files from the shared copy (agent start) and when the last Sync completed; the running agent's clock is not part of the comparison. -- **FR-002**: Before executing Sync, the system MUST identify shared-copy files that changed after the moment from FR-001 and that Sync would overwrite or remove. +- **FR-002**: Before executing Sync, the system MUST identify shared-copy files that changed after the moment from FR-001 (files *at risk* of being overwritten or removed; false positives are acceptable, missed files are not). - **FR-003**: When FR-002 finds at least one file at risk, the system MUST present the list to the operator and proceed only after explicit confirmation; cancelling MUST leave the shared copy untouched. - **FR-004**: When FR-002 finds nothing at risk, Sync MUST proceed without additional friction. - **FR-005**: Sync MUST continue to send only files actually changed in the agent's working copy and MUST NOT touch shared-copy files the agent never modified (preserve verified delta semantics). From eb3d93c406cf9a86f1d51bcfaec60bbed477691b Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Mon, 31 Aug 2026 16:26:06 +0300 Subject: [PATCH 3/5] docs(specs): dependency-ordered tasks for safe sync feature (CLEAN-50) Co-Authored-By: Claude Opus 4.7 --- specs/008-agent-files-sync-safety/tasks.md | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 specs/008-agent-files-sync-safety/tasks.md diff --git a/specs/008-agent-files-sync-safety/tasks.md b/specs/008-agent-files-sync-safety/tasks.md new file mode 100644 index 00000000..e4442bfd --- /dev/null +++ b/specs/008-agent-files-sync-safety/tasks.md @@ -0,0 +1,145 @@ +# Tasks: Agent Files — Visible Copy Model & Safe Sync + +**Input**: Design documents from `/specs/008-agent-files-sync-safety/` + +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/sync-api.md, quickstart.md + +**Tests**: unit tests for the guard logic are REQUIRED (quickstart.md "Unit tests" section); no other test tasks. + +**Organization**: phases map 1:1 to Jira subtasks — US1=CLEAN-52 (P1), US2=CLEAN-53 (P2), US3=CLEAN-54 (P3). Parent CLEAN-50. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: US1 / US2 / US3 per spec.md + +## Phase 1: Setup + +**Purpose**: schema groundwork every story reads from + +- [ ] T001 Add `lastPullAt DateTime?` and `lastSyncAt DateTime?` to the Agent model in api/src/slices/agent/agent/agent.prisma and create migration `agent-sync-markers` (`cd api && bun run prisma migrate dev --name agent-sync-markers` or repo's migrate script) + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: markers must be persisted before any guard/UI can use them + +**⚠️ CRITICAL**: blocks US1 and US2 (US3 is independent but sequenced last by priority) + +- [ ] T002 Add marker update methods (`setLastPullAt`, `setLastSyncAt`) to api/src/slices/agent/agent/data/agent.gateway.ts (near `updateStatus`, agent.gateway.ts:91-111) +- [ ] T003 [P] Persist `lastPullAt` on agent socket connect: hook the `connected` event path in api/src/slices/agent/agent/domain/agentStatus.service.ts (consumer of `agentEvents$` from bridle.gateway.ts:75) +- [ ] T004 [P] Persist `lastSyncAt` on `sync_done`: call the marker method from `handleSyncResponse` in api/src/slices/bridle/data/bridle.gateway.ts:317-332 (after `pendingSyncs.delete`, before resolve) + +**Checkpoint**: restart a dev agent → `lastPullAt` set; trigger sync → `lastSyncAt` set (verify in DB) + +--- + +## Phase 3: User Story 1 - Sync warns before destroying newer shared edits (Priority: P1) 🎯 MVP — CLEAN-52 + +**Goal**: no silent overwrite/removal of S3 files newer than the pod's last pull/push; warn-and-confirm flow; zero friction when nothing is at risk + +**Independent Test**: quickstart E2E scenarios 1-2 + legacy check (edit file → Sync → dialog lists it → cancel keeps edit / confirm proceeds; no dialog when clean or after restart) + +### Tests for User Story 1 (required by quickstart) + +- [ ] T005 [US1] Write failing jest specs for the guard in api/src/slices/agent/file/domain/syncGuard.service.spec.ts: baseline = max(lastSyncAt, lastPullAt − PULL_MARGIN); null-marker cases (skip check); at-risk filtering by S3 `updatedAt > baseline`; empty-list pass-through + +### Implementation for User Story 1 + +- [ ] T006 [US1] Implement guard domain service in api/src/slices/agent/file/domain/syncGuard.service.ts (PULL_MARGIN=60s const; inputs: agent markers + file list from existing file.gateway `list()`; output: `{baseline, atRisk[]}`); make T005 pass +- [ ] T007 [US1] Extend sync endpoint in api/src/slices/agent/file/file.controller.ts:102-112: optional body DTO `{confirm?: boolean}`; when baseline exists, atRisk non-empty and !confirm → HTTP 409 `{requiresConfirmation, atRisk[{path,updatedAt}], baseline}` with NO sync side effects; else current flow (contracts/sync-api.md) +- [ ] T008 [US1] Regenerate OpenAPI after DTO changes: `cd api && bun run generate:swagger` (build first if needed), then `cd admin && bun run build:api` +- [ ] T009 [US1] Pass `confirm` through admin gateway in admin/slices/agent/file/data/agentFile.gateway.ts:76-80 and type the 409 payload +- [ ] T010 [US1] Wire confirm flow in admin/slices/agent/file/components/agentFile/Provider.vue:63-90: catch 409 → `useConfirmStore().ask()` (admin/slices/common/stores/confirm.ts) listing atRisk paths + timestamps → confirm resends `{confirm:true}`, cancel does nothing; English copy +- [ ] T011 [US1] Validate quickstart E2E scenarios 1-2 + legacy agent check on dev installation; comment results on CLEAN-52 + +**Checkpoint**: US1 fully functional — MVP shippable + +--- + +## Phase 4: User Story 2 - The two-copy model is visible in the Files tab (Priority: P2) — CLEAN-53 + +**Goal**: operator can tell which copy they see, that a running pod may hold newer content, and how fresh each file is + +**Independent Test**: quickstart E2E scenario 3 (banner for Running agent with markers + Sync CTA; no banner when stopped; per-file last-modified visible) + +### Implementation for User Story 2 + +- [ ] T012 [US2] Expose `lastPullAt`/`lastSyncAt` in the agent response DTO (api/src/slices/agent/agent — controller/DTO layer next to agent.gateway.ts), then regenerate OpenAPI (same commands as T008) +- [ ] T013 [US2] Add Running-state banner to admin/slices/agent/file/components/agentFile/Provider.vue: shared-copy explanation, `lastPullAt`/`lastSyncAt` when present, Sync CTA; hidden for non-running agents; English copy (contracts/sync-api.md "UI contract") +- [ ] T014 [US2] Display per-file `updatedAt` (already returned by list endpoint) in the file list/details UI in admin/slices/agent/file/components/agentFile/Provider.vue +- [ ] T015 [US2] Validate quickstart E2E scenario 3; comment results on CLEAN-53 + +**Checkpoint**: US1 + US2 independently functional + +--- + +## Phase 5: User Story 3 - Admin agent honesty + restart surfacing (Priority: P3) — CLEAN-54 + +**Goal**: rancher never narrates actions it cannot perform; file writes always surface the restart requirement + +**Independent Test**: quickstart E2E scenario 4 (ask to create agent/bind knowledge base → honest limitation + manual path; file write reply mentions restart; deployed rancher runs the new SOUL.md) + +### Implementation for User Story 3 + +- [ ] T016 [P] [US3] Add honesty constraints to rancher/.agent/SOUL.md: no claiming create-agent / knowledge-binding ability (real tools arrive with CLEAN-51); state limitation + manual path; always surface restart after file writes +- [ ] T017 [P] [US3] Append explicit "Restart required — offer restart_agent" to the `write_agent_file` tool RESULT text in api/src/slices/rancher/rancher.tool.ts:476-502 +- [ ] T018 [US3] Implement SOUL.md propagation for deployed rancher agents: inspect seed flow in api/src/slices/rancher/rancher.service.ts (`seedTemplateFiles`), deliver update via reseed/write + rancher restart; document the chosen path in specs/008-agent-files-sync-safety/research.md (R6 follow-up) +- [ ] T019 [US3] Validate quickstart E2E scenario 4; comment results on CLEAN-54 + +**Checkpoint**: all three stories independently functional + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +- [ ] T020 Run full verification: `cd api && bun run test` green; `tsc --noEmit` on touched api TS (repo has no typecheck script — see quickstart); admin build passes (`cd admin && bun run build`) +- [ ] T021 Full quickstart.md pass end-to-end; open PR into `main` titled with CLEAN-50, link PR on CLEAN-50, move subtasks per board flow + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (T001)**: no dependencies — start immediately +- **Phase 2 (T002-T004)**: needs T001; T003 and T004 parallel after T002 +- **US1 (T005-T011)**: needs Phase 2 (guard reads markers). Internal: T005 → T006 → T007 → T008 → T009 → T010 → T011 +- **US2 (T012-T015)**: needs Phase 2 only (markers in DB). T012 → T013 → T014 → T015; can run in parallel with US1 by a second developer (different endpoints; both touch Provider.vue — coordinate T010/T013) +- **US3 (T016-T019)**: independent of all other phases (different slice). T016 ∥ T017 → T018 → T019 +- **Phase 6**: after all desired stories + +### Parallel Opportunities + +- T003 ∥ T004 (different files, both after T002) +- T016 ∥ T017 (SOUL.md vs rancher.tool.ts) +- US3 entirely parallel to US1/US2 (no shared files) +- US2's T012 parallel to US1's T005-T007 (different api files); Provider.vue tasks (T010, T013, T014) must be sequential + +### Parallel Example: after Phase 2 + +```bash +# Developer A (MVP): T005 → T006 → T007 → T008 → T009 → T010 → T011 +# Developer B: T012 (api DTO), then waits for A's T010 before touching Provider.vue (T013-T014) +# Developer C: T016 ∥ T017 → T018 → T019 +``` + +--- + +## Implementation Strategy + +**MVP first (US1 / CLEAN-52)**: T001 → T002 → T003+T004 → T005-T011 → validate → this alone stops the data loss and is shippable. + +**Incremental delivery**: each story ends with a quickstart validation task and a Jira comment on its subtask; PR can ship after US1 if needed (US2/US3 as follow-up commits on the same branch per current plan — single PR into main for CLEAN-50). + +**Solo execution order**: strictly T001 → T021 by number. + +--- + +## Notes + +- Commit after each task or logical group with `CLEAN-50` in the subject (Conventional Commits) +- Jira: move CLEAN-53/CLEAN-54 to In Progress when their phase starts; comment checkpoints (T011, T015, T019) +- Admin UI copy is English-only; no i18n sync needed +- Runtime repo is intentionally untouched — if any task seems to need it, re-read research.md R2/R3 first From 2af873b95974b946aafedf89d299f72ec51fe972 Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Mon, 31 Aug 2026 17:17:17 +0300 Subject: [PATCH 4/5] feat(admin,api): sync-conflict guard, copy-model visibility, rancher honesty (CLEAN-50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync could silently overwrite S3 files edited after the pod's boot pull (delta push never checks S3 freshness). The API now records lastPullAt / lastSyncAt markers and answers 409 with an at-risk file list unless the operator confirms; the Files tab explains the S3-vs-pod copy model and surfaces per-file freshness; rancher's SOUL.md gets exact create/bind recipes and write_agent_file результат reminds about the required restart. Co-Authored-By: Claude Opus 4.7 --- admin/slices/agent/agent/data/agent.mapper.ts | 2 + .../slices/agent/agent/domain/agent.types.ts | 5 ++ .../file/components/agentFile/Provider.vue | 86 ++++++++++++++++-- .../agent/file/components/agentFile/Tree.vue | 2 + .../file/components/agentFile/TreeNode.vue | 11 +++ .../agent/file/data/agentFile.gateway.ts | 31 ++++++- .../agent/file/domain/agentFile.gateway.ts | 9 +- .../agent/file/domain/agentFile.service.ts | 6 +- .../agent/file/domain/agentFile.types.ts | 18 ++++ admin/slices/agent/file/stores/agentFile.ts | 4 +- .../api/data/repositories/api/schemas.gen.ts | 67 ++++++++++++++ .../api/data/repositories/api/sdk.gen.ts | 9 +- .../api/data/repositories/api/types.gen.ts | 50 ++++++++++- .../migration.sql | 3 + api/src/slices/agent/agent/agent.prisma | 8 ++ .../slices/agent/agent/data/agent.gateway.ts | 15 ++++ .../slices/agent/agent/data/agent.mapper.ts | 2 + .../agent/agent/domain/agent.gateway.ts | 6 ++ .../slices/agent/agent/domain/agent.types.ts | 2 + .../agent/agent/domain/agentStatus.service.ts | 19 ++++ api/src/slices/agent/agent/dtos/agent.dto.ts | 19 ++++ api/src/slices/agent/file/domain/index.ts | 1 + .../file/domain/syncGuard.service.spec.ts | 88 +++++++++++++++++++ .../agent/file/domain/syncGuard.service.ts | 56 ++++++++++++ .../slices/agent/file/dtos/syncFiles.dto.ts | 50 ++++++++++- api/src/slices/agent/file/file.controller.ts | 65 ++++++++++++-- api/src/slices/agent/file/file.module.ts | 2 + api/src/slices/rancher/rancher.tool.ts | 13 ++- rancher/.agent/SOUL.md | 40 ++++++++- .../008-agent-files-sync-safety/quickstart.md | 4 +- specs/008-agent-files-sync-safety/research.md | 8 +- specs/008-agent-files-sync-safety/spec.md | 2 +- specs/008-agent-files-sync-safety/tasks.md | 36 ++++---- 33 files changed, 685 insertions(+), 54 deletions(-) create mode 100644 api/prisma/migrations/20260831120000_agent_sync_markers/migration.sql create mode 100644 api/src/slices/agent/file/domain/syncGuard.service.spec.ts create mode 100644 api/src/slices/agent/file/domain/syncGuard.service.ts diff --git a/admin/slices/agent/agent/data/agent.mapper.ts b/admin/slices/agent/agent/data/agent.mapper.ts index 011079a9..87a63699 100644 --- a/admin/slices/agent/agent/data/agent.mapper.ts +++ b/admin/slices/agent/agent/data/agent.mapper.ts @@ -61,6 +61,8 @@ export class AgentMapper { firstDeployedAt: typeof o.firstDeployedAt === 'string' ? o.firstDeployedAt : null, launchContext: this.toLaunchContext(o.launchContext), + lastPullAt: typeof o.lastPullAt === 'string' ? o.lastPullAt : null, + lastSyncAt: typeof o.lastSyncAt === 'string' ? o.lastSyncAt : null, config: o.config && typeof o.config === 'object' ? (o.config as Record) diff --git a/admin/slices/agent/agent/domain/agent.types.ts b/admin/slices/agent/agent/domain/agent.types.ts index d095de28..36abf26b 100644 --- a/admin/slices/agent/agent/domain/agent.types.ts +++ b/admin/slices/agent/agent/domain/agent.types.ts @@ -68,6 +68,11 @@ export interface IAgentData { /** Null ⇒ the agent has never been deployed. */ firstDeployedAt: string | null; launchContext: LaunchContextTypes | null; + /** When the running pod last pulled its file working copy from S3 (at + * boot). Null ⇒ not restarted since the field shipped. */ + lastPullAt: string | null; + /** When the last successful Sync push completed. */ + lastSyncAt: string | null; config: Record; resources: IAgentResources; isPublic: boolean; diff --git a/admin/slices/agent/file/components/agentFile/Provider.vue b/admin/slices/agent/file/components/agentFile/Provider.vue index f1d97627..5e4ad1d3 100644 --- a/admin/slices/agent/file/components/agentFile/Provider.vue +++ b/admin/slices/agent/file/components/agentFile/Provider.vue @@ -11,10 +11,12 @@ import { IconAlertTriangle, IconDownload, IconFiles, + IconInfoCircle, IconRefresh, IconX, } from '@tabler/icons-vue'; import { until } from '@vueuse/core'; +import type { IAgentData } from '#agent/domain'; import AgentFileTree from './Tree.vue'; import AgentFileViewer from './Viewer.vue'; @@ -24,6 +26,27 @@ const store = useAgentFileStore(); const agentStore = useAgentStore(); const confirmStore = useConfirmStore(); +// The two-copy model hint (CLEAN-50): while the agent is Running, this tab +// shows the S3 copy but the pod works on its own — surface that instead of +// letting the operator wonder why a chat-driven change is not visible. +const agent = ref(null); +const showCopyHint = computed(() => agent.value?.status === 'running'); + +function formatMoment(iso: string | null): string | null { + if (!iso) return null; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? null : d.toLocaleString(); +} + +const copyHintDetail = computed(() => { + const pulled = formatMoment(agent.value?.lastPullAt ?? null); + const synced = formatMoment(agent.value?.lastSyncAt ?? null); + const parts: string[] = []; + if (pulled) parts.push(`agent took its copy ${pulled}`); + if (synced) parts.push(`last sync ${synced}`); + return parts.length ? ` (${parts.join(', ')})` : ''; +}); + const syncing = ref(false); const syncError = ref(null); const syncMessage = ref(null); @@ -60,17 +83,51 @@ const sheetOpen = ref(false); const dirty = computed(() => content.value !== original.value); const pendingRestart = computed(() => store.isPendingRestart(props.id)); +// Compact one-line summary for the confirm dialog (renders as plain text). +function describeAtRisk(files: { path: string }[]): string { + const MAX_LISTED = 8; + const listed = files + .slice(0, MAX_LISTED) + .map((f) => f.path) + .join(', '); + const rest = files.length - MAX_LISTED; + return rest > 0 ? `${listed} and ${rest} more` : listed; +} + async function onSync() { syncing.value = true; syncError.value = null; syncMessage.value = null; let agentOnline = false; try { - const result = await store.sync(props.id); - agentOnline = result.agentOnline; - syncMessage.value = result.agentOnline - ? `Agent pushed ${result.pushed} file${result.pushed === 1 ? '' : 's'}` - : 'Agent is offline — files are still up to date in S3'; + let outcome = await store.sync(props.id); + if (outcome.status === 'conflict') { + const { atRisk } = outcome.conflict; + const ok = await confirmStore.ask({ + title: 'Overwrite newer files in S3?', + description: + `${atRisk.length} file${atRisk.length === 1 ? ' was' : 's were'} ` + + 'edited in S3 after the running agent last took its copy: ' + + `${describeAtRisk(atRisk)}. ` + + 'If the agent also changed them, Sync will overwrite the S3 ' + + 'version with the agent’s copy. Files changed only in S3 are safe.', + confirmLabel: 'Sync anyway', + cancelLabel: 'Cancel', + variant: 'destructive', + }); + if (!ok) { + syncing.value = false; + return; + } + outcome = await store.sync(props.id, true); + } + if (outcome.status === 'done') { + const result = outcome.result; + agentOnline = result.agentOnline; + syncMessage.value = result.agentOnline + ? `Agent pushed ${result.pushed} file${result.pushed === 1 ? '' : 's'}` + : 'Agent is offline — files are still up to date in S3'; + } } catch (err) { syncError.value = (err as Error).message || 'Sync failed'; } @@ -240,7 +297,12 @@ async function onDownload() { useAsyncData( `admin-agent-files-${props.id}`, async () => { - await store.fetchList(props.id); + const [agentData] = await Promise.all([ + // Hint-only: a failed agent fetch must not break the file browser. + agentStore.fetchById(props.id).catch(() => null), + store.fetchList(props.id), + ]); + agent.value = agentData; return true; }, { lazy: true }, @@ -249,6 +311,18 @@ useAsyncData(