Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 428c70beaa | |||
| 1c6dbcede8 | |||
| 3e59a257dc | |||
| 93da9b42a2 | |||
| 0dd5f2317b | |||
| ab93863ea9 | |||
| 85627793b4 | |||
| 8b6a4f7712 | |||
| bf698b3ff2 | |||
| d5119f29f6 | |||
| 563ff13969 | |||
| 39bac24100 | |||
| 889c598129 | |||
| ac56c5e391 | |||
| 17f4e91a84 | |||
| b3f47310c3 | |||
| 972dae64f2 | |||
| cbd5533adf | |||
| 6e419f815d | |||
| 4f4939406f | |||
| f47cf27720 | |||
| 6559c2a1d9 | |||
| 5d5b39bec1 | |||
| 38d116dcf7 | |||
| da3f8da3ac | |||
| 454a7845bb | |||
| 50e3af0d91 | |||
| 8e1af41cd3 | |||
| 355b471067 | |||
| a2f0847aad | |||
| 82ff8b8801 | |||
| a6ad8268b9 | |||
| 3358af57c2 | |||
| 6a61669294 | |||
| 29e48cdb65 | |||
| ed1f7805f6 | |||
| 1d345eba32 | |||
| e35e605914 | |||
| 94137c6586 | |||
| 1658767cf4 | |||
| 83928d0f02 | |||
| 8f648264f1 | |||
| b26ed7c3e4 | |||
| 6e33e8e4e9 | |||
| f0ae9483f3 | |||
| 90992e46a8 | |||
| 41f9427224 | |||
| 089d802f1d | |||
| a4e6c46a47 | |||
| 47de2a0133 | |||
| bd94cc9bbf | |||
| fe82a248ec | |||
| c8db6ce933 | |||
| 896674195c | |||
| ddd92e3dd4 | |||
| b4203a4a09 | |||
| 5fc8e035e6 | |||
| e6114ed18c | |||
| ac9f7a9299 | |||
| 84cf423684 | |||
| 4c14966ae3 | |||
| 6c31ef9577 | |||
| a12d6a8169 | |||
| 13b3c60abc | |||
| 23fc0a6b82 | |||
| 8b15689fb3 | |||
| 98d4393387 | |||
| 35f0a3ea2e | |||
| 1ae8d0e45f | |||
| 025ccc5817 | |||
| 350b393457 | |||
| 8f7f682a92 | |||
| 2ece7074f6 | |||
| c7ab2de25e | |||
| c9b0259993 | |||
| 1f2c3dbe2a | |||
| d247e44985 | |||
| d60dec9d62 | |||
| 4913cc7297 | |||
| 86360661e9 | |||
| 1d09193652 | |||
| 4ced6141ae | |||
| 530225d37c | |||
| e6ab77d123 | |||
| 4e1477c4be | |||
| 2b5b8363ca | |||
| 1e0f95f8bd | |||
| 6a20c54da7 | |||
| db6ae38568 | |||
| 1e43ea48aa | |||
| efe5e4ae50 | |||
| 31f1ce00d3 | |||
| d395aaf574 | |||
| d78ca8a9d5 | |||
| 19291d6be9 | |||
| 5409ba2b13 | |||
| 8bde9a213c | |||
| 315cb33e3f | |||
| 474843ffa6 | |||
| 766f7ff0d1 | |||
| f8b162ec5f | |||
| 8a8a9bc9e4 | |||
| 61f9c52455 | |||
| 43d65840ea | |||
| ad5a4b5000 | |||
| 73088b75ba | |||
| f23fadf52b | |||
| 9101033019 | |||
| 72bf4ed962 | |||
| 3b772ac239 | |||
| 8c1948d226 | |||
| 43f3865f69 | |||
| 7ef8b0eb36 | |||
| c6073fe9d2 | |||
| 81b9a66ef5 | |||
| 79be4eb525 | |||
| 04225ef890 | |||
| 7f78d92fef | |||
| bbfcde3d1b | |||
| 9f4e9845c0 | |||
| ce8b5dc86d | |||
| efb62fe41a | |||
| c26e9eacfa | |||
| aa49efb236 | |||
| 6b947b7593 | |||
| 3da7ea6408 | |||
| 79aabd6f43 | |||
| 40ac931c65 | |||
| aa51ba219b | |||
| 7623f29ffb | |||
| 3979b1f4e7 | |||
| caadd59441 | |||
| 30549f4863 | |||
| 070cd4d5a5 | |||
| 44ef62271e | |||
| 7440720b7b | |||
| 110844e597 | |||
| b4d08b0232 | |||
| f1180f6053 | |||
| 59bec31046 | |||
| 8a7bec8df2 | |||
| c881bbdad3 | |||
| f1b968bb88 | |||
| 3c222d4f0f | |||
| 1d10283fc9 | |||
| 886be83af5 | |||
| 82091e31a8 | |||
| b2c84e2064 | |||
| 680417a0a2 | |||
| e5e29aca49 | |||
| 486f3cbc44 | |||
| 152f87a254 | |||
| 349066bcfa | |||
| c353bceb97 | |||
| 64dcdc9d0d | |||
| 4ea2e3659d | |||
| 734bd9529a | |||
| 1ef9d66eed | |||
| 2169b24875 | |||
| f2a3399f27 | |||
| 5266e64be2 | |||
| 9a17916dd2 | |||
| a388a8bec9 | |||
| 51a98a0c63 | |||
| 6efe524974 | |||
| 88c56a83b7 | |||
| b4aa4c5fcb | |||
| 183e910afd | |||
| d80ee4157c | |||
| d8ab7734cb | |||
| de6a6a3b00 | |||
| 9503f6cb4f | |||
| 6b118307eb | |||
| 070e960c05 | |||
| 96ce3f4c53 | |||
| e49d049455 | |||
| e7f219f7c3 | |||
| d7d5baa41a | |||
| 61072f4c07 | |||
| 7070867393 | |||
| d472c41092 | |||
| a9e2dd3552 | |||
| 6553a8845b | |||
| 994b1cf3b7 | |||
| 6aea83bf17 | |||
| 8d51877afa |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"fingerprint": "c36b11ec5edebc02aa51b1113a7a11dc2559e812"
|
||||
"fingerprint": "639c16d45210921c3c8ece071ef18bbe0c426ea2"
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
# Skill Registry — headquarter
|
||||
# Skill Registry — workspace
|
||||
|
||||
<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->
|
||||
|
||||
Last updated: 2026-06-02
|
||||
Last updated: 2026-06-05
|
||||
|
||||
## Sources scanned
|
||||
|
||||
- .opencode/skills
|
||||
- .claude/skills
|
||||
- /home/alex/.config/opencode/skills
|
||||
|
||||
## Contract
|
||||
|
||||
@@ -20,12 +19,11 @@ Last updated: 2026-06-02
|
||||
|
||||
| Skill | Trigger / description | Scope | Path |
|
||||
| --- | --- | --- | --- |
|
||||
| `auto-commit` | Use when you are making multiple edits or completing significant work in a git repository to automatically create commits | user | `/home/alex/.config/opencode/skills/auto-commit/SKILL.md` |
|
||||
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-apply-change/SKILL.md` |
|
||||
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-archive-change/SKILL.md` |
|
||||
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-explore/SKILL.md` |
|
||||
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-propose/SKILL.md` |
|
||||
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/home/alex/projects/headquarter/.claude/skills/sift-backlog/SKILL.md` |
|
||||
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/workspace/.opencode/skills/openspec-apply-change/SKILL.md` |
|
||||
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/workspace/.opencode/skills/openspec-archive-change/SKILL.md` |
|
||||
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/workspace/.opencode/skills/openspec-explore/SKILL.md` |
|
||||
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/workspace/.opencode/skills/openspec-propose/SKILL.md` |
|
||||
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/workspace/.claude/skills/sift-backlog/SKILL.md` |
|
||||
|
||||
## Loading protocol
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# .claude (index)
|
||||
dir: .claude
|
||||
|
||||
## role
|
||||
Configuration directory for Claude AI assistant settings and custom instructions.
|
||||
## parent
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
## children
|
||||
- .claude/skills
|
||||
index: .claude/skills/.pi-map.index.md
|
||||
map: .claude/skills/.pi-map.md
|
||||
## files
|
||||
## links
|
||||
index: .claude/.pi-map.index.md
|
||||
map: .claude/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,18 @@
|
||||
# .claude
|
||||
dir: .claude
|
||||
|
||||
index: .claude/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Configuration directory for Claude AI assistant settings and custom instructions.
|
||||
## files
|
||||
## arch
|
||||
Simple flat-file configuration storage with no executable code, using human-readable text files for AI behavior customization.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,20 @@
|
||||
# .claude/skills (index)
|
||||
dir: .claude/skills
|
||||
|
||||
## role
|
||||
Contains skill definitions and configuration files for the Claude AI assistant to extend its capabilities with specialized behaviors and domain-specific expertise.
|
||||
## parent
|
||||
index: .claude/.pi-map.index.md
|
||||
map: .claude/.pi-map.md
|
||||
## children
|
||||
- .claude/skills/sift-backlog
|
||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
||||
map: .claude/skills/sift-backlog/.pi-map.md
|
||||
## files
|
||||
## links
|
||||
index: .claude/skills/.pi-map.index.md
|
||||
map: .claude/skills/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,18 @@
|
||||
# .claude/skills
|
||||
dir: .claude/skills
|
||||
|
||||
index: .claude/skills/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains skill definitions and configuration files for the Claude AI assistant to extend its capabilities with specialized behaviors and domain-specific expertise.
|
||||
## files
|
||||
## arch
|
||||
Declarative skill configuration system using structured metadata files (likely YAML/JSON) that define prompts, tools, and context boundaries for modular AI capability extensions.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .claude/skills/sift-backlog (index)
|
||||
dir: .claude/skills/sift-backlog
|
||||
|
||||
## role
|
||||
Provides a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
|
||||
## parent
|
||||
index: .claude/skills/.pi-map.index.md
|
||||
map: .claude/skills/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- SKILL.md
|
||||
## links
|
||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
||||
map: .claude/skills/sift-backlog/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .claude/skills/sift-backlog
|
||||
dir: .claude/skills/sift-backlog
|
||||
|
||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
|
||||
## files
|
||||
- SKILL.md | Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool. | dep: sf CLI (task, plan, dependency, update subcommands)
|
||||
## arch
|
||||
Skill-based workflow automation using markdown-defined CLI tooling and structured task management patterns.
|
||||
## tags
|
||||
skill, defines, workflow, triaging, organizing, activating, backlog, tasks
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -17,6 +17,7 @@ __pycache__/
|
||||
*.so
|
||||
.python-version
|
||||
.venv/
|
||||
.venv-test/
|
||||
venv/
|
||||
env/
|
||||
.pytest_cache/
|
||||
@@ -54,3 +55,6 @@ Thumbs.db
|
||||
.atl/
|
||||
.sisyphus/
|
||||
.pi-lens/
|
||||
minerv3/
|
||||
.cache/
|
||||
openspec-audit-report.md
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# .opencode (index)
|
||||
dir: .opencode
|
||||
|
||||
## role
|
||||
This appears to be a configuration or metadata directory for the OpenCode AI coding assistant tool.
|
||||
## parent
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
## children
|
||||
- .opencode/commands
|
||||
index: .opencode/commands/.pi-map.index.md
|
||||
map: .opencode/commands/.pi-map.md
|
||||
- .opencode/skills
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## files
|
||||
## links
|
||||
index: .opencode/.pi-map.index.md
|
||||
map: .opencode/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,18 @@
|
||||
# .opencode
|
||||
dir: .opencode
|
||||
|
||||
index: .opencode/.pi-map.index.md
|
||||
|
||||
## role
|
||||
This appears to be a configuration or metadata directory for the OpenCode AI coding assistant tool.
|
||||
## files
|
||||
## arch
|
||||
Hidden directory storing tool-specific state; no discernible software architecture as it contains no source files.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,22 @@
|
||||
# .opencode/commands (index)
|
||||
dir: .opencode/commands
|
||||
|
||||
## role
|
||||
Defines AI assistant command workflows for an OpenSpec-based development system, covering task execution, change archival, exploratory analysis, and new change proposal generation.
|
||||
## parent
|
||||
index: .opencode/.pi-map.index.md
|
||||
map: .opencode/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- opsx-apply.md
|
||||
- opsx-archive.md
|
||||
- opsx-explore.md
|
||||
- opsx-propose.md
|
||||
## links
|
||||
index: .opencode/commands/.pi-map.index.md
|
||||
map: .opencode/commands/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,22 @@
|
||||
# .opencode/commands
|
||||
dir: .opencode/commands
|
||||
|
||||
index: .opencode/commands/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Defines AI assistant command workflows for an OpenSpec-based development system, covering task execution, change archival, exploratory analysis, and new change proposal generation.
|
||||
## files
|
||||
- opsx-apply.md | Implement tasks from an OpenSpec change by selecting a change, reading context files, and executing pending tasks in a loop until completion or blocked. | dep: openspec CLI, AskUserQuestion tool, filesystem access
|
||||
- opsx-archive.md | Defines a workflow for archiving completed changes in an experimental openspec-based development system, including validation, spec sync assessment, and user confirmation steps. | dep: openspec CLI, AskUserQuestion tool, Task tool, Skill tool, filesystem operations
|
||||
- opsx-explore.md | Defines the "explore mode" stance and guidelines for an AI assistant to think through ideas, investigate problems, and clarify requirements without implementing code. | dep: OpenSpec system, ASCII diagrams, codebase investigation tools
|
||||
- opsx-propose.md | Defines an AI assistant workflow for proposing new changes by creating a scaffolded change directory and generating all required artifacts (proposal.md, design.md, tasks.md) in dependency order using the openspec CLI tool. | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool
|
||||
## arch
|
||||
Markdown-based command specifications using a structured workflow pattern with sequential steps, file I/O operations, CLI tool integration (openspec), and human-in-the-loop decision points for validation and confirmation.
|
||||
## tags
|
||||
opsx, tasks, openspec, change, openspec cli, askuserquestion tool, defines, explore
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,29 @@
|
||||
# .opencode/skills (index)
|
||||
dir: .opencode/skills
|
||||
|
||||
## role
|
||||
Contains custom AI agent skills or capabilities that extend the base functionality of the OpenCode framework with specialized tools and behaviors.
|
||||
## parent
|
||||
index: .opencode/.pi-map.index.md
|
||||
map: .opencode/.pi-map.md
|
||||
## children
|
||||
- .opencode/skills/openspec-apply-change
|
||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-apply-change/.pi-map.md
|
||||
- .opencode/skills/openspec-archive-change
|
||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-archive-change/.pi-map.md
|
||||
- .opencode/skills/openspec-explore
|
||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-explore/.pi-map.md
|
||||
- .opencode/skills/openspec-propose
|
||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-propose/.pi-map.md
|
||||
## files
|
||||
## links
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,18 @@
|
||||
# .opencode/skills
|
||||
dir: .opencode/skills
|
||||
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains custom AI agent skills or capabilities that extend the base functionality of the OpenCode framework with specialized tools and behaviors.
|
||||
## files
|
||||
## arch
|
||||
Modular skill plugin architecture where individual capabilities are registered as discrete, composable units that can be dynamically loaded and invoked by the agent system.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-apply-change (index)
|
||||
dir: .opencode/skills/openspec-apply-change
|
||||
|
||||
## role
|
||||
Provides an AI assistant skill that automates implementation of OpenSpec specification changes through a structured, CLI-driven workflow.
|
||||
## parent
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- SKILL.md
|
||||
## links
|
||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-apply-change/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-apply-change
|
||||
dir: .opencode/skills/openspec-apply-change
|
||||
|
||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides an AI assistant skill that automates implementation of OpenSpec specification changes through a structured, CLI-driven workflow.
|
||||
## files
|
||||
- SKILL.md | Defines an AI assistant skill for implementing tasks from an OpenSpec change using a spec-driven workflow with the openspec CLI. | dep: openspec CLI, AskUserQuestion tool, filesystem access
|
||||
## arch
|
||||
Skill-based plugin architecture using markdown-defined capabilities with structured workflow steps (analyze, plan, implement, verify) and external CLI tool integration.
|
||||
## tags
|
||||
skill, openspec, defines, assistant, implementing, tasks, change, spec
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-archive-change (index)
|
||||
dir: .opencode/skills/openspec-archive-change
|
||||
|
||||
## role
|
||||
Defines an automation skill for archiving completed changes in an experimental workflow using the openspec CLI.
|
||||
## parent
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- SKILL.md
|
||||
## links
|
||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-archive-change/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-archive-change
|
||||
dir: .opencode/skills/openspec-archive-change
|
||||
|
||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Defines an automation skill for archiving completed changes in an experimental workflow using the openspec CLI.
|
||||
## files
|
||||
- SKILL.md | Defines a skill for archiving completed changes in an experimental workflow using the openspec CLI. | dep: openspec CLI, AskUserQuestion tool, Task tool, Skill tool, openspec-sync-specs skill
|
||||
## arch
|
||||
Skill-based plugin architecture following a declarative markdown skill definition pattern for CLI workflow automation.
|
||||
## tags
|
||||
skill, openspec, defines, archiving, completed, changes, experimental, workflow
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-explore (index)
|
||||
dir: .opencode/skills/openspec-explore
|
||||
|
||||
## role
|
||||
Provides a conversational exploration skill for OpenSpec CLI that enables non-implementation ideation, problem investigation, and requirements clarification.
|
||||
## parent
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- SKILL.md
|
||||
## links
|
||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-explore/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-explore
|
||||
dir: .opencode/skills/openspec-explore
|
||||
|
||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides a conversational exploration skill for OpenSpec CLI that enables non-implementation ideation, problem investigation, and requirements clarification.
|
||||
## files
|
||||
- SKILL.md | Defines a conversational "explore mode" skill for OpenSpec CLI that acts as a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code. | dep: openspec CLI
|
||||
## arch
|
||||
Skill-based plugin architecture using markdown-defined behavioral specifications with conversational AI patterns, separating exploration/thinking workflows from implementation tasks.
|
||||
## tags
|
||||
skill, defines, conversational, explore, mode, openspec, cli, acts
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-propose (index)
|
||||
dir: .opencode/skills/openspec-propose
|
||||
|
||||
## role
|
||||
Automates the creation of structured software change proposals through a scaffolded directory workflow using the openspec CLI.
|
||||
## parent
|
||||
index: .opencode/skills/.pi-map.index.md
|
||||
map: .opencode/skills/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- SKILL.md
|
||||
## links
|
||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
||||
map: .opencode/skills/openspec-propose/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,19 @@
|
||||
# .opencode/skills/openspec-propose
|
||||
dir: .opencode/skills/openspec-propose
|
||||
|
||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Automates the creation of structured software change proposals through a scaffolded directory workflow using the openspec CLI.
|
||||
## files
|
||||
- SKILL.md | Defines an AI skill that automates proposing new software changes by scaffolding a directory structure and generating dependent artifacts (proposal, design, tasks) using the openspec CLI workflow. | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool
|
||||
## arch
|
||||
Template-driven skill architecture following a sequential artifact generation pattern (proposal → design → tasks) with CLI-based workflow automation.
|
||||
## tags
|
||||
skill, defines, automates, proposing, new, software, changes, scaffolding
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,79 @@
|
||||
# . (index)
|
||||
dir: .
|
||||
|
||||
## Project Map Protocol
|
||||
|
||||
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||
3. Load indexes before rich maps during task-start navigation.
|
||||
4. Read the local rich map and actual source before editing.
|
||||
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||
6. If source and generated artifacts disagree, trust source.
|
||||
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||
8. After editing source, run `project_map_patch` for each changed file.
|
||||
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
A self-hosted web platform for managing projects, git repositories, and development tools with OAuth2 authentication, built as a Dockerized multi-service application.
|
||||
## parent
|
||||
-
|
||||
## children
|
||||
- .atl
|
||||
index: .atl/.pi-map.index.md
|
||||
map: .atl/.pi-map.md
|
||||
- .claude
|
||||
index: .claude/.pi-map.index.md
|
||||
map: .claude/.pi-map.md
|
||||
- .opencode
|
||||
index: .opencode/.pi-map.index.md
|
||||
map: .opencode/.pi-map.md
|
||||
- .pi
|
||||
index: .pi/.pi-map.index.md
|
||||
map: .pi/.pi-map.md
|
||||
- .sisyphus
|
||||
index: .sisyphus/.pi-map.index.md
|
||||
map: .sisyphus/.pi-map.md
|
||||
- .stoneforge
|
||||
index: .stoneforge/.pi-map.index.md
|
||||
map: .stoneforge/.pi-map.md
|
||||
- apps
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
- docs
|
||||
index: docs/.pi-map.index.md
|
||||
map: docs/.pi-map.md
|
||||
- e2e
|
||||
index: e2e/.pi-map.index.md
|
||||
map: e2e/.pi-map.md
|
||||
- openspec
|
||||
index: openspec/.pi-map.index.md
|
||||
map: openspec/.pi-map.md
|
||||
- scripts
|
||||
index: scripts/.pi-map.index.md
|
||||
map: scripts/.pi-map.md
|
||||
- tool-images
|
||||
index: tool-images/.pi-map.index.md
|
||||
map: tool-images/.pi-map.md
|
||||
## files
|
||||
- .env.example
|
||||
- .gitignore
|
||||
- AGENTS.md
|
||||
- CHANGELOG.md
|
||||
- Makefile
|
||||
- README.md
|
||||
- docker-compose.traefik.yml
|
||||
- docker-compose.yml
|
||||
- progress.md
|
||||
- swap-pane
|
||||
- ui-audit-spacing-typography.md
|
||||
- ui-rework-foundations-apply.md
|
||||
- ui-rework-pass2-apply.md
|
||||
## links
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# .
|
||||
dir: .
|
||||
|
||||
index: ./.pi-map.index.md
|
||||
|
||||
## Project Map Protocol
|
||||
|
||||
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||
3. Load indexes before rich maps during task-start navigation.
|
||||
4. Read the local rich map and actual source before editing.
|
||||
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||
6. If source and generated artifacts disagree, trust source.
|
||||
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||
8. After editing source, run `project_map_patch` for each changed file.
|
||||
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||
|
||||
Trust boundary: index routes, map orients, source decides.
|
||||
|
||||
## role
|
||||
A self-hosted web platform for managing projects, git repositories, and development tools with OAuth2 authentication, built as a Dockerized multi-service application.
|
||||
## files
|
||||
- .env.example | Template environment configuration file defining all required and optional environment variables for a multi-service application stack
|
||||
- .gitignore | Specifies patterns for files and directories that Git should ignore in this project. | dep: git
|
||||
- AGENTS.md | Defines agent behavior rules, workflow procedures, and project conventions for AI agents working on an OpenSpec-driven codebase. | dep: OpenSpec, git, superpowers workflow system, conventional commits
|
||||
- CHANGELOG.md | Documents version history and notable changes to a project management and Git repository application
|
||||
- Makefile | Provides standard development commands for managing a Docker Compose-based multi-service application (API, web, Postgres, Redis) with testing, migration, linting, and build automation. | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm/node
|
||||
- README.md | A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication. | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik
|
||||
- docker-compose.traefik.yml | Deploys a multi-service web application stack (frontend, API, PostgreSQL, Redis) behind an existing Traefik reverse proxy with automatic HTTPS | dep: Docker, Docker Compose, Traefik, PostgreSQL, Redis, Authentik
|
||||
- docker-compose.yml | Defines a multi-container Docker application with PostgreSQL, Redis, API backend, and web frontend services for a "headquarter" application. | dep: Docker Compose, PostgreSQL 15, Redis 7, Python/FastAPI (implied by asyncpg), Vite/Node.js (implied by web build), Alpine Linux
|
||||
- progress.md | Documents the completed phases and remaining work of a backend-frontend code refactoring project involving modularization, subpackage extraction, and file reorganization.
|
||||
- swap-pane | Swaps two tmux panes between windows, preserving active pane state and layout | dep: tmux
|
||||
- ui-audit-spacing-typography.md | Documents a comprehensive UI audit of spacing, typography, and visual rhythm issues in a React web application, identifying missing CSS classes, unstyled mobile components, inconsistent design tokens, and layout bugs across component and stylesheet files. | dep: React/TSX components, CSS modules/stylesheets, CSS custom properties (tokens), mobile-specific components, design system tokens
|
||||
- ui-rework-foundations-apply.md | Documents the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, including token expansion, primitive CSS classes, component updates, and validation results. | dep: CSS design tokens, React/TSX components, OpenSpec documentation system, git/SSH, npm build toolchain
|
||||
- ui-rework-pass2-apply.md | Documents a completed web UI refactoring pass that converted inline styles to token-based utility classes, unified form patterns, and added component utilities across multiple React components. | dep: React, CSS custom properties, OpenSpec, Git, npm, TypeScript, ESLint
|
||||
## arch
|
||||
Full-stack monolithic architecture with React frontend, API backend, PostgreSQL and Redis services, containerized via Docker Compose, using Traefik for reverse proxy/HTTPS, and following a token-based design system for UI consistency.
|
||||
## tags
|
||||
docker, application, git, web, react, ui, documents, rework
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# .stoneforge (index)
|
||||
dir: .stoneforge
|
||||
|
||||
## role
|
||||
Infrastructure configuration and metadata storage for the Stoneforge tool
|
||||
## parent
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
## children
|
||||
- .stoneforge/sync
|
||||
index: .stoneforge/sync/.pi-map.index.md
|
||||
map: .stoneforge/sync/.pi-map.md
|
||||
## files
|
||||
- .dashboard-opened
|
||||
- .gitignore
|
||||
- config.yaml
|
||||
## links
|
||||
index: .stoneforge/.pi-map.index.md
|
||||
map: .stoneforge/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,21 @@
|
||||
# .stoneforge
|
||||
dir: .stoneforge
|
||||
|
||||
index: .stoneforge/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Infrastructure configuration and metadata storage for the Stoneforge tool
|
||||
## files
|
||||
- .dashboard-opened | Stores timestamp metadata for tracking when a dashboard was opened
|
||||
- .gitignore | Specifies files and patterns for Git to ignore in the repository | dep: git
|
||||
- config.yaml | YAML configuration file defining settings for the Stoneforge tool, including database, sync, playbooks, identity, merge, workflow, and agent permissions.
|
||||
## arch
|
||||
Dot-directory pattern for tool-specific state, using YAML for hierarchical configuration and dotfiles for ephemeral runtime metadata
|
||||
## tags
|
||||
git, config, stores, timestamp, metadata, tracking, dashboard, was
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,20 @@
|
||||
# .stoneforge/sync (index)
|
||||
dir: .stoneforge/sync
|
||||
|
||||
## role
|
||||
Provides persistent storage for entity relationships and AI worker agent records in a synchronization system.
|
||||
## parent
|
||||
index: .stoneforge/.pi-map.index.md
|
||||
map: .stoneforge/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- dependencies.jsonl
|
||||
- elements.jsonl
|
||||
## links
|
||||
index: .stoneforge/sync/.pi-map.index.md
|
||||
map: .stoneforge/sync/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,20 @@
|
||||
# .stoneforge/sync
|
||||
dir: .stoneforge/sync
|
||||
|
||||
index: .stoneforge/sync/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides persistent storage for entity relationships and AI worker agent records in a synchronization system.
|
||||
## files
|
||||
- dependencies.jsonl | Stores dependency relationships between entities in JSONL format, including parent-child, blocking, and reply-to relationships with timestamps and creators.
|
||||
- elements.jsonl | Stores JSONL-formatted entity records for AI worker agents with their session histories and metadata in a data persistence file.
|
||||
## arch
|
||||
Append-only JSONL document store with separate files for dependency graph edges (dependencies.jsonl) and entity nodes (elements.jsonl), using a log-structured data pattern for time-ordered, immutable record keeping.
|
||||
## tags
|
||||
stores, relationships, jsonl, dependencies, elements, dependency, entities, format
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps (index)
|
||||
dir: apps
|
||||
|
||||
## role
|
||||
Contains the main application entry points and executable modules for the project.
|
||||
## parent
|
||||
index: ./.pi-map.index.md
|
||||
map: ./.pi-map.md
|
||||
## children
|
||||
- apps/api
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
- apps/web
|
||||
index: apps/web/.pi-map.index.md
|
||||
map: apps/web/.pi-map.md
|
||||
## files
|
||||
## links
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,18 @@
|
||||
# apps
|
||||
dir: apps
|
||||
|
||||
index: apps/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains the main application entry points and executable modules for the project.
|
||||
## files
|
||||
## arch
|
||||
Modular application structure with separate deployable units, likely following microservices or multi-app monorepo pattern with shared infrastructure.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,39 @@
|
||||
# apps/api (index)
|
||||
dir: apps/api
|
||||
|
||||
## role
|
||||
FastAPI-based backend API service for managing projects, git repositories, and development tools in a self-hosted development platform.
|
||||
## parent
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
## children
|
||||
- apps/api/.pi-lens
|
||||
index: apps/api/.pi-lens/.pi-map.index.md
|
||||
map: apps/api/.pi-lens/.pi-map.md
|
||||
- apps/api/alembic
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
map: apps/api/alembic/.pi-map.md
|
||||
- apps/api/src
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
- apps/api/tests
|
||||
index: apps/api/tests/.pi-map.index.md
|
||||
map: apps/api/tests/.pi-map.md
|
||||
- apps/api/uploads
|
||||
index: apps/api/uploads/.pi-map.index.md
|
||||
map: apps/api/uploads/.pi-map.md
|
||||
## files
|
||||
- .dockerignore
|
||||
- Dockerfile
|
||||
- README.md
|
||||
- alembic.ini
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- wait-for-db.sh
|
||||
## links
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api
|
||||
dir: apps/api
|
||||
|
||||
index: apps/api/.pi-map.index.md
|
||||
|
||||
## role
|
||||
FastAPI-based backend API service for managing projects, git repositories, and development tools in a self-hosted development platform.
|
||||
## files
|
||||
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and improve build performance. | dep: Docker
|
||||
- Dockerfile | Multi-stage Docker image for a Python web application with Docker-in-Docker capabilities, database waiting, and Cloudflare tunnel support | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
|
||||
- README.md | README documentation for Headquarter API, a self-hosted FastAPI backend for managing projects, git repositories, and development tools. | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Docker Compose, Authentik, uvicorn, pytest, ruff, mypy
|
||||
- alembic.ini | Configuration file for Alembic database migration tool specifying script location, database connection URL, and logging settings | dep: alembic, sqlalchemy, asyncpg, postgresql
|
||||
- pyproject.toml | Defines Python project configuration, dependencies, and tool settings for a FastAPI-based backend API service | dep: fastapi, uvicorn, sqlalchemy, asyncpg, alembic, pydantic, pydantic-settings, python-multipart, httpx, structlog, cryptography, pytest, pytest-asyncio, mypy, ruff, aiosqlite
|
||||
- uv.lock | Lock file generated by uv package manager that records exact dependency versions, hashes, and download URLs for reproducible Python environment installation | dep: uv, PyPI, python
|
||||
- wait-for-db.sh | Waits for a PostgreSQL database to become available before executing the provided command | dep: nc (netcat), sh, sleep
|
||||
## arch
|
||||
Modern Python backend with FastAPI, SQLAlchemy/ORM with Alembic migrations, uv for dependency management, multi-stage Docker containerization with Docker-in-Docker support, and PostgreSQL database connectivity.
|
||||
## tags
|
||||
docker, alembic, python, database, fastapi, uvicorn, pyproject, readme
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/alembic (index)
|
||||
dir: apps/api/alembic
|
||||
|
||||
## role
|
||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
|
||||
## parent
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
## children
|
||||
- apps/api/alembic/versions
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
map: apps/api/alembic/versions/.pi-map.md
|
||||
## files
|
||||
- env.py
|
||||
- script.py.mako
|
||||
## links
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
map: apps/api/alembic/.pi-map.md
|
||||
## workflows
|
||||
- change alembic behavior
|
||||
read: env.py, script.py.mako
|
||||
- explore alembic subdirectories
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,30 @@
|
||||
# apps/api/alembic
|
||||
dir: apps/api/alembic
|
||||
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
|
||||
## files
|
||||
- env.py | Configures Alembic database migration environment with async SQLAlchemy support | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
||||
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQL schema revisions | dep: alembic, sqlalchemy
|
||||
## arch
|
||||
Template-based migration generation using Alembic with async SQLAlchemy engine configuration and environment setup.
|
||||
## tags
|
||||
migrations, run, async, sqlalchemy, alembic, call:context.configure, call:context.begin, transaction
|
||||
## symbols
|
||||
- run_migrations_offline
|
||||
- do_run_migrations
|
||||
- run_async_migrations
|
||||
- run_migrations_online
|
||||
- call:context.configure
|
||||
- call:context.begin_transaction
|
||||
- call:context.run_migrations
|
||||
- call:async_engine_from_config
|
||||
## workflows
|
||||
- change alembic behavior
|
||||
read: env.py, script.py.mako
|
||||
- explore alembic subdirectories
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,73 @@
|
||||
# apps/api/alembic/versions (index)
|
||||
dir: apps/api/alembic/versions
|
||||
|
||||
## role
|
||||
Manages database schema evolution and version control for the API application using Alembic migration scripts.
|
||||
## parent
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
map: apps/api/alembic/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- 0001_initial_schema.py
|
||||
- 0002_refresh_tokens.py
|
||||
- 0003_user_configs.py
|
||||
- 0004_tool_types.py
|
||||
- 0005_ssh_keys_timestamps.py
|
||||
- 0006_tool_instances.py
|
||||
- 0007_instance_container_name.py
|
||||
- 0008_tool_type_category.py
|
||||
- 0009_tool_configs.py
|
||||
- 0010_tool_type_default_port.py
|
||||
- 0011_tool_instance_tunnel_fields.py
|
||||
- 0012_default_port_req.py
|
||||
- 0013_add_config_profiles.py
|
||||
- 0013_add_probe_result.py
|
||||
- 0014_add_profile_resolver_fields.py
|
||||
- 0014_merge_heads.py
|
||||
- 0015_single_interface.py
|
||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py
|
||||
- 20260527160017_add_pi_agent_tool_type.py
|
||||
- 2026_05_22_add_clone_mode.py
|
||||
- 2026_05_23_remove_is_builtin.py
|
||||
- 2026_05_24_220141_add_startup_command.py
|
||||
- 2026_05_24_add_config_profiles.py
|
||||
- 2026_05_26_add_git_mounts.py
|
||||
- 2026_05_27_external_repos.py
|
||||
- 2026_05_28_add_monitoring_tables.py
|
||||
- 2026_05_28_add_terminal_sessions_table.py
|
||||
- 2026_05_28_add_tool_definition_manifests.py
|
||||
- 2026_05_28_drop_tool_configs_and_config_folders.py
|
||||
- 2026_05_29_add_notifications_table.py
|
||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py
|
||||
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py
|
||||
- 2026_05_29_fix_code_server_bind_addr.py
|
||||
- 2026_05_29_fix_code_server_bind_addr_port.py
|
||||
- 2026_05_29_fix_web_tool_bind_address.py
|
||||
- 2026_05_29_remove_lsio_command_override.py
|
||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
||||
- 2026_06_01_add_workspaces.py
|
||||
- 2026_06_13_make_clone_mode_nullable.py
|
||||
- 2026_06_14_104415_add_tool_type_home_directory.py
|
||||
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py
|
||||
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py
|
||||
- 398082499c30_add_tool_config_fields.py
|
||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py
|
||||
- 8ed7dd80973d_create_config_folders_table.py
|
||||
- af8512103d67_add_tool_type_fields.py
|
||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
||||
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py
|
||||
## links
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
map: apps/api/alembic/versions/.pi-map.md
|
||||
## workflows
|
||||
- change versions behavior
|
||||
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
|
||||
- change versions CLI
|
||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
||||
- change versions config
|
||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,80 @@
|
||||
# apps/api/alembic/versions
|
||||
dir: apps/api/alembic/versions
|
||||
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Manages database schema evolution and version control for the API application using Alembic migration scripts.
|
||||
## files
|
||||
- 0001_initial_schema.py | Creates the initial database schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) using Alembic migrations | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication/session management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
||||
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage linked to users via foreign key | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.JSON, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
||||
- 0004_tool_types.py | Alembic database migration that creates a tool_types table with metadata, templates, and user relationship columns | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
||||
- 0005_ssh_keys_timestamps.py | Alembic database migration that adds created_at and updated_at timestamp columns to the ssh_keys table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with UUID primary key, foreign key relationships, timestamps, and indexes for tracking deployed tool instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Integer, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
||||
- 0007_instance_container_name.py | Alembic database migration that adds a nullable container_name column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 0008_tool_type_category.py | Alembic database migration that adds 'category' and 'interfaces' columns to the 'tool_types' table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 0009_tool_configs.py | Alembic database migration that creates a tool_configs table for storing user tool configuration key-value pairs with optional project scoping | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 0010_tool_type_default_port.py | Alembic database migration that adds a nullable default_port column to the tool_types table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 0011_tool_instance_tunnel_fields.py | Alembic database migration that adds public_url and tunnel_id columns to the tool_instances table for tunnel functionality. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 0012_default_port_req.py | Alembic database migration that backfills default_port values for existing tool types and makes the column non-nullable | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.Integer, func:downgrade() → None, call:op.alter_column, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
||||
- 0013_add_config_profiles.py | Database migration that adds config profiles, profile includes, profile mounts, and tool instance profile selection tables with defensive idempotent checks | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:upgrade() → None, call:_table_exists, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:_index_exists, call:op.create_index, call:sa.Integer, call:_column_exists, call:op.add_column, call:_foreign_key_exists, call:op.create_foreign_key, func:downgrade() → None, call:op.drop_index, call:op.drop_constraint, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 0013_add_probe_result.py | Alembic database migration that adds a JSON probe_result column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 0014_add_profile_resolver_fields.py | Alembic database migration that adds profile resolver fields (project_id, tool_type_id, environment_variables, start_command, working_directory, port, is_default) to config_profiles table, renames mount_path to target_path and adds mode/files columns to config_mounts table while removing source_profile_id and content columns | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:_foreign_key_names_for_column(table_name: str, column_name: str) → list[str], call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, call:names.append, func:upgrade() → None, call:_column_exists, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.JSON, call:sa.Text, call:sa.Integer, call:sa.Boolean, call:_foreign_key_exists, call:op.create_foreign_key, call:_index_exists, call:op.create_index, call:op.alter_column, call:sa.String, call:_foreign_key_names_for_column, call:op.drop_constraint, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.Text, call:op.drop_column, call:op.alter_column, call:op.drop_index, call:op.drop_constraint | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 0014_merge_heads.py | Alembic merge migration that combines two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||
- 0015_single_interface.py | Alembic database migration that replaces a plural JSON interfaces column with singular interface_type and requires_port columns on tool_types table | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds an ssh_key_id foreign key column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a new "pi-agent" tool type with terminal-based Docker development environment configuration | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
||||
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories and clone_mode/branch columns to tool_instances | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 2026_05_23_remove_is_builtin.py | Alembic database migration that removes the `is_builtin` column from the `tool_types` table with downgrade support to restore it. | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
||||
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_24_add_config_profiles.py | Alembic database migration that creates config_profiles and config_profile_includes tables with indexes, and adds a selected_config_profile_id foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:sa.Integer, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 2026_05_26_add_git_mounts.py | Alembic database migration that adds a git_mounts JSON column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories to support external repositories and expands alembic_version version_num column to prevent truncation. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health check status. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with foreign key to tool_instances for tracking terminal session lifecycle | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest support to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile/compose templates to a JSON-based manifest system with a base Ubuntu image definition. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
||||
- 2026_05_28_drop_tool_configs_and_config_folders.py | Database migration that drops the `tool_configs` and `config_folders` tables with conditional existence checks, and provides downgrade to recreate them. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, and severity-based notification records including read/dismissed tracking and optimized indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON ssh_key_ids column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py | Alembic database migration that removes the ssh_key_id column from the config_profiles table | exp: func:upgrade() → None, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey | dep: alembic, sqlalchemy
|
||||
- 2026_05_29_fix_code_server_bind_addr.py | Alembic database migration that fixes code-server tool type compose templates by replacing deprecated --bind-addr flag with --host flag | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--bind-addr%' """) ).fetchall, call:sa.text, call:compose_template.replace( "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" ).replace, call:print, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates by replacing broken `--host` flags with correct `--bind-addr 0.0.0.0:{port}` configurations in both database tool_type templates and on-disk instance compose files. | exp: func:_fix_tool_type_templates(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, default_port FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--host%' """) ).fetchall, call:sa.text, call:compose_template.split, call:len, call:line.lstrip, call:new_lines.append, call:"\n".join, call:print, func:_fix_instance_compose_files(conn) → None, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:sa.text, call:print, call:conn.execute( sa.text(""" SELECT id, compose_path, tool_type_id FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:conn.execute( sa.text(""" SELECT default_port FROM tool_types WHERE id = :id """), {"id": tool_type_id}, ).fetchone, call:yaml.safe_load, call:data["services"].values, call:path.write_text, call:yaml.dump, func:upgrade() → None, call:op.get_bind, call:_fix_tool_type_templates, call:_fix_instance_compose_files, func:downgrade() → None | dep: typing, alembic, yaml, pathlib, sqlalchemy
|
||||
- 2026_05_29_fix_web_tool_bind_address.py | Alembic database migration that fixes code-server and jupyter-notebook tool compose templates to bind to 0.0.0.0 | exp: func:_fix_code_server_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'code-server' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:len, call:line.lstrip, call:new_lines.append, call:image_line.lstrip, call:new_lines.index, call:new_lines.insert, call:"\n".join, call:print, func:_fix_jupyter_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'jupyter-notebook' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:new_lines.append, call:len, call:line.lstrip, call:"\n".join, call:print, func:upgrade() → None, call:op.get_bind, call:_fix_code_server_compose, call:_fix_jupyter_compose, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
||||
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database records and on-disk compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy
|
||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (and can restore) the ssh_keys mount from the pi-agent tool definition manifest stored as JSON | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
||||
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with indexes and adds a workspace_id foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration to make the `clone_mode` column nullable in the `tool_instances` table | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
||||
- 2026_06_14_104415_add_tool_type_home_directory.py | Alembic database migration that adds a home_directory column to tool_types table and updates template strings from /workspace to a configurable path | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py | Alembic database migration that updates the pi-agent tool definition manifest to mount repos under the home directory instead of /workspace | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py | Alembic database migration that removes explicit repo mounts from pi-agent tool definition manifests since they are now synthesized by compile_compose | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:any, call:mount.get, call:mounts.append, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds configuration fields (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two branches (removing is_builtin and adding config_profiles) with no actual schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes pi_state and pi_config mounts from the pi-agent manifest in upgrade, and restores them in downgrade | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
|
||||
- 8ed7dd80973d_create_config_folders_table.py | Creates a database migration that adds a config_folders table for storing user configuration folders with JSONB metadata, foreign key relationships, and indexing. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
||||
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two parent revisions (single_interface and clone_mode) into a single branch point with no actual schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two branch heads (home directory and pi agent mount cleanup) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||
## arch
|
||||
Linear migration history with branching/merge resolution pattern; each migration is an immutable, timestamped or sequenced script containing declarative SQLAlchemy operations (create_table, add_column, drop_column) with bidirectional upgrade/downgrade functions; includes data migrations, idempotent guards, and cross-references to on-disk file mutations.
|
||||
## tags
|
||||
column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
|
||||
## symbols
|
||||
- upgrade
|
||||
- downgrade
|
||||
- _table_exists
|
||||
- _column_exists
|
||||
- _index_exists
|
||||
- _foreign_key_exists
|
||||
- _foreign_key_names_for_column
|
||||
- _get_dialect
|
||||
## workflows
|
||||
- change versions behavior
|
||||
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
|
||||
- change versions CLI
|
||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
||||
- change versions config
|
||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
||||
## dirty
|
||||
-
|
||||
@@ -104,11 +104,11 @@ RUN git config --global init.defaultBranch main \\
|
||||
&& git config --global user.name "Developer"
|
||||
|
||||
# Create default tmux config
|
||||
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||
RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||
|
||||
# Create default ranger config
|
||||
RUN mkdir -p /home/user/.config/ranger \\
|
||||
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||
&& printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||
|
||||
# Set up Pi config directory
|
||||
RUN mkdir -p /home/user/.pi/agent
|
||||
|
||||
@@ -232,20 +232,6 @@ def upgrade() -> None:
|
||||
"writable": True,
|
||||
"owner": "user",
|
||||
},
|
||||
{
|
||||
"name": "pi_state",
|
||||
"target": "/tmp/.pi/agents",
|
||||
"source_type": "instance",
|
||||
"writable": True,
|
||||
},
|
||||
{
|
||||
"name": "pi_config",
|
||||
"target": "/home/user/.pi",
|
||||
"source_type": "git_mount",
|
||||
"git_mount_ref": "dotfiles",
|
||||
"writable": True,
|
||||
"owner": "user",
|
||||
},
|
||||
],
|
||||
"runtime": {
|
||||
"command": ["/bin/bash"],
|
||||
@@ -322,10 +308,10 @@ RUN git config --global init.defaultBranch main \\
|
||||
&& git config --global user.email "dev@headquarter.local" \\
|
||||
&& git config --global user.name "Developer"
|
||||
|
||||
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||
RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||
|
||||
RUN mkdir -p /home/user/.config/ranger \\
|
||||
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||
&& printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||
|
||||
RUN mkdir -p /home/user/.pi/agent
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""make clone_mode nullable
|
||||
|
||||
Revision ID: 2026_06_13_make_clone_mode_nullable
|
||||
Revises: 86cec91fdb00
|
||||
Create Date: 2026-06-13 10:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2026_06_13_make_clone_mode_nullable"
|
||||
down_revision = "86cec91fdb00"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# The workspace-first cleanup no longer writes clone_mode; existing rows
|
||||
# keep their value, but new rows may be NULL.
|
||||
op.alter_column(
|
||||
"tool_instances",
|
||||
"clone_mode",
|
||||
existing_type=sa.String(20),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.alter_column(
|
||||
"tool_instances",
|
||||
"clone_mode",
|
||||
existing_type=sa.String(20),
|
||||
nullable=False,
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""add_tool_type_home_directory
|
||||
|
||||
Revision ID: 2026_06_14_104415
|
||||
Revises: f3d2dc90ba3a
|
||||
Create Date: 2026-06-14 10:44:15.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2026_06_14_104415"
|
||||
down_revision: Union[str, Sequence[str], None] = "f3d2dc90ba3a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
tool_types = table(
|
||||
"tool_types",
|
||||
column("id", sa.UUID),
|
||||
column("home_directory", sa.String),
|
||||
column("compose_template", sa.Text),
|
||||
column("dockerfile_template", sa.Text),
|
||||
)
|
||||
|
||||
OLD_WORKSPACE = "/workspace"
|
||||
NEW_WORKSPACE = "/home/user/{{WORKSPACE_NAME}}"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add the new column with a default that applies to existing rows.
|
||||
op.add_column(
|
||||
"tool_types",
|
||||
sa.Column(
|
||||
"home_directory",
|
||||
sa.String(255),
|
||||
nullable=False,
|
||||
server_default="/home/user",
|
||||
),
|
||||
)
|
||||
|
||||
# Rewrite legacy templates that mount the workspace at /workspace so they
|
||||
# use the new configurable home directory and preserve the workspace name.
|
||||
op.execute(
|
||||
sa.update(tool_types)
|
||||
.where(tool_types.c.compose_template.is_not(None))
|
||||
.values(
|
||||
compose_template=sa.func.replace(
|
||||
tool_types.c.compose_template, OLD_WORKSPACE, NEW_WORKSPACE
|
||||
)
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.update(tool_types)
|
||||
.where(tool_types.c.dockerfile_template.is_not(None))
|
||||
.values(
|
||||
dockerfile_template=sa.func.replace(
|
||||
tool_types.c.dockerfile_template, OLD_WORKSPACE, NEW_WORKSPACE
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Restore the original /workspace strings before dropping the column.
|
||||
op.execute(
|
||||
sa.update(tool_types)
|
||||
.where(tool_types.c.compose_template.is_not(None))
|
||||
.values(
|
||||
compose_template=sa.func.replace(
|
||||
tool_types.c.compose_template, NEW_WORKSPACE, OLD_WORKSPACE
|
||||
)
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.update(tool_types)
|
||||
.where(tool_types.c.dockerfile_template.is_not(None))
|
||||
.values(
|
||||
dockerfile_template=sa.func.replace(
|
||||
tool_types.c.dockerfile_template, NEW_WORKSPACE, OLD_WORKSPACE
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
op.drop_column("tool_types", "home_directory")
|
||||
@@ -0,0 +1,90 @@
|
||||
"""fix_pi_agent_home_directory_mount
|
||||
|
||||
Revision ID: 2026_06_14_182955
|
||||
Revises: fc8f1a20cbf6
|
||||
Create Date: 2026-06-14 18:29:55.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2026_06_14_182955"
|
||||
down_revision: Union[str, Sequence[str], None] = "fc8f1a20cbf6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
tool_definition_manifests = table(
|
||||
"tool_definition_manifests",
|
||||
column("id", sa.UUID),
|
||||
column("name", sa.String),
|
||||
column("manifest", sa.JSON),
|
||||
)
|
||||
|
||||
|
||||
def _find_pi_agent_manifest(conn: sa.Connection) -> tuple[Union[str, None], Union[dict, None]]:
|
||||
result = conn.execute(
|
||||
sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest)
|
||||
.where(tool_definition_manifests.c.name == "pi-agent")
|
||||
).fetchone()
|
||||
if result is None:
|
||||
return None, None
|
||||
return result.id, dict(result.manifest)
|
||||
|
||||
|
||||
def _update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) -> None:
|
||||
conn.execute(
|
||||
sa.update(tool_definition_manifests)
|
||||
.where(tool_definition_manifests.c.id == manifest_id)
|
||||
.values(manifest=manifest)
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||
if not manifest_id or not manifest:
|
||||
return
|
||||
|
||||
# Mount the repo under the configured home directory, preserving the repo
|
||||
# directory name via the WORKSPACE_NAME runtime variable.
|
||||
for mount in manifest.get("mounts", []):
|
||||
if mount.get("source_type") == "repo":
|
||||
mount["target"] = "~/{{WORKSPACE_NAME}}"
|
||||
|
||||
# Keep /workspace as a compatibility symlink to the real mount path.
|
||||
runtime = manifest.setdefault("runtime", {})
|
||||
runtime["working_dir"] = "/workspace"
|
||||
|
||||
# Update the startup script to chown the real mount path.
|
||||
scripts = manifest.setdefault("scripts", {})
|
||||
scripts["startup"] = [
|
||||
'if [ -n "$WORKSPACE_NAME" ]; then sudo chown -R user:user "$HOME/$WORKSPACE_NAME" 2>/dev/null || true; fi',
|
||||
]
|
||||
|
||||
_update_manifest(conn, manifest_id, manifest)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||
if not manifest_id or not manifest:
|
||||
return
|
||||
|
||||
for mount in manifest.get("mounts", []):
|
||||
if mount.get("source_type") == "repo":
|
||||
mount["target"] = "/workspace"
|
||||
|
||||
runtime = manifest.setdefault("runtime", {})
|
||||
runtime["working_dir"] = "/workspace"
|
||||
|
||||
scripts = manifest.setdefault("scripts", {})
|
||||
scripts["startup"] = [
|
||||
"if [ -d /workspace ]; then sudo chown -R user:user /workspace 2>/dev/null || true; fi",
|
||||
]
|
||||
|
||||
_update_manifest(conn, manifest_id, manifest)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""remove pi agent explicit repo mount
|
||||
|
||||
Revision ID: 2026_06_15_090500
|
||||
Revises: 2026_06_14_182955
|
||||
Create Date: 2026-06-15 09:05:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2026_06_15_090500"
|
||||
down_revision: Union[str, Sequence[str], None] = "2026_06_14_182955"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
tool_definition_manifests = table(
|
||||
"tool_definition_manifests",
|
||||
column("id", sa.UUID),
|
||||
column("name", sa.String),
|
||||
column("manifest", sa.JSON),
|
||||
)
|
||||
|
||||
|
||||
def _find_pi_agent_manifest(
|
||||
conn: sa.Connection,
|
||||
) -> tuple[Union[str, None], Union[dict, None]]:
|
||||
result = conn.execute(
|
||||
sa.select(
|
||||
tool_definition_manifests.c.id, tool_definition_manifests.c.manifest
|
||||
).where(tool_definition_manifests.c.name == "pi-agent")
|
||||
).fetchone()
|
||||
if result is None:
|
||||
return None, None
|
||||
return result.id, dict(result.manifest)
|
||||
|
||||
|
||||
def _update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) -> None:
|
||||
conn.execute(
|
||||
sa.update(tool_definition_manifests)
|
||||
.where(tool_definition_manifests.c.id == manifest_id)
|
||||
.values(manifest=manifest)
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||
if not manifest_id or not manifest:
|
||||
return
|
||||
|
||||
# The repo mount is now synthesized by compile_compose based on the
|
||||
# instance's repository, so the manifest no longer needs an explicit
|
||||
# repo mount with a {{WORKSPACE_NAME}} placeholder.
|
||||
manifest["mounts"] = [
|
||||
mount
|
||||
for mount in manifest.get("mounts", [])
|
||||
if mount.get("source_type") != "repo"
|
||||
]
|
||||
|
||||
_update_manifest(conn, manifest_id, manifest)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||
if not manifest_id or not manifest:
|
||||
return
|
||||
|
||||
mounts = manifest.setdefault("mounts", [])
|
||||
if not any(mount.get("source_type") == "repo" for mount in mounts):
|
||||
mounts.append(
|
||||
{
|
||||
"name": "workspace",
|
||||
"target": "~/{{WORKSPACE_NAME}}",
|
||||
"source_type": "repo",
|
||||
"writable": True,
|
||||
"owner": "user",
|
||||
}
|
||||
)
|
||||
|
||||
_update_manifest(conn, manifest_id, manifest)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""remove pi config and state mounts from pi-agent manifest
|
||||
|
||||
Revision ID: 8c6d1dbd4798
|
||||
Revises: 2026_06_13_make_clone_mode_nullable
|
||||
Create Date: 2026-06-13 11:21:25.983178
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8c6d1dbd4798'
|
||||
down_revision = '2026_06_13_make_clone_mode_nullable'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _load_manifest(manifest_json):
|
||||
return manifest_json if isinstance(manifest_json, dict) else json.loads(manifest_json)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Remove pi_state and pi_config mounts from the pi-agent manifest."""
|
||||
conn = op.get_bind()
|
||||
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT id, manifest FROM tool_definition_manifests WHERE name = 'pi-agent'"
|
||||
)
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
return
|
||||
|
||||
manifest_id, manifest_json = row
|
||||
manifest = _load_manifest(manifest_json)
|
||||
|
||||
mounts = manifest.get("mounts", [])
|
||||
original_count = len(mounts)
|
||||
|
||||
filtered_mounts = [
|
||||
m for m in mounts if m.get("name") not in ("pi_state", "pi_config")
|
||||
]
|
||||
|
||||
if len(filtered_mounts) < original_count:
|
||||
manifest["mounts"] = filtered_mounts
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE tool_definition_manifests SET manifest = :manifest WHERE id = :id"
|
||||
),
|
||||
{
|
||||
"manifest": json.dumps(manifest),
|
||||
"id": manifest_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Restore pi_state and pi_config mounts to the pi-agent manifest."""
|
||||
conn = op.get_bind()
|
||||
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
"SELECT id, manifest FROM tool_definition_manifests WHERE name = 'pi-agent'"
|
||||
)
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
return
|
||||
|
||||
manifest_id, manifest_json = row
|
||||
manifest = _load_manifest(manifest_json)
|
||||
|
||||
mounts = manifest.get("mounts", [])
|
||||
existing_names = {m.get("name") for m in mounts}
|
||||
|
||||
if "pi_state" not in existing_names:
|
||||
mounts.append(
|
||||
{
|
||||
"name": "pi_state",
|
||||
"target": "/tmp/.pi/agents",
|
||||
"source_type": "instance",
|
||||
"writable": True,
|
||||
}
|
||||
)
|
||||
|
||||
if "pi_config" not in existing_names:
|
||||
mounts.append(
|
||||
{
|
||||
"name": "pi_config",
|
||||
"target": "/home/user/.pi",
|
||||
"source_type": "git_mount",
|
||||
"git_mount_ref": "dotfiles",
|
||||
"writable": True,
|
||||
"owner": "user",
|
||||
}
|
||||
)
|
||||
|
||||
manifest["mounts"] = mounts
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE tool_definition_manifests SET manifest = :manifest WHERE id = :id"
|
||||
),
|
||||
{
|
||||
"manifest": json.dumps(manifest),
|
||||
"id": manifest_id,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""merge home directory and pi agent mount cleanup heads
|
||||
|
||||
Revision ID: fc8f1a20cbf6
|
||||
Revises: 2026_06_14_104415, 8c6d1dbd4798
|
||||
Create Date: 2026-06-14 11:08:41.273502
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fc8f1a20cbf6'
|
||||
down_revision = ('2026_06_14_104415', '8c6d1dbd4798')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,51 @@
|
||||
# apps/api/src (index)
|
||||
dir: apps/api/src
|
||||
|
||||
## role
|
||||
Core backend API package for the "Headquarter API" FastAPI application, handling configuration, database setup, logging, and application bootstrap.
|
||||
## parent
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/api
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
- apps/api/src/auth
|
||||
index: apps/api/src/auth/.pi-map.index.md
|
||||
map: apps/api/src/auth/.pi-map.md
|
||||
- apps/api/src/models
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
- apps/api/src/schemas
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
- apps/api/src/scripts
|
||||
index: apps/api/src/scripts/.pi-map.index.md
|
||||
map: apps/api/src/scripts/.pi-map.md
|
||||
- apps/api/src/seeds
|
||||
index: apps/api/src/seeds/.pi-map.index.md
|
||||
map: apps/api/src/seeds/.pi-map.md
|
||||
- apps/api/src/services
|
||||
index: apps/api/src/services/.pi-map.index.md
|
||||
map: apps/api/src/services/.pi-map.md
|
||||
- apps/api/src/utils
|
||||
index: apps/api/src/utils/.pi-map.index.md
|
||||
map: apps/api/src/utils/.pi-map.md
|
||||
## files
|
||||
- __init__.py
|
||||
- config.py
|
||||
- database.py
|
||||
- logging_config.py
|
||||
- main.py
|
||||
## links
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## workflows
|
||||
- change src behavior
|
||||
read: __init__.py, config.py, database.py
|
||||
- change src config
|
||||
read: config.py, logging_config.py
|
||||
- explore src subdirectories
|
||||
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,35 @@
|
||||
# apps/api/src
|
||||
dir: apps/api/src
|
||||
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Core backend API package for the "Headquarter API" FastAPI application, handling configuration, database setup, logging, and application bootstrap.
|
||||
## files
|
||||
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
||||
- config.py | Defines application configuration settings with environment variable loading, database URL construction, and computed properties for service URLs and security settings. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
||||
- database.py | Sets up an async SQLAlchemy database engine with retry logic and runs Alembic migrations via subprocess to initialize the database. | exp: func:init_database(max_retries, retry_delay) → bool, call:range, call:engine.connect, call:test_conn.execute, call:text, call:test_conn.close, call:logger.info, call:asyncio.get_event_loop().run_in_executor, call:subprocess.run, call:os.path.dirname, call:os.path.abspath, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, os, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy
|
||||
- logging_config.py | Configures structured JSON logging with correlation ID injection, request/response logging middleware, and exception handling for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation, starlette
|
||||
- main.py | Bootstraps a FastAPI application called "Headquarter API" with database initialization, health monitoring, CORS, logging middleware, and modular API routers. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api
|
||||
## arch
|
||||
Layered architecture with separation of concerns across config (settings/env), database (async SQLAlchemy with Alembic migrations), logging (structured JSON with middleware), and main (app composition with modular routers and health monitoring).
|
||||
## tags
|
||||
src, logging, database, api, call:logger.info, middleware, fastapi, filter
|
||||
## symbols
|
||||
- Settings
|
||||
- CorrelationIdFilter
|
||||
- JSONFormatter
|
||||
- RequestLoggingMiddleware
|
||||
- ExceptionLoggingMiddleware
|
||||
- build_database_url
|
||||
- init_database
|
||||
- filter
|
||||
## workflows
|
||||
- change src behavior
|
||||
read: __init__.py, config.py, database.py
|
||||
- change src config
|
||||
read: config.py, logging_config.py
|
||||
- explore src subdirectories
|
||||
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,40 @@
|
||||
# apps/api/src/api (index)
|
||||
dir: apps/api/src/api
|
||||
|
||||
## role
|
||||
Provides shared Pydantic validators and package initialization for API schema validation across the API layer.
|
||||
## parent
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/api/config
|
||||
index: apps/api/src/api/config/.pi-map.index.md
|
||||
map: apps/api/src/api/config/.pi-map.md
|
||||
- apps/api/src/api/project
|
||||
index: apps/api/src/api/project/.pi-map.index.md
|
||||
map: apps/api/src/api/project/.pi-map.md
|
||||
- apps/api/src/api/system
|
||||
index: apps/api/src/api/system/.pi-map.index.md
|
||||
map: apps/api/src/api/system/.pi-map.md
|
||||
- apps/api/src/api/tool
|
||||
index: apps/api/src/api/tool/.pi-map.index.md
|
||||
map: apps/api/src/api/tool/.pi-map.md
|
||||
- apps/api/src/api/user
|
||||
index: apps/api/src/api/user/.pi-map.index.md
|
||||
map: apps/api/src/api/user/.pi-map.md
|
||||
- apps/api/src/api/workspace
|
||||
index: apps/api/src/api/workspace/.pi-map.index.md
|
||||
map: apps/api/src/api/workspace/.pi-map.md
|
||||
## files
|
||||
- __init__.py
|
||||
- shared_validators.py
|
||||
## links
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## workflows
|
||||
- change api behavior
|
||||
read: __init__.py, shared_validators.py
|
||||
- explore api subdirectories
|
||||
index: apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md, apps/api/src/api/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,30 @@
|
||||
# apps/api/src/api
|
||||
dir: apps/api/src/api
|
||||
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides shared Pydantic validators and package initialization for API schema validation across the API layer.
|
||||
## files
|
||||
- __init__.py | Marks the directory as a Python package for API routers.
|
||||
- shared_validators.py | Provides reusable Pydantic validators for API schema fields including mount paths, file dictionaries, environment variables, and volume mounts. | exp: func:validate_mount_path(v: str | None) → str | None, call:v.startswith, raise:ValueError, func:validate_files(v: dict | None, max_size_bytes) → dict | None, call:v.items, call:path.startswith, call:len, call:content.encode, raise:ValueError, func:validate_env_vars(v: dict | None) → dict | None, call:isinstance, raise:ValueError, func:validate_volumes(v: list | None) → list | None, call:isinstance, call:enumerate, raise:ValueError
|
||||
## arch
|
||||
Utility module pattern with reusable cross-cutting Pydantic validators for common API field types (mount paths, files, env vars, volumes).
|
||||
## tags
|
||||
validate, raise:value, error, call:isinstance, validators, mount, api, init
|
||||
## symbols
|
||||
- validate_mount_path
|
||||
- validate_files
|
||||
- validate_env_vars
|
||||
- validate_volumes
|
||||
- call:v.startswith
|
||||
- raise:ValueError
|
||||
- call:v.items
|
||||
- call:path.startswith
|
||||
## workflows
|
||||
- change api behavior
|
||||
read: __init__.py, shared_validators.py
|
||||
- explore api subdirectories
|
||||
index: apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.pi-map.index.md, apps/api/src/api/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,24 @@
|
||||
# apps/api/src/api/config (index)
|
||||
dir: apps/api/src/api/config
|
||||
|
||||
## role
|
||||
Provides FastAPI API endpoints for managing user configuration settings and config profiles.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- config_profiles.py
|
||||
- user_config.py
|
||||
## links
|
||||
index: apps/api/src/api/config/.pi-map.index.md
|
||||
map: apps/api/src/api/config/.pi-map.md
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profiles.py, user_config.py
|
||||
- change config config
|
||||
read: config_profiles.py, user_config.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,31 @@
|
||||
# apps/api/src/api/config
|
||||
dir: apps/api/src/api/config
|
||||
|
||||
index: apps/api/src/api/config/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides FastAPI API endpoints for managing user configuration settings and config profiles.
|
||||
## files
|
||||
- __init__.py | Aggregates and exports config-related API routers for module-level access | dep: src.api.config.config_profiles, src.api.config.user_config
|
||||
- config_profiles.py | FastAPI router providing CRUD endpoints for user-owned config profiles with include resolution, default profile management, and git URL validation. | exp: func:list_config_profiles(project_id, tool_type_id, current_user_id, session), call:select(ConfigProfile) .where(ConfigProfile.user_id == user_uuid) .options, call:selectinload, call:uuid.UUID, call:conditions.append, call:ConfigProfile.project_id.is_, call:ConfigProfile.tool_type_id.is_, call:query.where, call:or_, call:session.execute, call:result.scalars().all, call:profile_to_response, func:create_config_profile(data: ConfigProfileCreate, current_user_id, session), call:create_profile, call:logger.debug, call:profile_to_response, func:get_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:profile_to_response, raise:HTTPException, func:update_config_profile(profile_id: str, data: ConfigProfileUpdate, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:update_profile, call:logger.debug, call:profile_to_response, raise:HTTPException, func:delete_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:session.delete, call:session.commit, call:logger.debug, raise:HTTPException, func:update_profile_includes_endpoint(profile_id: str, data: ConfigProfileIncludeUpdate, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:update_includes, call:session.execute, call:select(ConfigProfileInclude).where, call:inc_result.scalars().all, call:logger.debug, call:profile_to_response, call:list, raise:HTTPException, func:preview_config_profile(profile_id: str, current_user_id, session), call:get_profile_with_includes, call:uuid.UUID, call:resolve_profile, call:resolved_profile_to_dict, raise:HTTPException, func:resolve_default_profile_endpoint(project_id, tool_type_id, current_user_id, session), call:resolve_default_profile, call:uuid.UUID, func:get_default_profiles_endpoint(user_id, session) → dict, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, func:set_default_profiles_endpoint(data: DefaultProfilesUpdate, user_id, session) → dict, call:validate_default_profiles, call:get_or_create_user_config, call:session.commit, call:session.refresh, func:get_default_profile_for_tool_type_endpoint(tool_type_id: str, user_id, session) → dict, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:user_config.default_profiles.get, func:validate_git_url_endpoint(data: ValidateGitUrlRequest, current_user_id, session) → ValidateGitUrlResponse, call:validate_git_url | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.schemas.config, src.services.config.config_profile_resolver, src.services.config.crud_service, src.services.config.resolver_service
|
||||
- user_config.py | Provides FastAPI endpoints for retrieving and updating user configuration settings with automatic record creation. | exp: func:_get_or_create_config(session: AsyncSession, user_id: uuid.UUID) → UserConfig, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:UserConfig, call:session.add, call:session.commit, call:session.refresh, func:get_user_config(user_id, session) → UserConfigResponse, call:_get_user, call:_get_or_create_config, call:UserConfigResponse.model_validate, func:update_user_config(data: UserConfigUpdate, user_id, session) → UserConfigResponse, call:_get_user, call:_get_or_create_config, call:data.model_dump, call:logger.debug, call:session.commit, call:session.refresh, call:UserConfigResponse.model_validate | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.schemas.user
|
||||
## arch
|
||||
Modular router-based architecture using FastAPI with separate route modules for distinct config domains (profiles vs. user settings), CRUD patterns, and include resolution for hierarchical config.
|
||||
## tags
|
||||
config, profile, get, user, call:uuid.uuid, includes, or, default
|
||||
## symbols
|
||||
- list_config_profiles
|
||||
- create_config_profile
|
||||
- get_config_profile
|
||||
- update_config_profile
|
||||
- delete_config_profile
|
||||
- update_profile_includes_endpoint
|
||||
- preview_config_profile
|
||||
- resolve_default_profile_endpoint
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profiles.py, user_config.py
|
||||
- change config config
|
||||
read: config_profiles.py, user_config.py
|
||||
## dirty
|
||||
-
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Config profile API endpoints."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
@@ -12,10 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||
from src.models import ConfigProfile, ConfigProfileInclude
|
||||
from src.models.project import Project
|
||||
from src.models import ToolType
|
||||
from src.models import UserConfig
|
||||
from src.models import ConfigProfile, ConfigProfileInclude, UserConfig
|
||||
from src.schemas.config import (
|
||||
ConfigProfileCreate,
|
||||
ConfigProfileIncludeUpdate,
|
||||
@@ -27,125 +21,27 @@ from src.schemas.config import (
|
||||
)
|
||||
from src.services.config.config_profile_resolver import (
|
||||
ConfigProfileCycleError,
|
||||
check_include_cycle,
|
||||
resolve_profile,
|
||||
resolved_profile_to_dict,
|
||||
)
|
||||
from src.utils.git_url_parser import parse_git_url
|
||||
from src.services.config.crud_service import (
|
||||
create_profile,
|
||||
get_or_create_user_config,
|
||||
get_profile_with_includes,
|
||||
profile_to_response,
|
||||
update_includes,
|
||||
update_profile,
|
||||
validate_default_profiles,
|
||||
)
|
||||
from src.services.config.resolver_service import (
|
||||
resolve_default_profile,
|
||||
validate_git_url,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/config-profiles", tags=["config-profiles"])
|
||||
|
||||
MAX_PROFILE_SIZE_MB = 10
|
||||
MAX_PROFILE_SIZE_BYTES = MAX_PROFILE_SIZE_MB * 1024 * 1024
|
||||
|
||||
|
||||
def _calculate_profile_size(data: dict) -> int:
|
||||
"""Calculate approximate serialized size of profile data."""
|
||||
total = 0
|
||||
for key, value in data.get("env_vars", {}).items():
|
||||
total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
|
||||
for key, value in data.get("runtime_hints", {}).items():
|
||||
total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
|
||||
for mount in data.get("mounts", []):
|
||||
total += len(str(mount.get("target", "")).encode("utf-8"))
|
||||
total += len(str(mount.get("mode", "")).encode("utf-8"))
|
||||
for path, content in mount.get("files", {}).items():
|
||||
total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
|
||||
for path, content in data.get("files", {}).items():
|
||||
total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
|
||||
return total
|
||||
|
||||
|
||||
async def _get_profile_with_includes(
|
||||
session: AsyncSession, profile_id: uuid.UUID
|
||||
) -> ConfigProfile | None:
|
||||
"""Fetch a profile with includes eagerly loaded."""
|
||||
result = await session.execute(
|
||||
select(ConfigProfile)
|
||||
.where(ConfigProfile.id == profile_id)
|
||||
.options(selectinload(ConfigProfile.includes))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def _check_access(
|
||||
session: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
project_id: uuid.UUID | None = None,
|
||||
tool_type_id: uuid.UUID | None = None,
|
||||
) -> None:
|
||||
"""Verify user has access to referenced project and tool type."""
|
||||
if project_id is not None:
|
||||
project = await session.get(Project, project_id)
|
||||
if project is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
||||
)
|
||||
# Add ownership check if needed; for now just verify existence
|
||||
if tool_type_id is not None:
|
||||
tool_type = await session.get(ToolType, tool_type_id)
|
||||
if tool_type is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Tool type not found"
|
||||
)
|
||||
|
||||
|
||||
async def _validate_git_mounts(
|
||||
session: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
git_mounts: list[Any],
|
||||
project_id: uuid.UUID | None = None,
|
||||
) -> None:
|
||||
"""Validate git mount URLs.
|
||||
|
||||
Simply checks that remote_url looks like a valid git URL.
|
||||
Actual clone validation happens at instance startup time.
|
||||
"""
|
||||
for mount in git_mounts:
|
||||
remote_url = mount.get("remote_url")
|
||||
if not remote_url:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Git mount missing remote_url",
|
||||
)
|
||||
|
||||
if not remote_url.startswith(("http://", "https://", "git@", "ssh://")):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid git URL: {remote_url}",
|
||||
)
|
||||
|
||||
|
||||
def _profile_to_response(
|
||||
profile: ConfigProfile, includes: list[ConfigProfileInclude] | None = None
|
||||
) -> dict:
|
||||
return {
|
||||
"id": str(profile.id),
|
||||
"user_id": str(profile.user_id),
|
||||
"name": profile.name,
|
||||
"description": profile.description,
|
||||
"project_id": str(profile.project_id) if profile.project_id else None,
|
||||
"tool_type_id": str(profile.tool_type_id) if profile.tool_type_id else None,
|
||||
"env_vars": profile.env_vars or {},
|
||||
"runtime_hints": profile.runtime_hints or {},
|
||||
"mounts": profile.mounts or [],
|
||||
"git_mounts": profile.git_mounts or [],
|
||||
"files": profile.files or {},
|
||||
"is_default": profile.is_default,
|
||||
"includes": [
|
||||
{
|
||||
"id": str(inc.id),
|
||||
"included_profile_id": str(inc.included_profile_id),
|
||||
"order_index": inc.order_index,
|
||||
}
|
||||
for inc in (includes or profile.includes)
|
||||
],
|
||||
"created_at": profile.created_at.isoformat() if profile.created_at else None,
|
||||
"updated_at": profile.updated_at.isoformat() if profile.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=list[ConfigProfileResponse])
|
||||
async def list_config_profiles(
|
||||
@@ -165,26 +61,21 @@ async def list_config_profiles(
|
||||
)
|
||||
|
||||
if project_id or tool_type_id:
|
||||
# Compatibility filter: include portable profiles and matching scoped profiles
|
||||
from sqlalchemy import or_
|
||||
|
||||
project_uuid = uuid.UUID(project_id) if project_id else None
|
||||
tool_uuid = uuid.UUID(tool_type_id) if tool_type_id else None
|
||||
|
||||
from sqlalchemy import or_
|
||||
|
||||
conditions: list = []
|
||||
# Portable profiles (no project, no tool)
|
||||
conditions.append(
|
||||
(ConfigProfile.project_id.is_(None))
|
||||
& (ConfigProfile.tool_type_id.is_(None))
|
||||
)
|
||||
if project_uuid:
|
||||
# Profiles matching this project (with or without tool)
|
||||
conditions.append(ConfigProfile.project_id == project_uuid)
|
||||
if tool_uuid:
|
||||
# Profiles matching this tool (with or without project)
|
||||
conditions.append(ConfigProfile.tool_type_id == tool_uuid)
|
||||
if project_uuid and tool_uuid:
|
||||
# Exact match
|
||||
conditions.append(
|
||||
(ConfigProfile.project_id == project_uuid)
|
||||
& (ConfigProfile.tool_type_id == tool_uuid)
|
||||
@@ -194,7 +85,7 @@ async def list_config_profiles(
|
||||
|
||||
result = await session.execute(query)
|
||||
profiles = result.scalars().all()
|
||||
return [_profile_to_response(p) for p in profiles]
|
||||
return [profile_to_response(p) for p in profiles]
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -206,69 +97,9 @@ async def create_config_profile(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Create a new config profile."""
|
||||
user_uuid = current_user_id
|
||||
|
||||
# Check for duplicate name
|
||||
existing = await session.execute(
|
||||
select(ConfigProfile)
|
||||
.where(
|
||||
ConfigProfile.user_id == user_uuid,
|
||||
ConfigProfile.name == data.name,
|
||||
)
|
||||
.options(selectinload(ConfigProfile.includes))
|
||||
)
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Profile with name '{data.name}' already exists",
|
||||
)
|
||||
|
||||
# Validate references
|
||||
project_uuid = uuid.UUID(data.project_id) if data.project_id else None
|
||||
tool_uuid = uuid.UUID(data.tool_type_id) if data.tool_type_id else None
|
||||
await _check_access(session, user_uuid, project_uuid, tool_uuid)
|
||||
|
||||
# Validate git mounts reference existing repositories
|
||||
if data.git_mounts:
|
||||
git_mounts_data = [
|
||||
m.model_dump() if hasattr(m, "model_dump") else m for m in data.git_mounts
|
||||
]
|
||||
await _validate_git_mounts(session, user_uuid, git_mounts_data, project_uuid)
|
||||
|
||||
# Check size
|
||||
size = _calculate_profile_size(data.model_dump())
|
||||
if size > MAX_PROFILE_SIZE_BYTES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
|
||||
)
|
||||
|
||||
profile = ConfigProfile(
|
||||
user_id=user_uuid,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
project_id=project_uuid,
|
||||
tool_type_id=tool_uuid,
|
||||
env_vars=data.env_vars,
|
||||
runtime_hints=data.runtime_hints,
|
||||
mounts=[m.model_dump() for m in data.mounts],
|
||||
git_mounts=[m.model_dump() for m in data.git_mounts],
|
||||
files=data.files,
|
||||
is_default=data.is_default,
|
||||
)
|
||||
session.add(profile)
|
||||
await session.commit()
|
||||
|
||||
# Re-fetch with includes to avoid lazy loading issues
|
||||
result = await session.execute(
|
||||
select(ConfigProfile)
|
||||
.where(ConfigProfile.id == profile.id)
|
||||
.options(selectinload(ConfigProfile.includes))
|
||||
)
|
||||
profile = result.scalar_one()
|
||||
|
||||
logger.debug("Created config profile %s for user %s", profile.id, user_uuid)
|
||||
return _profile_to_response(profile)
|
||||
profile = await create_profile(session, current_user_id, data)
|
||||
logger.debug("Created config profile %s for user %s", profile.id, current_user_id)
|
||||
return profile_to_response(profile)
|
||||
|
||||
|
||||
@router.get("/{profile_id}", response_model=ConfigProfileResponse)
|
||||
@@ -278,7 +109,7 @@ async def get_config_profile(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Get a config profile by ID."""
|
||||
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
|
||||
@@ -287,7 +118,7 @@ async def get_config_profile(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
|
||||
)
|
||||
return _profile_to_response(profile)
|
||||
return profile_to_response(profile)
|
||||
|
||||
|
||||
@router.put("/{profile_id}", response_model=ConfigProfileResponse)
|
||||
@@ -298,7 +129,7 @@ async def update_config_profile(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Update a config profile."""
|
||||
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
|
||||
@@ -308,78 +139,9 @@ async def update_config_profile(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
|
||||
)
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# Handle name uniqueness
|
||||
if "name" in update_data:
|
||||
existing = await session.execute(
|
||||
select(ConfigProfile).where(
|
||||
ConfigProfile.user_id == profile.user_id,
|
||||
ConfigProfile.name == update_data["name"],
|
||||
ConfigProfile.id != profile.id,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Profile with name '{update_data['name']}' already exists",
|
||||
)
|
||||
|
||||
# Validate references
|
||||
project_uuid = (
|
||||
uuid.UUID(update_data["project_id"])
|
||||
if "project_id" in update_data and update_data["project_id"]
|
||||
else (profile.project_id if "project_id" not in update_data else None)
|
||||
)
|
||||
tool_uuid = (
|
||||
uuid.UUID(update_data["tool_type_id"])
|
||||
if "tool_type_id" in update_data and update_data["tool_type_id"]
|
||||
else (profile.tool_type_id if "tool_type_id" not in update_data else None)
|
||||
)
|
||||
await _check_access(session, profile.user_id, project_uuid, tool_uuid)
|
||||
|
||||
# Validate git mounts reference existing repositories
|
||||
if "git_mounts" in update_data and update_data["git_mounts"] is not None:
|
||||
git_mounts_data = [
|
||||
m.model_dump() if hasattr(m, "model_dump") else m
|
||||
for m in update_data["git_mounts"]
|
||||
]
|
||||
await _validate_git_mounts(
|
||||
session, profile.user_id, git_mounts_data, project_uuid
|
||||
)
|
||||
|
||||
# Check size
|
||||
current_data = _profile_to_response(profile)
|
||||
merged = {**current_data, **update_data}
|
||||
size = _calculate_profile_size(merged)
|
||||
if size > MAX_PROFILE_SIZE_BYTES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
|
||||
)
|
||||
|
||||
# Apply updates
|
||||
for field_name, value in update_data.items():
|
||||
if field_name in ("project_id", "tool_type_id"):
|
||||
value = uuid.UUID(value) if value else None
|
||||
elif field_name == "mounts" and value is not None:
|
||||
value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
|
||||
elif field_name == "git_mounts" and value is not None:
|
||||
value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
|
||||
setattr(profile, field_name, value)
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Re-fetch with includes to avoid lazy loading issues
|
||||
result = await session.execute(
|
||||
select(ConfigProfile)
|
||||
.where(ConfigProfile.id == profile.id)
|
||||
.options(selectinload(ConfigProfile.includes))
|
||||
)
|
||||
profile = result.scalar_one()
|
||||
|
||||
profile = await update_profile(session, profile, data)
|
||||
logger.debug("Updated config profile %s", profile.id)
|
||||
return _profile_to_response(profile)
|
||||
return profile_to_response(profile)
|
||||
|
||||
|
||||
@router.delete("/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -389,7 +151,7 @@ async def delete_config_profile(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Delete a config profile."""
|
||||
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
|
||||
@@ -407,14 +169,14 @@ async def delete_config_profile(
|
||||
|
||||
|
||||
@router.put("/{profile_id}/includes", response_model=ConfigProfileResponse)
|
||||
async def update_profile_includes(
|
||||
async def update_profile_includes_endpoint(
|
||||
profile_id: str,
|
||||
data: ConfigProfileIncludeUpdate,
|
||||
current_user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Update the ordered includes for a config profile."""
|
||||
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
|
||||
@@ -424,69 +186,8 @@ async def update_profile_includes(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
|
||||
)
|
||||
|
||||
# Validate all included profiles exist and belong to the user
|
||||
included_uuids = [uuid.UUID(inc_id) for inc_id in data.includes]
|
||||
for inc_uuid in included_uuids:
|
||||
inc_profile = await session.get(ConfigProfile, inc_uuid)
|
||||
if inc_profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Included profile not found: {inc_uuid}",
|
||||
)
|
||||
if inc_profile.user_id != current_user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Not authorized to include profile: {inc_uuid}",
|
||||
)
|
||||
if inc_uuid == profile.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Profile cannot include itself",
|
||||
)
|
||||
|
||||
# Check for cycles
|
||||
cycle = await check_include_cycle(session, profile.id, None)
|
||||
if cycle is None and included_uuids:
|
||||
# Check each new include would not create a cycle
|
||||
for inc_uuid in included_uuids:
|
||||
cycle = await check_include_cycle(session, profile.id, inc_uuid)
|
||||
if cycle is not None:
|
||||
break
|
||||
|
||||
if cycle is not None:
|
||||
cycle_str = " -> ".join(str(c) for c in cycle)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Include cycle detected: {cycle_str}",
|
||||
)
|
||||
|
||||
# Remove existing includes
|
||||
result = await session.execute(
|
||||
select(ConfigProfileInclude).where(
|
||||
ConfigProfileInclude.profile_id == profile.id
|
||||
)
|
||||
)
|
||||
for existing in result.scalars().all():
|
||||
await session.delete(existing)
|
||||
await session.flush()
|
||||
|
||||
# Add new includes
|
||||
for order_index, inc_uuid in enumerate(included_uuids):
|
||||
include = ConfigProfileInclude(
|
||||
profile_id=profile.id,
|
||||
included_profile_id=inc_uuid,
|
||||
order_index=order_index,
|
||||
)
|
||||
session.add(include)
|
||||
await session.flush()
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Re-fetch profile (includes loaded separately due to SQLite async issue)
|
||||
result = await session.execute(
|
||||
select(ConfigProfile).where(ConfigProfile.id == profile.id)
|
||||
)
|
||||
profile = result.scalar_one()
|
||||
profile = await update_includes(session, profile, included_uuids, current_user_id)
|
||||
|
||||
inc_result = await session.execute(
|
||||
select(ConfigProfileInclude).where(
|
||||
@@ -496,7 +197,7 @@ async def update_profile_includes(
|
||||
direct_includes = inc_result.scalars().all()
|
||||
|
||||
logger.debug("Updated includes for config profile %s", profile.id)
|
||||
return _profile_to_response(profile, list(direct_includes))
|
||||
return profile_to_response(profile, list(direct_includes))
|
||||
|
||||
|
||||
@router.get("/{profile_id}/preview")
|
||||
@@ -506,7 +207,7 @@ async def preview_config_profile(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Preview the resolved output of a config profile."""
|
||||
profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
profile = await get_profile_with_includes(session, uuid.UUID(profile_id))
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
|
||||
@@ -528,120 +229,19 @@ async def preview_config_profile(
|
||||
|
||||
|
||||
@router.get("/defaults/resolve")
|
||||
async def resolve_default_profile(
|
||||
async def resolve_default_profile_endpoint(
|
||||
project_id: str = Query(..., description="Project ID"),
|
||||
tool_type_id: str = Query(..., description="Tool type ID"),
|
||||
current_user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""Resolve the default config profile for a project/tool combination.
|
||||
|
||||
Selects by specificity:
|
||||
1. project+tool explicit default
|
||||
2. project explicit default
|
||||
3. tool explicit default
|
||||
4. global/user explicit default
|
||||
5. first created compatible profile
|
||||
6. none (returns null)
|
||||
"""
|
||||
user_uuid = current_user_id
|
||||
project_uuid = uuid.UUID(project_id)
|
||||
tool_uuid = uuid.UUID(tool_type_id)
|
||||
|
||||
# Fetch all compatible profiles ordered by created_at
|
||||
query = (
|
||||
select(ConfigProfile)
|
||||
.where(ConfigProfile.user_id == user_uuid)
|
||||
.where(
|
||||
(ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None))
|
||||
| (ConfigProfile.project_id == project_uuid)
|
||||
| (ConfigProfile.tool_type_id == tool_uuid)
|
||||
| (
|
||||
(ConfigProfile.project_id == project_uuid)
|
||||
& (ConfigProfile.tool_type_id == tool_uuid)
|
||||
)
|
||||
)
|
||||
.order_by(ConfigProfile.created_at)
|
||||
"""Resolve the default config profile for a project/tool combination."""
|
||||
return await resolve_default_profile(
|
||||
session,
|
||||
current_user_id,
|
||||
uuid.UUID(project_id),
|
||||
uuid.UUID(tool_type_id),
|
||||
)
|
||||
result = await session.execute(query)
|
||||
profiles = result.scalars().all()
|
||||
|
||||
if not profiles:
|
||||
return {"profile_id": None, "profile_name": None}
|
||||
|
||||
# Check explicit defaults by specificity
|
||||
explicit_defaults = [p for p in profiles if p.is_default]
|
||||
|
||||
# Most specific: project+tool
|
||||
for p in explicit_defaults:
|
||||
if p.project_id == project_uuid and p.tool_type_id == tool_uuid:
|
||||
return {"profile_id": str(p.id), "profile_name": p.name}
|
||||
|
||||
# Next: project only
|
||||
for p in explicit_defaults:
|
||||
if p.project_id == project_uuid and p.tool_type_id is None:
|
||||
return {"profile_id": str(p.id), "profile_name": p.name}
|
||||
|
||||
# Next: tool only
|
||||
for p in explicit_defaults:
|
||||
if p.project_id is None and p.tool_type_id == tool_uuid:
|
||||
return {"profile_id": str(p.id), "profile_name": p.name}
|
||||
|
||||
# Next: global/user (no project, no tool)
|
||||
for p in explicit_defaults:
|
||||
if p.project_id is None and p.tool_type_id is None:
|
||||
return {"profile_id": str(p.id), "profile_name": p.name}
|
||||
|
||||
# Fall back to first created compatible profile
|
||||
first = profiles[0]
|
||||
return {"profile_id": str(first.id), "profile_name": first.name}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default profile management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _get_or_create_user_config(
|
||||
session: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
) -> UserConfig:
|
||||
"""Get existing user config or create a new one."""
|
||||
result = await session.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == user_id)
|
||||
)
|
||||
user_config = result.scalar_one_or_none()
|
||||
if user_config is None:
|
||||
user_config = UserConfig(user_id=user_id, config={})
|
||||
session.add(user_config)
|
||||
return user_config
|
||||
|
||||
|
||||
async def _validate_default_profiles(
|
||||
session: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
default_profiles: dict[str, str],
|
||||
) -> None:
|
||||
"""Validate that all profile IDs in default_profiles belong to the user."""
|
||||
for tool_type_id, profile_id_str in default_profiles.items():
|
||||
try:
|
||||
profile_uuid = uuid.UUID(profile_id_str)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid profile ID for tool type {tool_type_id}: {profile_id_str}",
|
||||
)
|
||||
profile = await session.get(ConfigProfile, profile_uuid)
|
||||
if profile is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Profile not found: {profile_id_str}",
|
||||
)
|
||||
if profile.user_id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Profile does not belong to user: {profile_id_str}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/defaults")
|
||||
@@ -664,8 +264,8 @@ async def set_default_profiles_endpoint(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Set default profile mappings for the current user."""
|
||||
await _validate_default_profiles(session, user_id, data.default_profiles)
|
||||
user_config = await _get_or_create_user_config(session, user_id)
|
||||
await validate_default_profiles(session, user_id, data.default_profiles)
|
||||
user_config = await get_or_create_user_config(session, user_id)
|
||||
user_config.config = {
|
||||
**user_config.config,
|
||||
"default_profiles": data.default_profiles,
|
||||
@@ -691,152 +291,10 @@ async def get_default_profile_for_tool_type_endpoint(
|
||||
|
||||
|
||||
@router.post("/validate-git-url", response_model=ValidateGitUrlResponse)
|
||||
async def validate_git_url(
|
||||
async def validate_git_url_endpoint(
|
||||
data: ValidateGitUrlRequest,
|
||||
current_user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ValidateGitUrlResponse:
|
||||
"""Validate a git remote URL and list available branches.
|
||||
|
||||
Parses the URL, suggests corrections for browser URLs, and runs
|
||||
git ls-remote to verify reachability and enumerate branches.
|
||||
"""
|
||||
parse_result = parse_git_url(data.url)
|
||||
original_url = data.url.strip()
|
||||
url_to_check = parse_result.get("base_url") or original_url
|
||||
|
||||
if not url_to_check:
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error=parse_result.get("message", "Invalid URL"),
|
||||
error_code=parse_result.get("error_code", "INVALID_URL"),
|
||||
)
|
||||
|
||||
# If the URL needed parsing, return suggestion without checking remote
|
||||
if parse_result.get("needs_parsing") and url_to_check != original_url:
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
suggested_url=url_to_check,
|
||||
error=parse_result.get("message"),
|
||||
error_code=parse_result.get("error_code", "URL_NEEDS_PARSING"),
|
||||
)
|
||||
|
||||
# Optional SSH key for private repos
|
||||
env = None
|
||||
key_path = None
|
||||
if data.ssh_key_id:
|
||||
from src.models import SSHKey
|
||||
from src.services.shared.ssh_keys import _get_fernet
|
||||
|
||||
try:
|
||||
ssh_key_uuid = uuid.UUID(data.ssh_key_id)
|
||||
except ValueError:
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error="Invalid SSH key ID format",
|
||||
error_code="INVALID_SSH_KEY",
|
||||
)
|
||||
|
||||
ssh_key = await session.get(SSHKey, ssh_key_uuid)
|
||||
if ssh_key is None or ssh_key.user_id != current_user_id:
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error="SSH key not found or not authorized",
|
||||
error_code="SSH_KEY_NOT_FOUND",
|
||||
)
|
||||
|
||||
import tempfile
|
||||
|
||||
fernet = _get_fernet()
|
||||
private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
|
||||
fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
|
||||
try:
|
||||
os.write(fd, private_key.encode())
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.chmod(key_path, 0o600)
|
||||
env = {
|
||||
"GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
}
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "ls-remote", "--heads", url_to_check],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={**os.environ, **env} if env else None,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
if key_path and os.path.exists(key_path):
|
||||
os.unlink(key_path)
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error="Remote repository check timed out",
|
||||
error_code="TIMEOUT",
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if key_path and os.path.exists(key_path):
|
||||
os.unlink(key_path)
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error="git command not found on server",
|
||||
error_code="GIT_NOT_FOUND",
|
||||
)
|
||||
finally:
|
||||
if key_path and os.path.exists(key_path):
|
||||
os.unlink(key_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.strip()
|
||||
if (
|
||||
"could not resolve" in stderr.lower()
|
||||
or "unable to access" in stderr.lower()
|
||||
):
|
||||
error_msg = "Could not reach repository. Check the URL and network access."
|
||||
error_code = "UNREACHABLE"
|
||||
elif (
|
||||
"authentication" in stderr.lower() or "permission denied" in stderr.lower()
|
||||
):
|
||||
error_msg = (
|
||||
"Authentication failed. Provide an SSH key for private repositories."
|
||||
)
|
||||
error_code = "AUTH_FAILED"
|
||||
else:
|
||||
error_msg = f"Repository not accessible: {stderr[:200]}"
|
||||
error_code = "REMOTE_ERROR"
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error=error_msg,
|
||||
error_code=error_code,
|
||||
)
|
||||
|
||||
# Parse branches from ls-remote output
|
||||
branches: list[str] = []
|
||||
default_branch = "main"
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) == 2:
|
||||
ref = parts[1]
|
||||
# refs/heads/branch-name
|
||||
if ref.startswith("refs/heads/"):
|
||||
branch_name = ref[len("refs/heads/") :]
|
||||
branches.append(branch_name)
|
||||
if branch_name in ("main", "master"):
|
||||
default_branch = branch_name
|
||||
|
||||
if not branches:
|
||||
return ValidateGitUrlResponse(
|
||||
valid=False,
|
||||
error="No branches found in remote repository",
|
||||
error_code="NO_BRANCHES",
|
||||
)
|
||||
|
||||
return ValidateGitUrlResponse(
|
||||
valid=True,
|
||||
suggested_url=url_to_check if url_to_check != original_url else None,
|
||||
branches=branches,
|
||||
default_branch=default_branch,
|
||||
)
|
||||
"""Validate a git remote URL and list available branches."""
|
||||
return await validate_git_url(session, current_user_id, data.url, data.ssh_key_id)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# apps/api/src/api/project (index)
|
||||
dir: apps/api/src/api/project
|
||||
|
||||
## role
|
||||
Provides FastAPI REST API endpoints for project and Git repository management, exposing CRUD operations and version control functionality to API consumers.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- git_repositories.py
|
||||
- projects.py
|
||||
## links
|
||||
index: apps/api/src/api/project/.pi-map.index.md
|
||||
map: apps/api/src/api/project/.pi-map.md
|
||||
## workflows
|
||||
- change project behavior
|
||||
read: __init__.py, git_repositories.py, projects.py
|
||||
## dirty
|
||||
-
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
# apps/api/src/api/system (index)
|
||||
dir: apps/api/src/api/system
|
||||
|
||||
## role
|
||||
Provides system-level API endpoints for monitoring, administration, and operational infrastructure including health checks, dashboards, real-time events, notifications, and container terminal access.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- dashboard.py
|
||||
- events.py
|
||||
- health.py
|
||||
- instance_proxy.py
|
||||
- notifications.py
|
||||
- terminal.py
|
||||
## links
|
||||
index: apps/api/src/api/system/.pi-map.index.md
|
||||
map: apps/api/src/api/system/.pi-map.md
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, dashboard.py, events.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,33 @@
|
||||
# apps/api/src/api/system
|
||||
dir: apps/api/src/api/system
|
||||
|
||||
index: apps/api/src/api/system/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides system-level API endpoints for monitoring, administration, and operational infrastructure including health checks, dashboards, real-time events, notifications, and container terminal access.
|
||||
## files
|
||||
- __init__.py | Aggregates and re-exports system API routers from submodules for centralized access. | dep: src.api.system.dashboard, src.api.system.events, src.api.system.health, src.api.system.instance_proxy, src.api.system.notifications, src.api.system.terminal
|
||||
- dashboard.py | Provides a FastAPI endpoint that returns a dashboard summary with counts of projects, repositories, SSH keys, and recent activity for the authenticated user. | exp: func:get_dashboard_summary(user_id, session) → dict, call:session.execute, call:select(func.count()).select_from(Project).where, call:func.count, call:projects_result.scalar, call:select(func.count()).select_from(GitRepository).where, call:repos_result.scalar, call:select(func.count()).select_from(SSHKey).where, call:ssh_keys_result.scalar, call:select(Project) .where(Project.owner_id == user_id) .order_by(Project.created_at.desc()) .limit, call:Project.created_at.desc, call:recent_projects.scalars().all | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.models.project
|
||||
- events.py | Implements an SSE streaming endpoint that broadcasts instance events to authenticated users with per-user connection limits and backpressure handling. | exp: func:events_stream(request: Request, user_id) → StreamingResponse, call:_connection_counts.get, call:InstanceEventBus, call:asyncio.Queue, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait, call:event_bus.subscribe, call:asyncio.wait_for, call:queue.get, call:json.dumps, call:unsubscribe, call:max, call:_connection_counts.pop, call:StreamingResponse, call:event_generator, raise:HTTPException, func:event_generator() → AsyncGenerator[str, None], call:InstanceEventBus, call:asyncio.Queue, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait, call:event_bus.subscribe, call:asyncio.wait_for, call:queue.get, call:json.dumps, call:unsubscribe, call:max, call:_connection_counts.get, call:_connection_counts.pop, func:on_event(payload: InstanceEventPayload) → None, call:queue.put_nowait, call:contextlib.suppress, call:queue.get_nowait | dep: asyncio, contextlib, json, uuid, collections.abc, fastapi, fastapi.responses, src.auth.dependencies, src.services.instance.event_bus
|
||||
- health.py | Provides FastAPI health check endpoints that monitor database connectivity, disk space, and system uptime with performance timing. | exp: func:health_check() → dict[str, Any], call:HealthChecks, call:time_module.perf_counter, call:SessionLocal, call:session.execute, call:text, call:DatabaseHealth, call:round, call:shutil.disk_usage, call:DiskHealth, call:HealthResponse( status=overall_status, timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), version="0.1.0", checks=checks, uptime_seconds=round(time.time() - _start_time, 2), ).model_dump, call:datetime.now(timezone.utc).isoformat().replace, call:time.time, func:health_check_db() → dict[str, Any], call:time_module.perf_counter, call:SessionLocal, call:session.execute, call:text, call:DatabaseHealthResponse( status="healthy", response_time_ms=round(db_time, 2), ).model_dump, call:round, call:DatabaseHealthResponse( status="unhealthy", response_time_ms=0.0, ).model_dump | dep: time, datetime, typing, fastapi, sqlalchemy, src.database, src.schemas.system, shutil
|
||||
- instance_proxy.py | HTTP proxy router that forwards incoming requests to running containerized tool instances after verifying ownership and instance status. | exp: func:_proxy_request(request: Request, instance_id: uuid.UUID, path: str, user_id: uuid.UUID, session: AsyncSession) → Response, call:session.get, call:str, call:request.headers.items, call:key.lower, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:dict, call:response_headers.pop, call:Response, raise:HTTPException, func:proxy_to_instance(request: Request, instance_id: uuid.UUID, path, user_id, session) → Response, call:_proxy_request | dep: logging, uuid, httpx, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
|
||||
- notifications.py | Provides FastAPI REST endpoints for managing user notifications including listing, marking read/unread, dismissing, and clearing all notifications with support for muted categories. | exp: class:NotificationItem, class:NotificationListResponse, class:UnreadCountResponse, class:MarkAllReadResponse, class:ClearAllResponse, func:_get_mute_categories(session: AsyncSession, user_id: uuid.UUID) → list[str], call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:config.config.get, call:isinstance, func:list_notifications(limit, offset, unread_only, user, session) → NotificationListResponse, call:_get_mute_categories, call:notification_service.list_notifications, call:NotificationListResponse, call:NotificationItem.model_validate, func:get_unread_count(user, session) → UnreadCountResponse, call:notification_service.get_unread_count, call:UnreadCountResponse, func:mark_notification_read(notification_id: uuid.UUID, user, session) → NotificationItem, call:notification_service.mark_read, call:NotificationItem.model_validate, raise:HTTPException, func:mark_all_read(user, session) → MarkAllReadResponse, call:notification_service.mark_all_read, call:MarkAllReadResponse, func:clear_all_notifications(user, session) → ClearAllResponse, call:notification_service.dismiss_all, call:ClearAllResponse, func:dismiss_notification(notification_id: uuid.UUID, user, session) → None, call:notification_service.dismiss, raise:HTTPException | dep: uuid, datetime, fastapi, pydantic, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models.user, src.models, src.services.shared.notification_service, sqlalchemy
|
||||
- terminal.py | Provides WebSocket and REST endpoints for browser-based terminal access to running Docker container tool instances, supporting multiple named sessions, authentication, and terminal reset/reconnect functionality. | exp: class:SessionRef, method:__init__(self, session, slot_session_id), func:terminal_websocket_default(websocket: WebSocket, instance_id: str, db_session) → None, call:_handle_terminal_websocket, func:terminal_websocket_specific(websocket: WebSocket, instance_id: str, session_id: str, db_session) → None, call:_handle_terminal_websocket, func:_handle_terminal_websocket(websocket: WebSocket, instance_id: str, target_session_id: str | None, db_session: AsyncSession) → None, call:logger.debug, call:websocket.accept, call:uuid.UUID, call:logger.error, call:websocket.close, call:_get_user_from_websocket, call:logger.warning, call:db_session.get, call:get_container_status, call:terminal_manager.get_or_create_session, call:terminal_manager.get_session, call:logger.info, call:terminal_manager.create_session, call:terminal_manager._find_key_by_internal_id, call:terminal_manager.attach_websocket, call:websocket.send_json, call:SessionRef, call:asyncio.create_task, call:_write_loop, call:_heartbeat_loop, call:asyncio.wait, call:len, call:task.cancel, call:str, call:suppress, call:terminal_manager.detach_websocket, func:_write_loop(session_ref: SessionRef, websocket, instance_id: str) → None, call:session.is_alive, call:asyncio.sleep, call:websocket.receive, call:session.write_input, call:text.startswith, call:json.loads, call:ctrl.get, call:logger.debug, call:session.resize, call:session.acknowledge_data, call:websocket.send_json, call:terminal_manager.reset_session, call:terminal_manager.attach_websocket, call:text.encode, func:_heartbeat_loop(websocket: WebSocket) → None, call:asyncio.sleep, call:websocket.send_json, func:_get_terminal_instance(instance_id: uuid.UUID, user_id: uuid.UUID, db_session: AsyncSession) → ToolInstance, call:db_session.get, raise:HTTPException, func:list_terminal_sessions(instance_id: uuid.UUID, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.execute, call:select(TerminalSessionModel) .where(TerminalSessionModel.instance_id == instance_id) .where(TerminalSessionModel.status != "closed") .order_by, call:TerminalSessionModel.created_at.asc, call:result.scalars().all, call:terminal_manager.get_session, call:str, call:sessions.append, call:live_session.has_websockets, call:row.created_at.isoformat, call:row.last_activity_at.isoformat, func:create_terminal_session(instance_id: uuid.UUID, data: dict, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.get, call:data.get, call:terminal_manager.create_session, raise:HTTPException, func:close_terminal_session(instance_id: uuid.UUID, session_id: str, user_id, db_session) → dict, call:_get_terminal_instance, call:terminal_manager._find_key_by_internal_id, call:str, call:terminal_manager.get_session, call:terminal_manager.close_session, raise:HTTPException, func:reset_specific_terminal_session(instance_id: uuid.UUID, session_id: str, user_id, db_session) → dict, call:_get_terminal_instance, call:terminal_manager._find_key_by_internal_id, call:str, call:terminal_manager.get_session, call:db_session.get, call:terminal_manager.reset_session, raise:HTTPException, func:rename_terminal_session(instance_id: uuid.UUID, session_id: str, data: dict, user_id, db_session) → dict, call:_get_terminal_instance, call:data.get, call:isinstance, call:terminal_manager.get_session, call:str, call:db_session.get, call:uuid.UUID, call:db_session.commit, raise:HTTPException, func:reset_terminal_session(instance_id: uuid.UUID, user_id, db_session) → dict, call:_get_terminal_instance, call:db_session.get, call:terminal_manager.reset_session, call:logger.info, call:str, call:logger.error, raise:HTTPException, func:_get_user_from_websocket(websocket: WebSocket, db_session: AsyncSession) → uuid.UUID | None, call:websocket.cookies.get, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str | dep: asyncio, json, logging, uuid, contextlib, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, starlette.websockets, src.auth.dependencies, src.models, src.services.terminal.terminal_manager, src.services.docker, src.auth.session, src.config, starlette
|
||||
## arch
|
||||
FastAPI router composition pattern with modular sub-routers aggregated via __init__.py, combining synchronous REST endpoints, SSE streaming, and WebSocket connections for real-time features, with authentication enforcement and resource ownership validation across operational and infrastructure concerns.
|
||||
## tags
|
||||
terminal, session, call:terminal, call:, get, src, response, instance
|
||||
## symbols
|
||||
- NotificationItem
|
||||
- NotificationListResponse
|
||||
- UnreadCountResponse
|
||||
- MarkAllReadResponse
|
||||
- ClearAllResponse
|
||||
- SessionRef
|
||||
- get_dashboard_summary
|
||||
- events_stream
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, dashboard.py, events.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,26 @@
|
||||
# apps/api/src/api/tool (index)
|
||||
dir: apps/api/src/api/tool
|
||||
|
||||
## role
|
||||
Provides FastAPI REST API endpoints for managing containerized tools throughout their lifecycle, including type definitions, instance management, session tracking, and Docker operations.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- sessions.py
|
||||
- tool_definitions.py
|
||||
- tool_instances.py
|
||||
- tool_lifecycle.py
|
||||
- tool_types.py
|
||||
- tool_types_validation.py
|
||||
## links
|
||||
index: apps/api/src/api/tool/.pi-map.index.md
|
||||
map: apps/api/src/api/tool/.pi-map.md
|
||||
## workflows
|
||||
- change tool behavior
|
||||
read: __init__.py, sessions.py, tool_definitions.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,33 @@
|
||||
# apps/api/src/api/tool
|
||||
dir: apps/api/src/api/tool
|
||||
|
||||
index: apps/api/src/api/tool/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides FastAPI REST API endpoints for managing containerized tools throughout their lifecycle, including type definitions, instance management, session tracking, and Docker operations.
|
||||
## files
|
||||
- __init__.py | Aggregates and exports router modules for the Tool API package. | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_lifecycle, src.api.tool.tool_types
|
||||
- sessions.py | Provides a FastAPI endpoint to retrieve all active tool sessions (running instances) for the currently authenticated user with related metadata. | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
|
||||
- tool_definitions.py | Provides FastAPI CRUD endpoints for managing tool definition manifests with compilation to Dockerfile and Docker Compose previews. | exp: class:CreateToolDefinitionRequest, class:UpdateToolDefinitionRequest, func:create_tool_definition(data: CreateToolDefinitionRequest, user_id, session) → dict, call:uuid.UUID, call:session.get, call:session.execute, call:select(ToolDefinitionManifest).where, call:existing.scalar_one_or_none, call:ToolDefinitionManifest, call:session.add, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.created_at.isoformat, raise:HTTPException, func:list_tool_definitions(user_id, session, include_bases) → dict, call:select, call:query.where, call:ToolDefinitionManifest.is_base.is_, call:session.execute, call:query.order_by, call:ToolDefinitionManifest.created_at.desc, call:result.scalars().all, call:str, call:d.created_at.isoformat, func:get_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:str, call:definition.created_at.isoformat, call:definition.updated_at.isoformat, raise:HTTPException, func:update_tool_definition(definition_id: uuid.UUID, data: UpdateToolDefinitionRequest, user_id, session) → dict, call:session.get, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.updated_at.isoformat, raise:HTTPException, func:delete_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:session.execute, call:select(ToolType).where, call:result.scalars().all, call:", ".join, call:session.delete, call:session.commit, call:logger.info, call:str, raise:HTTPException, func:compile_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:dict, call:resolve_base, call:deep_merge, call:compile_dockerfile, call:compile_entrypoint, call:compute_image_tag, call:compile_compose, call:session.commit, call:str, raise:HTTPException | dep: logging, uuid, fastapi, pydantic, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.build.manifest_compiler
|
||||
- tool_instances.py | FastAPI router providing REST endpoints for managing tool instances (CRUD, logs, health checks, tunnel management, proxying, and event history) within project repositories. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service
|
||||
- tool_lifecycle.py | FastAPI router providing REST endpoints for managing Docker-based tool instance lifecycle operations (start, stop, restart, delete). | exp: func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.schemas.tool, src.services.tool.instance_service
|
||||
- tool_types.py | Defines FastAPI REST endpoints for CRUD operations on tool types with validation for Docker Compose, Dockerfile, and manifest definitions. | exp: func:_require_admin(user: User) → None, func:create_tool_type(data: ToolTypeCreate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.scalar, call:select(ToolType).where, call:ToolType, call:session.add, call:session.commit, call:session.refresh, raise:HTTPException, func:list_tool_types(user_id, session) → list[ToolType], call:_get_user, call:session.execute, call:select(ToolType).order_by, call:list, call:result.scalars().all, func:get_tool_type(tool_type_id: uuid.UUID, user_id, session) → ToolType, call:_get_user, call:session.get, raise:HTTPException, func:update_tool_type(tool_type_id: uuid.UUID, data: ToolTypeUpdate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.get, call:data.model_dump, call:update_data.get, call:validate_compose_yaml, call:check_port_exposed, call:validate_required_variables, call:update_data.items, call:setattr, call:session.commit, call:session.refresh, raise:HTTPException, func:validate_tool_type_template(data: ToolTypeValidateRequest, user_id, session) → dict, call:_get_user, call:errors.append, call:validate_compose_yaml, call:str, call:data.dockerfile_template.strip().startswith, call:len, func:validate_tool_type(tool_type_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:session.get, call:errors.append, call:validate_compose_yaml, call:str, call:tool_type.dockerfile_template.strip().startswith, call:len, raise:HTTPException, func:delete_tool_type(tool_type_id: uuid.UUID, user_id, session) → None, call:_get_user, call:_require_admin, call:session.get, call:session.delete, call:session.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.api.tool.tool_types_validation, src.auth.dependencies, src.models, src.models.user, src.schemas.tool
|
||||
- tool_types_validation.py | Provides validation utilities for Docker Compose templates including YAML parsing, port exposure checking, and required template variable verification. | exp: func:sanitize_template_vars(template: str) → str, call:re.sub, func:validate_compose_yaml(template: str) → dict, call:sanitize_template_vars, call:yaml.safe_load, call:isinstance, raise:ValueError, func:check_port_exposed(parsed: dict, port: int) → bool, call:str, call:isinstance, call:parsed["services"].values, call:port_mapping.split, call:mapping.split, func:validate_required_variables(template: str, variables: list[str]) → None, raise:HTTPException | dep: re, yaml, fastapi
|
||||
## arch
|
||||
Modular FastAPI router pattern with domain-driven separation of concerns (types, instances, sessions, lifecycle) and Docker-centric validation/compilation utilities for container orchestration.
|
||||
## tags
|
||||
get, call:, tool, raise:httpexception, user, instance, call:str, call:session.get
|
||||
## symbols
|
||||
- CreateToolDefinitionRequest
|
||||
- UpdateToolDefinitionRequest
|
||||
- get_user_sessions
|
||||
- create_tool_definition
|
||||
- list_tool_definitions
|
||||
- get_tool_definition
|
||||
- update_tool_definition
|
||||
- delete_tool_definition
|
||||
## workflows
|
||||
- change tool behavior
|
||||
read: __init__.py, sessions.py, tool_definitions.py
|
||||
## dirty
|
||||
-
|
||||
@@ -3,11 +3,13 @@
|
||||
from src.api.tool.sessions import sessions_router
|
||||
from src.api.tool.tool_definitions import router as tool_definitions_router
|
||||
from src.api.tool.tool_instances import router as tool_instances_router
|
||||
from src.api.tool.tool_lifecycle import router as tool_lifecycle_router
|
||||
from src.api.tool.tool_types import router as tool_types_router
|
||||
|
||||
__all__ = [
|
||||
"sessions_router",
|
||||
"tool_definitions_router",
|
||||
"tool_instances_router",
|
||||
"tool_lifecycle_router",
|
||||
"tool_types_router",
|
||||
]
|
||||
|
||||
@@ -53,6 +53,13 @@ async def get_user_sessions(
|
||||
repo = await session.get(GitRepository, instance.repository_id)
|
||||
project = await session.get(Project, instance.project_id)
|
||||
|
||||
workspace_name = None
|
||||
if instance.workspace_id:
|
||||
from src.models import Workspace as WorkspaceModel
|
||||
workspace = await session.get(WorkspaceModel, instance.workspace_id)
|
||||
if workspace:
|
||||
workspace_name = workspace.name
|
||||
|
||||
sessions.append(
|
||||
{
|
||||
"id": str(instance.id),
|
||||
@@ -64,10 +71,9 @@ async def get_user_sessions(
|
||||
"repository_id": str(instance.repository_id),
|
||||
"project_name": project.name if project else "unknown",
|
||||
"project_id": str(instance.project_id),
|
||||
"workspace_name": workspace_name,
|
||||
"status": instance.status,
|
||||
"url": instance.url,
|
||||
"clone_mode": instance.clone_mode,
|
||||
"branch": instance.branch,
|
||||
"selected_config_profile_id": str(instance.selected_config_profile_id)
|
||||
if instance.selected_config_profile_id
|
||||
else None,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
"""Tool instance lifecycle API endpoints."""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth.dependencies import (
|
||||
_get_owned_project,
|
||||
_get_user,
|
||||
get_current_user_id,
|
||||
get_db_session,
|
||||
)
|
||||
from src.schemas.tool import StartInstanceRequest
|
||||
from src.services.tool.instance_service import (
|
||||
delete_tool_instance,
|
||||
restart_tool_instance,
|
||||
start_tool_instance,
|
||||
stop_tool_instance,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
|
||||
summary="Start instance",
|
||||
description="Start a tool instance using Docker Compose.",
|
||||
)
|
||||
async def start_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
data: StartInstanceRequest | None = None,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
try:
|
||||
return await start_tool_instance(
|
||||
session, user_id, project_id, repo_id, instance_id, data
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop",
|
||||
summary="Stop instance",
|
||||
description="Stop a running tool instance.",
|
||||
)
|
||||
async def stop_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
try:
|
||||
return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart",
|
||||
summary="Restart instance",
|
||||
description="Restart a tool instance.",
|
||||
)
|
||||
async def restart_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
try:
|
||||
return await restart_tool_instance(
|
||||
session, user_id, project_id, repo_id, instance_id
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
|
||||
summary="Delete instance",
|
||||
description="Delete a tool instance and remove its Docker containers and files.",
|
||||
)
|
||||
async def delete_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
force: bool = False,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
try:
|
||||
await delete_tool_instance(
|
||||
session, user_id, project_id, repo_id, instance_id, force
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
|
||||
except RuntimeError as exc:
|
||||
detail = str(exc)
|
||||
if "uncommitted changes" in detail.lower():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail={
|
||||
"message": "Repository has uncommitted changes",
|
||||
"changed_files": detail,
|
||||
"force_required": True,
|
||||
},
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail
|
||||
)
|
||||
@@ -60,8 +60,14 @@ def check_port_exposed(parsed: dict, port: int) -> bool:
|
||||
for service_config in parsed["services"].values():
|
||||
if isinstance(service_config, dict) and "ports" in service_config:
|
||||
for port_mapping in service_config["ports"]:
|
||||
if isinstance(port_mapping, str) and port_str in port_mapping:
|
||||
return True
|
||||
if isinstance(port_mapping, str):
|
||||
# Port mappings can be "host:container", "ip:host:container",
|
||||
# "container", or ".../protocol". The container port is the
|
||||
# last numeric segment before any protocol suffix.
|
||||
mapping = port_mapping.split("/")[0]
|
||||
parts = mapping.split(":")
|
||||
if parts[-1] == port_str:
|
||||
return True
|
||||
elif isinstance(port_mapping, int) and port_mapping == port:
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/api/user (index)
|
||||
dir: apps/api/src/api/user
|
||||
|
||||
## role
|
||||
Provides user-facing API endpoints for authentication, profile management, and SSH key operations in the FastAPI backend.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- auth.py
|
||||
- ssh_keys.py
|
||||
- users.py
|
||||
## links
|
||||
index: apps/api/src/api/user/.pi-map.index.md
|
||||
map: apps/api/src/api/user/.pi-map.md
|
||||
## workflows
|
||||
- change user behavior
|
||||
read: __init__.py, auth.py, ssh_keys.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,30 @@
|
||||
# apps/api/src/api/user
|
||||
dir: apps/api/src/api/user
|
||||
|
||||
index: apps/api/src/api/user/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides user-facing API endpoints for authentication, profile management, and SSH key operations in the FastAPI backend.
|
||||
## files
|
||||
- __init__.py | Aggregates and exports user-related API routers from submodules for easy importing. | dep: src.api.user.auth, src.api.user.ssh_keys, src.api.user.users
|
||||
- auth.py | Implements OAuth2/OIDC authentication endpoints for a FastAPI application using Authentik as the identity provider, including login initiation, callback handling, session management, logout, and current user retrieval. | exp: func:get_db_session() → AsyncGenerator[AsyncSession, None], call:SessionLocal, func:login(next) → RedirectResponse, call:Settings, call:token_urlsafe, call:build_login_redirect_url, call:logger.debug, call:RedirectResponse, call:response.set_cookie, func:callback(code: str, state: str, auth_state, auth_next, session) → RedirectResponse, call:logger.debug, call:logger.warning, call:Settings, call:httpx.AsyncClient, call:exchange_code_for_tokens, call:logger.info, call:logger.error, call:fetch_user_info, call:str, call:user_info.get, call:session.scalar, call:select(User).where, call:User, call:session.add, call:session.commit, call:session.refresh, call:create_session_cookie, call:build_cookie_options, call:cast, call:bool, call:cookie_options.get, call:RedirectResponse, call:redirect_response.set_cookie, call:redirect_response.delete_cookie, raise:HTTPException, func:logout(response: Response) → dict[str, str], call:Settings, call:build_cookie_options, call:cast, call:bool, call:str, call:cookie_options.get, call:response.delete_cookie, func:me(session_cookie, session) → dict[str, Any], call:logger.debug, call:bool, call:logger.warning, call:Settings, call:decode_session_cookie, call:session.get, call:logger.info, call:str, raise:HTTPException | dep: logging, secrets, typing, httpx, fastapi, fastapi.responses, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.cookies, src.auth.oidc, src.auth.session, src.config, src.database, src.models.user
|
||||
- ssh_keys.py | FastAPI router for managing Ed25519 SSH keys including generation, storage with encrypted private keys, signing payloads, and signature verification | exp: func:_get_fernet() → Fernet, call:Settings, call:hashlib.sha256(settings.session_secret.encode()).digest, call:settings.session_secret.encode, call:base64.urlsafe_b64encode, call:Fernet, func:generate_ssh_key_pair() → tuple[str, str], call:Ed25519PrivateKey.generate, call:private_key.public_key, call:private_key.private_bytes, call:serialization.NoEncryption, call:public_key.public_bytes, call:private_bytes.decode, call:public_bytes.decode, func:create_ssh_key(data: SSHKeyCreate, user_id, session) → SSHKey, call:_get_user, call:generate_ssh_key_pair, call:_get_fernet, call:fernet.encrypt(private_key.encode()).decode, call:private_key.encode, call:SSHKey, call:session.add, call:session.commit, call:session.refresh, func:list_ssh_keys(user_id, session) → list[SSHKey], call:_get_user, call:session.execute, call:select(SSHKey).where, call:list, call:result.scalars().all, func:delete_ssh_key(key_id: uuid.UUID, user_id, session) → None, call:_get_user, call:session.get, call:session.delete, call:session.commit, raise:HTTPException, func:sign_payload(key_id: uuid.UUID, data: SignPayloadRequest, user_id, session) → SignatureResponse, call:_get_user, call:session.get, call:_get_fernet, call:fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode, call:ssh_key.private_key_encrypted.encode, call:serialization.load_ssh_private_key, call:private_key_pem.encode, call:private_key.sign, call:data.payload.encode, call:SignatureResponse, call:base64.b64encode(signature).decode, raise:HTTPException, func:verify_signature(key_id: uuid.UUID, data: VerifySignatureRequest, user_id, session) → VerifySignatureResponse, call:_get_user, call:session.get, call:serialization.load_ssh_public_key, call:ssh_key.public_key.encode, call:base64.b64decode, call:public_key.verify, call:data.payload.encode, call:VerifySignatureResponse, raise:HTTPException | dep: base64, uuid, cryptography.fernet, cryptography.hazmat.primitives, cryptography.hazmat.primitives.asymmetric.ed25519, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.config, src.models, src.schemas.project, hashlib, cryptography
|
||||
- users.py | Implements FastAPI endpoints for user profile management including retrieval, updates, and avatar uploads with file validation. | exp: func:get_profile(user_id, session) → User, call:_get_user, func:update_profile(data: UserProfileUpdate, user_id, session) → User, call:_get_user, call:len, call:data.name.strip, call:data.email.strip, call:session.commit, call:session.refresh, raise:HTTPException, func:upload_avatar(file: UploadFile, user_id, session) → User, call:_get_user, call:file.read, call:len, call:Path, call:old_path.exists, call:old_path.unlink, call:filename_part.split(".")[-1].lower, call:uuid.uuid4, call:file_path.write_bytes, call:session.commit, call:session.refresh, raise:HTTPException | dep: uuid, pathlib, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models.user, src.schemas.user, sqlalchemy
|
||||
## arch
|
||||
Modular FastAPI router pattern with separate submodules for distinct concerns (auth, profile, SSH keys), using OAuth2/OIDC external identity provider (Authentik), encrypted key storage, and file upload handling.
|
||||
## tags
|
||||
user, get, call:, key, ssh, src, cookie, raise:httpexception
|
||||
## symbols
|
||||
- get_db_session
|
||||
- login
|
||||
- callback
|
||||
- logout
|
||||
- me
|
||||
- _get_fernet
|
||||
- generate_ssh_key_pair
|
||||
- create_ssh_key
|
||||
## workflows
|
||||
- change user behavior
|
||||
read: __init__.py, auth.py, ssh_keys.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,24 @@
|
||||
# apps/api/src/api/workspace (index)
|
||||
dir: apps/api/src/api/workspace
|
||||
|
||||
## role
|
||||
Provides REST API endpoints for managing user workspaces, including file operations, Git version control, tool instances, and workspace lifecycle management.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- workspace_files.py
|
||||
- workspace_git.py
|
||||
- workspace_instances.py
|
||||
- workspaces.py
|
||||
## links
|
||||
index: apps/api/src/api/workspace/.pi-map.index.md
|
||||
map: apps/api/src/api/workspace/.pi-map.md
|
||||
## workflows
|
||||
- change workspace behavior
|
||||
read: __init__.py, workspace_files.py, workspace_git.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,31 @@
|
||||
# apps/api/src/api/workspace
|
||||
dir: apps/api/src/api/workspace
|
||||
|
||||
index: apps/api/src/api/workspace/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides REST API endpoints for managing user workspaces, including file operations, Git version control, tool instances, and workspace lifecycle management.
|
||||
## files
|
||||
- __init__.py | Aggregates and re-exports workspace-related API routers from submodules for centralized access. | dep: src.api.workspace.workspace_files, src.api.workspace.workspace_git, src.api.workspace.workspace_instances, src.api.workspace.workspaces
|
||||
- workspace_files.py | Provides FastAPI endpoints for listing, reading, and writing files within user-owned workspaces with optional Git commit integration. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:list_files(workspace_id: uuid.UUID, path, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.list_directory, raise:HTTPException, func:get_file_content(workspace_id: uuid.UUID, path: str, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.read_file, raise:HTTPException, func:write_file(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:FileService, call:data.get("path", "").strip, call:data.get("message", "").strip, call:service.write_file, call:GitOperations, call:git.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.shared.file_service, sqlalchemy, src.services.git.git_operations
|
||||
- workspace_git.py | Provides FastAPI REST endpoints for executing git operations (status, branches, commit, push, pull, fetch, checkout, history) on user workspaces with authentication and database persistence. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:git_status(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.status, raise:HTTPException, func:git_branches(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.branches, raise:HTTPException, func:git_commit(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("message", "").strip, call:GitOperations, call:git.commit, raise:HTTPException, func:git_push(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.push, raise:HTTPException, func:git_pull(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.pull, raise:HTTPException, func:git_fetch(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.fetch, raise:HTTPException, func:git_checkout(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("branch", "").strip, call:GitOperations, call:git.checkout, call:session.commit, raise:HTTPException, func:git_history(workspace_id: uuid.UUID, path, limit, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.history, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.git.git_operations, sqlalchemy
|
||||
- workspace_instances.py | Defines FastAPI endpoints for creating and listing tool instances associated with a specific workspace. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:create_workspace_instance(workspace_id: uuid.UUID, data: CreateWorkspaceInstanceRequest, user_id, session) → dict, call:_get_workspace, call:session.get, call:CreateInstanceRequest, call:str, call:create_tool_instance, call:instance.created_at.isoformat, raise:HTTPException, func:list_workspace_instances(workspace_id: uuid.UUID, user_id, session) → list[dict], call:_get_workspace, call:session.execute, call:select(ToolInstance) .where(ToolInstance.workspace_id == workspace_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.schemas.tool, src.services.tool.instance_service
|
||||
- workspaces.py | FastAPI router providing CRUD endpoints for managing Git workspaces with repository integration, instance tracking, and sync capabilities. | exp: func:list_all_workspaces(user_id, session) → list[dict], call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, GitRepository, instance_count.label("instance_count"), ) .join(GitRepository, Workspace.repo_id == GitRepository.id) .options(selectinload(GitRepository.project)) .where(Workspace.user_id == user_id) .order_by, call:instance_count.label, call:selectinload, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:delete_workspace_top_level(workspace_id: uuid.UUID, force, user_id, session) → dict, call:session.get, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:create_workspace_top_level(data: dict, user_id, session) → dict, call:data.get("repo_id", "").strip, call:uuid.UUID, call:session.get, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:list_workspaces(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_repo, call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, instance_count.label("instance_count"), ) .where(Workspace.repo_id == repo_id) .order_by, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:create_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:get_workspace_detail(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:session.execute, call:select(func.count(ToolInstance.id)).where, call:func.count, call:result.scalar, call:str, call:workspace.last_sync_at.isoformat, call:workspace.created_at.isoformat, call:workspace.updated_at.isoformat, func:update_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:data.get("name", "").strip, call:data.get("branch", "").strip, call:session.commit, call:session.rollback, call:logger.error, call:str, raise:HTTPException, func:delete_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, force, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:sync_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.sync, call:session.commit, call:workspace.last_sync_at.isoformat, raise:HTTPException, func:_get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID, user_id: uuid.UUID) → GitRepository, call:session.execute, call:select(GitRepository) .where( GitRepository.id == repo_id, GitRepository.project_id == project_id, ) .options, call:selectinload, call:result.scalar_one_or_none, raise:HTTPException, func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, repo_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.services.shared.workspace_manager
|
||||
## arch
|
||||
Modular FastAPI router decomposition with domain-driven vertical slicing (files/git/instances/workspaces as separate modules), aggregated through __init__.py barrel exports, combining database persistence with external Git repository integration.
|
||||
## tags
|
||||
workspace, get, raise:httpexception, call:, at.isoformat, git, call:select, call:data.get
|
||||
## symbols
|
||||
- _get_workspace
|
||||
- list_files
|
||||
- get_file_content
|
||||
- write_file
|
||||
- git_status
|
||||
- git_branches
|
||||
- git_commit
|
||||
- git_push
|
||||
## workflows
|
||||
- change workspace behavior
|
||||
read: __init__.py, workspace_files.py, workspace_git.py
|
||||
## dirty
|
||||
-
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||
from src.models import GitRepository
|
||||
from src.models import ToolInstance
|
||||
from src.models import Workspace
|
||||
from src.schemas.tool import CreateInstanceRequest, CreateWorkspaceInstanceRequest
|
||||
from src.services.tool.instance_service import create_tool_instance
|
||||
|
||||
router = APIRouter(prefix="/workspaces/{workspace_id}/instances")
|
||||
|
||||
@@ -30,6 +33,63 @@ async def _get_workspace(
|
||||
return workspace
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
summary="Create instance from workspace",
|
||||
description="Create a new tool instance mounted on this workspace.",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_workspace_instance(
|
||||
workspace_id: uuid.UUID,
|
||||
data: CreateWorkspaceInstanceRequest,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Create a tool instance directly on a workspace."""
|
||||
workspace = await _get_workspace(session, workspace_id, user_id)
|
||||
|
||||
repo = await session.get(GitRepository, workspace.repo_id)
|
||||
if repo is None:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
if repo.project_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Repository is not associated with a project",
|
||||
)
|
||||
|
||||
request = CreateInstanceRequest(
|
||||
tool_type_id=data.tool_type_id,
|
||||
display_name=data.display_name,
|
||||
workspace_id=str(workspace.id),
|
||||
config_profile_id=data.config_profile_id,
|
||||
ssh_key_ids=data.ssh_key_ids,
|
||||
)
|
||||
|
||||
try:
|
||||
instance = await create_tool_instance(
|
||||
session, user_id, repo.project_id, repo.id, request
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
)
|
||||
|
||||
return {
|
||||
"id": str(instance.id),
|
||||
"name": instance.name,
|
||||
"display_name": instance.display_name,
|
||||
"tool_type_id": str(instance.tool_type_id),
|
||||
"status": instance.status,
|
||||
"workspace_id": str(instance.workspace_id) if instance.workspace_id else None,
|
||||
"selected_config_profile_id": str(instance.selected_config_profile_id)
|
||||
if instance.selected_config_profile_id
|
||||
else None,
|
||||
"created_at": instance.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_workspace_instances(
|
||||
workspace_id: uuid.UUID,
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||
from src.models import GitRepository
|
||||
from src.models import Project
|
||||
from src.models import ToolInstance
|
||||
from src.models import Workspace
|
||||
from src.services.shared.workspace_manager import WorkspaceHasInstancesError, WorkspaceManager
|
||||
@@ -36,12 +37,11 @@ async def list_all_workspaces(
|
||||
result = await session.execute(
|
||||
select(
|
||||
Workspace,
|
||||
GitRepository.name.label("repo_name"),
|
||||
GitRepository.project_id,
|
||||
GitRepository.ssh_key_id.label("repo_ssh_key_id"),
|
||||
GitRepository,
|
||||
instance_count.label("instance_count"),
|
||||
)
|
||||
.join(GitRepository, Workspace.repo_id == GitRepository.id)
|
||||
.options(selectinload(GitRepository.project))
|
||||
.where(Workspace.user_id == user_id)
|
||||
.order_by(Workspace.created_at.desc())
|
||||
)
|
||||
@@ -51,11 +51,11 @@ async def list_all_workspaces(
|
||||
{
|
||||
"id": str(ws.id),
|
||||
"name": ws.name,
|
||||
"repo_id": str(ws.repo_id),
|
||||
"repo_name": repo_name or "",
|
||||
"repo_ssh_key_id": str(ssh_key_id) if ssh_key_id else None,
|
||||
"project_id": str(project_id) if project_id else "",
|
||||
"project_name": "",
|
||||
"repo_id": str(repo.id),
|
||||
"repo_name": repo.name or "",
|
||||
"repo_ssh_key_id": str(repo.ssh_key_id) if repo.ssh_key_id else None,
|
||||
"project_id": str(repo.project_id) if repo.project_id else "",
|
||||
"project_name": repo.project.name if repo.project else "",
|
||||
"user_id": str(ws.user_id),
|
||||
"branch": ws.branch,
|
||||
"path": ws.path,
|
||||
@@ -65,7 +65,7 @@ async def list_all_workspaces(
|
||||
"updated_at": ws.updated_at.isoformat() if ws.updated_at else None,
|
||||
"instance_count": count or 0,
|
||||
}
|
||||
for ws, repo_name, project_id, ssh_key_id, count in rows
|
||||
for ws, repo, count in rows
|
||||
]
|
||||
|
||||
|
||||
@@ -130,17 +130,26 @@ async def create_workspace_top_level(
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Workspace name is required")
|
||||
|
||||
project = await session.get(Project, repo.project_id) if repo.project_id else None
|
||||
manager = WorkspaceManager()
|
||||
try:
|
||||
workspace = await manager.create(repo, user_id, name, branch, session=session)
|
||||
workspace = await manager.create(
|
||||
repo, user_id, name, branch, session=session, project=project
|
||||
)
|
||||
session.add(workspace)
|
||||
await session.commit()
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
await session.rollback()
|
||||
logger.error("Failed to create workspace: %s", exc)
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
await session.rollback()
|
||||
logger.error("Failed to create workspace: %s", exc)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Workspace name already exists for this repository",
|
||||
status_code=500,
|
||||
detail=f"Failed to create workspace: {exc}",
|
||||
) from exc
|
||||
|
||||
await session.refresh(workspace)
|
||||
@@ -227,9 +236,12 @@ async def create_workspace(
|
||||
if not branch:
|
||||
raise HTTPException(status_code=400, detail="Branch is required")
|
||||
|
||||
project = await session.get(Project, project_id)
|
||||
manager = WorkspaceManager()
|
||||
try:
|
||||
workspace = await manager.create(repo, user_id, name, branch, session=session)
|
||||
workspace = await manager.create(
|
||||
repo, user_id, name, branch, session=session, project=project
|
||||
)
|
||||
session.add(workspace)
|
||||
await session.commit()
|
||||
except HTTPException:
|
||||
@@ -242,8 +254,8 @@ async def create_workspace(
|
||||
await session.rollback()
|
||||
logger.error("Failed to create workspace: %s", exc)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Workspace name already exists for this repository",
|
||||
status_code=500,
|
||||
detail=f"Failed to create workspace: {exc}",
|
||||
) from exc
|
||||
|
||||
await session.refresh(workspace)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# apps/api/src/auth (index)
|
||||
dir: apps/api/src/auth
|
||||
|
||||
## role
|
||||
Provides authentication and authorization infrastructure for the API, including OIDC login, session management, and dependency injection for protected routes.
|
||||
## parent
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- cookies.py
|
||||
- dependencies.py
|
||||
- oidc.py
|
||||
- session.py
|
||||
## links
|
||||
index: apps/api/src/auth/.pi-map.index.md
|
||||
map: apps/api/src/auth/.pi-map.md
|
||||
## workflows
|
||||
- change auth behavior
|
||||
read: __init__.py, cookies.py, dependencies.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,31 @@
|
||||
# apps/api/src/auth
|
||||
dir: apps/api/src/auth
|
||||
|
||||
index: apps/api/src/auth/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides authentication and authorization infrastructure for the API, including OIDC login, session management, and dependency injection for protected routes.
|
||||
## files
|
||||
- __init__.py | Exposes authentication utility functions as a public API through module-level imports | dep: src.auth.cookies, src.auth.oidc, src.auth.session
|
||||
- cookies.py | Builds a dictionary of HTTP cookie options from application settings | exp: func:build_cookie_options(settings: Settings) → dict[str, str | bool | None] | dep: src.config, src.config.Settings
|
||||
- dependencies.py | Provides FastAPI dependency injection functions for database sessions, user authentication, and project ownership verification. | exp: func:get_db_session(), call:SessionLocal, func:get_current_user_id(session_cookie) → uuid.UUID, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str, raise:HTTPException, func:get_current_user(session_cookie, db_session) → User, call:Settings, call:decode_session_cookie, call:uuid.UUID, call:str, call:db_session.get, raise:HTTPException, func:_get_user(session: AsyncSession, user_id: uuid.UUID) → User, call:session.get, raise:HTTPException, func:_get_owned_project(project_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession) → "Project", call:session.get, raise:HTTPException | dep: uuid, typing, fastapi, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.database, src.models.project, src.models.user, sqlalchemy
|
||||
- oidc.py | Implements OIDC authentication flow with Authentik for login redirect URL construction, authorization code exchange, and user info retrieval. | exp: func:build_login_redirect_url(settings: Settings, redirect_uri: str, state: str) → str, call:urlencode, func:exchange_code_for_tokens(settings: Settings, code: str, redirect_uri: str, client: httpx.AsyncClient) → dict[str, str], call:client.post, call:response.raise_for_status, call:response.json, call:payload.get, func:fetch_user_info(settings: Settings, access_token: str, client: httpx.AsyncClient) → dict[str, Any], call:client.get, call:response.raise_for_status, call:response.json | dep: typing, urllib.parse, httpx, src.config
|
||||
- session.py | Creates and verifies signed JWT-like session cookies with HMAC-SHA256 signatures and expiration checks. | exp: func:_base64url_encode(data: bytes) → str, call:base64.urlsafe_b64encode(data).rstrip(b"=").decode, func:_base64url_decode(data: str) → bytes, call:len, call:base64.urlsafe_b64decode, func:create_session_cookie(settings: Settings, user_id: str) → str, call:int, call:(datetime.now(timezone.utc) + timedelta(hours=settings.session_ttl_hours)).timestamp, call:datetime.now, call:timedelta, call:_base64url_encode, call:json.dumps({"alg": "HS256", "typ": "session"}).encode, call:json.dumps(payload).encode, call:hmac.new( settings.session_secret.encode(), message.encode(), hashlib.sha256, ).digest, call:settings.session_secret.encode, call:message.encode, func:decode_session_cookie(settings: Settings, cookie_value: str) → dict[str, Any], call:cookie_value.split, call:len, call:hmac.new( settings.session_secret.encode(), message.encode(), hashlib.sha256, ).digest, call:settings.session_secret.encode, call:message.encode, call:_base64url_encode, call:hmac.compare_digest, call:_base64url_decode, call:json.loads, call:payload.get, call:int, call:datetime.now(timezone.utc).timestamp, raise:ValueError | dep: hmac, hashlib, json, base64, datetime, typing, src.config, src.config.Settings
|
||||
## arch
|
||||
Modular security layer using FastAPI dependency injection pattern, with separation of concerns across cookie handling, OIDC flow, session state, and reusable auth dependencies.
|
||||
## tags
|
||||
session, src, cookie, user, get, base64url, raise:httpexception, call:
|
||||
## symbols
|
||||
- build_cookie_options
|
||||
- get_db_session
|
||||
- get_current_user_id
|
||||
- get_current_user
|
||||
- _get_user
|
||||
- _get_owned_project
|
||||
- build_login_redirect_url
|
||||
- exchange_code_for_tokens
|
||||
## workflows
|
||||
- change auth behavior
|
||||
read: __init__.py, cookies.py, dependencies.py
|
||||
## dirty
|
||||
-
|
||||
@@ -47,6 +47,11 @@ class Settings(BaseSettings):
|
||||
session_secret: str = "change-me-session-secret"
|
||||
session_ttl_hours: int = 24
|
||||
|
||||
# Internal JWT configuration (used for service-to-service tokens)
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_ttl_minutes: int = 15
|
||||
refresh_token_ttl_days: int = 7
|
||||
|
||||
# Repository storage
|
||||
repo_base_path: str = "/data/repos"
|
||||
|
||||
@@ -122,7 +127,7 @@ class Settings(BaseSettings):
|
||||
@property
|
||||
def cookie_samesite(self) -> str:
|
||||
if self.app_env == "production":
|
||||
return "none"
|
||||
return "strict"
|
||||
|
||||
return "lax"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
@@ -54,10 +55,10 @@ async def init_database(
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: subprocess.run(
|
||||
["alembic", "upgrade", "head"],
|
||||
["python3", "-m", "alembic", "upgrade", "head"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd="/app",
|
||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from src.api.tool import (
|
||||
sessions_router,
|
||||
tool_definitions_router,
|
||||
tool_instances_router,
|
||||
tool_lifecycle_router,
|
||||
tool_types_router,
|
||||
)
|
||||
from src.api.user import auth_router, ssh_keys_router, users_router
|
||||
@@ -165,6 +166,7 @@ app.include_router(tool_types_router)
|
||||
app.include_router(tool_definitions_router)
|
||||
app.include_router(config_profiles_router)
|
||||
app.include_router(tool_instances_router)
|
||||
app.include_router(tool_lifecycle_router)
|
||||
app.include_router(sessions_router)
|
||||
app.include_router(instance_proxy_router)
|
||||
app.include_router(terminal_router)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# apps/api/src/models (index)
|
||||
dir: apps/api/src/models
|
||||
|
||||
## role
|
||||
Provides the foundational database model layer and unified public API for all SQLAlchemy ORM entities in the API application.
|
||||
## parent
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/models/config
|
||||
index: apps/api/src/models/config/.pi-map.index.md
|
||||
map: apps/api/src/models/config/.pi-map.md
|
||||
- apps/api/src/models/project
|
||||
index: apps/api/src/models/project/.pi-map.index.md
|
||||
map: apps/api/src/models/project/.pi-map.md
|
||||
- apps/api/src/models/system
|
||||
index: apps/api/src/models/system/.pi-map.index.md
|
||||
map: apps/api/src/models/system/.pi-map.md
|
||||
- apps/api/src/models/tool
|
||||
index: apps/api/src/models/tool/.pi-map.index.md
|
||||
map: apps/api/src/models/tool/.pi-map.md
|
||||
- apps/api/src/models/user
|
||||
index: apps/api/src/models/user/.pi-map.index.md
|
||||
map: apps/api/src/models/user/.pi-map.md
|
||||
## files
|
||||
- __init__.py
|
||||
- base.py
|
||||
## links
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## workflows
|
||||
- change models behavior
|
||||
read: __init__.py, base.py
|
||||
- explore models subdirectories
|
||||
index: apps/api/src/models/config/.pi-map.index.md, apps/api/src/models/project/.pi-map.index.md, apps/api/src/models/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/src/models
|
||||
dir: apps/api/src/models
|
||||
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides the foundational database model layer and unified public API for all SQLAlchemy ORM entities in the API application.
|
||||
## files
|
||||
- __init__.py | Re-exports database models from submodules to provide a unified public API for the models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
|
||||
- base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm
|
||||
## arch
|
||||
Implements a layered model architecture using SQLAlchemy declarative base with reusable mixins (UUID primary keys, timestamp tracking) and a facade pattern via `__init__.py` to centralize model imports.
|
||||
## tags
|
||||
models, src, base, project, system, user, mixin, tool
|
||||
## symbols
|
||||
- Base
|
||||
- UUIDPrimaryKeyMixin
|
||||
- TimestampMixin
|
||||
## workflows
|
||||
- change models behavior
|
||||
read: __init__.py, base.py
|
||||
- explore models subdirectories
|
||||
index: apps/api/src/models/config/.pi-map.index.md, apps/api/src/models/project/.pi-map.index.md, apps/api/src/models/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/models/config (index)
|
||||
dir: apps/api/src/models/config
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for configuration profiles that store deployment settings like environment variables, mounts, and runtime hints.
|
||||
## parent
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- config_profile.py
|
||||
## links
|
||||
index: apps/api/src/models/config/.pi-map.index.md
|
||||
map: apps/api/src/models/config/.pi-map.md
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profile.py
|
||||
- change config config
|
||||
read: config_profile.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,24 @@
|
||||
# apps/api/src/models/config
|
||||
dir: apps/api/src/models/config
|
||||
|
||||
index: apps/api/src/models/config/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for configuration profiles that store deployment settings like environment variables, mounts, and runtime hints.
|
||||
## files
|
||||
- __init__.py | Exports config model classes from a subpackage for simplified importing | dep: src.models.config.config_profile
|
||||
- config_profile.py | Defines SQLAlchemy ORM models for configuration profiles and their include relationships, storing deployment settings like environment variables, mounts, and runtime hints. | exp: class:ConfigProfile, class:ConfigProfileInclude | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.user, src.models.ToolType
|
||||
## arch
|
||||
Package-level facade pattern with __init__.py exposing subpackage classes; domain model using SQLAlchemy ORM with relationship-based composition for profile includes.
|
||||
## tags
|
||||
config, models, src, profile, sqlalchemy, include, init, orm
|
||||
## symbols
|
||||
- ConfigProfile
|
||||
- ConfigProfileInclude
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profile.py
|
||||
- change config config
|
||||
read: config_profile.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/models/project (index)
|
||||
dir: apps/api/src/models/project
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for core project-related domain entities (GitRepository, Project, Workspace) with their relationships.
|
||||
## parent
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- git_repository.py
|
||||
- project.py
|
||||
- workspace.py
|
||||
## links
|
||||
index: apps/api/src/models/project/.pi-map.index.md
|
||||
map: apps/api/src/models/project/.pi-map.md
|
||||
## workflows
|
||||
- change project behavior
|
||||
read: __init__.py, git_repository.py, project.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/src/models/project
|
||||
dir: apps/api/src/models/project
|
||||
|
||||
index: apps/api/src/models/project/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for core project-related domain entities (GitRepository, Project, Workspace) with their relationships.
|
||||
## files
|
||||
- __init__.py | Exports the core project model classes (GitRepository, Project, Workspace) for the models package | dep: src.models.project.git_repository, src.models.project.project, src.models.project.workspace
|
||||
- git_repository.py | Defines a SQLAlchemy ORM model for Git repositories with relationships to projects, users, and SSH keys. | exp: class:GitRepository | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.user, src.models.SSHKey
|
||||
- project.py | Defines a SQLAlchemy ORM model for projects with relationships to users, Git repositories, and SSH keys. | exp: class:Project | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.user, src.models.GitRepository, src.models.SSHKey, src.models.user.User
|
||||
- workspace.py | Defines a SQLAlchemy ORM model for persistent writable Git repository clones associated with users | exp: class:Workspace | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.user, src.models.GitRepository
|
||||
## arch
|
||||
Data access layer using SQLAlchemy ORM with declarative models, explicit table relationships (many-to-many and foreign keys), and package-level facade pattern via __init__.py exports.
|
||||
## tags
|
||||
models, src, project, sqlalchemy, git, workspace, orm, repository
|
||||
## symbols
|
||||
- GitRepository
|
||||
- Project
|
||||
- Workspace
|
||||
## workflows
|
||||
- change project behavior
|
||||
read: __init__.py, git_repository.py, project.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,24 @@
|
||||
# apps/api/src/models/system (index)
|
||||
dir: apps/api/src/models/system
|
||||
|
||||
## role
|
||||
Provides system-level SQLAlchemy ORM models for operational concerns including health monitoring, audit logging, notifications, and terminal session management.
|
||||
## parent
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- health_check.py
|
||||
- instance_event.py
|
||||
- notification.py
|
||||
- terminal_session.py
|
||||
## links
|
||||
index: apps/api/src/models/system/.pi-map.index.md
|
||||
map: apps/api/src/models/system/.pi-map.md
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, health_check.py, instance_event.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,27 @@
|
||||
# apps/api/src/models/system
|
||||
dir: apps/api/src/models/system
|
||||
|
||||
index: apps/api/src/models/system/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides system-level SQLAlchemy ORM models for operational concerns including health monitoring, audit logging, notifications, and terminal session management.
|
||||
## files
|
||||
- __init__.py | Package initialization file that exports system-related model classes for the models.system module | dep: src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session
|
||||
- health_check.py | Defines a SQLAlchemy ORM model for storing health check snapshots of tool instances with various status and probe fields. | exp: class:HealthCheck | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm, src.models.base
|
||||
- instance_event.py | Defines a SQLAlchemy model for auditing instance lifecycle events with metadata, status tracking, and user attribution. | exp: class:InstanceEvent | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base
|
||||
- notification.py | Defines a SQLAlchemy ORM model for storing user notifications with support for categorization, severity levels, read/dismissed tracking, and polymorphic source references. | exp: class:Notification | dep: datetime, typing, uuid, sqlalchemy, sqlalchemy.orm, sqlalchemy.sql, src.models.base
|
||||
- terminal_session.py | Defines a SQLAlchemy database model for storing terminal session metadata with lifecycle tracking. | exp: class:TerminalSessionModel | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm, src.models.base
|
||||
## arch
|
||||
Standard SQLAlchemy declarative model pattern with polymorphic associations (notification sources), lifecycle state tracking, and timestamped audit fields across all models.
|
||||
## tags
|
||||
sqlalchemy, models, src, model, system, orm, notification, health
|
||||
## symbols
|
||||
- HealthCheck
|
||||
- InstanceEvent
|
||||
- Notification
|
||||
- TerminalSessionModel
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, health_check.py, instance_event.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/models/tool (index)
|
||||
dir: apps/api/src/models/tool
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
|
||||
## parent
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- tool_definition_manifest.py
|
||||
- tool_instance.py
|
||||
- tool_type.py
|
||||
## links
|
||||
index: apps/api/src/models/tool/.pi-map.index.md
|
||||
map: apps/api/src/models/tool/.pi-map.md
|
||||
## workflows
|
||||
- change tool behavior
|
||||
read: __init__.py, tool_definition_manifest.py, tool_instance.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/src/models/tool
|
||||
dir: apps/api/src/models/tool
|
||||
|
||||
index: apps/api/src/models/tool/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
|
||||
## files
|
||||
- __init__.py | Package initialization file that exports the public API for the tool models module. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type
|
||||
- tool_definition_manifest.py | Defines a SQLAlchemy model for tool definition manifests that can represent either base Docker images or tool definitions that inherit from bases, storing manifest JSON and cached Dockerfile/Compose outputs. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
|
||||
- tool_instance.py | Defines a SQLAlchemy ORM model representing a deployed tool instance with container/runtime metadata and relationships to users, projects, and workspaces. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace)
|
||||
- tool_type.py | Defines a SQLAlchemy ORM model for tool types that specify metadata, configuration, and deployment templates for containerized tools. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user
|
||||
## arch
|
||||
Uses SQLAlchemy ORM with declarative models, inheritance patterns (base vs. derived tool definitions), and relational associations linking tools to users, projects, and workspaces.
|
||||
## tags
|
||||
tool, models, src, sqlalchemy, definition, manifest, orm, instance
|
||||
## symbols
|
||||
- ToolDefinitionManifest
|
||||
- ToolInstance
|
||||
- ToolType
|
||||
## workflows
|
||||
- change tool behavior
|
||||
read: __init__.py, tool_definition_manifest.py, tool_instance.py
|
||||
## dirty
|
||||
-
|
||||
@@ -53,9 +53,11 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
)
|
||||
image_tag: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
probe_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
clone_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="mount")
|
||||
clone_mode: Mapped[str | None] = mapped_column(
|
||||
String(20), nullable=True, default=None
|
||||
)
|
||||
branch: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, default="main"
|
||||
String(255), nullable=True, default=None
|
||||
)
|
||||
selected_config_profile_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
|
||||
|
||||
@@ -39,6 +39,9 @@ class ToolType(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
)
|
||||
readiness_probe: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
startup_command: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
home_directory: Mapped[str] = mapped_column(
|
||||
String(255), nullable=False, default="/home/user"
|
||||
)
|
||||
required_variables: Mapped[list[str]] = mapped_column(
|
||||
JSON, default=list, nullable=False
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/src/models/user (index)
|
||||
dir: apps/api/src/models/user
|
||||
|
||||
## role
|
||||
Database models package defining user-related entities including users, SSH keys, and user configuration with their relationships.
|
||||
## parent
|
||||
index: apps/api/src/models/.pi-map.index.md
|
||||
map: apps/api/src/models/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- ssh_key.py
|
||||
- user.py
|
||||
- user_config.py
|
||||
## links
|
||||
index: apps/api/src/models/user/.pi-map.index.md
|
||||
map: apps/api/src/models/user/.pi-map.md
|
||||
## workflows
|
||||
- change user behavior
|
||||
read: __init__.py, ssh_key.py, user.py
|
||||
- change user config
|
||||
read: user_config.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,27 @@
|
||||
# apps/api/src/models/user
|
||||
dir: apps/api/src/models/user
|
||||
|
||||
index: apps/api/src/models/user/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Database models package defining user-related entities including users, SSH keys, and user configuration with their relationships.
|
||||
## files
|
||||
- __init__.py | Exports user-related model classes from a package for clean external imports. | dep: src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
|
||||
- ssh_key.py | Defines an SSHKey database model for storing encrypted SSH key pairs with associations to users and optional projects. | exp: class:SSHKey | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models.user
|
||||
- user.py | Defines a SQLAlchemy ORM model for a User entity with authentication fields and relationships to projects, SSH keys, and user configuration. | exp: class:User | dep: typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.project, src.models, src.models.SSHKey, src.models.UserConfig
|
||||
- user_config.py | Defines a SQLAlchemy model for storing per-user JSON configuration data with typed property accessors for profile IDs. | exp: class:UserConfig | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
|
||||
## arch
|
||||
SQLAlchemy ORM with declarative models, one-to-many relationships (User→SSHKey, User→Project, User→UserConfig), encrypted field storage for SSH keys, and JSON-typed configuration with property accessors for schema flexibility.
|
||||
## tags
|
||||
user, src, models, sqlalchemy, model, sshkey, ssh, orm
|
||||
## symbols
|
||||
- SSHKey
|
||||
- User
|
||||
- UserConfig
|
||||
## workflows
|
||||
- change user behavior
|
||||
read: __init__.py, ssh_key.py, user.py
|
||||
- change user config
|
||||
read: user_config.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,36 @@
|
||||
# apps/api/src/schemas (index)
|
||||
dir: apps/api/src/schemas
|
||||
|
||||
## role
|
||||
Provides data validation schemas for the API application
|
||||
## parent
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/schemas/config
|
||||
index: apps/api/src/schemas/config/.pi-map.index.md
|
||||
map: apps/api/src/schemas/config/.pi-map.md
|
||||
- apps/api/src/schemas/project
|
||||
index: apps/api/src/schemas/project/.pi-map.index.md
|
||||
map: apps/api/src/schemas/project/.pi-map.md
|
||||
- apps/api/src/schemas/system
|
||||
index: apps/api/src/schemas/system/.pi-map.index.md
|
||||
map: apps/api/src/schemas/system/.pi-map.md
|
||||
- apps/api/src/schemas/tool
|
||||
index: apps/api/src/schemas/tool/.pi-map.index.md
|
||||
map: apps/api/src/schemas/tool/.pi-map.md
|
||||
- apps/api/src/schemas/user
|
||||
index: apps/api/src/schemas/user/.pi-map.index.md
|
||||
map: apps/api/src/schemas/user/.pi-map.md
|
||||
## files
|
||||
- __init__.py
|
||||
## links
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
## workflows
|
||||
- change schemas behavior
|
||||
read: __init__.py
|
||||
- explore schemas subdirectories
|
||||
index: apps/api/src/schemas/config/.pi-map.index.md, apps/api/src/schemas/project/.pi-map.index.md, apps/api/src/schemas/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,22 @@
|
||||
# apps/api/src/schemas
|
||||
dir: apps/api/src/schemas
|
||||
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides data validation schemas for the API application
|
||||
## files
|
||||
- __init__.py | Swaps two tmux panes between windows, preserving active pane state and layout | dep: tmux
|
||||
## arch
|
||||
Package initialization with modular schema definitions using Pydantic or similar validation framework
|
||||
## tags
|
||||
tmux, init, swaps, two, panes, windows, preserving, active
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
- change schemas behavior
|
||||
read: __init__.py
|
||||
- explore schemas subdirectories
|
||||
index: apps/api/src/schemas/config/.pi-map.index.md, apps/api/src/schemas/project/.pi-map.index.md, apps/api/src/schemas/system/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/schemas/config (index)
|
||||
dir: apps/api/src/schemas/config
|
||||
|
||||
## role
|
||||
Provides Pydantic schemas for configuration profile API operations with validated request/response models.
|
||||
## parent
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- config_profile.py
|
||||
## links
|
||||
index: apps/api/src/schemas/config/.pi-map.index.md
|
||||
map: apps/api/src/schemas/config/.pi-map.md
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profile.py
|
||||
- change config config
|
||||
read: config_profile.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,30 @@
|
||||
# apps/api/src/schemas/config
|
||||
dir: apps/api/src/schemas/config
|
||||
|
||||
index: apps/api/src/schemas/config/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides Pydantic schemas for configuration profile API operations with validated request/response models.
|
||||
## files
|
||||
- __init__.py | Re-exports config profile schema classes from a submodule to provide a unified public interface for the config schemas module. | dep: src.schemas.config.config_profile
|
||||
- config_profile.py | Defines Pydantic request/response schemas for configuration profile API operations with comprehensive input validation. | exp: class:GitMountMapping, class:GitMountItem, class:MountItem, class:ConfigProfileCreate, class:ConfigProfileUpdate, class:ConfigProfileIncludeUpdate, class:ConfigProfileResponse, class:DefaultProfilesUpdate, class:ValidateGitUrlRequest, class:ValidateGitUrlResponse, func:_validate_uuid(v: str | None) → str | None, call:uuid.UUID, raise:ValueError | dep: uuid, pydantic, src.api.shared_validators
|
||||
## arch
|
||||
Layered schema module using submodule organization with clean public interface via __init__.py re-exports, following Pydantic-based validation pattern for API contract definitions.
|
||||
## tags
|
||||
config, profile, git, mount, update, validate, response, item
|
||||
## symbols
|
||||
- GitMountMapping
|
||||
- GitMountItem
|
||||
- MountItem
|
||||
- ConfigProfileCreate
|
||||
- ConfigProfileUpdate
|
||||
- ConfigProfileIncludeUpdate
|
||||
- ConfigProfileResponse
|
||||
- DefaultProfilesUpdate
|
||||
## workflows
|
||||
- change config behavior
|
||||
read: __init__.py, config_profile.py
|
||||
- change config config
|
||||
read: config_profile.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,23 @@
|
||||
# apps/api/src/schemas/project (index)
|
||||
dir: apps/api/src/schemas/project
|
||||
|
||||
## role
|
||||
Provides Pydantic data validation schemas for project-related API endpoints, serving as the contract layer between API requests/responses and internal business logic.
|
||||
## parent
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- git_repository.py
|
||||
- project.py
|
||||
- ssh_key.py
|
||||
## links
|
||||
index: apps/api/src/schemas/project/.pi-map.index.md
|
||||
map: apps/api/src/schemas/project/.pi-map.md
|
||||
## workflows
|
||||
- change project behavior
|
||||
read: __init__.py, git_repository.py, project.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,30 @@
|
||||
# apps/api/src/schemas/project
|
||||
dir: apps/api/src/schemas/project
|
||||
|
||||
index: apps/api/src/schemas/project/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides Pydantic data validation schemas for project-related API endpoints, serving as the contract layer between API requests/responses and internal business logic.
|
||||
## files
|
||||
- __init__.py | Re-exports Pydantic schema classes from submodules to provide a unified public API for project-related data models. | dep: src.schemas.project.git_repository, src.schemas.project.project, src.schemas.project.ssh_key
|
||||
- git_repository.py | Defines Pydantic request/response schemas for Git repository operations in an API. | exp: class:GitRepositoryCreate, class:URLParseRequest, class:URLParseResponse, class:GitRepositoryResponse, class:UpdateSSHKeyRequest, class:FileListResponse, class:FileContentResponse, class:BranchesResponse, class:FileUpdateRequest, class:FileUpdateResponse, class:StatusResponse, class:BranchCreateRequest, class:CheckoutRequest, class:CommitRequest, class:CommitResponse, class:FetchResponse, class:PullResponse, class:PushResponse, class:MergeRequest, class:MergeResponse | dep: uuid, datetime, pydantic
|
||||
- project.py | Defines Pydantic request/response schemas for project-related API operations including creation, updates, responses, and default SSH key assignment. | exp: class:ProjectCreate, class:ProjectUpdate, class:ProjectResponse, class:SetDefaultSSHKeyRequest | dep: uuid, pydantic
|
||||
- ssh_key.py | Defines Pydantic request/response schemas for SSH key management and signature operations in an API. | exp: class:SSHKeyCreate, class:SSHKeyResponse, class:SignPayloadRequest, class:SignatureResponse, class:VerifySignatureRequest, class:VerifySignatureResponse | dep: uuid, datetime, pydantic
|
||||
## arch
|
||||
Modular schema-per-domain pattern with clean separation of concerns (git repositories, projects, SSH keys) and centralized public API via `__init__.py` re-exports, following standard Pydantic model hierarchy with explicit request/response bifurcation.
|
||||
## tags
|
||||
response, request, project, create, update, sshkey, pydantic, git
|
||||
## symbols
|
||||
- GitRepositoryCreate
|
||||
- URLParseRequest
|
||||
- URLParseResponse
|
||||
- GitRepositoryResponse
|
||||
- UpdateSSHKeyRequest
|
||||
- FileListResponse
|
||||
- FileContentResponse
|
||||
- BranchesResponse
|
||||
## workflows
|
||||
- change project behavior
|
||||
read: __init__.py, git_repository.py, project.py
|
||||
## dirty
|
||||
-
|
||||
@@ -45,3 +45,91 @@ class GitRepositoryResponse(BaseModel):
|
||||
|
||||
class UpdateSSHKeyRequest(BaseModel):
|
||||
ssh_key_id: str | None = None
|
||||
|
||||
|
||||
class FileListResponse(BaseModel):
|
||||
path: str
|
||||
branch: str
|
||||
entries: list[dict]
|
||||
|
||||
|
||||
class FileContentResponse(BaseModel):
|
||||
path: str
|
||||
branch: str
|
||||
content: str
|
||||
size: int
|
||||
encoding: str
|
||||
language: str | None
|
||||
is_binary: bool
|
||||
last_commit: dict | None
|
||||
|
||||
|
||||
class BranchesResponse(BaseModel):
|
||||
branches: list[dict]
|
||||
default_branch: str
|
||||
|
||||
|
||||
class FileUpdateRequest(BaseModel):
|
||||
path: str
|
||||
branch: str
|
||||
content: str
|
||||
commit_message: str
|
||||
|
||||
|
||||
class FileUpdateResponse(BaseModel):
|
||||
commit_hash: str
|
||||
message: str
|
||||
branch: str
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
branch: str
|
||||
modified: list[str]
|
||||
added: list[str]
|
||||
deleted: list[str]
|
||||
untracked: list[str]
|
||||
renamed: list[str]
|
||||
ahead: int
|
||||
behind: int
|
||||
|
||||
|
||||
class BranchCreateRequest(BaseModel):
|
||||
name: str
|
||||
base_branch: str = "HEAD"
|
||||
|
||||
|
||||
class CheckoutRequest(BaseModel):
|
||||
branch: str
|
||||
|
||||
|
||||
class CommitRequest(BaseModel):
|
||||
message: str
|
||||
files: list[str] | None = None
|
||||
|
||||
|
||||
class CommitResponse(BaseModel):
|
||||
commit_hash: str
|
||||
message: str
|
||||
|
||||
|
||||
class FetchResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class PullResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class PushResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class MergeRequest(BaseModel):
|
||||
source_branch: str
|
||||
target_branch: str | None = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class MergeResponse(BaseModel):
|
||||
commit_hash: str
|
||||
message: str
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# apps/api/src/schemas/system (index)
|
||||
dir: apps/api/src/schemas/system
|
||||
|
||||
## role
|
||||
Provides Pydantic data models for system health monitoring API responses, enabling standardized health check serialization across the API.
|
||||
## parent
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- health.py
|
||||
## links
|
||||
index: apps/api/src/schemas/system/.pi-map.index.md
|
||||
map: apps/api/src/schemas/system/.pi-map.md
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, health.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/src/schemas/system
|
||||
dir: apps/api/src/schemas/system
|
||||
|
||||
index: apps/api/src/schemas/system/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides Pydantic data models for system health monitoring API responses, enabling standardized health check serialization across the API.
|
||||
## files
|
||||
- __init__.py | Re-exports health-related schema classes from a submodule to provide a unified interface for the system schemas module. | dep: src.schemas.system.health
|
||||
- health.py | Defines Pydantic data models for health check API responses including database, disk, and overall system health metrics. | exp: class:DatabaseHealth, class:DiskHealth, class:HealthChecks, class:HealthResponse, class:DatabaseHealthResponse | dep: pydantic
|
||||
## arch
|
||||
Simple submodule-based schema organization with explicit re-exports in __init__.py following a flat namespace pattern for cleaner imports.
|
||||
## tags
|
||||
health, database, response, system, disk, schemas, init, pydantic
|
||||
## symbols
|
||||
- DatabaseHealth
|
||||
- DiskHealth
|
||||
- HealthChecks
|
||||
- HealthResponse
|
||||
- DatabaseHealthResponse
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, health.py
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,22 @@
|
||||
# apps/api/src/schemas/tool (index)
|
||||
dir: apps/api/src/schemas/tool
|
||||
|
||||
## role
|
||||
Provides Pydantic data validation schemas for tool-related API request/response models.
|
||||
## parent
|
||||
index: apps/api/src/schemas/.pi-map.index.md
|
||||
map: apps/api/src/schemas/.pi-map.md
|
||||
## children
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- tool_instance.py
|
||||
- tool_type.py
|
||||
## links
|
||||
index: apps/api/src/schemas/tool/.pi-map.index.md
|
||||
map: apps/api/src/schemas/tool/.pi-map.md
|
||||
## workflows
|
||||
- change tool behavior
|
||||
read: __init__.py, tool_instance.py, tool_type.py
|
||||
## dirty
|
||||
-
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user