@invokehq/cli 0.2.5 → 0.2.7

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.
package/agentify.py CHANGED
@@ -1165,6 +1165,18 @@ def sample_sdk_source() -> str:
1165
1165
  def init_command(args: argparse.Namespace) -> int:
1166
1166
  root = Path(args.name)
1167
1167
  project_name = root.name
1168
+
1169
+ if args.template == "claude-agent":
1170
+ from invoke.deploy import copy_template
1171
+
1172
+ path = copy_template(root, force=args.force)
1173
+ print(f"Created Claude Agent SDK project at {path}")
1174
+ print("Next:")
1175
+ print(f" cd {path}")
1176
+ print(" npm install")
1177
+ print(" invoke deploy --dry-run")
1178
+ return 0
1179
+
1168
1180
  if root.exists() and any(root.iterdir()) and not args.force:
1169
1181
  raise ValueError(f"{root} already exists and is not empty. Pass --force to write into it.")
1170
1182
  root.mkdir(parents=True, exist_ok=True)
@@ -1214,6 +1226,25 @@ def read_project(root: Path) -> dict[str, Any]:
1214
1226
  return config
1215
1227
 
1216
1228
 
1229
+ def read_project_config(root: Path) -> dict[str, Any]:
1230
+ config_path = root / "invoke.json"
1231
+ if not config_path.exists():
1232
+ return {}
1233
+ config = load_json_file(config_path, {})
1234
+ if not isinstance(config, dict):
1235
+ raise ValueError("invoke.json must contain an object")
1236
+ return config
1237
+
1238
+
1239
+ def is_agent_project(config: dict[str, Any]) -> bool:
1240
+ runtime = str(config.get("runtime") or "").lower()
1241
+ agent_type = str(config.get("agent_type") or config.get("type") or "").lower()
1242
+ return bool(config.get("entrypoint")) and (
1243
+ runtime in {"node", "python", "claude-agent-sdk"}
1244
+ or agent_type in {"agent", "claude-agent", "claude-agent-sdk"}
1245
+ )
1246
+
1247
+
1217
1248
  def project_mcp_url(root: Path, config: dict[str, Any], explicit_mcp_url: str | None = None) -> str | None:
1218
1249
  if explicit_mcp_url:
1219
1250
  return explicit_mcp_url
@@ -1371,6 +1402,34 @@ def save_deployment(record: dict[str, Any]) -> None:
1371
1402
 
1372
1403
  def deploy_command(args: argparse.Namespace) -> int:
1373
1404
  root = Path(args.path)
1405
+ raw_config = read_project_config(root)
1406
+ if is_agent_project(raw_config):
1407
+ from dataclasses import asdict
1408
+
1409
+ from invoke.deploy import deploy_claude_agent
1410
+
1411
+ result = deploy_claude_agent(
1412
+ root,
1413
+ app_name=args.slug or raw_config.get("slug") or raw_config.get("name"),
1414
+ dry_run=args.dry_run,
1415
+ )
1416
+ record = {
1417
+ "project": str(root.resolve()),
1418
+ "name": result.plan.app_name,
1419
+ "provider_id": result.deployment_id,
1420
+ "slug": result.plan.app_name,
1421
+ "gateway_url": result.modal_app_name,
1422
+ "base_url": "modal",
1423
+ "tools": ["agent.run"],
1424
+ "deployed_at": dt.datetime.now(dt.timezone.utc).isoformat(),
1425
+ "mode": "modal_agent",
1426
+ "trace_path": result.trace_path,
1427
+ }
1428
+ if not args.dry_run:
1429
+ save_deployment(record)
1430
+ print(json.dumps({"success": result.success, "mode": "modal_agent", **asdict(result)}, indent=2))
1431
+ return 0
1432
+
1374
1433
  config = read_project(root)
1375
1434
  tools = config["tools"]
1376
1435
  mcp_url = project_mcp_url(root, config, args.mcp_url)
@@ -1773,7 +1832,7 @@ def build_parser() -> argparse.ArgumentParser:
1773
1832
 
1774
1833
  init = subparsers.add_parser("init", help="Scaffold an Invoke project.")
1775
1834
  init.add_argument("name", help="Project directory/name.")
