Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/architecture/FORGE_SERVER_DEPLOYMENT_TARGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,21 @@ The current repository-bound `.git/forge-runtime` placement is historical/bootst

Forge binds to EP and Workspace only through their versioned authenticated APIs. It never reads their databases or controls their services. The shared discovery/pairing vocabulary and descriptor are defined by Forge Platform's [contract](https://github.com/pcvantol/forge-platform/blob/main/docs/architecture/INSTANCE_DISCOVERY_AND_PAIRING_CONTRACT.md): LAN DNS-SD/mDNS and configured/unicast/tailnet endpoints locate candidates, while authenticated pairing pins peer product, stable instance ID, identity fingerprint, endpoint/version/capability set and scope in Forge-owned storage. Discovery is not authorization; a binding never silently retargets to a discovered instance.

The autonomy canary requires only installed Forge/EP storage, stable identities, the existing versioned authenticated Forge→EP HTTP seam, a configured/pinned EP binding and restart recovery. Workspace UI, LAN discovery and universal-installer completion are post-canary productization and must not block that proof.
## HTTP API implementation requirement

Forge does not yet expose its own HTTP server. When that implementation is
introduced, its versioned OpenAPI document is the canonical public transport
contract and must describe every implemented Forge-owned route, method,
authentication requirement, request/response/error envelope and version
behavior. The implementation must ship an exhaustive Postman collection
derived from that contract, covering every documented operation and its
declared authorization and error cases without embedding credentials.

CI must run the collection against an isolated Forge server state and fail on
any drift between the OpenAPI document, the routes actually exposed by that
server, and the Postman collection. A route may not be added, removed or
semantically changed without updating all three artifacts in the same change.
This requirement applies only to a future Forge-owned HTTP API; it neither
redefines the EP API nor makes Forge an EP proxy.

The autonomy canary requires only installed Forge/EP storage, stable identities, the existing versioned authenticated Forge→EP HTTP seam, a configured/pinned EP binding and restart recovery. Workspace UI, LAN discovery and universal-installer completion are post-canary productization and must not block that proof.
2 changes: 2 additions & 0 deletions forge/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
ExecutionEvidenceOutcome,
ExecutionHostContract,
ExecutionHostEvidence,
ExecutionHostTemporaryUnavailable,
ExecutionHostForbiddenResponsibility,
ExecutionHostLifecycleStage,
ExecutionHostResponsibility,
Expand Down Expand Up @@ -522,6 +523,7 @@
"ReasoningProfile",
"ExecutionHostContract",
"ExecutionHostEvidence",
"ExecutionHostTemporaryUnavailable",
"ExecutionHostForbiddenResponsibility",
"ExecutionHostLifecycleStage",
"ExecutionHostResponsibility",
Expand Down
20 changes: 17 additions & 3 deletions forge/models/execution_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class ExecutionEvidenceOutcome(str, Enum):
FAILED = "failed"


class ExecutionHostTemporaryUnavailable(RuntimeError):
"""A transport/read outage; the persisted request remains the recovery key."""


