Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d044b2
Add comprehensive tests for script execution engine, sandbox security…
markkr125 Mar 27, 2026
c2dddc0
Add comprehensive tests for script engine, sandbox, and version service
markkr125 Mar 28, 2026
0f21c3c
Refactor import statements for consistency and clarity; add test for …
markkr125 Mar 28, 2026
c6a235a
Refactor tab change handling to sync collection tree selection by def…
markkr125 Mar 30, 2026
425ee3e
feat: add diff viewer and toolbar for version history
markkr125 Apr 3, 2026
6b6b158
update
markkr125 Apr 5, 2026
3455e24
feat: add DenoManager and feature detection for advanced JavaScript f…
markkr125 Apr 21, 2026
02832f2
feat: add comprehensive agent instructions and skills documentation
markkr125 Apr 22, 2026
034b568
feat: integrate Esprima for JavaScript syntax validation and enhance …
markkr125 Apr 22, 2026
878ef3b
feat: enhance scripting capabilities with Deno and Pyodide integration
markkr125 May 6, 2026
98bab1e
update
markkr125 May 6, 2026
07c774f
update
markkr125 May 7, 2026
8c745be
update
markkr125 May 9, 2026
8749901
feat: enhance scripting capabilities with new snippet support and API…
markkr125 May 11, 2026
09056d6
feat: introduce comprehensive local script modules plan and UI enhanc…
markkr125 May 12, 2026
b6356fa
feat: enhance key-value table functionality with bulk editing support
markkr125 May 15, 2026
2038ad3
feat: enhance environment management with sidebar panel and UI updates
markkr125 May 17, 2026
b357182
feat: introduce LeftSidebar for enhanced navigation and UI structure
markkr125 May 18, 2026
763b276
feat: enhance local script management and UI components
markkr125 May 21, 2026
84948e2
feat: enhance dynamic variable support and UI components
markkr125 May 30, 2026
e8603e5
feat: enhance snippets management and UI interactions
markkr125 May 30, 2026
24b3f8b
feat: revamp README.md for enhanced clarity and feature overview
markkr125 May 30, 2026
92fb261
feat: update vendor libraries and enhance UI interactions
markkr125 May 30, 2026
2c6f270
fix: update type hinting and improve tree overlay functionality
markkr125 May 30, 2026
69cdf20
feat: enhance scripting security and UI interactions
markkr125 May 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
116 changes: 116 additions & 0 deletions .agents/skills/customization-guide/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
name: customization-guide
description: "How to create, update, or debug root and nested AGENTS.md files, .agents/skills, and project agent conventions. Use when adding a new agent instruction file, creating a new skill, or troubleshooting how instructions are discovered."
---

# Agent customization guide

How to create and manage **nested `AGENTS.md` files** and **on-demand skills**
(`.agents/skills/<name>/SKILL.md`) for the Postmark project.

## When to use nested AGENTS.md vs skills

| Feature | Nested `AGENTS.md` | Agent skills |
|---------|-------------------|--------------|
| **Location** | Repo root (`AGENTS.md`) or under a subtree (e.g. `src/ui/AGENTS.md`) | `.agents/skills/<name>/` |
| **Filename** | `AGENTS.md` | `SKILL.md` |
| **Loading** | Agents merge root + nearest nested files along the path to edited files | Read when the task matches the skill `description` (listed in root `AGENTS.md`) |
| **Best for** | Core rules per directory tree | Reference material, step-by-step guides, catalogues |
| **Context cost** | Included whenever working under that subtree | Only when relevant |

**Rule of thumb:** If an agent needs the information for *every* change under a
directory (e.g. all UI code), put it in `src/ui/AGENTS.md`. If it only applies
to *specific tasks* (e.g. "add a widget", "debug signals"), put it in a skill.

## Creating nested agent instructions

1. Add or extend **`AGENTS.md`** in the directory whose code the rules belong to:

```
src/ui/AGENTS.md # UI / PySide6 conventions
src/database/AGENTS.md # SQLAlchemy / repository conventions
src/AGENTS.md # Cross-cutting architecture under src/
tests/AGENTS.md # Pytest / fixture conventions
docs/AGENTS.md # Documentation authoring rules
```

2. Plain Markdown is enough — no glob metadata. Scope is defined by **where the file lives**
(nested files merge with the root `AGENTS.md`).

3. Write concise, imperative rules. Start with a "Quick rules" section.

4. Register the file in **root [`AGENTS.md`](../../../AGENTS.md)** (nested files + sync checklist tables).

5. Run `poetry run python scripts/check_md_links.py` to verify links.

### Nested file guidelines

- Keep files lean — they apply whenever editing under that tree.
- Use imperative tone ("Do X", "Never Y").
- Start with numbered "Quick rules" for the most critical constraints.
- Never duplicate rules across files — link to the canonical nested file instead.

