levain-cli 0.1.12__tar.gz → 0.1.15__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.
Files changed (26) hide show
  1. {levain_cli-0.1.12 → levain_cli-0.1.15}/.gitignore +7 -0
  2. {levain_cli-0.1.12 → levain_cli-0.1.15}/PKG-INFO +1 -1
  3. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/cli.py +85 -4
  4. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/models.py +4 -0
  5. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/validate.py +53 -1
  6. {levain_cli-0.1.12 → levain_cli-0.1.15}/pyproject.toml +1 -1
  7. {levain_cli-0.1.12 → levain_cli-0.1.15}/LICENSE +0 -0
  8. {levain_cli-0.1.12 → levain_cli-0.1.15}/README.md +0 -0
  9. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/__init__.py +0 -0
  10. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/__main__.py +0 -0
  11. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/_format.py +0 -0
  12. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/_java.py +0 -0
  13. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/auth.py +0 -0
  14. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/codegen.py +0 -0
  15. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/graph.py +0 -0
  16. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/integrations_check.py +0 -0
  17. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/known_recipes.py +0 -0
  18. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/mcp.py +0 -0
  19. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/parser.py +0 -0
  20. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/profile.py +0 -0
  21. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/py.typed +0 -0
  22. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/reducers.py +0 -0
  23. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/source_check.py +0 -0
  24. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/tlc.py +0 -0
  25. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/translate.py +0 -0
  26. {levain_cli-0.1.12 → levain_cli-0.1.15}/levain_recipe/user_input.py +0 -0
@@ -18,6 +18,10 @@ build/
18
18
  !**/platform_agents/build/
19
19
  !**/src/build/
20
20
 
21
+ # Maven build output
22
+ services/flink/target/
23
+ services/flink/dependency-reduced-pom.xml
24
+
21
25
  # Binaries
22
26
  bin/
23
27
 
@@ -136,3 +140,6 @@ assets/video/*/renders/
136
140
  # www/public/videos/, the composition that made them does not.
137
141
  assets/video/updates-*/