@dataclass(frozen=True)
class ExecutionHostContract:
"""A complete host declaration without a host implementation."""
Expand Down Expand Up @@ -160,6 +164,12 @@ def _default_producer_contract(self) -> ProducerContract:
("repository_id", self.repository_id),
("workspace_id", self.workspace_id),
)
# A rendered runtime prompt is the materialized Mission provenance.
# Carry its actual immutable revision through the Producer Contract;
# adapters must never supply a transport default such as "1".
mission_revision = getattr(self.runtime_prompt, "mission_revision", None)
if isinstance(mission_revision, str) and mission_revision:
metadata += (("mission_revision", mission_revision),)
return ProducerContract(
producer=Producer(prompt_producer),
correlation_id=self.correlation_id,
Expand Down Expand Up @@ -201,19 +211,21 @@ class ExecutionRepositoryEvidence:
correlation_id: str
host_run_id: str
repository_id: str
repository_revision: str
repository_revision: str | None
report_id: str
content_digest: str

def __post_init__(self) -> None:
if not all((self.mission_id, self.intent_id, self.intent_revision, self.action_id,
self.runtime_prompt_id, self.correlation_id, self.host_run_id,
self.repository_id, self.repository_revision, self.report_id,
self.repository_id, self.report_id,
self.content_digest)):
raise ValueError("repository evidence identity, provenance, revision, report, and digest are required")
digest = self.content_digest.removeprefix("sha256:")
if not self.content_digest.startswith("sha256:") or len(digest) != 64:
raise ValueError("repository evidence digest must be sha256")
if self.repository_revision is not None and not self.repository_revision:
raise ValueError("repository evidence revision cannot be empty")


@dataclass(frozen=True)
Expand Down Expand Up @@ -245,6 +257,8 @@ def __post_init__(self) -> None:
self.correlation_id, self.host_run_id, self.report_id,
):
raise ValueError("execution host evidence must match its repository evidence run and report")
if self.outcome is ExecutionEvidenceOutcome.COMPLETE and not repository.repository_revision:
raise ValueError("complete execution evidence requires a delivery revision")
for references, label in ((self.log_references, "log"), (self.diagnostic_references, "diagnostic"), (self.metric_references, "metric"), (self.validation_references, "validation")):
if any(not reference for reference in references) or len(references) != len(set(references)):
raise ValueError(f"execution host evidence {label} references must be unique and non-empty")
Expand All @@ -268,7 +282,7 @@ class ExecutionHost(Protocol):
a request before dispatching it without treating process memory as state.
"""

def dispatch(self, request: ExecutionRequest) -> ExecutionDispatch: ...
def dispatch(self, request: ExecutionRequest) -> ExecutionDispatch | None: ...

def recover_dispatch(self, request: ExecutionRequest) -> ExecutionDispatch | None: ...

Expand Down
37 changes: 37 additions & 0 deletions forge/operator_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,43 @@ def adopt_governance_capabilities(self, context):
row=self.db._connection.execute('SELECT created_at,version,status FROM installation_operator_binding WHERE installation_id=?',(context.installation_id,)).fetchone()
if not row or row['status']!='ACTIVE': raise PermissionError('active G001 binding required')
self._persist_governance_capabilities(context,'EXISTING_G001_GOVERNANCE_ADOPTION_V1',{'prior_binding_created_at':row['created_at'],'prior_binding_version':row['version'],'adopted_at':_timestamp()},row)
def upgrade_legacy_governance_capabilities(self, context, *, decision_source):
"""Add only the v2 programme capability to a verified v1 three-capability installation."""
if not self.authorize(context) or not decision_source: raise PermissionError('trusted operator and upgrade decision source required')
legacy=('ARCHITECTURE_APPROVAL','BUSINESS_APPROVAL','SECURITY_APPROVAL'); operator=self._governance_operator_id(context)
with self.db._connection:
binding=self.db._connection.execute('SELECT * FROM installation_operator_binding WHERE installation_id=?',(context.installation_id,)).fetchone()
if not binding or binding['status']!='ACTIVE': raise PermissionError('active binding required')
rows=self.db._connection.execute('SELECT * FROM governance_capability_grants WHERE installation_id=? AND operator_id=? ORDER BY capability',(context.installation_id,operator)).fetchall()
authorities=tuple(r['capability'] for r in self.db._connection.execute('SELECT capability FROM governance_authority WHERE installation_id=? AND operator_id=? ORDER BY capability',(context.installation_id,operator)))
expected=tuple(sorted((*legacy,'OWNER_PROGRAMME_AUTHORIZATION')))
legacy_rows=tuple(r for r in rows if r['capability'] in legacy)
def valid_legacy_grants():
if len(legacy_rows)!=3 or tuple(r['capability'] for r in legacy_rows)!=legacy:return False
for row in legacy_rows:
try: document=json.loads(row['bootstrap_provenance'])
except (TypeError,ValueError): return False
digest='sha256:'+hashlib.sha256(json.dumps(document,sort_keys=True,separators=(',',':')).encode()).hexdigest()
if digest!=row['digest'] or document.get('capability')!=row['capability'] or document.get('installation_id')!=context.installation_id or document.get('operator_id')!=operator:return False
kind=document.get('kind')
if kind=='LOCAL_INSTALLATION_BOOTSTRAP_V1':
if document.get('binding_version')!=binding['version'] or not isinstance(document.get('first_bind_at'),str):return False
elif kind=='EXISTING_G001_GOVERNANCE_ADOPTION_V1':
if document.get('prior_binding_version')!=binding['version'] or not isinstance(document.get('prior_binding_created_at'),str):return False
else:return False
return True
if authorities==expected:
if len(rows)!=4 or tuple(r['capability'] for r in rows)!=expected: raise PermissionError('upgraded capability grants are incomplete')
if not valid_legacy_grants(): raise PermissionError('legacy capability provenance is invalid')
upgraded=next(r for r in rows if r['capability']=='OWNER_PROGRAMME_AUTHORIZATION')
document=json.loads(upgraded['bootstrap_provenance']); digest='sha256:'+hashlib.sha256(json.dumps(document,sort_keys=True,separators=(',',':')).encode()).hexdigest()
old={r['grant_id']:r['digest'] for r in rows if r['capability']!='OWNER_PROGRAMME_AUTHORIZATION'}
if digest!=upgraded['digest'] or document.get('kind')!='G001_OWNER_PROGRAMME_CAPABILITY_UPGRADE_V1' or dict(document.get('legacy_grants',()))!=old or document.get('binding_version')!=binding['version']: raise PermissionError('upgraded capability provenance is invalid')
return
if authorities!=legacy or tuple(r['capability'] for r in rows)!=legacy: raise PermissionError('state is not the recognized legacy capability set')
if not valid_legacy_grants(): raise PermissionError('legacy capability provenance is invalid')
now=_timestamp(); provenance={'kind':'G001_OWNER_PROGRAMME_CAPABILITY_UPGRADE_V1','upgrade_version':'1','decision_source':decision_source,'legacy_grants':[(r['grant_id'],r['digest']) for r in rows],'binding_version':binding['version'],'added_capability':'OWNER_PROGRAMME_AUTHORIZATION','occurred_at':now,'installation_id':context.installation_id,'operator_id':operator,'capability':'OWNER_PROGRAMME_AUTHORIZATION'}; encoded=json.dumps(provenance,sort_keys=True,separators=(',',':')); digest='sha256:'+hashlib.sha256(encoded.encode()).hexdigest()
self.db._insert_governance_grant(digest,context.installation_id,operator,'OWNER_PROGRAMME_AUTHORIZATION',encoded,digest,now); self.db._insert_governance_authority(context.installation_id,operator,'OWNER_PROGRAMME_AUTHORIZATION',now); self._audit(context.installation_id,NamedOperatorIdentity(context.generated_uid,0),'LEGACY_CAPABILITY_UPGRADE',now,'ALLOW')
def revoke(self, context):
if not self.authorize(context):raise PermissionError('denied')
with self.db._connection:
Expand Down
28 changes: 18 additions & 10 deletions forge/programme_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class CandidateQualification:
owner_workflow_evidence: str
owner_workflow_head_sha: str
merge_method: str = "squash"
mission_id: str | None = None
action_id: str | None = None

def __post_init__(self) -> None:
if (self.pull_request <= 0 or not _SHA.fullmatch(self.head_sha)
Expand Down Expand Up @@ -122,20 +124,23 @@ def qualify(self, authorization_id: str, candidate: CandidateQualification) -> s
def record_repair_attempt(self, authorization_id: str, candidate: CandidateQualification) -> str:
authorization = self._authorization(authorization_id)
self._validate_candidate(authorization, candidate, require_passes=False)
existing = self._repair_attempts(authorization_id, candidate.pull_request, candidate.head_sha)
if not candidate.mission_id or not candidate.action_id:
raise PermissionError("repair authorization requires the Forge Mission and Engineering Action lineage")
existing = self._repair_attempts(authorization_id, candidate.mission_id, candidate.action_id)
if existing >= authorization.repair_attempt_limit:
raise PermissionError("bounded repair budget exhausted for this exact PR head")
raise PermissionError("bounded repair budget exhausted for this Engineering Action lineage")
attempt = existing + 1
return self.repository.record(GovernanceDecision(
decision_id=f"{authorization_id}:repair:{candidate.pull_request}:{candidate.head_sha}:{attempt}",
subject_id=f"{authorization.programme_id}:repair:pr-{candidate.pull_request}:attempt-{attempt}",
decision_id=f"{authorization_id}:repair:{candidate.mission_id}:{candidate.action_id}:{attempt}",
subject_id=f"{authorization.programme_id}:repair:{candidate.mission_id}:{candidate.action_id}:attempt-{attempt}",
subject_revision=candidate.head_sha,
capability=GovernanceCapability.OWNER_PROGRAMME_AUTHORIZATION,
decision="repair-authorized",
scope=candidate.changed_scopes,
gates=("same-approved-scope", "exact-head"),
predecessor_digest=self._decision_digest(authorization_id),
evidence={"kind": "BOUNDED_REPAIR_ATTEMPT_V1", "authorization_id": authorization_id,
evidence={"kind": "BOUNDED_ACTION_REPAIR_ATTEMPT_V2", "authorization_id": authorization_id,
"mission_id": candidate.mission_id, "action_id": candidate.action_id,
"pull_request": candidate.pull_request, "head_sha": candidate.head_sha,
"attempt": attempt},
), self.context)
Expand Down Expand Up @@ -171,16 +176,19 @@ def _decision_digest(self, decision_id: str) -> str:
raise PermissionError("authorization evidence is absent")
return row["digest"]

def _repair_attempts(self, authorization_id: str, pull_request: int, head_sha: str) -> int:
def _repair_attempts(self, authorization_id: str, mission_id: str, action_id: str) -> int:
rows = self.repository.database._connection.execute(
"SELECT document FROM governance_decisions WHERE capability = ?",
(GovernanceCapability.OWNER_PROGRAMME_AUTHORIZATION.value,),
).fetchall()
import json
return sum(
1 for row in rows
if (lambda evidence: evidence.get("kind") == "BOUNDED_REPAIR_ATTEMPT_V1"
and evidence.get("authorization_id") == authorization_id
and evidence.get("pull_request") == pull_request
and evidence.get("head_sha") == head_sha)(json.loads(row["document"]).get("evidence", {}))
if (lambda evidence: evidence.get("authorization_id") == authorization_id and (
(evidence.get("kind") == "BOUNDED_ACTION_REPAIR_ATTEMPT_V2"
and evidence.get("mission_id") == mission_id and evidence.get("action_id") == action_id)
# V1 had no Action identity. Count it conservatively rather
# than silently resetting a pre-existing programme budget.
or evidence.get("kind") == "BOUNDED_REPAIR_ATTEMPT_V1"
))(json.loads(row["document"]).get("evidence", {}))
)
3 changes: 2 additions & 1 deletion forge/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
)
from .evidence import RuntimeDecisionEvidenceReference, RuntimeEvidence
from .runner import BootstrapMissionRunner, MissionRunnerError, RuntimePromptFactory
from .service import ForgeRuntimeService, RuntimeServiceTick

__all__ = [
"BootstrapMissionRunner", "MissionRunnerError", "RuntimePromptFactory",
"BootstrapMissionRunner", "MissionRunnerError", "RuntimePromptFactory", "ForgeRuntimeService", "RuntimeServiceTick",
"RUNTIME_SCHEMA_VERSION", "RuntimeDatabase", "RuntimeDatabaseError", "RuntimeIntegrityError", "RuntimeDecisionEvidenceReference", "RuntimeEvidence",
"RUNTIME_INSTANCE_VERSION", "RUNTIME_INITIALIZATION_VERSION", "RuntimeBootstrap", "RuntimeIdentity", "RuntimeInstance", "RuntimeLocation", "RuntimeRecovery", "RuntimeResolutionError", "RuntimeResolver", "repository_identity", "repository_uuid",
]
Loading