## Creating a new skill

1. Create a directory under `.agents/skills/`:

```
.agents/skills/my-skill/
```

2. Create `SKILL.md` with YAML frontmatter:

```yaml
---
name: my-skill
description: >-
Detailed description of what this skill does and when an agent should
read it. Include trigger phrases like "Use when adding new X" or
"Use when debugging Y".
---
```

3. Write the skill body in Markdown — procedures, templates, tables, checklists.

4. Add the skill to the **skills table** in root [`AGENTS.md`](../../../AGENTS.md).

### Skill naming conventions

- Directory name: lowercase, hyphens (e.g. `signal-flow`).
- `name` in frontmatter: matches folder name.
- `description`: tells humans and agents **when** to open this file.

## Existing structure

### Nested `AGENTS.md` (merged by path)

| File | Scope |
|------|-------|
| [`AGENTS.md`](../../../AGENTS.md) | Project-wide — overview, architecture tree, validation gate |
| [`src/AGENTS.md`](../../../src/AGENTS.md) | Architecture & data flow for `src/` |
| [`src/ui/AGENTS.md`](../../../src/ui/AGENTS.md) | PySide6 / UI |
| [`src/database/AGENTS.md`](../../../src/database/AGENTS.md) | SQLAlchemy / DB |
| [`tests/AGENTS.md`](../../../tests/AGENTS.md) | Testing |
| [`docs/AGENTS.md`](../../../docs/AGENTS.md) | Docs authoring |

### Skills (on-demand)

| Skill | Trigger |
|-------|---------|
| `signal-flow` | Signals, wiring, data flow |
| `service-repository-reference` | Repository/service APIs, TypedDicts |
| `widget-patterns` | Widgets, delegates, workers |
| `test-writing` | New tests |
| `import-parser` | New import format |
| `customization-guide` | Changing agent layout |

## Mandatory sync after changes

After modifying any `AGENTS.md`, skill, or linked doc, follow the checklist in
root [`AGENTS.md`](../../../AGENTS.md) under **CRITICAL — Keeping instructions in sync**.

After modifying any `.md` file, run:

```bash
poetry run python scripts/check_md_links.py
```
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ class TestYourParser:

After adding a new parser:

1. Add the file to the architecture tree in `copilot-instructions.md`.
2. Add test file to the test tree in `testing.instructions.md`.
1. Add the file to the architecture tree in root [`AGENTS.md`](../../../AGENTS.md).
2. Add the test file to the test tree in [`tests/AGENTS.md`](../../../tests/AGENTS.md).
3. Add the parser to the ImportService section in the
`service-repository-reference` skill.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,39 @@ cross-layer data interchange.
| `delete_environment(id)` | `None` | Delete environment |
| `update_environment_values(id, values)` | `None` | Replace key-value pairs |

### Run history repository (`run_history_repository.py`)

| Function | Returns | Purpose |
|----------|---------|---------|
| `create_run(collection_id, source?)` | `RunHistoryModel` | Start a new run record |
| `finish_run(run_id, **stats)` | `None` | Finalise a run with duration, test counts, status |
| `add_result(run_id, **fields)` | `RunResultModel` | Add a per-request result |
| `get_runs_for_collection(collection_id, limit?)` | `list[RunHistoryModel]` | Runs for a collection, newest first |
| `get_run_results(run_id)` | `list[RunResultModel]` | Results for a run, ordered by ID |
| `delete_run(run_id)` | `bool` | Delete a single run (True if found) |
| `delete_runs_for_collection(collection_id)` | `int` | Delete all runs for a collection, return count |

### Local script repository (`local_script_repository.py`)

| Function | Returns | Purpose |
|----------|---------|---------|
| `create_folder(name, parent_id?)` | `LocalScriptFolderModel` | Create folder |
| `create_script(folder_id, name, *, language, module_format="esm", content)` | `LocalScriptModel` | Create script; ``module_format`` validated via ``_normalize_module_format`` |
| `rename_script_and_rewrite_refs(script_id, new_name, *, language?, module_format?)` | `int` | Rename + rewrite ``pm.require("local:…")`` when virtual path changes (``.js`` ↔ ``.cjs``) |
| `move_script_and_rewrite_refs(script_id, new_folder_id)` | `int` | Move + rewrite local refs |
| `update_script_content(script_id, content, language?, module_format?)` | `None` | Persist editor body |

``module_format="commonjs"`` is only valid when ``language=="javascript"``; otherwise
``ValueError``. TypeScript/Python rows always store ``"esm"``.

### Local script query repository (`local_script_query_repository.py`)

