abstractgateway 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractgateway-0.1.0/.gitignore +178 -0
- abstractgateway-0.1.0/PKG-INFO +101 -0
- abstractgateway-0.1.0/README.md +83 -0
- abstractgateway-0.1.0/docs/architecture.md +71 -0
- abstractgateway-0.1.0/pyproject.toml +39 -0
- abstractgateway-0.1.0/src/abstractgateway/__init__.py +11 -0
- abstractgateway-0.1.0/src/abstractgateway/app.py +55 -0
- abstractgateway-0.1.0/src/abstractgateway/cli.py +30 -0
- abstractgateway-0.1.0/src/abstractgateway/config.py +94 -0
- abstractgateway-0.1.0/src/abstractgateway/hosts/__init__.py +6 -0
- abstractgateway-0.1.0/src/abstractgateway/hosts/bundle_host.py +626 -0
- abstractgateway-0.1.0/src/abstractgateway/hosts/visualflow_host.py +213 -0
- abstractgateway-0.1.0/src/abstractgateway/routes/__init__.py +5 -0
- abstractgateway-0.1.0/src/abstractgateway/routes/gateway.py +393 -0
- abstractgateway-0.1.0/src/abstractgateway/runner.py +429 -0
- abstractgateway-0.1.0/src/abstractgateway/security/__init__.py +5 -0
- abstractgateway-0.1.0/src/abstractgateway/security/gateway_security.py +504 -0
- abstractgateway-0.1.0/src/abstractgateway/service.py +134 -0
- abstractgateway-0.1.0/src/abstractgateway/stores.py +34 -0
- abstractgateway-0.1.0/tests/test_gateway_bundle_llm_tools_agents.py +778 -0
- abstractgateway-0.1.0/tests/test_gateway_http_api.py +309 -0
- abstractgateway-0.1.0/tests/test_gateway_security_middleware_unit.py +137 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.pyc
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
pip-wheel-metadata/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# pipenv
|
|
89
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
90
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
91
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
92
|
+
# install all needed dependencies.
|
|
93
|
+
#Pipfile.lock
|
|
94
|
+
|
|
95
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
96
|
+
__pypackages__/
|
|
97
|
+
|
|
98
|
+
# Celery stuff
|
|
99
|
+
celerybeat-schedule
|
|
100
|
+
celerybeat.pid
|
|
101
|
+
|
|
102
|
+
# SageMath parsed files
|
|
103
|
+
*.sage.py
|
|
104
|
+
|
|
105
|
+
# Environments
|
|
106
|
+
.env
|
|
107
|
+
.venv
|
|
108
|
+
env/
|
|
109
|
+
venv/
|
|
110
|
+
ENV/
|
|
111
|
+
env.bak/
|
|
112
|
+
venv.bak/
|
|
113
|
+
|
|
114
|
+
# Spyder project settings
|
|
115
|
+
.spyderproject
|
|
116
|
+
.spyproject
|
|
117
|
+
|
|
118
|
+
# Rope project settings
|
|
119
|
+
.ropeproject
|
|
120
|
+
|
|
121
|
+
# mkdocs documentation
|
|
122
|
+
/site
|
|
123
|
+
|
|
124
|
+
# mypy
|
|
125
|
+
.mypy_cache/
|
|
126
|
+
.dmypy.json
|
|
127
|
+
dmypy.json
|
|
128
|
+
|
|
129
|
+
# Pyre type checker
|
|
130
|
+
.pyre/
|
|
131
|
+
|
|
132
|
+
# IDE
|
|
133
|
+
.vscode/
|
|
134
|
+
.idea/
|
|
135
|
+
*.swp
|
|
136
|
+
*.swo
|
|
137
|
+
*~
|
|
138
|
+
|
|
139
|
+
# macOS
|
|
140
|
+
.DS_Store
|
|
141
|
+
|
|
142
|
+
# Windows
|
|
143
|
+
Thumbs.db
|
|
144
|
+
ehthumbs.db
|
|
145
|
+
Desktop.ini
|
|
146
|
+
|
|
147
|
+
# Node.js / Frontend
|
|
148
|
+
node_modules/
|
|
149
|
+
web/frontend/node_modules/
|
|
150
|
+
web/frontend/dist/
|
|
151
|
+
web/frontend/.vite/
|
|
152
|
+
*.local
|
|
153
|
+
|
|
154
|
+
# Package manager locks (optional - uncomment if you want to ignore)
|
|
155
|
+
# package-lock.json
|
|
156
|
+
# yarn.lock
|
|
157
|
+
# pnpm-lock.yaml
|
|
158
|
+
|
|
159
|
+
# TypeScript
|
|
160
|
+
*.tsbuildinfo
|
|
161
|
+
|
|
162
|
+
# ESLint
|
|
163
|
+
.eslintcache
|
|
164
|
+
|
|
165
|
+
# Vite
|
|
166
|
+
*.local
|
|
167
|
+
|
|
168
|
+
# SQLite databases (for flow storage)
|
|
169
|
+
*.db
|
|
170
|
+
*.sqlite
|
|
171
|
+
*.sqlite3
|
|
172
|
+
flows.db
|
|
173
|
+
|
|
174
|
+
# Monaco Editor cache
|
|
175
|
+
.monaco/
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
web/runtime/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstractgateway
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AbstractGateway: deployable Run Gateway host for AbstractRuntime (commands + ledger).
|
|
5
|
+
Project-URL: GitHub, https://github.com/lpalbou/abstractgateway
|
|
6
|
+
Author: Laurent-Philippe Albou
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: abstractruntime>=0.4.0
|
|
10
|
+
Requires-Dist: fastapi>=0.100.0
|
|
11
|
+
Requires-Dist: uvicorn[standard]>=0.23.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
15
|
+
Provides-Extra: visualflow
|
|
16
|
+
Requires-Dist: abstractflow>=0.3.0; extra == 'visualflow'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# AbstractGateway
|
|
20
|
+
|
|
21
|
+
AbstractGateway is the **deployable Run Gateway host** for AbstractRuntime runs:
|
|
22
|
+
- durable command inbox
|
|
23
|
+
- ledger replay/stream
|
|
24
|
+
- security baseline (token + origin + limits)
|
|
25
|
+
|
|
26
|
+
This decouples the gateway service from any specific UI (AbstractFlow, AbstractCode, web/PWA thin clients).
|
|
27
|
+
|
|
28
|
+
## What it does (contract)
|
|
29
|
+
- Clients **act** by submitting durable commands: `start`, `resume`, `pause`, `cancel`, `emit_event`
|
|
30
|
+
- Clients **render** by replaying/streaming the durable ledger (cursor-based, replay-first)
|
|
31
|
+
|
|
32
|
+
Endpoints:
|
|
33
|
+
- `POST /api/gateway/runs/start`
|
|
34
|
+
- `GET /api/gateway/runs/{run_id}`
|
|
35
|
+
- `GET /api/gateway/runs/{run_id}/ledger`
|
|
36
|
+
- `GET /api/gateway/runs/{run_id}/ledger/stream` (SSE)
|
|
37
|
+
- `POST /api/gateway/commands`
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
### Default (bundle mode)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install abstractgateway
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Bundle mode executes **WorkflowBundles** (`.flow`) via **WorkflowArtifacts** without importing `abstractflow`.
|
|
48
|
+
|
|
49
|
+
### Optional (compatibility): VisualFlow JSON
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install "abstractgateway[visualflow]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This mode depends on the **AbstractFlow compiler library** (`abstractflow`) to interpret VisualFlow JSON (it does **not** require the AbstractFlow web UI/app).
|
|
56
|
+
|
|
57
|
+
## Run
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export ABSTRACTGATEWAY_DATA_DIR="./runtime"
|
|
61
|
+
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles-or-flow"
|
|
62
|
+
|
|
63
|
+
# Security (recommended)
|
|
64
|
+
export ABSTRACTGATEWAY_AUTH_TOKEN="your-token"
|
|
65
|
+
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="*"
|
|
66
|
+
|
|
67
|
+
abstractgateway serve --host 127.0.0.1 --port 8080
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Notes:
|
|
71
|
+
- `ABSTRACTGATEWAY_WORKFLOW_SOURCE` defaults to `bundle`. Valid values:
|
|
72
|
+
- `bundle` (default): `ABSTRACTGATEWAY_FLOWS_DIR` points to a directory containing `*.flow` bundles (or a single `.flow` file)
|
|
73
|
+
- `visualflow` (compat): `ABSTRACTGATEWAY_FLOWS_DIR` points to a directory containing `*.json` VisualFlow files
|
|
74
|
+
- For production, run behind HTTPS (reverse proxy) and set exact allowed origins.
|
|
75
|
+
|
|
76
|
+
## Creating a `.flow` bundle (authoring)
|
|
77
|
+
|
|
78
|
+
Use AbstractFlow to pack a bundle:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
abstractflow bundle pack /path/to/root.json --out /path/to/bundles/my.flow --flows-dir /path/to/flows
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Starting a run (bundle mode)
|
|
85
|
+
|
|
86
|
+
The stable way is to pass `bundle_id` + `flow_id`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -sS -X POST "http://localhost:8080/api/gateway/runs/start" \
|
|
90
|
+
-H "Content-Type: application/json" \
|
|
91
|
+
-H "Authorization: Bearer your-token" \
|
|
92
|
+
-d '{"bundle_id":"my-bundle","flow_id":"ac-echo","input_data":{}}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For backwards-compatible clients, you can also pass a namespaced id as `flow_id` (`"my-bundle:ac-echo"`).
|
|
96
|
+
|
|
97
|
+
## Docs
|
|
98
|
+
- Architecture: `docs/architecture.md` (framework) and `abstractgateway/docs/architecture.md` (this package)
|
|
99
|
+
- Deployment: `docs/guide/deployment.md`
|
|
100
|
+
|
|
101
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# AbstractGateway
|
|
2
|
+
|
|
3
|
+
AbstractGateway is the **deployable Run Gateway host** for AbstractRuntime runs:
|
|
4
|
+
- durable command inbox
|
|
5
|
+
- ledger replay/stream
|
|
6
|
+
- security baseline (token + origin + limits)
|
|
7
|
+
|
|
8
|
+
This decouples the gateway service from any specific UI (AbstractFlow, AbstractCode, web/PWA thin clients).
|
|
9
|
+
|
|
10
|
+
## What it does (contract)
|
|
11
|
+
- Clients **act** by submitting durable commands: `start`, `resume`, `pause`, `cancel`, `emit_event`
|
|
12
|
+
- Clients **render** by replaying/streaming the durable ledger (cursor-based, replay-first)
|
|
13
|
+
|
|
14
|
+
Endpoints:
|
|
15
|
+
- `POST /api/gateway/runs/start`
|
|
16
|
+
- `GET /api/gateway/runs/{run_id}`
|
|
17
|
+
- `GET /api/gateway/runs/{run_id}/ledger`
|
|
18
|
+
- `GET /api/gateway/runs/{run_id}/ledger/stream` (SSE)
|
|
19
|
+
- `POST /api/gateway/commands`
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
### Default (bundle mode)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install abstractgateway
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Bundle mode executes **WorkflowBundles** (`.flow`) via **WorkflowArtifacts** without importing `abstractflow`.
|
|
30
|
+
|
|
31
|
+
### Optional (compatibility): VisualFlow JSON
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "abstractgateway[visualflow]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This mode depends on the **AbstractFlow compiler library** (`abstractflow`) to interpret VisualFlow JSON (it does **not** require the AbstractFlow web UI/app).
|
|
38
|
+
|
|
39
|
+
## Run
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export ABSTRACTGATEWAY_DATA_DIR="./runtime"
|
|
43
|
+
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles-or-flow"
|
|
44
|
+
|
|
45
|
+
# Security (recommended)
|
|
46
|
+
export ABSTRACTGATEWAY_AUTH_TOKEN="your-token"
|
|
47
|
+
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="*"
|
|
48
|
+
|
|
49
|
+
abstractgateway serve --host 127.0.0.1 --port 8080
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Notes:
|
|
53
|
+
- `ABSTRACTGATEWAY_WORKFLOW_SOURCE` defaults to `bundle`. Valid values:
|
|
54
|
+
- `bundle` (default): `ABSTRACTGATEWAY_FLOWS_DIR` points to a directory containing `*.flow` bundles (or a single `.flow` file)
|
|
55
|
+
- `visualflow` (compat): `ABSTRACTGATEWAY_FLOWS_DIR` points to a directory containing `*.json` VisualFlow files
|
|
56
|
+
- For production, run behind HTTPS (reverse proxy) and set exact allowed origins.
|
|
57
|
+
|
|
58
|
+
## Creating a `.flow` bundle (authoring)
|
|
59
|
+
|
|
60
|
+
Use AbstractFlow to pack a bundle:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
abstractflow bundle pack /path/to/root.json --out /path/to/bundles/my.flow --flows-dir /path/to/flows
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Starting a run (bundle mode)
|
|
67
|
+
|
|
68
|
+
The stable way is to pass `bundle_id` + `flow_id`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -sS -X POST "http://localhost:8080/api/gateway/runs/start" \
|
|
72
|
+
-H "Content-Type: application/json" \
|
|
73
|
+
-H "Authorization: Bearer your-token" \
|
|
74
|
+
-d '{"bundle_id":"my-bundle","flow_id":"ac-echo","input_data":{}}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For backwards-compatible clients, you can also pass a namespaced id as `flow_id` (`"my-bundle:ac-echo"`).
|
|
78
|
+
|
|
79
|
+
## Docs
|
|
80
|
+
- Architecture: `docs/architecture.md` (framework) and `abstractgateway/docs/architecture.md` (this package)
|
|
81
|
+
- Deployment: `docs/guide/deployment.md`
|
|
82
|
+
|
|
83
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# AbstractGateway — Architecture (Living)
|
|
2
|
+
|
|
3
|
+
> Updated: 2026-01-08
|
|
4
|
+
> Status: implemented (see completed backlog 318)
|
|
5
|
+
|
|
6
|
+
AbstractGateway is the **deployable control-plane host** for AbstractRuntime runs:
|
|
7
|
+
- clients submit **durable commands** (start/resume/pause/cancel/emit_event)
|
|
8
|
+
- clients render by replaying/streaming the **append-only ledger** (cursor-based)
|
|
9
|
+
- the gateway host owns the durable stores and is the single authority for a run (ADR‑0020)
|
|
10
|
+
|
|
11
|
+
## Diagram (v0)
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
subgraph Clients["Clients (stateless thin UIs)"]
|
|
16
|
+
ACode["AbstractCode (TUI)"]
|
|
17
|
+
Web["Web/PWA Thin Client"]
|
|
18
|
+
FlowUI["AbstractFlow UI"]
|
|
19
|
+
Third["3rd-party apps"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
subgraph GW["AbstractGateway (service)"]
|
|
23
|
+
API["HTTP API + SSE\n/api/gateway/*"]
|
|
24
|
+
Inbox["Durable Command Inbox\n(CommandStore)"]
|
|
25
|
+
Runner["GatewayRunner\npoll + tick"]
|
|
26
|
+
Stores["RunStore / LedgerStore / ArtifactStore"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph RT["AbstractRuntime (kernel)"]
|
|
30
|
+
Runtime["Runtime.tick / resume"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Clients -->|commands| API
|
|
34
|
+
API --> Inbox
|
|
35
|
+
API -->|ledger replay/stream| Stores
|
|
36
|
+
Inbox --> Runner
|
|
37
|
+
Runner --> Runtime
|
|
38
|
+
Runtime --> Stores
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Scope and packaging
|
|
42
|
+
AbstractGateway should be deployable without installing authoring tools (AbstractFlow).
|
|
43
|
+
Workflow loading must therefore be pluggable:
|
|
44
|
+
- core `abstractgateway` depends on `abstractruntime`
|
|
45
|
+
- default workflow source: **WorkflowBundles (.flow)** containing **VisualFlow JSON** (`manifest.flows`), compiled via `abstractruntime.visualflow_compiler` (no `abstractflow` import)
|
|
46
|
+
- optional extras can add additional workflow sources (e.g. a “directory of VisualFlow JSON files” host wired with authoring-side helpers)
|
|
47
|
+
|
|
48
|
+
Bundle-mode execution wiring:
|
|
49
|
+
- VisualFlow is compiled via `abstractruntime.visualflow_compiler` (single semantics engine).
|
|
50
|
+
- LLM/tool workflows are supported by wiring `abstractruntime.integrations.abstractcore`:
|
|
51
|
+
- `LLM_CALL` handler (AbstractCore-backed)
|
|
52
|
+
- `TOOL_CALLS` handler (host-configured tool executor)
|
|
53
|
+
- Visual Agent nodes are supported by registering deterministic per-node ReAct workflows (requires `abstractagent`).
|
|
54
|
+
- Visual “On Event” nodes are supported by compiling derived listener workflows and starting them as child runs in the same session.
|
|
55
|
+
|
|
56
|
+
Runtime configuration (env):
|
|
57
|
+
- `ABSTRACTGATEWAY_PROVIDER` / `ABSTRACTGATEWAY_MODEL`: default provider/model for bundle-mode runs that contain LLM nodes.
|
|
58
|
+
- `ABSTRACTGATEWAY_TOOL_MODE`:
|
|
59
|
+
- `passthrough` (default): tool calls enter a durable wait (safest for untrusted hosts)
|
|
60
|
+
- `local`: tool calls execute in the gateway process (dev only)
|
|
61
|
+
|
|
62
|
+
Run start identifiers (bundle mode):
|
|
63
|
+
- The **bundle** is the portable distribution unit. Clients should primarily identify “what to run” via `bundle_id`.
|
|
64
|
+
- The **flow id** selects *which entrypoint/subflow* within the bundle to start:
|
|
65
|
+
- If a bundle has a single `manifest.entrypoints[]` item **or** declares `manifest.default_entrypoint`, the gateway can start it with `{bundle_id, input_data}` (no `flow_id`).
|
|
66
|
+
- If the bundle has multiple entrypoints and no `default_entrypoint`, clients must specify `flow_id` (or pass a fully-qualified workflow id like `bundle:flow`).
|
|
67
|
+
|
|
68
|
+
## Related
|
|
69
|
+
- Backlog 318: `docs/backlog/completed/318-framework-abstractgateway-extract-run-gateway-host.md`
|
|
70
|
+
- ADR‑0018: `docs/adr/0018-durable-run-gateway-and-remote-host-control-plane.md`
|
|
71
|
+
- ADR‑0020: `docs/adr/0020-agent-host-pool-and-orchestrator-placement.md`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "abstractgateway"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AbstractGateway: deployable Run Gateway host for AbstractRuntime (commands + ledger)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Laurent-Philippe Albou" }]
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"abstractruntime>=0.4.0",
|
|
16
|
+
"fastapi>=0.100.0",
|
|
17
|
+
"uvicorn[standard]>=0.23.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
# Optional support for starting runs from VisualFlow JSON (compiled via AbstractFlow).
|
|
22
|
+
visualflow = [
|
|
23
|
+
"abstractflow>=0.3.0",
|
|
24
|
+
]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7.0.0",
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
abstractgateway = "abstractgateway.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
"GitHub" = "https://github.com/lpalbou/abstractgateway"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/abstractgateway"]
|
|
38
|
+
|
|
39
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""AbstractGateway.
|
|
2
|
+
|
|
3
|
+
AbstractGateway is a deployable Run Gateway host for AbstractRuntime:
|
|
4
|
+
- durable command inbox (start/resume/pause/cancel/emit_event)
|
|
5
|
+
- ledger replay + SSE streaming (replay-first)
|
|
6
|
+
- security middleware for network-safe deployments
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""AbstractGateway FastAPI application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
|
|
10
|
+
from .routes import gateway_router
|
|
11
|
+
from .security import GatewaySecurityMiddleware, load_gateway_auth_policy_from_env
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@asynccontextmanager
|
|
15
|
+
async def _lifespan(_app: FastAPI):
|
|
16
|
+
# Start the background worker that polls the durable command inbox and ticks runs.
|
|
17
|
+
from .service import start_gateway_runner, stop_gateway_runner
|
|
18
|
+
|
|
19
|
+
start_gateway_runner()
|
|
20
|
+
try:
|
|
21
|
+
yield
|
|
22
|
+
finally:
|
|
23
|
+
stop_gateway_runner()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = FastAPI(
|
|
27
|
+
title="AbstractGateway",
|
|
28
|
+
description="Durable Run Gateway for AbstractRuntime (commands + ledger replay/stream).",
|
|
29
|
+
version="0.1.0",
|
|
30
|
+
lifespan=_lifespan,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Gateway security (backlog 309).
|
|
34
|
+
app.add_middleware(GatewaySecurityMiddleware, policy=load_gateway_auth_policy_from_env())
|
|
35
|
+
|
|
36
|
+
# CORS for browser clients. In production, prefer configuring exact origins and terminating TLS at a reverse proxy.
|
|
37
|
+
#
|
|
38
|
+
# IMPORTANT: add after GatewaySecurityMiddleware so CORS headers are present even on early security rejections
|
|
39
|
+
# (otherwise browsers surface a generic "NetworkError").
|
|
40
|
+
app.add_middleware(
|
|
41
|
+
CORSMiddleware,
|
|
42
|
+
allow_origins=["*"],
|
|
43
|
+
allow_credentials=True,
|
|
44
|
+
allow_methods=["*"],
|
|
45
|
+
allow_headers=["*"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
app.include_router(gateway_router, prefix="/api")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.get("/api/health")
|
|
52
|
+
async def health_check():
|
|
53
|
+
return {"status": "healthy", "service": "abstractgateway"}
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(argv: list[str] | None = None) -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(prog="abstractgateway", description="AbstractGateway (Run Gateway host)")
|
|
8
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
9
|
+
|
|
10
|
+
serve = sub.add_parser("serve", help="Run the AbstractGateway HTTP/SSE server")
|
|
11
|
+
serve.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
|
|
12
|
+
serve.add_argument("--port", type=int, default=8080, help="Bind port (default: 8080)")
|
|
13
|
+
serve.add_argument("--reload", action="store_true", help="Enable auto-reload (dev only)")
|
|
14
|
+
|
|
15
|
+
args = parser.parse_args(argv)
|
|
16
|
+
|
|
17
|
+
if args.cmd == "serve":
|
|
18
|
+
import uvicorn
|
|
19
|
+
|
|
20
|
+
uvicorn.run(
|
|
21
|
+
"abstractgateway.app:app",
|
|
22
|
+
host=str(args.host),
|
|
23
|
+
port=int(args.port),
|
|
24
|
+
reload=bool(args.reload),
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
raise SystemExit(2)
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _as_bool(raw: Any, default: bool) -> bool:
|
|
10
|
+
if raw is None:
|
|
11
|
+
return default
|
|
12
|
+
if isinstance(raw, bool):
|
|
13
|
+
return raw
|
|
14
|
+
s = str(raw).strip().lower()
|
|
15
|
+
if not s:
|
|
16
|
+
return default
|
|
17
|
+
if s in {"1", "true", "yes", "on"}:
|
|
18
|
+
return True
|
|
19
|
+
if s in {"0", "false", "no", "off"}:
|
|
20
|
+
return False
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_int(raw: Optional[str], default: int) -> int:
|
|
25
|
+
if raw is None or not str(raw).strip():
|
|
26
|
+
return default
|
|
27
|
+
try:
|
|
28
|
+
return int(str(raw).strip())
|
|
29
|
+
except Exception:
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_float(raw: Optional[str], default: float) -> float:
|
|
34
|
+
if raw is None or not str(raw).strip():
|
|
35
|
+
return default
|
|
36
|
+
try:
|
|
37
|
+
return float(str(raw).strip())
|
|
38
|
+
except Exception:
|
|
39
|
+
return default
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _env(name: str, fallback: Optional[str] = None) -> Optional[str]:
|
|
43
|
+
v = os.getenv(name)
|
|
44
|
+
if v is not None and str(v).strip():
|
|
45
|
+
return v
|
|
46
|
+
if fallback:
|
|
47
|
+
v2 = os.getenv(fallback)
|
|
48
|
+
if v2 is not None and str(v2).strip():
|
|
49
|
+
return v2
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class GatewayHostConfig:
|
|
55
|
+
"""Process-level configuration for the AbstractGateway host."""
|
|
56
|
+
|
|
57
|
+
data_dir: Path
|
|
58
|
+
flows_dir: Path
|
|
59
|
+
|
|
60
|
+
runner_enabled: bool = True
|
|
61
|
+
poll_interval_s: float = 0.25
|
|
62
|
+
command_batch_limit: int = 200
|
|
63
|
+
tick_max_steps: int = 100
|
|
64
|
+
tick_workers: int = 2
|
|
65
|
+
run_scan_limit: int = 200
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def from_env() -> "GatewayHostConfig":
|
|
69
|
+
# NOTE: We intentionally use ABSTRACTGATEWAY_* as the canonical namespace.
|
|
70
|
+
# For a transition period, we accept legacy ABSTRACTFLOW_* names as fallbacks.
|
|
71
|
+
data_dir_raw = _env("ABSTRACTGATEWAY_DATA_DIR", "ABSTRACTFLOW_RUNTIME_DIR") or "./runtime"
|
|
72
|
+
flows_dir_raw = _env("ABSTRACTGATEWAY_FLOWS_DIR", "ABSTRACTFLOW_FLOWS_DIR") or "./flows"
|
|
73
|
+
|
|
74
|
+
enabled_raw = _env("ABSTRACTGATEWAY_RUNNER", "ABSTRACTFLOW_GATEWAY_RUNNER") or "1"
|
|
75
|
+
runner_enabled = _as_bool(enabled_raw, True)
|
|
76
|
+
|
|
77
|
+
poll_s = _as_float(_env("ABSTRACTGATEWAY_POLL_S", "ABSTRACTFLOW_GATEWAY_POLL_S"), 0.25)
|
|
78
|
+
tick_workers = _as_int(_env("ABSTRACTGATEWAY_TICK_WORKERS", "ABSTRACTFLOW_GATEWAY_TICK_WORKERS"), 2)
|
|
79
|
+
tick_steps = _as_int(_env("ABSTRACTGATEWAY_TICK_MAX_STEPS", "ABSTRACTFLOW_GATEWAY_TICK_MAX_STEPS"), 100)
|
|
80
|
+
batch = _as_int(_env("ABSTRACTGATEWAY_COMMAND_BATCH_LIMIT", "ABSTRACTFLOW_GATEWAY_COMMAND_BATCH_LIMIT"), 200)
|
|
81
|
+
scan = _as_int(_env("ABSTRACTGATEWAY_RUN_SCAN_LIMIT", "ABSTRACTFLOW_GATEWAY_RUN_SCAN_LIMIT"), 200)
|
|
82
|
+
|
|
83
|
+
return GatewayHostConfig(
|
|
84
|
+
data_dir=Path(data_dir_raw).expanduser().resolve(),
|
|
85
|
+
flows_dir=Path(flows_dir_raw).expanduser().resolve(),
|
|
86
|
+
runner_enabled=bool(runner_enabled),
|
|
87
|
+
poll_interval_s=float(poll_s),
|
|
88
|
+
command_batch_limit=max(1, int(batch)),
|
|
89
|
+
tick_max_steps=max(1, int(tick_steps)),
|
|
90
|
+
tick_workers=max(1, int(tick_workers)),
|
|
91
|
+
run_scan_limit=max(1, int(scan)),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|