First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
# RunContext — Session Permissions & Configuration
**Single source of truth for RunContext.** Consolidates resolution, fields, usage, and API.
---
## Overview
Each session can have an active **RunContext** that controls:
- **Approval policy** — which permission group (`security_group`) applies to tool calls
- **System prompt injection** — dynamic prompt fragments per session
- **File-write pre-authorization** — paths auto-approved for writes (`allow_fs_writes`)
- **File-read auto-allow** — paths auto-approved for reads (working dir + `docs/` + `skills/` + `allow_fs_reads` + anything writable)
- **Working directory** — effective CWD for tool calls and file operations
`RunContext` is a JSON blob stored in the DB:
- `chat_sessions.run_context` — interactive web/mobile sessions
- `scheduled_jobs.run_context` — cron tasks
- `projects.run_context` — project-level defaults
- `project_tickets.run_context` — ticket-level overrides
---
## Fields
| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `security_group` | `Option<String>` | `null` | Permission group ID for approval rule lookup. Rules in this group take precedence over `"default"`. |
| `system_prompt` | `Vec<String>` | `[]` | Prompt fragments injected as dynamic system context every turn (joined with `"\n\n"`). |
| `allow_fs_writes` | `Vec<String>` | `[]` | Paths pre-authorized for file writes. Resolved against the working dir; recursive directory prefix match (no globs). Entries are also readable (write implies read). |
| `allow_fs_reads` | `Vec<String>` | `[]` | Extra **read-only** grants, beyond the working dir / `docs/` / `skills/` (always-safe baseline) and `allow_fs_writes`. Same prefix-match semantics. |
| `working_directory` | `Option<String>` | `null` | Effective WD for tool calls; `null` = Skald's process cwd. |
---
## Applicative Methods
`RunContext` exposes these methods (the session handler is agnostic to internal fields):
```rust
rc.tool_group_id() -> Option<&str> // for approval rule lookup
rc.extra_system_prompt() -> Option<String> // joins system_prompt with "\n\n"
rc.effective_working_dir() -> PathBuf // configured path or process cwd
rc.is_write_allowed(path) -> bool // pre-auth check for file writes
rc.is_read_allowed(path) -> bool // pre-auth check for file reads
```
Both `is_*_allowed` canonicalize the path first (resolving `..` and symlinks via
`tools::fs::canonicalize_for_policy`) so traversal/symlink escapes cannot widen a grant.
---
## Resolution at Session Creation
**Order of precedence** (`ChatSessionManager::create_session` or `ChatHub::provision_session`):
1. **Explicit `run_context` parameter** — JSON blob passed at session creation. Persisted in DB immediately so the handler reads it at construction.
2. **Config-driven defaults** — from `config.yml` (per-source or per-agent), or TIC's `tic.run_context` key.
3. **None** — all RunContext methods return zero values (`tool_group_id()``None`, `is_write_allowed()``false`, `effective_working_dir()` → process cwd).
The `RunContext` is stored in `ChatSessionHandler::run_context` (`RwLock<Option<RunContext>>`). The handler **reads it once at construction** and **never directly accesses its internal fields** — only calls applicative methods.
### Session Handler Usage
| Method | Used for |
|--------|----------|
| `tool_group_id()` | Approval rule lookup (passed to `ApprovalManager::check()`) |
| `extra_system_prompt()` | Injected as dynamic system tail in `build_agent_config` (see [llm-loop.md](llm-loop.md)) |
| `is_write_allowed(path)` | Fast-path for file write tools; upgrades a `Require` decision to `Allow` |
| `is_read_allowed(path)` | Fast-path for file read tools; upgrades a `Require` decision to `Allow` |
| `effective_working_dir()` | WD injection for file tools and `execute_cmd` |
---
## Runtime Update
**Endpoint:** `POST /api/sessions/{id}/run_context`
**Body:** Full `RunContext` JSON (or `null` to clear):
```json
{
"security_group": "cron_restrictive",
"system_prompt": ["Always reply in English.", "Use metric units."],
"allow_fs_writes": ["data/output", "logs/*"],
"working_directory": "/projects/skald"
}
```
**Effects:**
- Updates `chat_sessions.run_context` in DB
- If the handler is live in memory, calls `handler.set_run_context()` immediately (no restart needed)
- Changes take effect on the next turn
---
## Approval Gate Integration
Rules are scoped to **permission groups** (`tool_permission_groups` table). A session's `RunContext.security_group` field references a group; rules in that group take precedence over `"default"`.
### Evaluation Chain
```
chat_session.run_context (JSON blob)
└─► RunContext.tool_group_id() → e.g. "cron_restrictive"
├─ rules WHERE group_id = "cron_restrictive" ← evaluated first
└─ rules WHERE group_id = "default" ← fallback
```
**Default behavior:** If a session has no `run_context` or the blob has no `security_group`, only `"default"` group rules apply.
The `"default"` group is seeded automatically at startup and **cannot be deleted**. Its rules can be freely edited.
See [approval/index.md](../approval/index.md) for rule evaluation and pattern matching.
---
## Filesystem fast-paths and gate precedence
`RunContext` pre-authorizes filesystem access **without a human approval prompt**, but it
**never overrides an explicit `Deny`**. The gate in `llm_loop.rs` evaluates in this order:
```
1. ApprovalManager::check() → Allow | Deny | Require
2. if Require and is_file_read_tool → rc.is_read_allowed(path) ? Allow : Require
if Require and is_file_write_tool → rc.is_write_allowed(path) ? Allow : Require
```
So a `Deny` (e.g. the seeded `secrets/` rule) always wins, and the fast-path only relaxes
a `Require` to `Allow` — the same semantics as a session bypass. When the session has no
`RunContext`, a default one is used (working dir = process cwd), so `docs/`/`skills/` and the
cwd are still auto-readable.
### Writes — `allow_fs_writes`
Pre-authorizes writes to the listed paths. Resolved against the working dir; matching is a
recursive directory prefix (canonicalized — no globs). `memory/` is always writable via a
separate hardcoded exception in the approval engine.
**Common presets:** `["data"]` (project output), `["logs", "tmp"]` (temporary files).
### Reads — auto-allow roots
Reads are auto-allowed (no prompt) for, in order:
1. the **working directory** itself,
2. its **`docs/`** and **`skills/`** subtrees (always-safe baseline),
3. every **`allow_fs_reads`** entry (read-only grants),
4. everything in **`allow_fs_writes`** (write implies read).
Anything else falls back to the approval rules of the `security_group`. Under a
`require`-default group this means reads outside these roots prompt for approval.
The built-in **`default`** group is itself require-by-default (its final catch-all is
`require *`; see [approval/index.md](../approval/index.md)), so these auto-allow roots are
load-bearing even for the default group — not just for explicitly-restrictive custom groups.
> **`secrets/` is denied.** The approval engine seeds `deny` rules for the read tools on
> `secrets` / `secrets/*` (see [approval/index.md](../approval/index.md)). Because `Deny`
> is evaluated first and is non-bypassable, `secrets/` stays unreadable even though it sits
> inside the auto-read working dir. The recursive read tools (`grep_files`, `list_files`)
> additionally skip any `secrets` directory during traversal, so a search rooted higher up
> cannot leak secret values.
---
## Project Integration
Projects can set default `RunContext` for all interactive and ticket chats under that project:
- **Project-level:** `POST /api/projects/{id}/run_context` → stored in `projects.run_context`
- **Ticket override:** `POST /api/projects/{project_id}/tickets/{ticket_id}/run_context` → stored in `project_tickets.run_context`
Ticket override **takes precedence** over project-level when a ticket chat is opened.
See [projects.md](../projects.md) for project lifecycle and `build_runtime_run_context`.
---
## Example Scenarios
### Scenario 1: Cron job with restricted permissions
**Config:**
```json
{
"security_group": "cron_restrictive",
"allow_fs_writes": ["logs/*"],
"working_directory": "/tmp"
}
```
**Effect:**
- All tool calls evaluated against `cron_restrictive` group rules (typically more restrictive)
- File writes to `logs/*` bypass approval entirely
- File operations use `/tmp` as working directory
### Scenario 2: Project ticket with context injection
**Config:**
```json
{
"system_prompt": ["You are fixing ticket #42: DB migration failure. Current logs are in `logs/migration.log`."],
"working_directory": "/projects/skald",
"allow_fs_writes": ["data/migrations/", "logs/*"]
}
```
**Effect:**
- Extra context prepended to system prompt every turn
- Ticket-scoped working directory for `execute_cmd`
- Migration files and logs can be written without approval
### Scenario 3: Interactive session (default)
**Config:**
```json
{
"security_group": null,
"system_prompt": [],
"allow_fs_writes": [],
"working_directory": null
}
```
**Effect:**
- Default approval group rules apply
- No extra system prompt
- All file writes require approval (if rule triggers)
- Reads of the cwd, `docs/` and `skills/` are auto-allowed; `secrets/` is denied; other reads follow the default group rules
- Uses Skald's process CWD
---
## When to Update This File
- Adding a new field to `RunContext`
- Changing resolution order or approval gate behavior
- Adding new pre-authorization scenarios
- Documenting config best practices