forgexa-cli 1.86.0__tar.gz → 1.86.2__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 (34) hide show
  1. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/agent_core.py +77 -0
  4. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/daemon.py +54 -4
  5. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/pyproject.toml +1 -1
  7. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/README.md +0 -0
  8. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/setup.cfg +0 -0
  25. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.86.0
3
+ Version: 1.86.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.86.0"
2
+ __version__ = "1.86.2"
@@ -747,6 +747,83 @@ def task_failure_domain(
747
747
  return "execution"
748
748
 
749
749
 
750
+ def lock_provenance_projection(path: str, source: str) -> str | None:
751
+ if path != "composer.lock" or len(source.encode()) > 10_000_000:
752
+ return None
753
+ try:
754
+ lock = json.loads(source)
755
+ except (ValueError, TypeError):
756
+ return None
757
+ if not isinstance(lock, dict) or not isinstance(lock.get("packages"), list):
758
+ return None
759
+ if not isinstance(lock.get("packages-dev", []), list):
760
+ return None
761
+ packages = []
762
+ for package in [*lock["packages"], *(lock.get("packages-dev") or [])]:
763
+ if not isinstance(package, dict) or not isinstance(package.get("name"), str) or not isinstance(package.get("version"), str):
764
+ return None
765
+ item = {"name": package["name"][:200], "version": package["version"][:100]}
766
+ for field in ("source", "dist"):
767
+ reference = (package.get(field) or {}).get("reference") if isinstance(package.get(field), dict) else None
768
+ if isinstance(reference, str) and re.fullmatch(r"[0-9a-fA-F]{7,64}", reference):
769
+ item[field + "_reference"] = reference
770
+ packages.append(item)
771
+ return json.dumps({"representation": "dependency_provenance_projection", "source_path": path,
772
+ "source_sha256": hashlib.sha256(source.encode()).hexdigest(), "source_bytes": len(source.encode()),
773
+ "limitation": "Parsed dependency identities only; not full lock source or implementation semantics", "packages": packages,
774
+ }, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
775
+
776
+
777
+ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None, policy_supported: bool = True) -> dict:
778
+ baseline = task_input.get("approved_plan_baseline")
779
+ reasons = []
780
+ if policy_supported is not True:
781
+ reasons.append("runtime_policy_upgrade_required")
782
+ fingerprint = None
783
+ if isinstance(baseline, dict):
784
+ expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
785
+ fingerprint = hashlib.sha256(json.dumps(expected, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
786
+ if type(baseline.get("version")) is not int or baseline["version"] != 1 or baseline.get("fingerprint") != fingerprint:
787
+ reasons.append("baseline_invalid")
788
+ else:
789
+ reasons.append("baseline_unavailable")
790
+ planned = (baseline or {}).get("task") if isinstance(baseline, dict) else None
791
+ planned = planned if isinstance(planned, dict) else {}
792
+ files = missing_planned_task_files({"files": planned.get("files") or [], "items": planned.get("items") or []}, [])
793
+ files = sorted(set(files) | {path for path in changed_files or [] if isinstance(path, str)})
794
+ audit = task_input.get("implementation_audit_contract") or {}
795
+ if node_type not in {"coding", "fix", "testing"} or planned.get("type") not in {"coding", "fix", "testing"}:
796
+ reasons.append("review_stage")
797
+ if isinstance(audit, dict) and audit.get("final_verification") is True or task_input.get("verification_work_item"):
798
+ reasons.append("final_verification")
799
+ if not files:
800
+ reasons.append("scope_unknown")
801
+ patterns = {
802
+ "sensitive_configuration": r"(?:^|/)(?:config|system|di|acl|webapi|db_schema)\.xml$|(?:^|/)(?:\.github|deploy|infrastructure)/|(?:^|/)(?:dockerfile|compose[^/]*)$",
803
+ "payment_or_crypto": r"payment|checkout|encrypt|decrypt|crypto|secret|credential|webhook|idempot|refund|compensat",
804
+ "data_migration": r"migrat|alembic|(?:^|/)setup/|db_schema|(?:^|/)(?:composer\.lock|package-lock\.json|yarn\.lock|cargo\.lock)$",
805
+ "authorization": r"(?:^|/)(?:auth[^/]*|permission[^/]*|acl|security|tenant[^/]*)[/.]",
806
+ }
807
+ for reason, pattern in patterns.items():
808
+ if any(re.search(pattern, path, re.IGNORECASE) for path in files):
809
+ reasons.append(reason)
810
+ if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
811
+ reasons.append("scope_unknown")
812
+ decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
813
+ decision["fingerprint"] = hashlib.sha256(json.dumps({
814
+ **decision, "baseline_fingerprint": fingerprint,
815
+ "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
816
+ "change_scope": str(task_input.get("change_scope") or ""),
817
+ }, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
818
+ return decision
819
+
820
+
821
+ def review_blocks_completion(policy: dict, assessment: dict) -> bool:
822
+ return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
823
+ policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
824
+ )
825
+
826
+
750
827
  _WORKSPACE_INSTRUCTION_PATHS = (
751
828
  "AGENTS.md",
752
829
  "CLAUDE.md",
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
1234
1234
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1235
1235
  # Kept in sync with pyproject.toml version via bump-version.sh.
1236
1236
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1237
- DAEMON_VERSION = "1.86.0"
1237
+ DAEMON_VERSION = "1.86.2"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -10219,6 +10219,8 @@ class ServerConnection:
10219
10219
  },
10220
10220
  "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10221
10221
  "task_delivery": {"protocol_version": 1},
10222
+ "verification_recovery": {"protocol_version": 1},
10223
+ "task_review_policy": {"protocol_version": 1},
10222
10224
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10223
10225
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10224
10226
  },
@@ -10645,6 +10647,8 @@ class ServerConnection:
10645
10647
  },
10646
10648
  "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10647
10649
  "task_delivery": {"protocol_version": 1},
10650
+ "verification_recovery": {"protocol_version": 1},
10651
+ "task_review_policy": {"protocol_version": 1},
10648
10652
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10649
10653
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10650
10654
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
@@ -13120,6 +13124,11 @@ class RuntimeDaemon:
13120
13124
  "This indicates a git clone or worktree setup failure (check SSH/network)."
13121
13125
  )
13122
13126
  logger.info("Workspace ready: %s", workspace_path)
13127
+ recovery = (task.input_data or {}).get("verification_recovery")
13128
+ if isinstance(recovery, dict) and recovery.get("version") == 1 and recovery.get("mode") == "review_only":
13129
+ result = await self._execute_review_only(task, agent, workspace_path)
13130
+ await report_task_complete(result)
13131
+ return
13123
13132
  self._ensure_task_output_dir(workspace_path, task)
13124
13133
 
13125
13134
  try:
@@ -13937,7 +13946,9 @@ class RuntimeDaemon:
13937
13946
  before_sha=node_before_sha,
13938
13947
  )
