forgexa-cli 1.86.0__tar.gz → 1.86.1__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.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/agent_core.py +75 -0
  4. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/daemon.py +50 -4
  5. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/README.md +0 -0
  8. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/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.1
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.1"
@@ -747,6 +747,81 @@ 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) -> dict:
778
+ baseline = task_input.get("approved_plan_baseline")
779
+ reasons = []
780
+ fingerprint = None
781
+ if isinstance(baseline, dict):
782
+ expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
783
+ fingerprint = hashlib.sha256(json.dumps(expected, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
784
+ if type(baseline.get("version")) is not int or baseline["version"] != 1 or baseline.get("fingerprint") != fingerprint:
785
+ reasons.append("baseline_invalid")
786
+ else:
787
+ reasons.append("baseline_unavailable")
788
+ planned = (baseline or {}).get("task") if isinstance(baseline, dict) else None
789
+ planned = planned if isinstance(planned, dict) else {}
790
+ files = missing_planned_task_files({"files": planned.get("files") or [], "items": planned.get("items") or []}, [])
791
+ files = sorted(set(files) | {path for path in changed_files or [] if isinstance(path, str)})
792
+ audit = task_input.get("implementation_audit_contract") or {}
793
+ if node_type not in {"coding", "fix", "testing"} or planned.get("type") not in {"coding", "fix", "testing"}:
794
+ reasons.append("review_stage")
795
+ if isinstance(audit, dict) and audit.get("final_verification") is True or task_input.get("verification_work_item"):
796
+ reasons.append("final_verification")
797
+ if not files:
798
+ reasons.append("scope_unknown")
799
+ patterns = {
800
+ "sensitive_configuration": r"(?:^|/)(?:config|system|di|acl|webapi|db_schema)\.xml$|(?:^|/)(?:\.github|deploy|infrastructure)/|(?:^|/)(?:dockerfile|compose[^/]*)$",
801
+ "payment_or_crypto": r"payment|checkout|encrypt|decrypt|crypto|secret|credential|webhook|idempot|refund|compensat",
802
+ "data_migration": r"migrat|alembic|(?:^|/)setup/|db_schema|(?:^|/)(?:composer\.lock|package-lock\.json|yarn\.lock|cargo\.lock)$",
803
+ "authorization": r"(?:^|/)(?:auth[^/]*|permission[^/]*|acl|security|tenant[^/]*)[/.]",
804
+ }
805
+ for reason, pattern in patterns.items():
806
+ if any(re.search(pattern, path, re.IGNORECASE) for path in files):
807
+ reasons.append(reason)
808
+ if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
809
+ reasons.append("scope_unknown")
810
+ decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
811
+ decision["fingerprint"] = hashlib.sha256(json.dumps({
812
+ **decision, "baseline_fingerprint": fingerprint,
813
+ "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
814
+ "change_scope": str(task_input.get("change_scope") or ""),
815
+ }, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
816
+ return decision
817
+
818
+
819
+ def review_blocks_completion(policy: dict, assessment: dict) -> bool:
820
+ return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
821
+ policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
822
+ )
823
+
824
+
750
825
  _WORKSPACE_INSTRUCTION_PATHS = (
751
826
  "AGENTS.md",
752
827
  "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.1"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -13120,6 +13120,11 @@ class RuntimeDaemon:
13120
13120
  "This indicates a git clone or worktree setup failure (check SSH/network)."
13121
13121
  )
13122
13122
  logger.info("Workspace ready: %s", workspace_path)
13123
+ recovery = (task.input_data or {}).get("verification_recovery")
13124
+ if isinstance(recovery, dict) and recovery.get("version") == 1 and recovery.get("mode") == "review_only":
13125
+ result = await self._execute_review_only(task, agent, workspace_path)
13126
+ await report_task_complete(result)
13127
+ return
13123
13128
  self._ensure_task_output_dir(workspace_path, task)
13124
13129
 
13125
13130
  try:
@@ -13937,7 +13942,9 @@ class RuntimeDaemon:
13937
13942
  before_sha=node_before_sha,
13938
13943
  )
13939
13944
 
13940
- if result.status == "success" and task.critic_review_required:
13945
+ review_policy = agent_core.task_review_policy(task.node_type, task.input_data or {}, changed_files=result.files_changed)
13946
+ result.metrics["task_review_policy"] = review_policy
13947
+ if result.status == "success" and review_policy["mode"] == "required" and (task.critic_review_required or isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13941
13948
  await self._collect_task_delivery_git(
13942
13949
  task, workspace_path, result, attempt_before_sha=node_before_sha,
13943
13950
  )
@@ -13990,6 +13997,25 @@ class RuntimeDaemon:
13990
13997
  project_lock.release()
13991
13998
  _release_local_workspace_file_lock(local_bind_file_lock)
13992
13999
 
14000
+ async def _execute_review_only(self, task, agent, workspace_path) -> TaskResult:
14001
+ recovery = (task.input_data or {})["verification_recovery"]
14002
+ snapshot = recovery.get("snapshot") or {}
14003
+ expected = recovery.get("commit_sha")
14004
+ head = (await self.workspace_manager._git("rev-parse", "HEAD", cwd=workspace_path, timeout=10)).strip()
14005
+ if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{40}", expected) or head != expected:
14006
+ return TaskResult(status="failed", exit_code=-1, stdout="", stderr="", failure_code="approved_plan_baseline_modified",
14007
+ error="Read-only review source changed; preserve the checkout and refresh recovery evidence")
14008
+ result = TaskResult(status="success", exit_code=0, stdout="", stderr="",
14009
+ metrics=dict(snapshot.get("metrics") or {}), git=dict(snapshot.get("git") or {}),
14010
+ artifacts=list(snapshot.get("artifacts") or []), observations=list(snapshot.get("observations") or []),
14011
+ evidence_summary=snapshot.get("evidence_summary"), files_changed=list((snapshot.get("git") or {}).get("files_changed") or []))
14012
+ for key in ("token_input", "token_output", "token_total", "input_tokens", "output_tokens", "cost_usd", "num_turns", "duration_ms"):
14013
+ result.metrics.pop(key, None)
14014
+ result.critic_review = await self._run_runtime_critic_review(task, agent, workspace_path, result,
14015
+ before_sha=str(result.git.get("before_sha") or expected))
14016
+ result.metrics["verification_recovery_mode"] = "review_only"
14017
+ return result
14018
+
13993
14019
  async def _collect_runtime_critic_diff(
13994
14020
  self,
13995
14021
  workspace_path: Path,
@@ -14061,8 +14087,10 @@ class RuntimeDaemon:
14061
14087
 
14062
14088
  after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
14063
14089
  committed_sources: dict[str, str] = {}
14090
+ projected_provenance = set()
14064
14091
  unavailable_sources: list[dict[str, str]] = []
14065
14092
  source_bytes = 0
14093
+ assigned_paths = []
14066
14094
  coverage = {
14067
14095
  "candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
14068
14096
  "skipped_paths": 0, "omitted_files": 0, "prompt_excerpt_truncated": False,
@@ -14266,6 +14294,15 @@ class RuntimeDaemon:
14266
14294
  unavailable_sources.append({"path": path, "reason": reason})
14267
14295
  continue
14268
14296
  encoded_size = len(source.encode("utf-8"))
14297
+ if source_bytes + encoded_size > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14298
+ projection = agent_core.lock_provenance_projection(path, source)
14299
+ if projection is not None and source_bytes + len(projection.encode()) <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14300
+ committed_sources[path + ".provenance.json"] = projection
14301
+ projected_provenance.add(path)
14302
+ source_bytes += len(projection.encode())
14303
+ source_context.append("\n## Parsed dependency provenance (not complete source or semantic proof)\n" + projection[:max(remaining, 0)])
14304
+ remaining -= len(projection)
14305
+ continue
14269
14306
  if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14270
14307
  committed_sources[path] = source
14271
14308
  source_bytes += encoded_size
@@ -14279,14 +14316,23 @@ class RuntimeDaemon:
14279
14316
  changed_files.append(path)
14280
14317
  if source_context:
14281
14318
  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"]
14319
+ coverage["full_source_files"] = len(committed_sources) - len(projected_provenance)
14320
+ coverage["projected_provenance_files"] = len(projected_provenance)
14321
+ coverage["omitted_files"] = coverage["candidate_files"] - len(committed_sources)
14284
14322
  coverage["capsule_incomplete"] = (
14285
14323
  coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
14286
14324
  or coverage.get("verification_inventory_unavailable", False)
14287
14325
  or coverage.get("design_reference_documents_unavailable", False)
14288
14326
  )
14289
14327
  result.metrics["runtime_critic_coverage"] = coverage
14328
+ required_sources = set(assigned_paths) | set((result.git or {}).get("files_changed") or result.files_changed)
14329
+ required_sources = {path for path in required_sources if isinstance(path, str)}
14330
+ coverage["required_source_files"] = len(required_sources)
14331
+ coverage["missing_required_source_files"] = len(required_sources - set(committed_sources) - projected_provenance)
14332
+ if required_sources and coverage["missing_required_source_files"]:
14333
+ if not is_local_bind:
14334
+ result.metrics["runtime_critic_evidence"] = {"reviewed_commit_sha": after_sha, "unavailable_sources": unavailable_sources}
14335
+ return {"collection_status": "unavailable", "failure_code": "runtime_critic_evidence_incomplete", **metadata}
14290
14336
  if not diff:
14291
14337
  return {
14292
14338
  "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.1
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.1"
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