Skip to content
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,10 @@ whiteboard.md
README_ORIGINAL_DISABLED.md
readme_history/
src/aipass/trigger/trigger_data.lock

# Private integrations — driver layer (@api) and wrapper layer (all branches)
# Per DPLAN-0133. Contents are gitignored; only the scaffold README.md is tracked.
# Drop project-specific code into src/aipass/{branch}/apps/integrations/{project}/
# It stays local. Never appears in git.
src/aipass/*/apps/integrations/**
!src/aipass/*/apps/integrations/README.md
2 changes: 1 addition & 1 deletion STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@

### Friction notes

- **seedgo __init__.py false positive**: `imports: Failed` and `naming: invalid characters` fire on Python reserved filename. Need special-case in seedgo's naming standard and imports standard. Encountered in another project; likely also in AIPass repo. Open a seedgo standard PR when there's bandwidth.
(none active — seedgo __init__.py false positive FIXED by @seedgo in PR #279, 2026-04-14)

### S92 (2026-04-14 ~12:45 PT) — PRE-COMPACT, system prompt reformat work in flight

Expand Down
64 changes: 64 additions & 0 deletions src/aipass/ai_mail/apps/integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# apps/integrations/

Private integration space for this branch.

**This folder is gitignored.** Only this README is tracked. Everything else you drop in here stays local and never appears in git, PRs, or the public repo. Safe by construction, not by discipline.

## What goes here

**Branch-specific wrappers** that consume external systems via the @api driver layer. Each wrapper handles how THIS branch uses an external system in its own domain.

```
apps/integrations/
└── {project}/
├── wrapper.py # How this branch uses the driver
├── config.json # Optional — local config
└── tests/ # Private tests colocated
```

Wrappers should call into `@api`'s generic contracts (e.g. `api.memory_backend.query(...)`), never reference the private project by name in any tracked code. The private project name lives in the @api driver, not here.

## What does NOT go here

- **Driver code** — that belongs in `@api/apps/integrations/{project}/driver.py` (the connection layer).
- **Public business logic** — use `apps/modules/` or `apps/handlers/` for that.
- **Drone plugins** — use `apps/plugins/` for those.
- **Secrets** — they live in `~/.secrets/aipass/`, never in the repo.

## Architecture

The full design is in DPLAN-0133 (private integrations architecture). Three layers:

1. **@api driver layer** (`@api/apps/integrations/{project}/`) — owns the physical connection, auth, transport. Knows the private project name.
2. **Per-branch wrapper layer** (`{this_folder}/{project}/`) — owns how this branch consumes the driver's output in its domain. Calls generic contracts, never names private projects.
3. **Public drone commands** (`drone @api integrations list`, `drone @api integrations call <contract>`) — advertise the extension points without naming specifics. Fork-safe.

## Usage

```python
# Your public code (committed, in apps/modules/ or apps/handlers/)
from aipass.api import memory_backend

results = memory_backend.query("when did we ship watchdog?")
# memory_backend is a generic contract. In your local setup it routes to whatever
# driver you registered in @api/apps/integrations/. In a fresh clone with nothing
# registered, it returns NotConfigured gracefully.
```

```python
# Your private wrapper (in this folder, gitignored)
# apps/integrations/someproject/wrapper.py

from aipass.api import memory_backend

def domain_specific_query(context):
"""Branch-specific query pattern for domain needs."""
hint = build_query_from_context(context)
return memory_backend.query(hint, top_k=5, filter={"kind": "decision"})
```

The wrapper stays here, the call into the contract stays here, no private name leaks into tracked code.

---

See DPLAN-0133 for the full design rationale.
10 changes: 10 additions & 0 deletions src/aipass/api/.seedgo/bypass.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@
"file": "apps/modules/openrouter_client.py",
"standard": "modules",
"reason": "make_call() is a CLI orchestrator — parses args then delegates to client.get_response(). All business logic lives in handlers/openrouter/client.py."
},
{
"file": "apps/integrations/testcontract/driver.py",
"standard": "architecture",
"reason": "Integration drivers live in apps/integrations/{project}/driver.py — gitignored private space intentionally outside the 3-layer structure. Loaded via importlib, not standard package import. See DPLAN-0133."
},
{
"file": "tests/test_integrations.py",
"standard": "architecture",
"reason": "Test file — lives in tests/ by convention, not in the 3-layer app structure. Test files are exempt from layer architecture standard."
}
],
"notes": {
Expand Down
Empty file.
40 changes: 40 additions & 0 deletions src/aipass/api/apps/handlers/integrations/call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# =================== AIPass ====================
# Name: call.py
# Description: Handler for invoking a registered integration contract driver
# Version: 1.0.0
# Created: 2026-04-15
# Modified: 2026-04-15
# =============================================
"""
Call a registered integration contract.

Accepts a resolved driver callable and args from the calling module layer.
No module imports — the module layer owns bridge access and passes the driver in.
Returns a result dict; display is handled by the module layer.
"""
from typing import Callable

from aipass.prax import logger
from aipass.api.apps.handlers.json import json_handler


def invoke(driver_fn: Callable, contract_name: str, args: list[str]) -> dict:
"""
Invoke a contract driver with the given args.

Args:
driver_fn: Resolved callable from bridge (already looked up by module layer).
contract_name: Name of the contract being called (for logging).
args: Arguments forwarded to the driver function.

Returns:
dict with keys: result (str | None), success (bool), error (str | None).
"""
try:
result = driver_fn(*args)
json_handler.log_operation("integrations_call", {"contract": contract_name, "args": args, "success": True})
return {"result": str(result) if result is not None else None, "success": True, "error": None}
except Exception as e:
logger.error(f"[call] Driver '{contract_name}' raised: {e}")
json_handler.log_operation("integrations_call", {"contract": contract_name, "success": False, "error": str(e)})
return {"result": None, "success": False, "error": str(e)}
33 changes: 33 additions & 0 deletions src/aipass/api/apps/handlers/integrations/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# =================== AIPass ====================
# Name: list.py
# Description: Handler for listing registered integration contracts
# Version: 1.0.0
# Created: 2026-04-15
# Modified: 2026-04-15
# =============================================
"""
List registered integration contracts.

Accepts a pre-fetched list of contract names from the calling module layer.
No module imports — the module layer owns bridge access and passes data in.
"""
from aipass.prax import logger
from aipass.api.apps.handlers.json import json_handler


def get_contracts(contracts: list[str]) -> dict:
"""
Log and return contract listing result.

Args:
contracts: Pre-fetched sorted list of contract names from bridge.

Returns:
dict with keys: contracts (list[str]), count (int), success (bool).
"""
try:
json_handler.log_operation("integrations_list", {"count": len(contracts), "contracts": contracts})
return {"contracts": contracts, "count": len(contracts), "success": True}
except Exception as e:
logger.error(f"[list] Failed to process contracts: {e}")
return {"contracts": [], "count": 0, "success": False}
64 changes: 64 additions & 0 deletions src/aipass/api/apps/integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# apps/integrations/

Private integration space for this branch.

**This folder is gitignored.** Only this README is tracked. Everything else you drop in here stays local and never appears in git, PRs, or the public repo. Safe by construction, not by discipline.

## What goes here

**Branch-specific wrappers** that consume external systems via the @api driver layer. Each wrapper handles how THIS branch uses an external system in its own domain.

```
apps/integrations/
└── {project}/
├── wrapper.py # How this branch uses the driver
├── config.json # Optional — local config
└── tests/ # Private tests colocated
```

Wrappers should call into `@api`'s generic contracts (e.g. `api.memory_backend.query(...)`), never reference the private project by name in any tracked code. The private project name lives in the @api driver, not here.

## What does NOT go here

- **Driver code** — that belongs in `@api/apps/integrations/{project}/driver.py` (the connection layer).
- **Public business logic** — use `apps/modules/` or `apps/handlers/` for that.
- **Drone plugins** — use `apps/plugins/` for those.
- **Secrets** — they live in `~/.secrets/aipass/`, never in the repo.

## Architecture

The full design is in DPLAN-0133 (private integrations architecture). Three layers:

1. **@api driver layer** (`@api/apps/integrations/{project}/`) — owns the physical connection, auth, transport. Knows the private project name.
2. **Per-branch wrapper layer** (`{this_folder}/{project}/`) — owns how this branch consumes the driver's output in its domain. Calls generic contracts, never names private projects.
3. **Public drone commands** (`drone @api integrations list`, `drone @api integrations call <contract>`) — advertise the extension points without naming specifics. Fork-safe.

## Usage

```python
# Your public code (committed, in apps/modules/ or apps/handlers/)
from aipass.api import memory_backend

results = memory_backend.query("when did we ship watchdog?")
# memory_backend is a generic contract. In your local setup it routes to whatever
# driver you registered in @api/apps/integrations/. In a fresh clone with nothing
# registered, it returns NotConfigured gracefully.
```

```python
# Your private wrapper (in this folder, gitignored)
# apps/integrations/someproject/wrapper.py

from aipass.api import memory_backend

def domain_specific_query(context):
"""Branch-specific query pattern for domain needs."""
hint = build_query_from_context(context)
return memory_backend.query(hint, top_k=5, filter={"kind": "decision"})
```

The wrapper stays here, the call into the contract stays here, no private name leaks into tracked code.

---

See DPLAN-0133 for the full design rationale.
58 changes: 58 additions & 0 deletions src/aipass/api/apps/modules/bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# =================== AIPass ====================
# Name: bridge.py
# Description: Generic contract registry — maps contract names to driver functions
# Version: 1.0.0
# Created: 2026-04-15
# Modified: 2026-04-15
# =============================================
"""
Generic contract registry for @api driver layer.

Contracts are string names (e.g. "memory", "search") that map to driver functions.
Drivers register themselves; callers resolve by name.
Bridge itself is stateless beyond the registry dict — no threading, no startup side effects.
"""
from typing import Callable

from aipass.api.apps.handlers.json import json_handler
from aipass.cli.apps.modules import console, header


def print_introspection() -> None:
"""Show bridge registry introspection."""
console.print()
header("Bridge — Contract Registry")
console.print()
console.print("[cyan]Purpose:[/cyan] Generic contract registry mapping names to driver functions")
console.print()
contracts = list_contracts()
if contracts:
console.print("[cyan]Registered contracts:[/cyan]")
for name in contracts:
console.print(f" • {name}")
else:
console.print("[dim]No contracts registered.[/dim]")
console.print()
json_handler.log_operation("bridge_introspection", {"contracts": contracts})

_registry: dict[str, Callable] = {}


def register(contract_name: str, driver_fn: Callable) -> None:
"""Register a driver function under a contract name."""
_registry[contract_name] = driver_fn


def resolve(contract_name: str) -> Callable | None:
"""Return the registered driver for contract_name, or None."""
return _registry.get(contract_name)


def list_contracts() -> list[str]:
"""Return all registered contract names, sorted."""
return sorted(_registry.keys())


def clear() -> None:
"""Clear all registrations. Intended for test teardown only."""
_registry.clear()
Loading
Loading