| Function | Returns | Purpose |
|----------|---------|---------|
| `fetch_all_local_scripts_tree()` | `dict[str, Any]` | Nested tree; script nodes include ``module_format`` |
| `get_script_by_id(script_id)` | `LocalScriptModel \| None` | PK lookup |
| `get_local_script_breadcrumb(script_id)` | `list[dict[str, Any]]` | Breadcrumb segments |

## Service method catalogue

### CollectionService
Expand Down Expand Up @@ -164,6 +197,41 @@ variable substitution via `{{variable}}` syntax.
| `add_variable(source, source_id, key, value)` | Add (or update) a variable to a collection or environment |
| `substitute(text, variables)` | Replace `{{key}}` placeholders in text |

### RunHistoryService

All methods are `@staticmethod`. Wraps `run_history_repository` for run
history CRUD.

| Method | Purpose |
|--------|---------|
| `create_run(collection_id, source?)` | Start a new run record |
| `finish_run(run_id, **stats)` | Finalise a run with stats (incl. `skipped`) |
| `add_result(run_id, **fields)` | Add a per-request result |
| `get_runs(collection_id, limit?)` | Runs for a collection as list of dicts |
| `get_results(run_id)` | Results for a run as list of dicts |
| `delete_run(run_id)` | Delete a single run |
| `delete_runs(collection_id)` | Delete all runs for a collection |

### LocalScriptService (`services/local_script_service.py`)

All methods are `@staticmethod`. UI must use this module, not `database/`.

| Method | Purpose |
|--------|---------|
| `fetch_all()` | Nested local-scripts tree dict (includes ``module_format`` on script nodes) |
| `list_virtual_paths(*, language)` | Virtual paths for ``pm.require("local:…")`` autocomplete |
| `get_script_load_dict(script_id)` | Editor open payload (see ``LocalScriptLoadDict``) |
| `create_script(folder_id, name, *, language, module_format="esm", content)` | Create script |
| `rename_script(script_id, new_name, *, language?, module_format?)` | Rename + ref rewrite |
| `save_script_content(script_id, content, language?, module_format?)` | Persist buffer |

**CJS policy:** ``.cjs`` local scripts are leaf modules — no ``pm.require("local:…")``
inside CJS bodies (enforced in ``local_script_modules.resolve_required``). Consumers
use ``pm.require("local:…/file.cjs")`` from ESM pre-request/test scripts only.

**UI signals (local scripts tree):** ``new_script_clicked(str, str)`` (language,
module_format); ``new_script_requested(object, str, str)`` on header; ``script_rename_requested(int, str, str, str)`` on ``CollectionTree`` and ``CollectionWidget``.

### GraphQLSchemaService

All methods are `@staticmethod`.
Expand Down Expand Up @@ -228,6 +296,70 @@ fragment capture), Password Credentials (direct POST), Client Credentials
(direct POST). Browser-based flows open the system browser and start a
local HTTP server to capture the callback.

### ScriptService

All methods are `@staticmethod`. Resolves inherited script chains
by walking the collection ancestor tree.

| Method | Returns | Purpose |
|--------|---------|---------|
| `build_script_chain(request_id)` | `tuple[list[ScriptEntry], list[ScriptEntry]]` | Collect pre-request and test scripts from ancestors + self |

### ScriptEngine

All methods are `@staticmethod`. Orchestrates script execution across
`DenoRuntime` / `JSRuntime` (Deno ``deno run`` subprocess) and `PyRuntime` (RestrictedPython subprocess).

| Method | Returns | Purpose |
|--------|---------|---------|
| `run_pre_request_scripts(chain, context)` | `ScriptOutput` | Run pre-request chain, merge outputs |
| `run_test_scripts(chain, context)` | `ScriptOutput` | Run test chain, merge outputs |
| `run_single(script, language, context)` | `ScriptOutput` | Run one script in specified runtime |

Module-level helper in `services/scripting/engine.py` (used by the script
editor gutter; not a `ScriptEngine` method):

| Function | Returns | Purpose |
|----------|---------|---------|
| `find_pm_tests(source, language)` | `list[dict[str, Any]]` | `{"name", "line"}` (1-based) for each `pm.test` (Python AST, JS esprima + regex fallback) |
| `find_top_level_statement_lines(source, language)` | `set[int]` | 0-based lines of top-level statements (step-debugger checkpoints); empty set means do not style breakpoints as unreachable |

`ScriptLinter` exposes `_esprima_parse_result` for the shared esprima JSON
parse (linting + `find_pm_tests` and `find_top_level_statement_lines`).

**Response assertions (Postman-compat):** In `data/scripts/pm_bootstrap.js`,
`pm.response.to` is a getter that returns a new `__Expectation` wrapping the
response (so `pm.response.to.have.status(200)` works). **`pm.require`** in JS
loads `npm:` / `jsr:` packages only when the specifier is a **string literal**
in the user script: `js_runtime._detect_pm_require_specs` validates the name and
exact semver, `js_runtime._pm_require_imports_block` emits static ESM `import`s
and registers `globalThis.__pm_require_modules` (see `deno_runtime.deno_ipc_argv_and_env`
for cache + optional `--allow-net`). In
`services/scripting/_py_sandbox.py`, `_PmResponse.to` returns `_Expectation(self)`;
`jsonBody` is aliased to `json_body` on the Python side.