13939
13948
 
13940
- if result.status == "success" and task.critic_review_required:
13949
+ review_policy = agent_core.task_review_policy(task.node_type, task.input_data or {}, changed_files=result.files_changed)
13950
+ result.metrics["task_review_policy"] = review_policy
13951
+ if result.status == "success" and (task.critic_review_required or review_policy["mode"] == "required" and isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13941
13952
  await self._collect_task_delivery_git(
13942
13953
  task, workspace_path, result, attempt_before_sha=node_before_sha,
13943
13954
  )
@@ -13990,6 +14001,25 @@ class RuntimeDaemon:
13990
14001
  project_lock.release()
13991
14002
  _release_local_workspace_file_lock(local_bind_file_lock)
13992
14003
 
14004
+ async def _execute_review_only(self, task, agent, workspace_path) -> TaskResult:
14005
+ recovery = (task.input_data or {})["verification_recovery"]
14006
+ snapshot = recovery.get("snapshot") or {}
14007
+ expected = recovery.get("commit_sha")
14008
+ head = (await self.workspace_manager._git("rev-parse", "HEAD", cwd=workspace_path, timeout=10)).strip()
14009
+ if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{40}", expected) or head != expected:
14010
+ return TaskResult(status="failed", exit_code=-1, stdout="", stderr="", failure_code="approved_plan_baseline_modified",
14011
+ error="Read-only review source changed; preserve the checkout and refresh recovery evidence")
14012
+ result = TaskResult(status="success", exit_code=0, stdout="", stderr="",
14013
+ metrics=dict(snapshot.get("metrics") or {}), git=dict(snapshot.get("git") or {}),
14014
+ artifacts=list(snapshot.get("artifacts") or []), observations=list(snapshot.get("observations") or []),
14015
+ evidence_summary=snapshot.get("evidence_summary"), files_changed=list((snapshot.get("git") or {}).get("files_changed") or []))
14016
+ for key in ("token_input", "token_output", "token_total", "input_tokens", "output_tokens", "cost_usd", "num_turns", "duration_ms"):
14017
+ result.metrics.pop(key, None)
14018
+ result.critic_review = await self._run_runtime_critic_review(task, agent, workspace_path, result,
14019
+ before_sha=str(result.git.get("before_sha") or expected))
14020
+ result.metrics["verification_recovery_mode"] = "review_only"
14021
+ return result
14022
+
13993
14023
  async def _collect_runtime_critic_diff(
13994
14024
  self,
13995
14025
  workspace_path: Path,
@@ -14061,8 +14091,10 @@ class RuntimeDaemon:
14061
14091
 
14062
14092
  after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
14063
14093
  committed_sources: dict[str, str] = {}
14094
+ projected_provenance = set()
14064
14095
  unavailable_sources: list[dict[str, str]] = []
14065
14096
  source_bytes = 0
14097
+ assigned_paths = []
14066
14098
  coverage = {
14067
14099
  "candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
14068
14100
  "skipped_paths": 0, "omitted_files": 0, "prompt_excerpt_truncated": False,
@@ -14266,6 +14298,15 @@ class RuntimeDaemon:
14266
14298
  unavailable_sources.append({"path": path, "reason": reason})
14267
14299
  continue
14268
14300
  encoded_size = len(source.encode("utf-8"))
14301
+ if source_bytes + encoded_size > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14302
+ projection = agent_core.lock_provenance_projection(path, source)
14303
+ if projection is not None and source_bytes + len(projection.encode()) <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14304
+ committed_sources[path + ".provenance.json"] = projection
14305
+ projected_provenance.add(path)
14306
+ source_bytes += len(projection.encode())
14307
+ source_context.append("\n## Parsed dependency provenance (not complete source or semantic proof)\n" + projection[:max(remaining, 0)])
14308
+ remaining -= len(projection)
14309
+ continue
14269
14310
  if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14270
14311
  committed_sources[path] = source
14271
14312
  source_bytes += encoded_size
@@ -14279,14 +14320,23 @@ class RuntimeDaemon:
14279
14320
  changed_files.append(path)
14280
14321
  if source_context:
14281
14322
  diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
14282
- coverage["full_source_files"] = len(committed_sources)
14283
- coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
14323
+ coverage["full_source_files"] = len(committed_sources) - len(projected_provenance)
14324
+ coverage["projected_provenance_files"] = len(projected_provenance)
14325
+ coverage["omitted_files"] = coverage["candidate_files"] - len(committed_sources)
14284
14326
  coverage["capsule_incomplete"] = (
14285
14327
  coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
14286
14328
  or coverage.get("verification_inventory_unavailable", False)
14287
14329
  or coverage.get("design_reference_documents_unavailable", False)
14288
14330
  )
14289
14331
  result.metrics["runtime_critic_coverage"] = coverage
14332
+ required_sources = set(assigned_paths) | set((result.git or {}).get("files_changed") or result.files_changed)
14333
+ required_sources = {path for path in required_sources if isinstance(path, str)}
14334
+ coverage["required_source_files"] = len(required_sources)
14335
+ coverage["missing_required_source_files"] = len(required_sources - set(committed_sources) - projected_provenance)
14336
+ if required_sources and coverage["missing_required_source_files"]:
14337
+ if not is_local_bind:
14338
+ result.metrics["runtime_critic_evidence"] = {"reviewed_commit_sha": after_sha, "unavailable_sources": unavailable_sources}
14339
+ return {"collection_status": "unavailable", "failure_code": "runtime_critic_evidence_incomplete", **metadata}
14290
14340
  if not diff:
14291
14341
  return {
14292
14342
  "collection_status": "unavailable",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.86.0
3
+ Version: 1.86.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.86.0"
3
+ version = "1.86.2"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes