Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| add7c1b500 | |||
| f9f9372ee7 | |||
| ea42165ed2 | |||
| c178225c8b | |||
| b5e961ebe9 | |||
| 5d379c5f8b | |||
| 671540ded9 | |||
| ad26fd9f35 | |||
| 415aecc0dd | |||
| bb38b37ceb | |||
| 6698c20f25 | |||
| 5e06a2a226 | |||
| 49180e4c6d | |||
| 7a5538b53f | |||
| 41d24beade | |||
| d1777b88ad | |||
| 691bbaa87b | |||
| 6191565e80 | |||
| 468f34202a | |||
| 996a858892 | |||
| ff7fcb6e8d | |||
| 3f06224b75 | |||
| a1a77c99a6 | |||
| 3c96c7b153 | |||
| 10955dfe8e | |||
| 6d7f538a79 | |||
| b32fea671f | |||
| 19f91c085e | |||
| 155d950556 | |||
| 9f720930ea | |||
| 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": "e7b3130f52a328d4051e75364e5394ac63df60c6"
|
||||
}
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->
|
||||
|
||||
Last updated: 2026-06-02
|
||||
Last updated: 2026-06-17
|
||||
|
||||
## 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 | `/home/user/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/user/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/user/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/user/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/user/headquarter/.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,82 @@
|
||||
# . (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
|
||||
Package .
|
||||
## 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
|
||||
- uploads
|
||||
index: uploads/.pi-map.index.md
|
||||
map: uploads/.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
|
||||
Package .
|
||||
## files
|
||||
- .env.example | Provides example environment variable configuration for a full-stack application with database, caching, authentication, and deployment settings
|
||||
- .gitignore | Specifies files and directories for Git to ignore across a project using Beads/Dolt, Python, Node, and various IDE/OS tooling. | dep: git, dolt, beads, python, node, npm, yarn, pnpm, pytest, mypy, ruff, coverage
|
||||
- AGENTS.md | Defines operational guidelines and workflows for AI agents collaborating on a software project governed by OpenSpec | dep: OpenSpec, superpowers (brainstorming, writing-plans, test-driven-development, systematic-debugging, verification-before-completion, using-git-worktrees, dispatching-parallel-agents), git
|
||||
- CHANGELOG.md | Documents version history and notable changes for a project management and Git repository application
|
||||
- Makefile | Provides standardized development commands for managing a Docker-based full-stack application with API, web frontend, database, and testing infrastructure | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
|
||||
- README.md | Documentation for a self-hosted development platform that manages 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 application (PostgreSQL, Redis, web frontend, API) behind an existing Traefik reverse proxy with TLS termination | dep: docker, docker-compose, traefik, postgres, redis, node/vite, python/fastapi
|
||||
- docker-compose.yml | Defines a multi-service Docker Compose stack for a web application with PostgreSQL, Redis, API backend, and web frontend services | dep: Docker, Docker Compose, PostgreSQL, Redis, Vite, asyncpg, Python/FastAPI (implied), Node.js (implied)
|
||||
- progress.md | Documents the progress and remaining tasks for a backend-frontend refactoring project involving modularization, code reorganization, and verification.
|
||||
- swap-pane | Provides a command to swap the position of two tmux panes within a window or between windows | dep: tmux, client, window, layout, cmd-find, cmd-parse, options
|
||||
- ui-audit-spacing-typography.md | A detailed audit report identifying critical CSS styling gaps, mobile layout failures, and design system inconsistencies in a web application's UI components and stylesheets. | dep: React/TSX components, CSS stylesheets (global.css, utilities.css, tokens.css, page-specific CSS), JSX/TSX files in apps/web/src/components and apps/web/src/pages
|
||||
- ui-rework-foundations-apply.md | Documents the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, including design token expansion, primitive CSS class additions, component refactors, and OpenSpec documentation. | dep: CSS design tokens, React/TSX components, OpenSpec documentation system, Git/SSH, npm build toolchain
|
||||
- ui-rework-pass2-apply.md | Documents the implementation and verification of Pass 2 of a web UI spacing/typography rework, refactoring inline styles into utility classes and design tokens across six components. | dep: CSS custom properties, BEM methodology, utility-first CSS, React/TSX components, OpenSpec documentation system
|
||||
## arch
|
||||
Contains 13 files.
|
||||
## tags
|
||||
docker, web, git, application, ui, rework, python, redis
|
||||
## 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
|
||||
Package apps
|
||||
## 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
|
||||
Package apps
|
||||
## files
|
||||
## arch
|
||||
Contains 0 files.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,54 @@
|
||||
# apps/api (index)
|
||||
dir: apps/api
|
||||
|
||||
## role
|
||||
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
|
||||
## parent
|
||||
index: apps/.pi-map.index.md
|
||||
map: apps/.pi-map.md
|
||||
## children
|
||||
- apps/api/.mypy_cache
|
||||
index: apps/api/.mypy_cache/.pi-map.index.md
|
||||
map: apps/api/.mypy_cache/.pi-map.md
|
||||
- apps/api/.pi-lens
|
||||
index: apps/api/.pi-lens/.pi-map.index.md
|
||||
map: apps/api/.pi-lens/.pi-map.md
|
||||
- apps/api/.pytest_cache
|
||||
index: apps/api/.pytest_cache/.pi-map.index.md
|
||||
map: apps/api/.pytest_cache/.pi-map.md
|
||||
- apps/api/.ruff_cache
|
||||
index: apps/api/.ruff_cache/.pi-map.index.md
|
||||
map: apps/api/.ruff_cache/.pi-map.md
|
||||
- apps/api/alembic
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
map: apps/api/alembic/.pi-map.md
|
||||
- apps/api/app
|
||||
index: apps/api/app/.pi-map.index.md
|
||||
map: apps/api/app/.pi-map.md
|
||||
- apps/api/headquarter_api.egg-info
|
||||
index: apps/api/headquarter_api.egg-info/.pi-map.index.md
|
||||
map: apps/api/headquarter_api.egg-info/.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
|
||||
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
|
||||
## files
|
||||
- .dockerignore | Specifies files and directories to exclude from the Docker build context to optimize image build times and prevent sensitive or unnecessary files from being included.
|
||||
- Dockerfile | Multi-stage Dockerfile that builds and runs a Python application with Docker CLI access, cloudflared, and database readiness checks. | dep: python:3.11-slim, libpq5, git, openssh-client, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, netcat-openbsd
|
||||
- README.md | Provides comprehensive documentation for the Headquarter API, a self-hosted platform for managing projects, git repositories, and development tools. | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Authentik, Pydantic, Ruff, mypy, pytest
|
||||
- alembic.ini | Configuration file for Alembic database migration tool, defining database connection and logging settings. | dep: alembic, sqlalchemy, asyncpg, postgresql
|
||||
- pyproject.toml | Defines project metadata, dependencies, and tool configuration for the Headquarter platform API. | dep: fastapi, uvicorn, sqlalchemy, asyncpg, alembic, pydantic, pydantic-settings, httpx, structlog, cryptography, pytest, mypy, ruff
|
||||
- uv.lock | This file is a UV lockfile that pins exact versions, hashes, and metadata for all Python project dependencies to ensure reproducible environments. | dep: uv, aiosqlite, alembic, annotated-types, anyio, asyncpg, sqlalchemy, mako
|
||||
- wait-for-db.sh | Polls a PostgreSQL host/port until it is available or a retry limit is reached, then executes the passed command. | dep: nc, sleep
|
||||
## arch
|
||||
Modern Python async API using FastAPI/Starlette with SQLAlchemy ORM, Alembic migrations, multi-stage Docker containerization with cloudflared tunneling, UV package management, and PostgreSQL database with health-checked startup orchestration.
|
||||
## tags
|
||||
alembic, docker, sqlalchemy, asyncpg, dockerfile, database, postgresql, pydantic
|
||||
## symbols
|
||||
-
|
||||
## workflows
|
||||
-
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,25 @@
|
||||
# apps/api/alembic (index)
|
||||
dir: apps/api/alembic
|
||||
|
||||
## role
|
||||
Database migration tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy 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 tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy 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 Python migration files for SQLAlchemy database schema changes | dep: alembic, sqlalchemy, mako
|
||||
## arch
|
||||
Template-based migration generation using Alembic's standard env.py configuration pattern with async SQLAlchemy engine integration and Mako templating for migration script scaffolding.
|
||||
## tags
|
||||
migrations, run, sqlalchemy, async, 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,78 @@
|
||||
# apps/api/alembic/versions (index)
|
||||
dir: apps/api/alembic/versions
|
||||
|
||||
## role
|
||||
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
|
||||
## parent
|
||||
index: apps/api/alembic/.pi-map.index.md
|
||||
map: apps/api/alembic/.pi-map.md
|
||||
## children
|
||||
- apps/api/alembic/versions/.ruff_cache
|
||||
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||
map: apps/api/alembic/versions/.ruff_cache/.pi-map.md
|
||||
## 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
|
||||
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.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
|
||||
- explore versions subdirectories
|
||||
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,83 @@
|
||||
# apps/api/alembic/versions
|
||||
dir: apps/api/alembic/versions
|
||||
|
||||
index: apps/api/alembic/versions/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
|
||||
## files
|
||||
- 0001_initial_schema.py | Alembic database migration that creates the initial schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) for a Git/SSH management application. | 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 secure 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, sqlalchemy.dialects.postgresql
|
||||
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage per user | 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 tracking fields | 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 foreign key relationships to tool_types, git_repositories, projects, and users tables, plus indexes for common query patterns. | 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, sqlalchemy.dialects.postgresql
|
||||
- 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 with UUID primary keys, foreign keys to users/tool_types/projects, key-value configuration storage, and supporting indexes. | 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 tunnel-related fields (public_url and tunnel_id) 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
|
||||
- 0012_default_port_req.py | Alembic database migration that populates 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 | Alembic database migration that adds config_profiles, config_includes, config_mounts tables and a selected_profile_id column to tool_instances with defensive checks for idempotent execution | 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 | Database migration to add 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 to config_profiles and config_mounts tables with idempotent schema 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:_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 reconciles two divergent migration branches without applying schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with a single `interface_type` string column and adds a `requires_port` boolean column, with dialect-specific SQL for PostgreSQL and SQLite data migration. | 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 | Adds a database migration that inserts a new "pi-agent" tool type into a tool_types table, defining a terminal-based Docker development environment with nvim, ranger, and tmux. | 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 table and clone_mode/branch columns to tool_instances table | 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 a downgrade path 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 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 | Database migration to add 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 table to support external repositories and expands alembic_version version_num column to VARCHAR(64). | 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 | Creates two database tables (instance_events and health_checks) for monitoring tool instances via an Alembic migration. | 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, including indexes and audit timestamps | 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 to a manifest-based definition with a base Ubuntu image. | 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 | Alembic 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 | Creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed states and optimized querying via partial 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 incorrect `--host` flags with proper `--bind-addr 0.0.0.0:PORT` configurations in both database-stored 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 web tool (code-server and jupyter-notebook) compose templates to bind to 0.0.0.0 for network accessibility. | 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 from LinuxServer.io (LSIO) code-server Docker Compose templates in both database tool_types records and on-disk instance 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 the ssh_keys mount from the pi-agent manifest in the tool_definition_manifests table, with downgrade support to restore it. | 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 foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column 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 that makes the `clone_mode` column in the `tool_instances` table nullable to allow NULL values for new rows. | 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 | An Alembic database migration that adds a `home_directory` column to `tool_types` table and updates template strings from `/workspace` to a configurable `/home/user/{{WORKSPACE_NAME}}` 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 fixes the pi-agent tool definition manifest's home directory mount path from /workspace to ~/{{WORKSPACE_NAME}} with a compatibility symlink and updated startup script. | 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 the pi-agent tool definition manifest and restores them on downgrade | 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
|
||||
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.py | Alembic database migration that updates the pi-agent tool definition manifest to remove the /workspace symlink dependency and adjust working directory and startup script accordingly. | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[str | None, 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.setdefault, call:runtime.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:_update_manifest | dep: collections.abc, 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 validation 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 merge migration that combines two parallel migration branches (remove_is_builtin and add_config_profiles) into a single revision history | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Merges two Alembic migration branches (profile resolver and workspaces) into a single migration head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes or restores pi_state and pi_config mounts from a JSON manifest stored in the tool_definition_manifests table for the 'pi-agent' tool. | 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 | Alembic database migration that creates a config_folders table with UUID primary key, user foreign key, JSONB fields for files and project overrides, and supporting indexes/constraints | 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 columns (definition_type, dockerfile_template, build_context, readiness_probe) and a check constraint to the tool_types table. | 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 previous migrations (single_interface and clone_mode) into a single revision point | 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 occasional branch/merge patterns (using merge heads), sequential numbered and timestamped revision files, each containing declarative schema changes (CREATE TABLE/ALTER TABLE/DROP TABLE) with idempotent guards, downgrade paths, and occasional data migrations; supports PostgreSQL and SQLite dialects.
|
||||
## 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
|
||||
- explore versions subdirectories
|
||||
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||
## 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,87 @@
|
||||
"""remove pi agent workspace symlink
|
||||
|
||||
Revision ID: 2026_06_19_113000
|
||||
Revises: 2026_06_15_090500
|
||||
Create Date: 2026-06-19 11:30:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.sql import column, table
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2026_06_19_113000"
|
||||
down_revision: str | Sequence[str] | None = "2026_06_15_090500"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: 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[str | None, 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
|
||||
|
||||
runtime = manifest.setdefault("runtime", {})
|
||||
# The /workspace compatibility symlink is no longer created by the
|
||||
# compiler/entrypoint. Leaving working_dir set to /workspace causes the
|
||||
# container to start in an empty directory. Let compile_compose default
|
||||
# to /home/user/{workspace_name} instead.
|
||||
if runtime.get("working_dir") == "/workspace":
|
||||
del runtime["working_dir"]
|
||||
|
||||
scripts = manifest.setdefault("scripts", {})
|
||||
# Update the startup script to operate on the real repo-named directory.
|
||||
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
|
||||
|
||||
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,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,57 @@
|
||||
# apps/api/src (index)
|
||||
dir: apps/api/src
|
||||
|
||||
## role
|
||||
Core FastAPI application package that initializes and configures the Headquarter API with database, authentication, logging, and middleware infrastructure.
|
||||
## parent
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/.ruff_cache
|
||||
index: apps/api/src/.ruff_cache/.pi-map.index.md
|
||||
map: apps/api/src/.ruff_cache/.pi-map.md
|
||||
- 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/headquarter_api.egg-info
|
||||
index: apps/api/src/headquarter_api.egg-info/.pi-map.index.md
|
||||
map: apps/api/src/headquarter_api.egg-info/.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/.ruff_cache/.pi-map.index.md, apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.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 FastAPI application package that initializes and configures the Headquarter API with database, authentication, logging, and middleware infrastructure.
|
||||
## files
|
||||
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
||||
- config.py | Defines application configuration settings using pydantic-settings, including database connectivity, Authentik SSO, JWT, session, and domain-based URL resolution. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
||||
- database.py | Configures an async SQLAlchemy database engine/session and provides a retry-based initialization function that runs Alembic migrations via subprocess. | 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 and provides ASGI middleware for logging HTTP requests, responses, and unhandled exceptions. | 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 | Initializes and configures the FastAPI application, setting up middleware, routers, database connections, and lifecycle event handlers for the Headquarter API. | 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, src.seeds, src.services
|
||||
## arch
|
||||
Layered architecture using Pydantic-settings for configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracking, and FastAPI lifecycle management with dependency injection for cross-cutting concerns.
|
||||
## tags
|
||||
src, logging, database, call:logger.info, api, middleware, filter, call:logging.get
|
||||
## 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/.ruff_cache/.pi-map.index.md, apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,43 @@
|
||||
# apps/api/src/api (index)
|
||||
dir: apps/api/src/api
|
||||
|
||||
## role
|
||||
Defines the core API router package with reusable Pydantic validation utilities for container and filesystem-related API schemas.
|
||||
## parent
|
||||
index: apps/api/src/.pi-map.index.md
|
||||
map: apps/api/src/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/api/.ruff_cache
|
||||
index: apps/api/src/api/.ruff_cache/.pi-map.index.md
|
||||
map: apps/api/src/api/.ruff_cache/.pi-map.md
|
||||
- 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/.ruff_cache/.pi-map.index.md, apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.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
|
||||
Defines the core API router package with reusable Pydantic validation utilities for container and filesystem-related API schemas.
|
||||
## files
|
||||
- __init__.py | Marks the directory as a Python package for API routers.
|
||||
- shared_validators.py | Provides reusable Pydantic validator functions for validating mount paths, file contents, environment variables, and volume mounts in API schemas. | 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
|
||||
Modular package structure separating router organization from shared cross-cutting validation concerns using Pydantic validators.
|
||||
## tags
|
||||
validate, raise:value, error, call:isinstance, mount, api, init, path
|
||||
## 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/.ruff_cache/.pi-map.index.md, apps/api/src/api/config/.pi-map.index.md, apps/api/src/api/project/.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, ToolInstance, UserConfig
|
||||
from src.schemas.config import (
|
||||
ConfigProfileCreate,
|
||||
ConfigProfileIncludeUpdate,
|
||||
@@ -27,124 +21,54 @@ 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(
|
||||
async def _running_profile_outcomes(
|
||||
session: AsyncSession, profile_id: uuid.UUID
|
||||
) -> ConfigProfile | None:
|
||||
"""Fetch a profile with includes eagerly loaded."""
|
||||
) -> list[dict[str, str]]:
|
||||
"""Report running instances that must restart to adopt a profile revision."""
|
||||
result = await session.execute(
|
||||
select(ConfigProfile)
|
||||
.where(ConfigProfile.id == profile_id)
|
||||
.options(selectinload(ConfigProfile.includes))
|
||||
select(ToolInstance).where(ToolInstance.status == "running")
|
||||
)
|
||||
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"
|
||||
outcomes: list[dict[str, str]] = []
|
||||
for instance in result.scalars().all():
|
||||
if instance.selected_config_profile_id is None:
|
||||
continue
|
||||
resolved = await resolve_profile(session, instance.selected_config_profile_id)
|
||||
dependencies = {resolved.profile_id} | {
|
||||
uuid.UUID(item["id"])
|
||||
for item in resolved.included_profiles
|
||||
if item.get("id")
|
||||
}
|
||||
if profile_id in dependencies:
|
||||
outcomes.append(
|
||||
{
|
||||
"instance_id": str(instance.id),
|
||||
"status": "restart_required",
|
||||
"reason": "Existing instance must restart to adopt shared profile mounts",
|
||||
}
|
||||
)
|
||||
# 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,
|
||||
}
|
||||
return outcomes
|
||||
|
||||
|
||||
@router.get("", response_model=list[ConfigProfileResponse])
|
||||
@@ -165,26 +89,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 +113,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 +125,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 +137,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 +146,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 +157,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 +167,11 @@ 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)
|
||||
response = profile_to_response(profile)
|
||||
response["refresh_outcomes"] = await _running_profile_outcomes(session, profile.id)
|
||||
logger.debug("Updated config profile %s", profile.id)
|
||||
return _profile_to_response(profile)
|
||||
return response
|
||||
|
||||
|
||||
@router.delete("/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -389,7 +181,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"
|
||||
@@ -399,6 +191,16 @@ async def delete_config_profile(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
|
||||
)
|
||||
|
||||
outcomes = await _running_profile_outcomes(session, profile.id)
|
||||
if outcomes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail={
|
||||
"message": "Profile is still used by running instances",
|
||||
"outcomes": outcomes,
|
||||
},
|
||||
)
|
||||
|
||||
await session.delete(profile)
|
||||
await session.commit()
|
||||
|
||||
@@ -407,14 +209,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 +226,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 +237,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 +247,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 +269,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 +304,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 +331,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,30 @@
|
||||
# apps/api/src/api/system (index)
|
||||
dir: apps/api/src/api/system
|
||||
|
||||
## role
|
||||
Provides system-level API endpoints for monitoring, administration, and infrastructure operations including dashboards, health checks, event streaming, instance proxying, notifications, and terminal access.
|
||||
## parent
|
||||
index: apps/api/src/api/.pi-map.index.md
|
||||
map: apps/api/src/api/.pi-map.md
|
||||
## children
|
||||
- apps/api/src/api/system/.ruff_cache
|
||||
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
|
||||
map: apps/api/src/api/system/.ruff_cache/.pi-map.md
|
||||
## 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
|
||||
- explore system subdirectories
|
||||
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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 infrastructure operations including dashboards, health checks, event streaming, instance proxying, notifications, and terminal access.
|
||||
## files
|
||||
- __init__.py | Aggregates and exports system API routers for a modular web application framework. | 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 delivers instance events to authenticated users with per-user connection limits and heartbeat pings. | dep: asyncio, contextlib, json, uuid, collections.abc, fastapi, src.auth.dependencies, src.services.instance.event_bus
|
||||
- health.py | Provides FastAPI health check endpoints that monitor system health including database connectivity/response time and disk usage, returning structured health status responses. | 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 | Proxies HTTP requests from authenticated users 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 | Defines FastAPI REST endpoints for user notification management including listing, marking as read, dismissing, and retrieving unread counts 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 endpoints for browser-based terminal access to running Docker container tool instances, handling authentication, session management, input/output streaming, and terminal resize/reset operations. | 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:_resolve_container_user(db_session: AsyncSession, instance: ToolInstance) → str | None, call:db_session.get, call:dict, call:resolve_base, call:deep_merge, call:get_manifest_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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:_resolve_container_user, 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.build.manifest_compiler, src.services.terminal.terminal_manager, src.services.docker, src.auth.session, src.config, starlette
|
||||
## arch
|
||||
Modular FastAPI router pattern with per-feature separation, combining standard REST endpoints, SSE streaming, and WebSocket connections, all with unified authentication and user-scoped access control.
|
||||
## tags
|
||||
terminal, session, call:terminal, call:, src, get, response, instance
|
||||
## symbols
|
||||
- NotificationItem
|
||||
- NotificationListResponse
|
||||
- UnreadCountResponse
|
||||
- MarkAllReadResponse
|
||||
- ClearAllResponse
|
||||
- SessionRef
|
||||
- get_dashboard_summary
|
||||
- health_check
|
||||
## workflows
|
||||
- change system behavior
|
||||
read: __init__.py, dashboard.py, events.py
|
||||
- explore system subdirectories
|
||||
index: apps/api/src/api/system/.ruff_cache/.pi-map.index.md
|
||||
## dirty
|
||||
-
|
||||
@@ -3,6 +3,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from asyncio import QueueFull
|
||||
from json import JSONDecodeError
|
||||
import uuid
|
||||
from contextlib import suppress
|
||||
|
||||
@@ -12,14 +14,28 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||
from src.models import TerminalSessionModel
|
||||
from src.models import ToolInstance
|
||||
from src.models import ToolType
|
||||
from src.services.terminal.terminal_manager import MaxSessionsExceededError, terminal_manager
|
||||
from src.models import (
|
||||
TerminalSessionModel,
|
||||
ToolDefinitionManifest,
|
||||
ToolInstance,
|
||||
ToolType,
|
||||
)
|
||||
from src.services.build.manifest_compiler import (
|
||||
deep_merge,
|
||||
get_manifest_container_user,
|
||||
resolve_base,
|
||||
)
|
||||
from src.services.terminal.terminal_manager import (
|
||||
MaxSessionsExceededError,
|
||||
terminal_manager,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_PENDING_INPUT_MESSAGES = 64
|
||||
MAX_TERMINAL_INPUT_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
class SessionRef:
|
||||
"""Mutable reference to a terminal session, allowing updates during reset."""
|
||||
@@ -57,6 +73,39 @@ async def terminal_websocket_specific(
|
||||
await _handle_terminal_websocket(websocket, instance_id, session_id, db_session)
|
||||
|
||||
|
||||
async def _resolve_container_user(
|
||||
db_session: AsyncSession,
|
||||
instance: ToolInstance,
|
||||
) -> str | None:
|
||||
"""Resolve the container user for docker exec from the tool manifest.
|
||||
|
||||
For manifest-based tools, the user declared in the manifest (or its base
|
||||
definition) is returned so terminal sessions run with the same privileges
|
||||
as the main container process. Legacy tools return None, preserving the
|
||||
previous behavior.
|
||||
"""
|
||||
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
||||
if not tool_type or tool_type.definition_type != "manifest":
|
||||
return None
|
||||
|
||||
if not tool_type.manifest_id:
|
||||
return None
|
||||
|
||||
manifest_def = await db_session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
||||
if not manifest_def:
|
||||
return None
|
||||
|
||||
manifest = dict(manifest_def.manifest)
|
||||
if manifest_def.base_definition_id:
|
||||
base_def = await db_session.get(
|
||||
ToolDefinitionManifest, manifest_def.base_definition_id
|
||||
)
|
||||
if base_def:
|
||||
manifest = resolve_base(deep_merge(dict(base_def.manifest), manifest))
|
||||
|
||||
return get_manifest_container_user(manifest)
|
||||
|
||||
|
||||
async def _handle_terminal_websocket(
|
||||
websocket: WebSocket,
|
||||
instance_id: str,
|
||||
@@ -139,7 +188,7 @@ async def _handle_terminal_websocket(
|
||||
)
|
||||
return
|
||||
|
||||
# Fetch tool type to get startup_command
|
||||
# Fetch tool type to get startup_command and container_user
|
||||
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
||||
startup_command = tool_type.startup_command if tool_type else None
|
||||
if startup_command:
|
||||
@@ -149,6 +198,14 @@ async def _handle_terminal_websocket(
|
||||
startup_command,
|
||||
)
|
||||
|
||||
container_user = await _resolve_container_user(db_session, instance)
|
||||
if container_user:
|
||||
logger.debug(
|
||||
"Terminal sessions for instance %s will run as user %s",
|
||||
instance_id,
|
||||
container_user,
|
||||
)
|
||||
|
||||
session = None
|
||||
|
||||
# Get or create terminal session
|
||||
@@ -159,6 +216,7 @@ async def _handle_terminal_websocket(
|
||||
instance_uuid,
|
||||
instance.container_id,
|
||||
startup_command=startup_command,
|
||||
container_user=container_user,
|
||||
)
|
||||
slot_session_id = "default"
|
||||
else:
|
||||
@@ -189,6 +247,7 @@ async def _handle_terminal_websocket(
|
||||
startup_command=startup_command,
|
||||
name=db_row.name,
|
||||
session_id=target_session_id,
|
||||
container_user=container_user,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -266,8 +325,38 @@ async def _handle_terminal_websocket(
|
||||
)
|
||||
|
||||
|
||||
async def _input_write_loop(input_queue: asyncio.Queue[tuple[object, bytes]]) -> None:
|
||||
"""Serialize PTY writes without blocking terminal control messages."""
|
||||
while True:
|
||||
session, data = await input_queue.get()
|
||||
try:
|
||||
await session.write_input(data) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
logger.debug("Terminal input write failed", exc_info=True)
|
||||
finally:
|
||||
input_queue.task_done()
|
||||
|
||||
|
||||
def _queue_terminal_input(
|
||||
input_queue: asyncio.Queue[tuple[object, bytes]], session: object, data: bytes
|
||||
) -> bool:
|
||||
"""Queue bounded terminal input without blocking control-message processing."""
|
||||
if len(data) > MAX_TERMINAL_INPUT_BYTES:
|
||||
return False
|
||||
try:
|
||||
input_queue.put_nowait((session, data))
|
||||
except QueueFull:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> None:
|
||||
"""Read input from WebSocket and send to container."""
|
||||
"""Receive terminal messages while a dedicated worker serializes PTY input."""
|
||||
input_queue: asyncio.Queue[tuple[object, bytes]] = asyncio.Queue(
|
||||
maxsize=MAX_PENDING_INPUT_MESSAGES
|
||||
)
|
||||
input_writer = asyncio.create_task(_input_write_loop(input_queue))
|
||||
|
||||
try:
|
||||
while True:
|
||||
session = session_ref.session
|
||||
@@ -277,72 +366,100 @@ async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> N
|
||||
message = await websocket.receive()
|
||||
if message["type"] == "websocket.receive":
|
||||
if "bytes" in message:
|
||||
await session.write_input(message["bytes"])
|
||||
if not _queue_terminal_input(
|
||||
input_queue, session, message["bytes"]
|
||||
):
|
||||
logger.warning("Terminal input buffer exceeded for %s", instance_id)
|
||||
await websocket.close(code=1009, reason="Terminal input buffer full")
|
||||
break
|
||||
elif "text" in message:
|
||||
text = message["text"]
|
||||
# A text frame that parses to a JSON object with a
|
||||
# "type" field is a control message and must NEVER be
|
||||
# written to the PTY (e.g. the heartbeat {"type":"pong"}
|
||||
# must be consumed, not typed into the shell/pi). Handle
|
||||
# known types and ignore unknown ones. Everything else
|
||||
# (keystrokes, bracketed-paste content, plain text) is
|
||||
# forwarded as raw terminal input.
|
||||
ctrl = None
|
||||
if text.startswith("{"):
|
||||
# Control message (JSON)
|
||||
try:
|
||||
ctrl = json.loads(text)
|
||||
msg_type = ctrl.get("type")
|
||||
parsed = json.loads(text)
|
||||
except JSONDecodeError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict) and "type" in parsed:
|
||||
ctrl = parsed
|
||||
|
||||
if msg_type == "resize":
|
||||
cols = ctrl.get("cols", 80)
|
||||
rows = ctrl.get("rows", 24)
|
||||
logger.debug(
|
||||
"Received resize message for instance %s: %sx%s",
|
||||
instance_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
await session.resize(cols, rows)
|
||||
elif msg_type == "ack":
|
||||
char_count = ctrl.get("chars", 0)
|
||||
if char_count > 0:
|
||||
session.acknowledge_data(char_count)
|
||||
elif msg_type == "reset":
|
||||
# Reset terminal session (scoped to current slot)
|
||||
logger.debug(
|
||||
"Resetting terminal session for instance %s (slot=%s)",
|
||||
session.instance_id,
|
||||
session_ref.slot_session_id,
|
||||
)
|
||||
await websocket.send_json(
|
||||
{"type": "status", "status": "resetting"}
|
||||
)
|
||||
if ctrl is None:
|
||||
if not _queue_terminal_input(
|
||||
input_queue, session, text.encode("utf-8")
|
||||
):
|
||||
logger.warning(
|
||||
"Terminal input buffer exceeded for %s", instance_id
|
||||
)
|
||||
await websocket.close(
|
||||
code=1009, reason="Terminal input buffer full"
|
||||
)
|
||||
break
|
||||
continue
|
||||
|
||||
# Reset the session scoped to its slot
|
||||
new_session = await terminal_manager.reset_session(
|
||||
session.instance_id,
|
||||
session.container_id,
|
||||
startup_command=session.startup_command,
|
||||
session_id=session_ref.slot_session_id,
|
||||
name=session.name,
|
||||
)
|
||||
msg_type = ctrl["type"]
|
||||
if msg_type == "resize":
|
||||
cols = ctrl.get("cols", 80)
|
||||
rows = ctrl.get("rows", 24)
|
||||
logger.debug(
|
||||
"Received resize message for instance %s: %sx%s",
|
||||
instance_id,
|
||||
cols,
|
||||
rows,
|
||||
)
|
||||
await session.resize(cols, rows)
|
||||
elif msg_type == "ack":
|
||||
char_count = ctrl.get("chars", 0)
|
||||
if char_count > 0:
|
||||
session.acknowledge_data(char_count)
|
||||
elif msg_type == "reset":
|
||||
# Reset terminal session (scoped to current slot)
|
||||
logger.debug(
|
||||
"Resetting terminal session for instance %s (slot=%s)",
|
||||
session.instance_id,
|
||||
session_ref.slot_session_id,
|
||||
)
|
||||
await websocket.send_json(
|
||||
{"type": "status", "status": "resetting"}
|
||||
)
|
||||
|
||||
# Update the mutable session reference
|
||||
session_ref.session = new_session
|
||||
# Reset the session scoped to its slot
|
||||
new_session = await terminal_manager.reset_session(
|
||||
session.instance_id,
|
||||
session.container_id,
|
||||
startup_command=session.startup_command,
|
||||
session_id=session_ref.slot_session_id,
|
||||
name=session.name,
|
||||
container_user=session.container_user,
|
||||
)
|
||||
|
||||
# Attach to new session
|
||||
await terminal_manager.attach_websocket(
|
||||
new_session, websocket
|
||||
)
|
||||
await websocket.send_json(
|
||||
{"type": "status", "status": "connected"}
|
||||
)
|
||||
# Update the mutable session reference
|
||||
session_ref.session = new_session
|
||||
|
||||
# Continue the loop with the new session
|
||||
continue
|
||||
# Attach to new session
|
||||
await terminal_manager.attach_websocket(new_session, websocket)
|
||||
await websocket.send_json(
|
||||
{"type": "status", "status": "connected"}
|
||||
)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# Not a valid JSON control message, treat as regular input
|
||||
await session.write_input(text.encode("utf-8"))
|
||||
else:
|
||||
await session.write_input(text.encode("utf-8"))
|
||||
# Continue the loop with the new session
|
||||
continue
|
||||
elif message["type"] == "websocket.disconnect":
|
||||
break
|
||||
except Exception:
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except (RuntimeError, TypeError, ValueError) as exc:
|
||||
logger.debug("Terminal WebSocket receive loop ended: %s", exc)
|
||||
finally:
|
||||
input_writer.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await input_writer
|
||||
|
||||
|
||||
async def _heartbeat_loop(websocket: WebSocket) -> None:
|
||||
@@ -486,6 +603,7 @@ async def create_terminal_session(
|
||||
startup_command = tool_type.startup_command if tool_type else None
|
||||
|
||||
name = data.get("name")
|
||||
container_user = await _resolve_container_user(db_session, instance)
|
||||
|
||||
try:
|
||||
session = await terminal_manager.create_session(
|
||||
@@ -493,6 +611,7 @@ async def create_terminal_session(
|
||||
instance.container_id,
|
||||
startup_command=startup_command,
|
||||
name=name,
|
||||
container_user=container_user,
|
||||
)
|
||||
except MaxSessionsExceededError:
|
||||
raise HTTPException(
|
||||
@@ -595,6 +714,7 @@ async def reset_specific_terminal_session(
|
||||
# Preserve name if possible
|
||||
live_session = terminal_manager.get_session(str(instance_id), session_id)
|
||||
name = live_session.name if live_session else None
|
||||
container_user = await _resolve_container_user(db_session, instance)
|
||||
|
||||
new_session = await terminal_manager.reset_session(
|
||||
instance_id,
|
||||
@@ -602,6 +722,7 @@ async def reset_specific_terminal_session(
|
||||
startup_command=startup_command,
|
||||
session_id=key[1],
|
||||
name=name,
|
||||
container_user=container_user,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -687,6 +808,7 @@ async def reset_terminal_session(
|
||||
# Fetch tool type to get startup_command
|
||||
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
||||
startup_command = tool_type.startup_command if tool_type else None
|
||||
container_user = await _resolve_container_user(db_session, instance)
|
||||
|
||||
try:
|
||||
# Reset the default session
|
||||
@@ -694,6 +816,7 @@ async def reset_terminal_session(
|
||||
instance_id,
|
||||
instance.container_id,
|
||||
startup_command=startup_command,
|
||||
container_user=container_user,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -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
|
||||
-
|
||||
@@ -240,6 +240,12 @@ class ConfigProfileIncludeUpdate(BaseModel):
|
||||
return v
|
||||
|
||||
|
||||
class ConfigProfileRefreshOutcome(BaseModel):
|
||||
instance_id: str
|
||||
status: str
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class ConfigProfileResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
@@ -256,6 +262,7 @@ class ConfigProfileResponse(BaseModel):
|
||||
includes: list[dict]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
refresh_outcomes: list[ConfigProfileRefreshOutcome] = Field(default_factory=list)
|
||||
|
||||
|
||||
class DefaultProfilesUpdate(BaseModel):
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user