138
142
  assets/video/*/snapshots/
143
+
144
+ # Maven build output (Flink job); the image build compiles from source.
145
+ services/flink/target/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: levain-cli
3
- Version: 0.1.12
3
+ Version: 0.1.15
4
4
  Summary: Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -337,14 +337,28 @@ def git_credential_cmd(
337
337
  raise typer.Exit(1)
338
338
  scope, slug = repo
339
339
 
340
+ # A ``workspace-<slug>`` repo in a workspace org is an agent's
341
+ # durable workspace, minted on a dedicated route. An agent whose
342
+ # own slug carries the prefix shadows it, so the recipe route is
343
+ # tried first and the workspace route is the 404 fallback.
344
+ mint_paths = [f"{api_base()}/api/v1/{scope}/{slug}/git-credential"]
345
+ if scope == "agents" and slug.startswith("workspace-"):
346
+ mint_paths.append(
347
+ f"{api_base()}/api/v1/agents/"
348
+ f"{slug.removeprefix('workspace-')}/workspace/git-credential"
349
+ )
350
+
340
351
  api = api_base()
341
352
  try:
342
353
  with _client() as client:
343
354
  token = access_token(api, client)
344
- resp = client.post(
345
- f"{api}/api/v1/{scope}/{slug}/git-credential",
346
- headers={"Authorization": f"Bearer {token}"},
347
- )
355
+ for mint_path in mint_paths:
356
+ resp = client.post(
357
+ mint_path,
358
+ headers={"Authorization": f"Bearer {token}"},
359
+ )
360
+ if resp.status_code != 404:
361
+ break
348
362
  except AuthError as exc:
349
363
  print(f"levain git-credential: {exc}", file=sys.stderr)
350
364
  raise typer.Exit(1) from None
@@ -378,3 +392,70 @@ def _repo_from_git_path(path: str) -> tuple[str, str] | None:
378
392
  else:
379
393
  return None
380
394
  return scope, repo.removesuffix(".git")
395
+
396
+
397
+ # ── Inbound webhooks ──────────────────────────────────────
398
+
399
+
400
+ webhook_app = typer.Typer(
401
+ name="webhook",
402
+ help="Manage an agent's inbound webhook.",
403
+ add_completion=False,
404
+ )
405
+ app.add_typer(webhook_app)
406
+
407
+
408
+ def _webhook_request(method: str, path: str) -> None:
409
+ from .auth import AuthError, access_token, api_base
410
+
411
+ api = api_base()
412
+ try:
413
+ with _client() as client:
414
+ token = access_token(api, client)
415
+ resp = client.request(
416
+ method,
417
+ f"{api}{path}",
418
+ headers={"Authorization": f"Bearer {token}"},
419
+ )
420
+ except AuthError as exc:
421
+ print(f"Error: {exc}", file=sys.stderr)
422
+ raise typer.Exit(1) from None
423
+ if resp.status_code >= 400:
424
+ try:
425
+ detail = resp.json().get("detail")
426
+ except ValueError:
427
+ detail = None
428
+ suffix = f" — {detail}" if detail else ""
429
+ print(f"Error: HTTP {resp.status_code}{suffix}", file=sys.stderr)
430
+ raise typer.Exit(1)
431
+ print(json.dumps(resp.json(), indent=2))
432
+
433
+
434
+ @webhook_app.command("create")
435
+ def webhook_create_cmd(
436
+ agent: Annotated[str, typer.Argument(help="Agent id or slug.")],
437
+ ) -> None:
438
+ """Create the agent's inbound webhook.
439
+
440
+ Prints the webhook once, including the URL with its embedded
441
+ secret — store it; it cannot be retrieved later. POST a JSON body
442
+ to the URL to dispatch a run of the agent, or include a top-level
443
+ ``session_id`` to take a turn on an existing session.
444
+ """
445
+ _webhook_request("POST", f"/api/v1/agents/{agent}/webhooks")
446
+
447
+
448
+ @webhook_app.command("rotate")
449
+ def webhook_rotate_cmd(
450
+ agent: Annotated[str, typer.Argument(help="Agent id or slug.")],
451
+ ) -> None:
452
+ """Rotate the webhook secret and print the fresh URL (shown once)."""
453
+ _webhook_request("POST", f"/api/v1/agents/{agent}/webhooks/rotate")
454
+
455
+
456
+ @webhook_app.command("delete")
457
+ def webhook_delete_cmd(
458
+ agent: Annotated[str, typer.Argument(help="Agent id or slug.")],
459
+ ) -> None:
460
+ """Delete the agent's inbound webhook."""
461
+ _webhook_request("DELETE", f"/api/v1/agents/{agent}/webhooks")
@@ -134,6 +134,10 @@ class ValidatedSpec:
134
134
  routing: list[RoutingTable]
135
135
  invariants: list[str] = field(default_factory=list)
136
136
  constants: dict[str, Any] = field(default_factory=dict)
137
+ # Lanes the recipe splits its turns into (``\* @lanes [...]``):
138
+ # runs in different lanes may share the agent's workspace at the
139
+ # same time. Empty when the recipe runs one turn at a time.
140
+ lanes: list[str] = field(default_factory=list)
137
141
 
138
142
 
139
143
  @dataclass(frozen=True)
@@ -6,11 +6,12 @@ validation (PlusCal + Python source + pyproject.toml).
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import json
9
10
  import logging
10
11
  import os
11
12
  import re
12
13
  import shutil
13
- from typing import cast
14
+ from typing import Any, cast
14
15
 
15
16
  from ._java import resolve_jar
16
17
  from .graph import check_graph
@@ -52,6 +53,53 @@ _RE_COMMENT_PREFIX = re.compile(r"^\\[*]\s?")
52
53
  # excluded from the free-form description so the help text never
53
54
  # accidentally includes machine-readable metadata.
54
55
  _RE_ANNOTATION = re.compile(r"^\\[*]\s*@\w+")
56
+ # Module-level ``\* @lanes ["people"]``: the lanes whose turns may run
57
+ # at once on the agent's persistent workspace. ``people`` is the
58
+ # only lane a recipe can split out today; a trigger delivery or a
59
+ # scheduled fire stays in the default lane.
60
+ _RE_LANES = re.compile(r"^\s*\\[*]\s*@lanes\s+(\[.*\])\s*$")
61
+ KNOWN_LANES = frozenset({"people"})
62
+
63
+
64
+ def extract_lanes(source: str) -> list[str] | ValidationError:
65
+ r"""The lanes a ``\* @lanes`` line declares; ``[]`` without one."""
66
+ found = [
67
+ (n, m.group(1))
68
+ for n, line in enumerate(source.splitlines(), start=1)
69
+ if (m := _RE_LANES.match(line))
70
+ ]
71
+ if not found:
72
+ return []
73
+ line_no, raw = found[0]
74
+ lanes: Any = None
75
+ message: str | None
76
+ if len(found) > 1:
77
+ line_no, message = found[1][0], "@lanes may be declared once"
78
+ else:
79
+ try:
80
+ lanes = json.loads(raw)
81
+ except json.JSONDecodeError as e:
82
+ message = f"Invalid @lanes JSON: {e}"
83
+ else:
84
+ message = _lanes_problem(lanes)
85
+ if message:
86
+ return ValidationError(stage="profile", message=message, line=line_no)
87
+ return cast(list[str], lanes)
88
+
89
+
90
+ def _lanes_problem(lanes: Any) -> str | None:
91
+ if (
92
+ not isinstance(lanes, list)
93
+ or not lanes
94
+ or not all(isinstance(s, str) for s in lanes)
95
+ ):
96
+ return "@lanes must be a non-empty JSON list of strings"
97
+ unknown = sorted(set(lanes) - KNOWN_LANES)
98
+ if unknown:
99
+ return f"Unknown lane(s) {unknown}; known: {sorted(KNOWN_LANES)}"
100
+ if len(set(lanes)) != len(lanes):
101
+ return "@lanes repeats a lane"
102
+ return None
55
103
 
56
104
 
57
105
  def validate(
@@ -130,6 +178,9 @@ def validate_fast( # noqa: PLR0911
130
178
 
131
179
  constants = extract_constants(algo)
132
180
  invariants = [d for d in extract_defines(algo) if d not in constants]
181
+ lanes = extract_lanes(source)
182
+ if isinstance(lanes, ValidationError):
183
+ return [lanes]
133
184
 
134
185
  # Stage 3: Graph check. Tell the reachability check that
135
186
  # ``interrupt`` nodes count as termination — a chat-shape
@@ -177,6 +228,7 @@ def validate_fast( # noqa: PLR0911
177
228
  routing=routing,
178
229
  invariants=invariants,
179
230
  constants=constants,
231
+ lanes=lanes,
180
232
  )
181
233
 
182
234
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "levain-cli"
3
- version = "0.1.12"
3
+ version = "0.1.15"
4
4
  description = "Levain recipe toolchain: PlusCal validator, TLC model checking, and GraphState codegen for agent recipes"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes