Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateAgenstraAttachments1791900000000 implements MigrationInterface {
name = 'CreateAgenstraAttachments1791900000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "agenstra_attachments" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"client_id" uuid NOT NULL,
"owner_type" character varying(32) NOT NULL,
"owner_id" uuid NOT NULL,
"file_name" character varying(255) NOT NULL,
"content_type" character varying(255) NOT NULL,
"byte_size" bigint NOT NULL,
"storage_key" character varying(512) NOT NULL,
"content_sha256" character varying(64) NOT NULL,
"created_by_user_id" uuid,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "PK_agenstra_attachments" PRIMARY KEY ("id"),
CONSTRAINT "FK_agenstra_attachments_client" FOREIGN KEY ("client_id") REFERENCES "clients"("id") ON DELETE CASCADE,
CONSTRAINT "FK_agenstra_attachments_user" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL
)
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_agenstra_attachments_client" ON "agenstra_attachments" ("client_id")`,
);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_agenstra_attachments_owner" ON "agenstra_attachments" ("owner_type", "owner_id")`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "agenstra_attachments"`);
}
}
2 changes: 2 additions & 0 deletions apps/agenstra/backend-agent-controller/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
TicketAutomationRunStepEntity,
TicketAutomationEntity,
ChatPlanEntity,
AgenstraAttachmentEntity,
TicketBodyGenerationSessionEntity,
TicketCommentEntity,
TicketEntity,
Expand Down Expand Up @@ -105,6 +106,7 @@ export const typeormConfig: DataSourceOptions = {
TicketAutomationLeaseEntity,
TicketAutomationRunStepEntity,
ChatPlanEntity,
AgenstraAttachmentEntity,
ClientAgentAutonomyEntity,
AtlassianSiteConnectionEntity,
ExternalImportConfigEntity,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddAgentMessageAttachments1791800000000 implements MigrationInterface {
name = 'AddAgentMessageAttachments1791800000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "agent_messages" ADD COLUMN IF NOT EXISTS "attachments" jsonb`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "agent_messages" DROP COLUMN IF EXISTS "attachments"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateProjectTicketAttachments1778200000000 implements MigrationInterface {
name = 'CreateProjectTicketAttachments1778200000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "billing_project_ticket_attachments" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"ticket_id" uuid NOT NULL,
"file_name" character varying(255) NOT NULL,
"content_type" character varying(255) NOT NULL,
"byte_size" bigint NOT NULL,
"storage_key" character varying(512) NOT NULL,
"stored_file_id" uuid,
"created_by_user_id" uuid,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "PK_billing_project_ticket_attachments" PRIMARY KEY ("id"),
CONSTRAINT "FK_billing_project_ticket_attachments_ticket" FOREIGN KEY ("ticket_id") REFERENCES "billing_project_tickets"("id") ON DELETE CASCADE,
CONSTRAINT "FK_billing_project_ticket_attachments_user" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL
)
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_billing_project_ticket_attachments_ticket" ON "billing_project_ticket_attachments" ("ticket_id")`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "billing_project_ticket_attachments"`);
}
}
2 changes: 2 additions & 0 deletions apps/decabill/backend-billing-manager/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
ProjectTicketActivityEntity,
ProjectTicketCommentEntity,
ProjectTicketEntity,
ProjectTicketAttachmentEntity,
ProjectTimeEntryEntity,
ProviderPriceSnapshotEntity,
ReservedHostnameEntity,
Expand Down Expand Up @@ -144,6 +145,7 @@ export const typeormConfig: DataSourceOptions = {
ProjectEntity,
ProjectMilestoneEntity,
ProjectTicketEntity,
ProjectTicketAttachmentEntity,
ProjectTicketCommentEntity,
ProjectTicketActivityEntity,
ProjectTimeEntryEntity,
Expand Down
1 change: 1 addition & 0 deletions docs/agenstra/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Agenstra provides a complete set of features for managing distributed AI agent i
- **Version Control** Git operations directly from the web interface
- **Web IDE** Monaco Editor integration for code editing
- **Chat Interface** AI chat functionality with real-time responses
- **[File attachments](./file-attachments.md)** Attach/download files on tickets, knowledge pages, and chat context with read-only agent mirroring
- **[Chat plan mode](./chat-plan-mode.md)** Explore-only OpenCode planning with refine and execute-into-chat
- **Deployment** CI/CD pipeline management and deployment functionality
- **Authentication** Multiple authentication methods with configurable user registration
Expand Down
19 changes: 19 additions & 0 deletions docs/agenstra/features/file-attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# File attachments

Agenstra supports attachments on tickets, knowledge pages, and chat prompts.

## Supported objects

- Tickets: attachments appear below the ticket description and are returned in ticket list/detail DTOs.
- Knowledge pages: attachments appear below the page description and are returned in page/tree DTOs.
- Chat prompts and plan refinement: files can be uploaded from the context picker or dropped on the chat input. The message stores attachment metadata and renders a small downloadable card below the user message.

## Storage and agent context

Attachment bytes are stored with the shared file-storage abstraction under the `agenstraAttachments` scope. The database stores only metadata and a generated storage key; user supplied file names are sanitized and never used as path roots.

When a chat prompt references tickets or pages, their attachments are resolved during context enrichment. Explicit chat attachments are resolved the same way. Resolved files are copied to each selected agent container under `/opt/agenstra/context/...`; the manager writes those files as root and hardens them to be readable but not writable by the runtime user.

## Security

All upload, list, and download operations resolve the owning workspace before file access. Downloads require the attachment metadata row and workspace access, so raw storage keys are not exposed as authorization credentials.
1 change: 1 addition & 0 deletions docs/decabill/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Decabill provides a complete set of capabilities for subscription billing, invoi
- **CloudInit Configs** Admin-managed Docker templates for the custom service kind
- **Projects** Customer-assigned work tracking with admin CRUD and billable time
- **Project Board** Live ticket board with swimlanes and WebSocket updates
- **[Ticket attachments](./ticket-attachments.md)** Upload, list, download, and file-explorer integration for project ticket files
- **OpenTelemetry** Optional Prometheus metrics and OTLP export (disabled by default)

## Features
Expand Down
14 changes: 14 additions & 0 deletions docs/decabill/features/ticket-attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Project ticket attachments

Decabill project tickets support file attachments below the ticket description.

Attachments are stored through the shared file-storage abstraction in the `projectTicketAttachments` scope and registered in the existing `billing_stored_files` registry, including content hashes and signatures when signing is enabled. Files are visible in the admin File Explorer under `project/tickets/attachments/...`.

## Security

Attachment endpoints require project access in addition to PAT scopes:

- `attachments:read` for listing/downloading.
- `attachments:write` for uploading/deleting metadata.

The API always resolves the project and ticket first; download access is never granted by storage key alone.
Loading
Loading