**Python (Pyodide):** When `data/scripts/vendor_pyodide/pyodide.asm.wasm` exists and
Deno is available, `PyRuntime.execute` uses `pyodide_runtime.PyodideRuntime` →
`data/scripts/pyodide_run.mjs` (`loadPyodide` from `./vendor_pyodide/pyodide.mjs`,
`micropip` for `py_runtime.detect_pm_require_py_specs` literals, then
`data/scripts/pm_bootstrap.py` — hand-maintained Pyodide bootstrap (mirrors
`_py_sandbox.py` / `pm_bootstrap.js`; do **not** run deprecated
`scripts/gen_pm_bootstrap_pyodide.py`), with
`postmark_ipc.send_request_sync`, `pm.require`, safe builtins including `print` → `_console_emit`,
and `collect_pm_output`. Otherwise execution
stays on `_py_sandbox.py` + RestrictedPython (`PyRuntime.execute_restricted`).

Context builders and utilities in `services/scripting/context.py`:

| Function | Purpose |
|----------|---------|
| `build_pre_request_context(...)` | Build `ScriptInput` for pre-request scripts |
| `build_test_context(...)` | Build `ScriptInput` for test/post-response scripts |
| `normalize_events(events)` | Convert Postman-style event list to `{pre_request, test}` dict |
| `execute_sub_request(spec)` | HTTP bridge for `pm.sendRequest()` (scheme whitelist, rate-limited) |
| `load_globals()` | Load persisted global variables from `data/globals.json` |
| `save_globals(changes)` | Merge changes into persisted globals file |

## TypedDict schemas

### SnippetOptions (`services/http/snippet_generator/generator.py`)
Expand Down Expand Up @@ -316,6 +448,17 @@ class RequestLoadDict(TypedDict, total=False):
auth: dict[str, Any] | None
```

### LocalScriptService TypedDicts (`services/local_script_service.py`)

```python
class LocalScriptLoadDict(TypedDict, total=False):
id: int
name: str
language: str
module_format: str # "esm" | "commonjs"
content: str
```

### EnvironmentService TypedDicts (`services/environment_service.py`)

```python
Expand Down Expand Up @@ -360,6 +503,46 @@ class ImportSummary(TypedDict): ...

See `services/import_parser/models.py` for full field definitions.

### Scripting TypedDicts (`services/scripting/__init__.py`)

```python
class ScriptInput(TypedDict):
request: dict[str, Any] # method, url, headers, body
response: dict[str, Any] # status, headers, body, elapsed_ms (test only)
variables: dict[str, str] # combined environment + collection vars
environment_vars: dict[str, str] # environment-scoped variables
collection_vars: dict[str, str] # collection-scoped variables
global_vars: NotRequired[dict[str, str]] # persisted global variables
info: dict[str, Any] # request name, iteration index
iteration_data: NotRequired[dict[str, Any]] # data-driven row (runner only)

class ScriptOutput(TypedDict):
test_results: list[TestResult] # pm.test() assertion results
console_logs: list[ConsoleLog] # console.log/warn/error output
variable_changes: dict[str, str] # pm.variables/environment/collection changes
global_variable_changes: NotRequired[dict[str, str]] # pm.globals changes
request_mutations: dict[str, Any] | None # pm.request.* mutations
next_request: NotRequired[str | None] # pm.execution.setNextRequest()
skip_request: NotRequired[bool] # pm.execution.skipRequest()

class ScriptEntry(TypedDict):
code: str # script source code
language: str # "javascript", "typescript", or "python"
source_name: str # display label (e.g. "Collection > Test")

class TestResult(TypedDict):
name: str # test description
passed: bool # assertion outcome
error: str | None # failure message
duration_ms: float # execution time

class ConsoleLog(TypedDict):
level: str # "log", "warn", "error", "info"
message: str # formatted message
timestamp: float # time.time() value
source_line: NotRequired[int | None] # 0-based editor line (best-effort)
```

### Theme TypedDict (`ui/styling/theme.py`)

```python
Expand All @@ -370,8 +553,8 @@ See `ui/styling/theme.py` for full field definitions.

## Response viewer and popup system

`ResponseViewerWidget` displays the HTTP response with four tabs:
Body, Headers, Cookies, and Saved.
`ResponseViewerWidget` displays the HTTP response with five tabs:
Body, Headers, Cookies, Test Results (hidden), and Pre-request (hidden).

### Body tab

Expand Down
Loading
Loading