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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"baseline_version": "2.22.0",
"classification_rationale": "L3-PROJECTLESS-INSTALLATION-IDLE-V1-20261017",
"component": "product",
"event_lineage": "L3-PROJECTLESS-INSTALLATION-IDLE-V1-20261017",
"expected_head": "523a13f3196c096da217421e74130df474fadcc6",
"operation_id": "forge-r58-projectless-idle-20261017",
"policy_revision": "forge-bootstrap-release-cadence-v2",
"product": "forge",
"projection_paths": "product-version.json",
"release_class": "PATCH",
"requested_bump": "patch",
"requested_version": null,
"schema_version": "1",
"target_version": "2.22.1"
}
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ jobs:
name: forge-genesis-bootstrap-${{ github.sha }}
path: ${{ runner.temp }}/forge-genesis-proof/*.public.json
if-no-files-found: error
- name: Projectless installed scheduler HTTP and restart
timeout-minutes: 3
run: |
set -euo pipefail
wheel=("$RUNNER_TEMP"/forge-genesis-wheel/forge_autonomy-*.whl)
test "${#wheel[@]}" -eq 1
cd "$RUNNER_TEMP"
"$RUNNER_TEMP/forge-genesis-installed/bin/python" -I \
"$GITHUB_WORKSPACE/scripts/qualification/qualify_installed_projectless_idle.py" \
--wheel "${wheel[0]}" --source-revision "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" \
--output-dir "$RUNNER_TEMP/forge-projectless-idle"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: forge-projectless-idle-${{ github.sha }}
path: ${{ runner.temp }}/forge-projectless-idle/installed-projectless-idle.public.json
if-no-files-found: error
- run: bash scripts/validate.sh
env:
FORGE_WITH_COVERAGE: "1"
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/forge-production-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ jobs:
PYTHONNOUSERSITE=1 PYTHONSAFEPATH=1 "$RUNNER_TEMP/forge-wheel/bin/python" -I -c 'import forge, pathlib, tempfile; from importlib.metadata import version; from forge._version import canonical_version; from forge.qualification.installed_smoke import run, assert_runtime_persistence; assert version("forge-autonomy") == canonical_version() == __import__("os").environ["VERSION"]; assert "/site-packages/forge/" in str(pathlib.Path(forge.__file__).resolve()); run(); root=tempfile.TemporaryDirectory(); workspace=pathlib.Path(root.name)/"workspace"; workspace.mkdir(); assert_runtime_persistence(str(workspace), str(pathlib.Path(root.name)/"runtime"), canonical_version()); root.cleanup()'
"$RUNNER_TEMP/forge-wheel/bin/python" -I "$GITHUB_WORKSPACE/scripts/qualification/qualify_installed_installation_detach.py" --wheel "$GITHUB_WORKSPACE/dist/forge_autonomy-$VERSION-py3-none-any.whl" --source-revision "$SOURCE_SHA" --output-dir "$RUNNER_TEMP/forge-installation-detach-wheel"
cp "$RUNNER_TEMP/forge-installation-detach-wheel/installed-installation-detach.public.json" "$GITHUB_WORKSPACE/dist/installed-installation-detach-qualified.json"
"$RUNNER_TEMP/forge-wheel/bin/python" -I \
"$GITHUB_WORKSPACE/scripts/qualification/qualify_installed_projectless_idle.py" \
--wheel "$GITHUB_WORKSPACE/dist/forge_autonomy-$VERSION-py3-none-any.whl" \
--source-revision "$SOURCE_SHA" --output-dir "$RUNNER_TEMP/forge-projectless-idle-wheel"
cp "$RUNNER_TEMP/forge-projectless-idle-wheel/installed-projectless-idle.public.json" \
"$GITHUB_WORKSPACE/dist/installed-projectless-idle-qualified.json"
"$RUNNER_TEMP/forge-wheel/bin/forge" --help >/dev/null
test "$("$RUNNER_TEMP/forge-wheel/bin/forge" --version)" = "$VERSION"
"$RUNNER_TEMP/forge-wheel/bin/python" -I -m forge.qualification.criterion_completion --output-dir "$RUNNER_TEMP/forge-criterion-wheel"
Expand Down
19 changes: 19 additions & 0 deletions docs/architecture/FORGE_SERVER_RUNTIME_LIFECYCLE_V1.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,22 @@ normal recovery. Its controller correction is a new exact source binding;
the original 2.7.38 wheel and historical controlled-interruption evidence
retain their identities. This matrix makes no host reboot, power-loss or
installer acceptance claim.

## Projectless installation idle (R58)

A new installed instance can be scheduler `IDLE` before a project is selected.
The Server still requires its instance-owned provider readiness. Under the
canonical runtime mutation lease it opens a validated read-only storage snapshot
and proves absence of Mission state/allocation/intake/projection, submissions,
approved worksets, dispatcher/planning/bootstrap portfolio state, execution-host
bindings and immutable exchange audit. Only this narrow empty-work proof avoids
composing the project-bound execution runtime. Missing, corrupt, fenced or
unknown storage fails closed. Any retained work/history takes the existing
strict runtime path with all provider, project, EP-host and authority checks;
this change does not classify historical terminal records as an empty instance.

Installation connectivity is still independently authenticated. Installation
service readiness is not project authorization or execution readiness. No
project, Mission, grant or repository is created by the empty probe. Each tick
rechecks storage under the shared lease; no cached empty classification survives
new work or restart. Runtime storage schema remains46.
22 changes: 22 additions & 0 deletions forge/server_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,28 @@ def _tick(self) -> None:
provider = self.services.provider_readiness()
if not provider.get("ready"):
raise ForgeServerRuntimeError("instance-owned planning provider is not ready")
# A fresh installation has no project execution context yet. Prove the
# narrow empty-work condition from one validated read-only snapshot,
# under the same lease used by Mission intake and scheduler mutation.
# Any work/history, unreadable store or uncertainty retains the normal
# strict composition below; no peer exception is treated as IDLE.
from .runtime.bootstrap import RuntimeBootstrap
with RuntimeServiceLock(self.root / "forge.db").acquire():
database = RuntimeBootstrap(data_root=self.root, forge_version=canonical_version()).open_read_snapshot()
try:
work_tables = ("mission_state", "mission_id_allocations", "mission_intake_evidence",
"mission_runtime_projections", "scheduler_submissions", "approved_worksets",
"dispatcher_state", "planning_state", "bootstrap_portfolio_state",
"execution_host_bindings", "execution_host_exchange_audit")
empty = all(database._connection.execute(f"SELECT 1 FROM {table} LIMIT 1").fetchone() is None
for table in work_tables)
if empty:
if self.root_identity.drifted():
raise ForgeServerRuntimeError("Forge Server selected instance root changed")
self.state.scheduler("IDLE")
return
finally:
database.close()
with InstalledDynamicMissionRuntime.open(str(self.root), provider_id=self.provider_id) as runtime:
service = ForgeRuntimeService(_ResumeOnlyLoop(runtime), runtime.states, runtime_database=runtime.database)
tick = service.tick()
Expand Down
2 changes: 1 addition & 1 deletion product-version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"product": "forge",
"schema_version": 1,
"version": "2.22.0"
"version": "2.22.1"
}
12 changes: 11 additions & 1 deletion scripts/qualification/qualify_installed_candidate_decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ def boundary_counts(root):
decisions=db.execute('SELECT count(*) FROM governance_decisions').fetchone()[0]
return intents,decisions,receipts

def _publish_boundary_pid(root):
# Publish only complete PID bytes; existence is the parent's crash signal.
pending = root / 'decision-boundary.pending.private'
pending.touch(mode=0o600, exist_ok=False)
try:
pending.write_text(str(os.getpid()))
os.replace(pending, root / 'decision-boundary.ready.private')
finally:
pending.unlink(missing_ok=True)

def phase(root,stage):
cases=test_driver();q=cases.qual;original=q.utils.fixture.IDENTITY
payload=json.loads((root/'decision-request.private.json').read_text());baseline=0 if payload['kind']=='BUSINESS' else 1
Expand All @@ -31,7 +41,7 @@ def identity():
intents,decisions,receipts=boundary_counts(root)
wanted={'crash-before-effect':(baseline,baseline),'crash-between-stores':(baseline+1,baseline),'crash-after-effect':(baseline+1,baseline+1)}[stage]
if intents==baseline+1 and (decisions,receipts)==wanted:
(root/'decision-boundary.ready.private').write_text(str(os.getpid()));time.sleep(20)
_publish_boundary_pid(root);time.sleep(20)
return original
with patch.object(q.utils.composition.MacOSGeneratedUIDIdentityAdapter,'resolve',side_effect=identity),ExitStack() as stack:
server=q.ForgeServerRuntime(data_root=root/'runtime',credential_file=root/'admin.private',host='127.0.0.1',port=0,provider_id=q.utils.fixture.PROVIDER)
Expand Down
95 changes: 82 additions & 13 deletions scripts/qualification/qualify_installed_http_successor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,7 @@ def _phase(root: Path, scenario: str, phase: str, endpoint: str) -> None:
if time.monotonic() >= deadline:
raise RuntimeError("concurrent start barrier timed out")
time.sleep(0.01)
_install_late_preflight(runtime, root, phase)
try:
runtime.start(mission_id, _initial_truth())
except Exception as error:
Expand All @@ -1291,6 +1292,8 @@ def _phase(root: Path, scenario: str, phase: str, endpoint: str) -> None:
})
else:
fixture._write(root / f"{phase}.race.private.json", {"result": "STARTED"})
finally:
_mark_start_finished(root, phase)
elif phase == "after-a-cut":
def before_successor() -> bool:
observed = runtime.states.get(mission_id)
Expand Down Expand Up @@ -2512,9 +2515,77 @@ def _scenario(root: Path, scenario: str, wheel: Path, *, observer=None) -> dict:
**({"recovery": recovery_summary} if recovery_summary is not None else {})}


def _concurrent_start_case(root: Path, wheel: Path) -> dict:
def _install_late_preflight(runtime, root, phase):
if not (root / "controlled-late-start.private").exists():
return
original_preflight = runtime.preflight
def controlled_preflight(*args, **kwargs):
result = original_preflight(*args, **kwargs)
marker = root / ("race-start-b.preflight.private" if phase == "race-start-a" else "race-start-a.finished.private")
if phase == "race-start-b":
(root / "race-start-b.preflight.private").write_text("observed")
deadline = time.monotonic() + 20
while not marker.exists():
if time.monotonic() >= deadline:
raise RuntimeError("controlled late-start barrier timed out")
time.sleep(0.01)
return result
runtime.preflight = controlled_preflight


def _mark_start_finished(root, phase):
if phase == "race-start-a" and (root / "controlled-late-start.private").exists():
(root / "race-start-a.finished.private").write_text("finished")


def _validate_late_control(args, parser):
if args.late_start_control and (args.scenario != "concurrent-start" or args.phase is not None):
parser.error("late-start control requires only the concurrent-start parent case")


def _attach_single_flight(report, error, scenario):
if scenario == "concurrent-start" and hasattr(error, "single_flight_observation"):
report["failure"]["single_flight"] = error.single_flight_observation


def _concurrent_accepted(outcomes, submissions, invocations):
"""One actual winner, one exact canonical refusal, and one original effect."""
expected = {
"RuntimeServiceBusy": ("canonical runtime is busy",),
"MissionStateStoreError": (
"mission state transition CREATED -> CREATED is not permitted",
"mission state transition WAITING_FOR_EXECUTION -> CREATED is not permitted",
),
"InstalledDynamicMissionError": ("public start requires a canonically admitted zero-Action Mission",),
}
rejection = next((item for item in outcomes if item.get("result") == "REJECTED"), {})
return (sorted(item.get("result", "UNKNOWN") for item in outcomes) == ["REJECTED", "STARTED"]
and rejection.get("reason") in expected.get(rejection.get("type"), ())
and submissions == 1 and invocations == 1)


def _concurrent_observation(outcomes, submissions, invocations):
"""Closed public facts, excluding arbitrary exception messages and paths."""
from forge.state import MissionExecutionStatus
known_types = {"RuntimeServiceBusy", "MissionStateStoreError", "InstalledDynamicMissionError"}
known_reasons = {
"canonical runtime is busy": "RUNTIME_BUSY",
"public start requires a canonically admitted zero-Action Mission": "NOT_ZERO_ACTION",
**{f"mission state transition {state.value} -> CREATED is not permitted":
"TRANSITION_" + state.value + "_TO_CREATED_DENIED" for state in MissionExecutionStatus},
}
return {"outcomes": [{"result": item.get("result") if item.get("result") in {"STARTED", "REJECTED"} else "UNKNOWN",
"type": item.get("type") if item.get("type") in known_types else "OTHER",
"reason_code": known_reasons.get(item.get("reason"), "UNCLASSIFIED")}
for item in outcomes],
"submission_count": submissions, "planner_invocation_count": invocations}


def _concurrent_start_case(root: Path, wheel: Path, late_start_control: bool = False) -> dict:
"""Race two installed Forge processes at the public zero-Action start gate."""
root.mkdir()
if late_start_control:
(root / "controlled-late-start.private").write_text("fixture-only")
fixture._write(root / "artifact-a.json", {
"report": {"fields": ["report_data"]},
"policy": {"authorization_required": True},
Expand Down Expand Up @@ -2568,17 +2639,12 @@ def _concurrent_start_case(root: Path, wheel: Path) -> dict:
handle.close()
outcomes = [fixture._read(root / f"{phase}.race.private.json")
for phase in ("race-start-a", "race-start-b")]
rejection = next((item for item in outcomes if item["result"] == "REJECTED"), {})
expected_rejections = {
RuntimeServiceBusy.__name__: "canonical runtime is busy",
MissionStateStoreError.__name__: "mission state transition CREATED -> CREATED is not permitted",
InstalledDynamicMissionError.__name__: "public start requires a canonically admitted zero-Action Mission",
}
if (sorted(item["result"] for item in outcomes) != ["REJECTED", "STARTED"]
or rejection.get("reason") != expected_rejections.get(rejection.get("type"))
or len(simulator.submission_ids()) != 1
or len(fixture._read(root / "provider-inputs.private.json")) != 1):
raise RuntimeError("concurrent starters crossed the single-flight Action boundary")
if not _concurrent_accepted(outcomes, len(simulator.submission_ids()),
len(fixture._read(root / "provider-inputs.private.json"))):
error = RuntimeError("concurrent starters crossed the single-flight Action boundary")
error.single_flight_observation = _concurrent_observation(
outcomes, len(simulator.submission_ids()), len(fixture._read(root / "provider-inputs.private.json")))
raise error
submission_id = simulator.submission_ids()[0]
payload = simulator.submitted_payload(submission_id)
pending = validate_fixture(
Expand Down Expand Up @@ -2743,6 +2809,7 @@ def main() -> int:
parser.add_argument("--output-dir", required=True, type=Path)
parser.add_argument("--source-revision")
parser.add_argument("--scenario", choices=SCENARIOS)
parser.add_argument("--late-start-control", action="store_true")
parser.add_argument("--governance-case", choices=GOVERNANCE_CASES)
parser.add_argument("--preflight-case", choices=tuple(PREFLIGHT_CASES))
parser.add_argument("--provider-case", choices=PROVIDER_CASES)
Expand All @@ -2759,6 +2826,7 @@ def main() -> int:
"preflight-stage", "preflight-first", "preflight-repeat"))
parser.add_argument("--endpoint")
args = parser.parse_args()
_validate_late_control(args, parser)
if args.source_revision is not None and (
len(args.source_revision) != 40 or any(character not in "0123456789abcdef" for character in args.source_revision)
):
Expand Down Expand Up @@ -2943,7 +3011,7 @@ def main() -> int:
try:
if scenario == "concurrent-start":
summaries.append(_with_case_cleanup(
root / scenario, _concurrent_start_case, args.wheel.resolve(),
root / scenario, _concurrent_start_case, args.wheel.resolve(), args.late_start_control,
))
else:
summaries.append(_with_case_cleanup(
Expand All @@ -2957,6 +3025,7 @@ def main() -> int:
for frame in traceback.extract_tb(error.__traceback__)],
"stage_trace": fixture._read(
root / scenario / "stage-trace.public.json", [])})
_attach_single_flight(report, error, scenario)
fixture._write(root / "installed-http-successor.public.json", report)
print(json.dumps(report, sort_keys=True))
return 1
Expand Down
Loading