optio-antigravity 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.
- optio_antigravity-0.1.0/PKG-INFO +67 -0
- optio_antigravity-0.1.0/README.md +35 -0
- optio_antigravity-0.1.0/pyproject.toml +56 -0
- optio_antigravity-0.1.0/setup.cfg +4 -0
- optio_antigravity-0.1.0/src/optio_antigravity/__init__.py +37 -0
- optio_antigravity-0.1.0/src/optio_antigravity/auth_scrape.py +117 -0
- optio_antigravity-0.1.0/src/optio_antigravity/conversation.py +581 -0
- optio_antigravity-0.1.0/src/optio_antigravity/conversation_listener.py +316 -0
- optio_antigravity-0.1.0/src/optio_antigravity/cred_watcher.py +132 -0
- optio_antigravity-0.1.0/src/optio_antigravity/fs_allowlist.py +88 -0
- optio_antigravity-0.1.0/src/optio_antigravity/host_actions.py +1335 -0
- optio_antigravity-0.1.0/src/optio_antigravity/models.py +115 -0
- optio_antigravity-0.1.0/src/optio_antigravity/prompt.py +190 -0
- optio_antigravity-0.1.0/src/optio_antigravity/seed_manifest.py +178 -0
- optio_antigravity-0.1.0/src/optio_antigravity/session.py +933 -0
- optio_antigravity-0.1.0/src/optio_antigravity/snapshots.py +103 -0
- optio_antigravity-0.1.0/src/optio_antigravity/types.py +340 -0
- optio_antigravity-0.1.0/src/optio_antigravity/verify.py +311 -0
- optio_antigravity-0.1.0/src/optio_antigravity.egg-info/PKG-INFO +67 -0
- optio_antigravity-0.1.0/src/optio_antigravity.egg-info/SOURCES.txt +50 -0
- optio_antigravity-0.1.0/src/optio_antigravity.egg-info/dependency_links.txt +1 -0
- optio_antigravity-0.1.0/src/optio_antigravity.egg-info/requires.txt +10 -0
- optio_antigravity-0.1.0/src/optio_antigravity.egg-info/top_level.txt +1 -0
- optio_antigravity-0.1.0/tests/test_antigravity_cache.py +366 -0
- optio_antigravity-0.1.0/tests/test_auth_scrape.py +125 -0
- optio_antigravity-0.1.0/tests/test_config.py +120 -0
- optio_antigravity-0.1.0/tests/test_conversation.py +277 -0
- optio_antigravity-0.1.0/tests/test_conversation_controls.py +58 -0
- optio_antigravity-0.1.0/tests/test_conversation_listener.py +234 -0
- optio_antigravity-0.1.0/tests/test_conversation_sandbox_enforce.py +103 -0
- optio_antigravity-0.1.0/tests/test_conversation_teardown.py +74 -0
- optio_antigravity-0.1.0/tests/test_cred_watcher.py +235 -0
- optio_antigravity-0.1.0/tests/test_file_download.py +182 -0
- optio_antigravity-0.1.0/tests/test_file_upload.py +30 -0
- optio_antigravity-0.1.0/tests/test_fs_allowlist.py +267 -0
- optio_antigravity-0.1.0/tests/test_host_actions.py +87 -0
- optio_antigravity-0.1.0/tests/test_import.py +4 -0
- optio_antigravity-0.1.0/tests/test_input_wiring.py +99 -0
- optio_antigravity-0.1.0/tests/test_models.py +139 -0
- optio_antigravity-0.1.0/tests/test_ported_features.py +271 -0
- optio_antigravity-0.1.0/tests/test_prompt.py +36 -0
- optio_antigravity-0.1.0/tests/test_real_binary_gates.py +219 -0
- optio_antigravity-0.1.0/tests/test_sandbox_enforce.py +101 -0
- optio_antigravity-0.1.0/tests/test_seed_manifest.py +121 -0
- optio_antigravity-0.1.0/tests/test_session_conversation.py +422 -0
- optio_antigravity-0.1.0/tests/test_session_lease.py +214 -0
- optio_antigravity-0.1.0/tests/test_session_local.py +112 -0
- optio_antigravity-0.1.0/tests/test_session_remote.py +81 -0
- optio_antigravity-0.1.0/tests/test_session_resume.py +145 -0
- optio_antigravity-0.1.0/tests/test_session_seed.py +112 -0
- optio_antigravity-0.1.0/tests/test_snapshots.py +81 -0
- optio_antigravity-0.1.0/tests/test_verify.py +261 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optio-antigravity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run Google Antigravity (agy) as an optio task; local subprocess or remote via SSH; ttyd-served TUI iframe or transcript-driven conversation.
|
|
5
|
+
Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/deai-network/optio
|
|
8
|
+
Project-URL: Repository, https://github.com/deai-network/optio
|
|
9
|
+
Project-URL: Issues, https://github.com/deai-network/optio/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Classifier: Framework :: AsyncIO
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: optio-core<0.4,>=0.3
|
|
24
|
+
Requires-Dist: optio-host<0.3,>=0.2
|
|
25
|
+
Requires-Dist: optio-agents<0.5,>=0.4
|
|
26
|
+
Requires-Dist: asyncssh>=2.14
|
|
27
|
+
Requires-Dist: aiohttp>=3.9
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-xdist>=3.6; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# optio-antigravity
|
|
34
|
+
|
|
35
|
+
Run Google Antigravity (`agy`) as an `optio` task — either as a local
|
|
36
|
+
subprocess or on a remote host over SSH — with the interactive TUI
|
|
37
|
+
embedded in the optio dashboard via an iframe widget served by `ttyd`,
|
|
38
|
+
or as a transcript-driven conversation rendered in the dashboard chat
|
|
39
|
+
widget.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install optio-antigravity
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Python 3.11+. Pulls `optio-core`, `optio-host`, `optio-agents`,
|
|
48
|
+
`asyncssh`, and `aiohttp`.
|
|
49
|
+
|
|
50
|
+
## What it does
|
|
51
|
+
|
|
52
|
+
Antigravity is Google's CLI coding agent (`agy`), sharing state with the
|
|
53
|
+
Gemini CLI under `~/.gemini`. optio-antigravity adapts the optio wrapper
|
|
54
|
+
machinery: it launches `agy` under a PTY, serves the TUI over `ttyd`, and
|
|
55
|
+
coordinates with the host harness through the `optio.log` keyword channel
|
|
56
|
+
(STATUS / DELIVERABLE / DONE / ERROR). The agent reads its task from an
|
|
57
|
+
`AGENTS.md` file planted in the workdir.
|
|
58
|
+
|
|
59
|
+
Because `agy --print` runs one-shot only (no ACP/stream-json) and swallows
|
|
60
|
+
stdout under a non-TTY, conversation mode is **synthetic**: each turn is
|
|
61
|
+
driven with `agy -p --conversation <id>` under a PTY and events are read
|
|
62
|
+
from `~/.gemini/antigravity/transcript.jsonl`.
|
|
63
|
+
|
|
64
|
+
## Status
|
|
65
|
+
|
|
66
|
+
Stage 0 (MVP): package scaffold. Iframe/ttyd mode, remote/SSH, resume,
|
|
67
|
+
seeds, conversation mode, and filesystem isolation arrive in later stages.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# optio-antigravity
|
|
2
|
+
|
|
3
|
+
Run Google Antigravity (`agy`) as an `optio` task — either as a local
|
|
4
|
+
subprocess or on a remote host over SSH — with the interactive TUI
|
|
5
|
+
embedded in the optio dashboard via an iframe widget served by `ttyd`,
|
|
6
|
+
or as a transcript-driven conversation rendered in the dashboard chat
|
|
7
|
+
widget.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install optio-antigravity
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.11+. Pulls `optio-core`, `optio-host`, `optio-agents`,
|
|
16
|
+
`asyncssh`, and `aiohttp`.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
Antigravity is Google's CLI coding agent (`agy`), sharing state with the
|
|
21
|
+
Gemini CLI under `~/.gemini`. optio-antigravity adapts the optio wrapper
|
|
22
|
+
machinery: it launches `agy` under a PTY, serves the TUI over `ttyd`, and
|
|
23
|
+
coordinates with the host harness through the `optio.log` keyword channel
|
|
24
|
+
(STATUS / DELIVERABLE / DONE / ERROR). The agent reads its task from an
|
|
25
|
+
`AGENTS.md` file planted in the workdir.
|
|
26
|
+
|
|
27
|
+
Because `agy --print` runs one-shot only (no ACP/stream-json) and swallows
|
|
28
|
+
stdout under a non-TTY, conversation mode is **synthetic**: each turn is
|
|
29
|
+
driven with `agy -p --conversation <id>` under a PTY and events are read
|
|
30
|
+
from `~/.gemini/antigravity/transcript.jsonl`.
|
|
31
|
+
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
Stage 0 (MVP): package scaffold. Iframe/ttyd mode, remote/SSH, resume,
|
|
35
|
+
seeds, conversation mode, and filesystem isolation arrive in later stages.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "optio-antigravity"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Run Google Antigravity (agy) as an optio task; local subprocess or remote via SSH; ttyd-served TUI iframe or transcript-driven conversation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Kristof Csillag", email = "kristof.csillag@deai-labs.com" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
"Topic :: Software Development :: Code Generators",
|
|
26
|
+
"Framework :: AsyncIO",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"optio-core>=0.3,<0.4",
|
|
30
|
+
"optio-host>=0.2,<0.3",
|
|
31
|
+
"optio-agents>=0.4,<0.5",
|
|
32
|
+
"asyncssh>=2.14",
|
|
33
|
+
"aiohttp>=3.9",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"pytest-asyncio>=0.23",
|
|
40
|
+
"pytest-xdist>=3.6",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/deai-network/optio"
|
|
45
|
+
Repository = "https://github.com/deai-network/optio"
|
|
46
|
+
Issues = "https://github.com/deai-network/optio/issues"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
markers = [
|
|
55
|
+
"serial: spawn-heavy, timing-fragile test that must run in a final non-parallel phase",
|
|
56
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""optio-antigravity — run Google Antigravity (agy) as an optio task."""
|
|
2
|
+
|
|
3
|
+
import logging as _logging
|
|
4
|
+
|
|
5
|
+
from optio_antigravity.seed_manifest import (
|
|
6
|
+
ANTIGRAVITY_CRED_MANIFEST,
|
|
7
|
+
ANTIGRAVITY_SEED_MANIFEST,
|
|
8
|
+
ANTIGRAVITY_SEED_SUFFIX,
|
|
9
|
+
delete_seed,
|
|
10
|
+
list_seeds,
|
|
11
|
+
purge_seed,
|
|
12
|
+
)
|
|
13
|
+
from optio_antigravity.session import (
|
|
14
|
+
create_antigravity_task,
|
|
15
|
+
run_antigravity_session,
|
|
16
|
+
)
|
|
17
|
+
from optio_antigravity.types import AntigravityTaskConfig
|
|
18
|
+
from optio_antigravity.verify import verify_and_refresh_seed
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# asyncssh emits per-connection INFO lines that flood worker stdout
|
|
22
|
+
# once an SSH-backed session starts. Quiet by default.
|
|
23
|
+
_logging.getLogger("asyncssh").setLevel(_logging.WARNING)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"create_antigravity_task",
|
|
28
|
+
"run_antigravity_session",
|
|
29
|
+
"AntigravityTaskConfig",
|
|
30
|
+
"ANTIGRAVITY_SEED_MANIFEST",
|
|
31
|
+
"ANTIGRAVITY_CRED_MANIFEST",
|
|
32
|
+
"ANTIGRAVITY_SEED_SUFFIX",
|
|
33
|
+
"delete_seed",
|
|
34
|
+
"list_seeds",
|
|
35
|
+
"purge_seed",
|
|
36
|
+
"verify_and_refresh_seed",
|
|
37
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Surface agy's print-only login URL as a clean, copyable ``BROWSER:`` link.
|
|
2
|
+
|
|
3
|
+
agy authenticates with a hosted-redirect OAuth flow that PRINTS the Google
|
|
4
|
+
authorize URL into the TUI ("Open the URL below in your browser:") and never
|
|
5
|
+
shells out to a browser — so the redirect browser-shims never fire (an empty
|
|
6
|
+
``optio.log`` on the login step is the tell). Worse, agy HARD-wraps the long URL
|
|
7
|
+
to the box width, so ttyd linkifies only the first line and the operator can't
|
|
8
|
+
copy it in one piece.
|
|
9
|
+
|
|
10
|
+
This module reads the tmux pane (the ttyd surface's backing terminal),
|
|
11
|
+
reassembles the wrapped OAuth URL, and appends a ``BROWSER: "<url>"`` marker to
|
|
12
|
+
``optio.log`` — the same channel the redirect shim uses, so the log-protocol
|
|
13
|
+
driver parses it and calls ``ctx.request_browser_open`` (a single clean link in
|
|
14
|
+
the dashboard). Best-effort and idempotent: each distinct URL is emitted once.
|
|
15
|
+
|
|
16
|
+
iframe/ttyd path only — conversation mode has no tmux pane.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import re
|
|
23
|
+
import shlex
|
|
24
|
+
|
|
25
|
+
from optio_host.host import Host
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# A reassembled candidate must look like an OAuth *authorize* URL, so we never
|
|
29
|
+
# surface arbitrary links the agent prints during normal work.
|
|
30
|
+
_OAUTH_MARKERS = re.compile(r"(oauth|client_id=|redirect_uri=|response_type=code)", re.I)
|
|
31
|
+
_URL_START = re.compile(r"https?://")
|
|
32
|
+
_HAS_ALNUM = re.compile(r"[A-Za-z0-9]")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extract_auth_url(pane: str) -> "str | None":
|
|
36
|
+
"""Reassemble a hard-wrapped OAuth authorize URL from a captured tmux pane.
|
|
37
|
+
|
|
38
|
+
agy prints the URL inside a box, hard-wrapped at the box content width, so
|
|
39
|
+
the fragments arrive as separate physical lines (``capture-pane -J`` joins
|
|
40
|
+
only soft wraps, not these). We start at a line that begins with ``http`` and
|
|
41
|
+
greedily append following fragment lines — non-empty, whitespace-free, and
|
|
42
|
+
containing alphanumerics (so box rules like ``────`` and prose like "After
|
|
43
|
+
authenticating…" both terminate the run). Returns the URL only if the
|
|
44
|
+
assembled string looks like an OAuth authorize URL, else ``None``.
|
|
45
|
+
"""
|
|
46
|
+
parts: "list[str]" = []
|
|
47
|
+
collecting = False
|
|
48
|
+
for raw in pane.splitlines():
|
|
49
|
+
s = raw.strip().strip("│").strip()
|
|
50
|
+
if not collecting:
|
|
51
|
+
if _URL_START.match(s) and " " not in s:
|
|
52
|
+
parts = [s]
|
|
53
|
+
collecting = True
|
|
54
|
+
continue
|
|
55
|
+
# A continuation fragment: no whitespace + has alphanumerics (a border of
|
|
56
|
+
# box-drawing chars has none → stops; prose has spaces → stops).
|
|
57
|
+
if s and " " not in s and _HAS_ALNUM.search(s):
|
|
58
|
+
parts.append(s)
|
|
59
|
+
else:
|
|
60
|
+
break
|
|
61
|
+
if not parts:
|
|
62
|
+
return None
|
|
63
|
+
url = "".join(parts)
|
|
64
|
+
if not _URL_START.match(url) or not _OAUTH_MARKERS.search(url):
|
|
65
|
+
return None
|
|
66
|
+
return url
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _capture_pane(
|
|
70
|
+
host: Host, tmux_path: str, socket_path: str, session_name: str, lines: int,
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Joined dump of the tmux pane's recent scrollback (best-effort)."""
|
|
73
|
+
cmd = (
|
|
74
|
+
f"{shlex.quote(tmux_path)} -S {shlex.quote(socket_path)} "
|
|
75
|
+
f"capture-pane -t {shlex.quote(session_name)} -pJ -S -{int(lines)}"
|
|
76
|
+
)
|
|
77
|
+
r = await host.run_command(cmd)
|
|
78
|
+
return r.stdout if r.exit_code == 0 else ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _emit_browser_marker(host: Host, url: str) -> None:
|
|
82
|
+
"""Append a ``BROWSER: "<url>"`` line to optio.log (same format as the shim,
|
|
83
|
+
so the log-protocol parser surfaces it via ctx.request_browser_open)."""
|
|
84
|
+
log = f"{host.workdir}/optio.log"
|
|
85
|
+
await host.run_command(
|
|
86
|
+
f"printf 'BROWSER: \"%s\"\\n' {shlex.quote(url)} >> {shlex.quote(log)}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def run_auth_url_scraper(
|
|
91
|
+
host: Host,
|
|
92
|
+
*,
|
|
93
|
+
tmux_path: str,
|
|
94
|
+
socket_path: str,
|
|
95
|
+
session_name: str,
|
|
96
|
+
interval: float = 2.0,
|
|
97
|
+
capture_lines: int = 400,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Poll the tmux pane; surface each distinct printed OAuth URL once.
|
|
100
|
+
|
|
101
|
+
Runs for the life of the iframe session (the caller cancels it on teardown).
|
|
102
|
+
Never raises out of the loop — a transient capture failure is skipped, and
|
|
103
|
+
``CancelledError`` (a BaseException) propagates so teardown is clean.
|
|
104
|
+
"""
|
|
105
|
+
emitted: "set[str]" = set()
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
pane = await _capture_pane(
|
|
109
|
+
host, tmux_path, socket_path, session_name, capture_lines,
|
|
110
|
+
)
|
|
111
|
+
url = extract_auth_url(pane)
|
|
112
|
+
if url and url not in emitted:
|
|
113
|
+
emitted.add(url)
|
|
114
|
+
await _emit_browser_marker(host, url)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # best-effort; a poll failure must not kill the session
|
|
117
|
+
await asyncio.sleep(interval)
|