1776
- init.add_argument("--template", choices=["default", "linear", "crm-guardrail"], default="default")
1835
+ init.add_argument("--template", choices=["default", "linear", "crm-guardrail", "claude-agent"], default="default")
1777
1836
  init.add_argument("--force", action="store_true", help="Write into an existing non-empty directory.")
1778
1837
  init.set_defaults(func=init_command)
1779
1838
 
@@ -0,0 +1,5 @@
1
+ """Invoke agent deployment and supervision primitives."""
2
+
3
+ from .deploy import DeployPlan, DeployResult, deploy_claude_agent
4
+
5
+ __all__ = ["DeployPlan", "DeployResult", "deploy_claude_agent"]
@@ -0,0 +1,229 @@
1
+ """Modal-backed deployment planning for Claude Agent SDK projects.
2
+
3
+ This module is intentionally small and import-safe. The public CLI can call
4
+ `deploy_claude_agent(path)` without importing Modal unless the caller actually
5
+ wants to deploy. That lets the local CLI stay lightweight while giving us a
6
+ real place to evolve hosted agent deployment.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import datetime as dt
12
+ import json
13
+ import os
14
+ import shutil
15
+ from dataclasses import asdict, dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from .onyx.analyzer import OnyxSuggestion, analyze_traces
20
+ from .supervisor.monitor import TraceStore, build_boot_trace
21
+
22
+
23
+ DEFAULT_ENTRYPOINTS = ("src/index.ts", "src/index.js", "index.ts", "index.js")
24
+ DEFAULT_MODAL_IMAGE = "invoke/claude-agent-sdk:latest"
25
+
26
+
27
+ @dataclass
28
+ class DeployPlan:
29
+ """A concrete deployment plan for one agent project."""
30
+
31
+ project_root: str
32
+ app_name: str
33
+ entrypoint: str
34
+ modal_volume: str
35
+ modal_image: str = DEFAULT_MODAL_IMAGE
36
+ env: dict[str, str] = field(default_factory=dict)
37
+ tracing_enabled: bool = True
38
+ persistence: dict[str, str] = field(default_factory=dict)
39
+ robustness: dict[str, bool] = field(default_factory=dict)
40
+
41
+
42
+ @dataclass
43
+ class DeployResult:
44
+ """Result returned by a deploy attempt or dry run."""
45
+
46
+ success: bool
47
+ plan: DeployPlan
48
+ deployment_id: str
49
+ status: str
50
+ message: str
51
+ modal_app_name: str | None = None
52
+ trace_path: str | None = None
53
+ onyx_suggestions: list[OnyxSuggestion] = field(default_factory=list)
54
+
55
+
56
+ def slugify(value: str) -> str:
57
+ cleaned = "".join(ch.lower() if ch.isalnum() else "-" for ch in value)
58
+ return "-".join(part for part in cleaned.split("-") if part) or "invoke-agent"
59
+
60
+
61
+ def now_iso() -> str:
62
+ return dt.datetime.now(dt.timezone.utc).isoformat()
63
+
64
+
65
+ def load_project_config(project_root: Path) -> dict[str, Any]:
66
+ for name in ("invoke.json", "agent.json", "package.json"):
67
+ path = project_root / name
68
+ if path.exists():
69
+ try:
70
+ data = json.loads(path.read_text(encoding="utf-8"))
71
+ except json.JSONDecodeError as exc:
72
+ raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
73
+ if isinstance(data, dict):
74
+ return data
75
+ return {}
76
+
77
+
78
+ def infer_entrypoint(project_root: Path, config: dict[str, Any]) -> str:
79
+ configured = config.get("entrypoint") or config.get("main")
80
+ candidates = (str(configured),) if configured else DEFAULT_ENTRYPOINTS
81
+ for candidate in candidates:
82
+ if candidate and (project_root / candidate).exists():
83
+ return candidate
84
+ raise FileNotFoundError(
85
+ "Could not find an agent entrypoint. Expected one of: "
86
+ + ", ".join(DEFAULT_ENTRYPOINTS)
87
+ + ". You can also set `entrypoint` in invoke.json."
88
+ )
89
+
90
+
91
+ def build_deploy_plan(project_path: str | Path, *, app_name: str | None = None) -> DeployPlan:
92
+ project_root = Path(project_path).expanduser().resolve()
93
+ if not project_root.exists() or not project_root.is_dir():
94
+ raise FileNotFoundError(f"Agent project does not exist: {project_root}")
95
+
96
+ config = load_project_config(project_root)
97
+ inferred_name = app_name or config.get("name") or project_root.name
98
+ slug = slugify(str(inferred_name))
99
+ entrypoint = infer_entrypoint(project_root, config)
100
+
101
+ env_keys = [
102
+ "ANTHROPIC_API_KEY",
103
+ "OPENAI_API_KEY",
104
+ "INVOKE_API_KEY",
105
+ "INVOKE_BASE_URL",
106
+ ]
107
+ env = {key: os.environ[key] for key in env_keys if os.environ.get(key)}
108
+
109
+ return DeployPlan(
110
+ project_root=str(project_root),
111
+ app_name=slug,
112
+ entrypoint=entrypoint,
113
+ modal_volume=f"{slug}-state",
114
+ env=env,
115
+ persistence={
116
+ "kind": "modal_volume",
117
+ "mount_path": "/state",
118
+ "trace_path": "/state/traces.jsonl",
119
+ "checkpoint_path": "/state/checkpoints.jsonl",
120
+ },
121
+ robustness={
122
+ "schema_checks": True,
123
+ "policy_checks": True,
124
+ "state_reconciliation": True,
125
+ "freeze_thaw_hitl": True,
126
+ "structured_tracing": True,
127
+ },
128
+ )
129
+
130
+
131
+ def write_local_deploy_record(result: DeployResult) -> Path:
132
+ root = Path(result.plan.project_root)
133
+ state_dir = root / ".invoke"
134
+ state_dir.mkdir(parents=True, exist_ok=True)
135
+ path = state_dir / "deployment.json"
136
+ path.write_text(json.dumps(asdict(result), indent=2, sort_keys=True) + "\n", encoding="utf-8")
137
+ return path
138
+
139
+
140
+ def copy_template(destination: str | Path, *, force: bool = False) -> Path:
141
+ """Scaffold a Claude Agent SDK starter project."""
142
+
143
+ destination = Path(destination).expanduser().resolve()
144
+ template_root = Path(__file__).with_name("templates") / "claude-agent-sdk"
145
+ if destination.exists() and any(destination.iterdir()) and not force:
146
+ raise FileExistsError(f"{destination} is not empty. Pass force=True to overwrite template files.")
147
+ destination.mkdir(parents=True, exist_ok=True)
148
+ for source in template_root.rglob("*"):
149
+ target = destination / source.relative_to(template_root)
150
+ if source.is_dir():
151
+ target.mkdir(parents=True, exist_ok=True)
152
+ continue
153
+ target.parent.mkdir(parents=True, exist_ok=True)
154
+ if target.exists() and not force:
155
+ continue
156
+ shutil.copyfile(source, target)
157
+ return destination
158
+
159
+
160
+ def deploy_claude_agent(
161
+ project_path: str | Path,
162
+ *,
163
+ app_name: str | None = None,
164
+ dry_run: bool = False,
165
+ run_onyx: bool = True,
166
+ ) -> DeployResult:
167
+ """Build and optionally deploy a Claude Agent SDK project to Modal."""
168
+
169
+ plan = build_deploy_plan(project_path, app_name=app_name)
170
+ deployment_id = "dep_" + dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d%H%M%S")
171
+
172
+ trace_store = TraceStore(Path(plan.project_root) / ".invoke" / "traces.jsonl")
173
+ boot_trace = build_boot_trace(plan.app_name, plan.entrypoint, plan.robustness)
174
+ trace_store.append(boot_trace)
175
+
176
+ suggestions = analyze_traces(trace_store.recent(limit=100)) if run_onyx else []
177
+
178
+ if dry_run:
179
+ result = DeployResult(
180
+ success=True,
181
+ plan=plan,
182
+ deployment_id=deployment_id,
183
+ status="planned",
184
+ message="Deployment plan generated. Modal deploy was not executed.",
185
+ trace_path=str(trace_store.path),
186
+ onyx_suggestions=suggestions,
187
+ )
188
+ write_local_deploy_record(result)
189
+ return result
190
+
191
+ from .sandbox import deploy_modal_app
192
+
193
+ modal_result = deploy_modal_app(plan)
194
+ result = DeployResult(
195
+ success=True,
196
+ plan=plan,
197
+ deployment_id=deployment_id,
198
+ status="deployed",
199
+ message="Agent deployed to Modal with Invoke tracing and persistence enabled.",
200
+ modal_app_name=modal_result.get("app_name", plan.app_name),
201
+ trace_path=str(trace_store.path),
202
+ onyx_suggestions=suggestions,
203
+ )
204
+ write_local_deploy_record(result)
205
+ return result
206
+
207
+
208
+ def main() -> int:
209
+ import argparse
210
+
211
+ parser = argparse.ArgumentParser(description="Deploy a Claude Agent SDK project with Invoke.")
212
+ parser.add_argument("path", help="Agent project path.")
213
+ parser.add_argument("--app-name")
214
+ parser.add_argument("--dry-run", action="store_true")
215
+ parser.add_argument("--template", action="store_true", help="Scaffold a starter project at path.")
216
+ args = parser.parse_args()
217
+
218
+ if args.template:
219
+ path = copy_template(args.path)
220
+ print(f"Created Claude Agent SDK template at {path}")
221
+ return 0
222
+
223
+ result = deploy_claude_agent(args.path, app_name=args.app_name, dry_run=args.dry_run)
224
+ print(json.dumps(asdict(result), indent=2, sort_keys=True))
225
+ return 0
226
+
227
+
228
+ if __name__ == "__main__":
229
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ """Onyx Lite trace analyzer."""
2
+
3
+ from .analyzer import OnyxSuggestion, analyze_traces
4
+
5
+ __all__ = ["OnyxSuggestion", "analyze_traces"]
@@ -0,0 +1,75 @@
1
+ """Onyx Lite: trace analysis and concrete repair suggestions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class OnyxSuggestion:
12
+ title: str
13
+ severity: str
14
+ reason: str
15
+ apply: dict[str, Any] = field(default_factory=dict)
16
+
17
+
18
+ def _flatten_events(trace: dict[str, Any]) -> list[dict[str, Any]]:
19
+ events = trace.get("events") or trace.get("trace") or []
20
+ if isinstance(events, list):
21
+ return [event for event in events if isinstance(event, dict)]
22
+ return []
23
+
24
+
25
+ def analyze_traces(traces: list[dict[str, Any]]) -> list[OnyxSuggestion]:
26
+ """Detect repeated failures and emit safe, concrete fixes."""
27
+
28
+ if not traces:
29
+ return []
30
+
31
+ outcomes = Counter(str(trace.get("final_outcome") or trace.get("status") or "unknown") for trace in traces)
32
+ steps = Counter(event.get("step") for trace in traces for event in _flatten_events(trace) if event.get("step"))
33
+ suggestions: list[OnyxSuggestion] = []
34
+
35
+ if outcomes.get("timeout") or steps.get("tool_timeout"):
36
+ suggestions.append(
37
+ OnyxSuggestion(
38
+ title="Add reconciliation before retry",
39
+ severity="high",
40
+ reason="Recent traces include timeout patterns where the side effect may have succeeded.",
41
+ apply={"retry_strategy": "reconcile_before_retry", "unknown_effect": True},
42
+ )
43
+ )
44
+
45
+ if outcomes.get("idempotent_replay") or steps.get("duplicate_retry_detected"):
46
+ suggestions.append(
47
+ OnyxSuggestion(
48
+ title="Require idempotency keys for this action",
49
+ severity="medium",
50
+ reason="Duplicate retry patterns were detected in recent executions.",
51
+ apply={"idempotency": {"mode": "required", "key_fields": ["customer_id", "amount", "action"]}},
52
+ )
53
+ )
54
+
55
+ if outcomes.get("blocked_by_policy") or steps.get("policy_evaluated"):
56
+ suggestions.append(
57
+ OnyxSuggestion(
58
+ title="Move policy warning into preflight",
59
+ severity="medium",
60
+ reason="Policy blocked work after planning. Warn the agent before it attempts the action.",
61
+ apply={"preflight": {"policy_preview": True, "risk_threshold": "medium"}},
62
+ )
63
+ )
64
+
65
+ if not suggestions:
66
+ suggestions.append(
67
+ OnyxSuggestion(
68
+ title="No repeated failure pattern detected",
69
+ severity="info",
70
+ reason="Recent traces do not show recurring timeout, duplicate retry, or policy-block patterns.",
71
+ apply={},
72
+ )
73
+ )
74
+
75
+ return suggestions
@@ -0,0 +1,106 @@
1
+ """Modal sandbox definitions for hosted Invoke agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .deploy import DeployPlan
10
+
11
+
12
+ def _import_modal():
13
+ try:
14
+ import modal # type: ignore
15
+ except ImportError as exc:
16
+ raise RuntimeError(
17
+ "Modal is required for hosted agent deployment. Install it with `pip install modal` "
18
+ "and run `modal setup`, or use `--dry-run` to inspect the deployment plan."
19
+ ) from exc
20
+ return modal
21
+
22
+
23
+ def modal_objects(plan: DeployPlan):
24
+ """Return Modal app/image/volume objects for a deployment plan."""
25
+
26
+ modal = _import_modal()
27
+ app = modal.App(plan.app_name)
28
+ image = (
29
+ modal.Image.debian_slim(python_version="3.11")
30
+ .apt_install("nodejs", "npm")
31
+ .pip_install("anthropic", "httpx")
32
+ .env(plan.env)
33
+ )
34
+ volume = modal.Volume.from_name(plan.modal_volume, create_if_missing=True)
35
+ return modal, app, image, volume
36
+
37
+
38
+ def deploy_modal_app(plan: DeployPlan) -> dict[str, Any]:
39
+ """Deploy the Modal app skeleton for one Invoke agent.
40
+
41
+ The actual Modal CLI invocation happens outside this module. This function
42
+ validates that Modal is importable and returns the deployable object names,
43
+ which lets the Python CLI remain deterministic in tests.
44
+ """
45
+
46
+ modal, app, image, volume = modal_objects(plan)
47
+ project_root = Path(plan.project_root)
48
+ entrypoint = plan.entrypoint
49
+
50
+ @app.function(
51
+ image=image,
52
+ volumes={"/state": volume},
53
+ timeout=60 * 20,
54
+ secrets=[],
55
+ )
56
+ def run_agent(prompt: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
57
+ """Run one agent task inside the Modal sandbox."""
58
+
59
+ import json
60
+ import subprocess
61
+ import time
62
+ import uuid
63
+
64
+ execution_id = "exec_" + uuid.uuid4().hex[:12]
65
+ started_at = time.time()
66
+ trace = {
67
+ "execution_id": execution_id,
68
+ "event": "agent_run_started",
69
+ "prompt": prompt,
70
+ "context": context or {},
71
+ "entrypoint": entrypoint,
72
+ "started_at": started_at,
73
+ }
74
+ with open("/state/traces.jsonl", "a", encoding="utf-8") as fh:
75
+ fh.write(json.dumps(trace, sort_keys=True) + "\n")
76
+
77
+ command = ["node", entrypoint, prompt]
78
+ completed = subprocess.run(
79
+ command,
80
+ cwd="/workspace",
81
+ text=True,
82
+ capture_output=True,
83
+ timeout=60 * 15,
84
+ check=False,
85
+ )
86
+ final = {
87
+ "execution_id": execution_id,
88
+ "event": "agent_run_completed",
89
+ "returncode": completed.returncode,
90
+ "stdout": completed.stdout[-8000:],
91
+ "stderr": completed.stderr[-8000:],
92
+ "latency_ms": round((time.time() - started_at) * 1000, 2),
93
+ }
94
+ with open("/state/traces.jsonl", "a", encoding="utf-8") as fh:
95
+ fh.write(json.dumps(final, sort_keys=True) + "\n")
96
+ return final
97
+
98
+ return {
99
+ "app_name": plan.app_name,
100
+ "volume": plan.modal_volume,
101
+ "entrypoint": plan.entrypoint,
102
+ "project_root": str(project_root),
103
+ "plan": asdict(plan),
104
+ "modal_app": app,
105
+ "modal_run_function": run_agent,
106
+ }
@@ -0,0 +1,23 @@
1
+ """Supervision primitives for Invoke-hosted agents."""
2
+
3
+ from .monitor import (
4
+ Checkpoint,
5
+ ExecutionTrace,
6
+ PolicyDecision,
7
+ TraceEvent,
8
+ TraceStore,
9
+ build_boot_trace,
10
+ evaluate_policy,
11
+ reconcile_state,
12
+ )
13
+
14
+ __all__ = [
15
+ "Checkpoint",
16
+ "ExecutionTrace",
17
+ "PolicyDecision",
18
+ "TraceEvent",
19
+ "TraceStore",
20
+ "build_boot_trace",
21
+ "evaluate_policy",
22
+ "reconcile_state",
23
+ ]
@@ -0,0 +1,149 @@
1
+ """Structured tracing, policy checks, and freeze/thaw checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import json
7
+ import uuid
8
+ from dataclasses import asdict, dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def utc_now() -> str:
14
+ return dt.datetime.now(dt.timezone.utc).isoformat()
15
+
16
+
17
+ @dataclass
18
+ class TraceEvent:
19
+ step: str
20
+ status: str
21
+ detail: dict[str, Any] = field(default_factory=dict)
22
+ timestamp: str = field(default_factory=utc_now)
23
+
24
+
25
+ @dataclass
26
+ class ExecutionTrace:
27
+ execution_id: str
28
+ agent_id: str
29
+ action: str
30
+ status: str
31
+ risk: str
32
+ events: list[TraceEvent] = field(default_factory=list)
33
+ final_outcome: str | None = None
34
+ created_at: str = field(default_factory=utc_now)
35
+
36
+
37
+ @dataclass
38
+ class PolicyDecision:
39
+ effect: str
40
+ risk: str
41
+ reason: str
42
+ requires_approval: bool = False
43
+ guardrails: list[str] = field(default_factory=list)
44
+
45
+
46
+ @dataclass
47
+ class Checkpoint:
48
+ checkpoint_id: str
49
+ execution_id: str
50
+ action: str
51
+ params: dict[str, Any]
52
+ context_snapshot: dict[str, Any]
53
+ created_at: str = field(default_factory=utc_now)
54
+
55
+
56
+ class TraceStore:
57
+ """Append-only JSONL trace store."""
58
+
59
+ def __init__(self, path: str | Path):
60
+ self.path = Path(path)
61
+ self.path.parent.mkdir(parents=True, exist_ok=True)
62
+
63
+ def append(self, trace: ExecutionTrace | dict[str, Any]) -> None:
64
+ payload = asdict(trace) if hasattr(trace, "__dataclass_fields__") else trace
65
+ with self.path.open("a", encoding="utf-8") as fh:
66
+ fh.write(json.dumps(payload, sort_keys=True) + "\n")
67
+
68
+ def recent(self, *, limit: int = 50) -> list[dict[str, Any]]:
69
+ if not self.path.exists():
70
+ return []
71
+ lines = self.path.read_text(encoding="utf-8").splitlines()[-limit:]
72
+ traces: list[dict[str, Any]] = []
73
+ for line in lines:
74
+ try:
75
+ traces.append(json.loads(line))
76
+ except json.JSONDecodeError:
77
+ continue
78
+ return traces
79
+
80
+
81
+ def evaluate_policy(action: str, params: dict[str, Any]) -> PolicyDecision:
82
+ lowered = action.lower()
83
+ sql = str(params.get("sql", "")).lower()
84
+ if action == "database.execute" or "disable row level security" in sql or "drop table" in sql:
85
+ return PolicyDecision(
86
+ effect="block",
87
+ risk="high",
88
+ reason="Direct database execution can bypass application policy or row-level security.",
89
+ guardrails=["policy_block", "sandbox_required"],
90
+ )
91
+ if any(word in lowered for word in ("delete", "refund", "charge", "transfer")):
92
+ return PolicyDecision(
93
+ effect="require_approval",
94
+ risk="high",
95
+ reason="Financial or destructive action requires approval and reconciliation.",
96
+ requires_approval=True,
97
+ guardrails=["approval", "idempotency_key", "state_reconciliation"],
98
+ )
99
+ return PolicyDecision(effect="allow", risk="low", reason="No blocking policy matched.", guardrails=["trace"])
100
+
101
+
102
+ def reconcile_state(expected: dict[str, Any], live: dict[str, Any]) -> dict[str, Any]:
103
+ drift = []
104
+ for key, expected_value in expected.items():
105
+ if live.get(key) != expected_value:
106
+ drift.append({"key": key, "expected": expected_value, "live": live.get(key)})
107
+ return {
108
+ "status": "changed" if drift else "valid",
109
+ "drift": drift,
110
+ "checked_at": utc_now(),
111
+ }
112
+
113
+
114
+ def freeze_execution(execution_id: str, action: str, params: dict[str, Any], context: dict[str, Any]) -> Checkpoint:
115
+ return Checkpoint(
116
+ checkpoint_id="freeze_" + uuid.uuid4().hex[:12],
117
+ execution_id=execution_id,
118
+ action=action,
119
+ params=params,
120
+ context_snapshot=context,
121
+ )
122
+
123
+
124
+ def thaw_checkpoint(checkpoint: Checkpoint, live_context: dict[str, Any]) -> dict[str, Any]:
125
+ return {
126
+ "checkpoint_id": checkpoint.checkpoint_id,
127
+ "execution_id": checkpoint.execution_id,
128
+ "revalidation": reconcile_state(checkpoint.context_snapshot, live_context),
129
+ "thawed_at": utc_now(),
130
+ }
131
+
132
+
133
+ def build_boot_trace(app_name: str, entrypoint: str, robustness: dict[str, bool]) -> ExecutionTrace:
134
+ trace = ExecutionTrace(
135
+ execution_id="exec_" + uuid.uuid4().hex[:12],
136
+ agent_id=app_name,
137
+ action="invoke.deploy",
138
+ status="planned",
139
+ risk="low",
140
+ final_outcome="deployment_plan_recorded",
141
+ )
142
+ trace.events.extend(
143
+ [
144
+ TraceEvent("project_loaded", "ok", {"entrypoint": entrypoint}),
145
+ TraceEvent("persistence_selected", "ok", {"kind": "modal_volume"}),
146
+ TraceEvent("robustness_layer_enabled", "ok", robustness),
147
+ ]
148
+ )
149
+ return trace
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "my-claude-agent",
3
+ "entrypoint": "src/index.ts",
4
+ "runtime": "node",
5
+ "tools": [],
6
+ "policy": {
7
+ "approval_required_for": ["charge", "refund", "delete", "database.execute"],
8
+ "block": ["database.execute"]
9
+ }
10
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "invoke-claude-agent",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx src/index.ts",
8
+ "start": "node dist/index.js",
9
+ "build": "tsc -p tsconfig.json"
10
+ },
11
+ "dependencies": {
12
+ "@anthropic-ai/claude-agent-sdk": "latest"
13
+ },
14
+ "devDependencies": {
15
+ "tsx": "latest",
16
+ "typescript": "latest"
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk"
2
+
3
+ const prompt = process.argv.slice(2).join(" ") || "Summarize what this agent can do."
4
+
5
+ for await (const message of query({
6
+ prompt,
7
+ options: {
8
+ allowedTools: [],
9
+ },
10
+ })) {
11
+ if ("result" in message) {
12
+ console.log(message.result)
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "outDir": "dist",
8
+ "skipLibCheck": true
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invokehq/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "CLI for Invoke, execution reliability infrastructure for AI agents.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -10,6 +10,13 @@
10
10
  "files": [
11
11
  "agentify.py",
12
12
  "bin",
13
+ "invoke/*.py",
14
+ "invoke/onyx/*.py",
15
+ "invoke/supervisor/*.py",
16
+ "invoke/templates/claude-agent-sdk/invoke.json",
17
+ "invoke/templates/claude-agent-sdk/package.json",
18
+ "invoke/templates/claude-agent-sdk/src/index.ts",
19
+ "invoke/templates/claude-agent-sdk/tsconfig.json",
13
20
  "README.md",
14
21
  "pyproject.toml"
15
22
  ],
package/pyproject.toml CHANGED
@@ -24,5 +24,9 @@ dev = ["build", "twine"]
24
24
  invoke = "agentify:main"
25
25
  agentify = "agentify:main"
26
26
 
27
- [tool.setuptools.py-modules]
28
- modules = ["agentify"]
27
+ [tool.setuptools]
28
+ py-modules = ["agentify"]
29
+ packages = ["invoke", "invoke.onyx", "invoke.supervisor"]
30
+
31
+ [tool.setuptools.package-data]
32
+ invoke = ["templates/claude-agent-sdk/*", "templates/claude-agent-sdk/src/*"]