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.
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/PKG-INFO +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/agent_core.py +77 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/daemon.py +54 -4
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/pyproject.toml +1 -1
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/README.md +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/setup.cfg +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.86.0 → forgexa_cli-1.86.2}/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.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.
|
|
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
|
-
|
|
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["
|
|
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",
|
|
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
|