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.
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/PKG-INFO +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/agent_core.py +75 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/daemon.py +50 -4
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/pyproject.toml +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/README.md +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/setup.cfg +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.1}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.86.
|
|
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.
|
|
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
|
-
|
|
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["
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|