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
14 changes: 9 additions & 5 deletions docker/bake-domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path
Expand Down Expand Up @@ -55,11 +54,16 @@ def main() -> int:
check=True,
)
baked.append(name)
print(f" baked {name} ({repo}) @ {Path(dest + '.commit').read_text().strip()[:12]}",
flush=True)
print(
f" baked {name} ({repo}) @ {Path(dest + '.commit').read_text().strip()[:12]}",
flush=True,
)
except subprocess.CalledProcessError as e:
print(f" WARN: could not bake {name} ({repo}): {e} — falls back to bind-mount",
file=sys.stderr, flush=True)
print(
f" WARN: could not bake {name} ({repo}): {e} — falls back to bind-mount",
file=sys.stderr,
flush=True,
)

existing = BAKED_LIST.read_text().split() if BAKED_LIST.exists() else []
all_baked = sorted(set(existing) | set(baked))
Expand Down
4 changes: 3 additions & 1 deletion facetwork/_pkg_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ def _event_facet_namespaces(node: dict, prefix: str = "") -> set[str]:
here = prefix.rstrip(".")
for decl in node.get("declarations", []):
t = decl.get("type", "")
if t in ("EventFacetDecl", "EventFacet") or (t.startswith("Facet") and decl.get("is_event")):
if t in ("EventFacetDecl", "EventFacet") or (
t.startswith("Facet") and decl.get("is_event")
):
if here:
out.add(here)
elif t in ("Namespace", "NamespaceDecl"):
Expand Down
2 changes: 1 addition & 1 deletion facetwork/dashboard/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async def dispatch(self, request: Request, call_next):
return await call_next(request) # auth disabled — unchanged behavior

presented = self._presented_token(request)
valid = bool(presented) and hmac.compare_digest(presented, token)
valid = presented is not None and hmac.compare_digest(presented, token)

if request.method.upper() in _MUTATING_METHODS and not valid:
return JSONResponse(
Expand Down
4 changes: 1 addition & 3 deletions facetwork/dashboard/routes/v3/infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ def infra_page(request: Request):
minio_url = os.environ.get("FW_MINIO_CONSOLE_URL", "").strip() or _host_url(
request, _MINIO_CONSOLE_PORT
)
mongo_url = os.environ.get("FW_MONGO_UI_URL", "").strip() or _host_url(
request, _MONGO_UI_PORT
)
mongo_url = os.environ.get("FW_MONGO_UI_URL", "").strip() or _host_url(request, _MONGO_UI_PORT)

services = [
{
Expand Down
14 changes: 10 additions & 4 deletions facetwork/migration/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ def main(argv: list[str] | None = None) -> int:
print(f" L{ln}: {note}")
if not args.write:
for line in difflib.unified_diff(
src.splitlines(), res.source.splitlines(), lineterm="", n=1,
fromfile=f, tofile=f + " (migrated)",
src.splitlines(),
res.source.splitlines(),
lineterm="",
n=1,
fromfile=f,
tofile=f + " (migrated)",
):
print(" " + line)
else:
Expand All @@ -86,8 +90,10 @@ def main(argv: list[str] | None = None) -> int:
verb = "wrote" if args.write else "would change"
print(f"\n{verb} {changed} file(s); {manual_total} manual site(s) across {len(files)} scanned.")
if manual_total and not args.write:
print("Manual sites are sibling-block `andThen when` — reattach the when to the "
"step it gates and rewrite its refs to $.")
print(
"Manual sites are sibling-block `andThen when` — reattach the when to the "
"step it gates and rewrite its refs to $."
)
return 0


Expand Down
42 changes: 28 additions & 14 deletions facetwork/migration/relative_scope_migrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,7 @@ def _frame_depth_named(self, step_name: str) -> int | None:
return depth
return None

def _rewrite_ref(
self, ref: Reference, block_steps: set[str], loop_vars: set[str]
) -> None:
def _rewrite_ref(self, ref: Reference, block_steps: set[str], loop_vars: set[str]) -> None:
loc = ref.location
if loc is None or loc.end_line is None or loc.end_column is None:
return
Expand All @@ -144,8 +142,14 @@ def _rewrite_ref(
return # already correct, or unknown → leave for manual review
new = "$" * (depth + 1) + "." + ".".join(ref.path)
self.edits.append(
_Edit(loc.line, loc.column, loc.end_line, loc.end_column, new,
f"$.{head} → {'$' * (depth + 1)}.{head} (outer container)")
_Edit(
loc.line,
loc.column,
loc.end_line,
loc.end_column,
new,
f"$.{head} → {'$' * (depth + 1)}.{head} (outer container)",
)
)
else:
if not ref.path:
Expand All @@ -159,15 +163,27 @@ def _rewrite_ref(
# (REF_CROSS_BLOCK_STEP, e.g. sibling `andThen when`). Structural;
# report for manual handling.
self.manual.append(
_Edit(loc.line, loc.column, loc.end_line, loc.end_column, "",
f"'{step_name}.…' references a sibling block — reattach "
f"the when/clause to the step (manual)")
_Edit(
loc.line,
loc.column,
loc.end_line,
loc.end_column,
"",
f"'{step_name}.…' references a sibling block — reattach "
f"the when/clause to the step (manual)",
)
)
return
new = "$" * (depth + 1) + ("." + ".".join(ref.path[1:]) if len(ref.path) > 1 else "")
self.edits.append(
_Edit(loc.line, loc.column, loc.end_line, loc.end_column, new,
f"{step_name}.… → {'$' * (depth + 1)}.… (enclosing step by name)")
_Edit(
loc.line,
loc.column,
loc.end_line,
loc.end_column,
new,
f"{step_name}.… → {'$' * (depth + 1)}.… (enclosing step by name)",
)
)

# -- expression / node traversal ----------------------------------------
Expand Down Expand Up @@ -206,9 +222,7 @@ def _step_attrs(self, call: Any) -> set[str]:
name = call.name
return self._attrs.get(name) or self._attrs.get(name.split(".")[-1]) or set()

def _walk_and_then_block(
self, body: AndThenBlock, loop_vars: set[str]
) -> None:
def _walk_and_then_block(self, body: AndThenBlock, loop_vars: set[str]) -> None:
if not isinstance(body, AndThenBlock):
return # PromptBlock / ScriptBlock facet bodies carry no step refs
if body.script:
Expand Down Expand Up @@ -270,7 +284,7 @@ def walk_decl(self, decl: Any) -> None:
if body is None:
return
self._stack = [_Frame(name=decl.sig.name, attrs=_sig_attrs(decl.sig))]
self._current_block_steps = set()
self._current_block_steps: set[str] = set()
blocks = body if isinstance(body, list) else [body]
for blk in blocks:
self._walk_and_then_block(blk, set())
Expand Down
4 changes: 1 addition & 3 deletions facetwork/runtime/base_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,7 @@ def _update_runner_terminal_state(self, workflow_id: str, status: str) -> None:
try:
now = _current_time_ms()
target_state = (
RunnerState.COMPLETED
if status == ExecutionStatus.COMPLETED
else RunnerState.FAILED
RunnerState.COMPLETED if status == ExecutionStatus.COMPLETED else RunnerState.FAILED
)
if target_state == RunnerState.COMPLETED and self._has_non_terminal_tasks(workflow_id):
logger.warning(
Expand Down
4 changes: 1 addition & 3 deletions facetwork/runtime/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ def preload(self, *, verify: bool = False) -> int:
# must not pay to `find_spec` (and, for cross-domain deps, import)
# the whole 500+-registration handler universe just to keep its own
# ~few dozen. Non-matching registrations are simply not ours.
if self._topics and not any(
fnmatch.fnmatch(reg.facet_name, t) for t in self._topics
):
if self._topics and not any(fnmatch.fnmatch(reg.facet_name, t) for t in self._topics):
continue
if verify and not registration_module_available(reg.module_uri):
skipped.append(reg.facet_name)
Expand Down
2 changes: 1 addition & 1 deletion facetwork/runtime/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def _find_step(self, step_id: StepId | BlockId) -> StepDefinition | None:
# Check persistence
return self.persistence.get_step(step_id)

def _find_statement_body(self, step: StepDefinition) -> dict | None:
def _find_statement_body(self, step: StepDefinition) -> dict | list | None:
"""Find the inline andThen body for a step's statement.

Looks up the step's statement AST node in the containing block's
Expand Down
8 changes: 2 additions & 6 deletions facetwork/runtime/handlers/block_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str):
)

scope_stack = (
build_scope_stack(self.context, self.step)
if relative_scoping_enabled()
else None
build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None
)
eval_ctx = EvaluationContext(
inputs=inputs,
Expand Down Expand Up @@ -304,9 +302,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str):
)

scope_stack = (
build_scope_stack(self.context, self.step)
if relative_scoping_enabled()
else None
build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None
)
eval_ctx = EvaluationContext(
inputs=inputs,
Expand Down
4 changes: 1 addition & 3 deletions facetwork/runtime/handlers/catch_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,7 @@ def get_mixin_step_by_alias(parent_step_id: str, alias: str):
)

scope_stack = (
build_scope_stack(self.context, self.step)
if relative_scoping_enabled()
else None
build_scope_stack(self.context, self.step) if relative_scoping_enabled() else None
)
eval_ctx = EvaluationContext(
inputs=inputs,
Expand Down
4 changes: 1 addition & 3 deletions facetwork/runtime/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,9 +850,7 @@ def update_server_ping(self, server_id: str, ping_time: int) -> None:
ping_time: The new ping time in milliseconds
"""

def update_server_handled(
self, server_id: str, handled: "list[HandledCount]"
) -> None:
def update_server_handled(self, server_id: str, handled: "list[HandledCount]") -> None:
"""Replace ONLY a server's ``handled`` stats (a targeted field write).

Unlike ``save_server`` this must not read-modify-write the whole server
Expand Down
4 changes: 4 additions & 0 deletions facetwork/runtime/registry_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class RegistryRunner(BaseRunner):
``(module_uri, checksum)`` for efficiency.
"""

# Narrow the inherited BaseRunner._config to this runner's config subtype
# so its registry-refresh field is visible to the type checker.
_config: RegistryRunnerConfig

def __init__(
self,
persistence: PersistenceAPI,
Expand Down
8 changes: 5 additions & 3 deletions facetwork/runtime/runner/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ class RunnerService(BaseRunner):
and resumes workflows via the Evaluator.
"""

# Narrow the inherited BaseRunner._config to this runner's config subtype
# so its HTTP-server / shutdown fields are visible to the type checker.
_config: RunnerConfig

def __init__(
self,
persistence: PersistenceAPI,
Expand Down Expand Up @@ -709,9 +713,7 @@ def _reconcile_with_db(self) -> None:
self._release_timed_out_task(task_id)
reset += 1
if reset:
logger.warning(
"Reconciliation: reset %d orphaned DB task(s) to pending", reset
)
logger.warning("Reconciliation: reset %d orphaned DB task(s) to pending", reset)

# =========================================================================
# Polling
Expand Down
12 changes: 3 additions & 9 deletions facetwork/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,9 +1156,7 @@ def _validate_and_then_block_impl(
# (Under relative scoping the resolver checks `$.` collections too — the
# step frame's surface is fully known, and an `open` base frame stays
# lenient about pre-script keys, so no false-positive.)
if body.foreach and (
self._relative_scoping or not body.foreach.iterable.is_input
):
if body.foreach and (self._relative_scoping or not body.foreach.iterable.is_input):
self._validate_reference(
body.foreach.iterable,
input_attrs,
Expand Down Expand Up @@ -1263,9 +1261,7 @@ def _validate_and_then_block_impl(
if step.catch:
step_attrs = step_attrs | {"error", "error_type"}
self._container_stack.append(
_ContainerFrame(
name=step.name, kind="step", attrs=step_attrs, open=False
)
_ContainerFrame(name=step.name, kind="step", attrs=step_attrs, open=False)
)
step_frame_pushed = True
try:
Expand All @@ -1276,9 +1272,7 @@ def _validate_and_then_block_impl(
step_clauses = ([step.body] if step.body else []) + list(step.extra_bodies)
if step_clauses:
step_target = step.call.name.split(".")[-1]
clause_step_names = [
self._collect_block_step_names(c) for c in step_clauses
]
clause_step_names = [self._collect_block_step_names(c) for c in step_clauses]
for i, clause in enumerate(step_clauses):
co_clause_steps: set[str] = set()
for j, names in enumerate(clause_step_names):
Expand Down
1 change: 1 addition & 0 deletions tests/dashboard/test_auth_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
requests (POST/PUT/PATCH/DELETE) require it; reads stay open; a query-param token
is persisted as a cookie for the browser UI.
"""

from __future__ import annotations

import pytest
Expand Down
1 change: 1 addition & 0 deletions tests/dashboard/test_rerun_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Regression for the old start_time heuristic that deleted every sibling which
merely *started* after the target — destroying unrelated parallel-block work.
"""

from facetwork.dashboard.routes.execution.steps import _find_downstream_by_name


Expand Down
26 changes: 19 additions & 7 deletions tests/dashboard/test_step_recovery_force.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,29 @@ def _seed(self, store):
)
compiled_ast = {
"steps": [
{"type": "Step", "id": "stmt-up", "name": "up",
"call": {"target": "t.Upstream", "args": [], "mixins": []}},
{"type": "Step", "id": "stmt-dn", "name": "dn",
"call": {"target": "t.Downstream",
"args": [{"name": "v", "value": {"type": "StepRef", "path": ["up"]}}],
"mixins": []}},
{
"type": "Step",
"id": "stmt-up",
"name": "up",
"call": {"target": "t.Upstream", "args": [], "mixins": []},
},
{
"type": "Step",
"id": "stmt-dn",
"name": "dn",
"call": {
"target": "t.Downstream",
"args": [{"name": "v", "value": {"type": "StepRef", "path": ["up"]}}],
"mixins": [],
},
},
],
"yield": None,
}
store.save_runner(
RunnerDefinition(uuid="wf-1", workflow_id="wf-1", workflow=wf, compiled_ast=compiled_ast)
RunnerDefinition(
uuid="wf-1", workflow_id="wf-1", workflow=wf, compiled_ast=compiled_ast
)
)
return upstream, downstream

Expand Down
19 changes: 10 additions & 9 deletions tests/mcp/test_postgis_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
writable CTEs, and `pg_sleep` DoS — while keeping legitimate SELECTs (incl.
semicolons/keywords inside string literals) allowed.
"""

import pytest

from facetwork.mcp.server import _reject_reason
Expand All @@ -31,18 +32,18 @@ def test_allows_read_queries(sql):
@pytest.mark.parametrize(
"sql",
[
"SELECT set_config('transaction_read_only','off',false)", # setting escape
"SELECT 1; DROP TABLE osm_nodes", # multi-statement
"SELECT 1 INTO public.pwned", # SELECT INTO create
"SELECT set_config('transaction_read_only','off',false)", # setting escape
"SELECT 1; DROP TABLE osm_nodes", # multi-statement
"SELECT 1 INTO public.pwned", # SELECT INTO create
"SELECT * INTO pwned FROM osm_nodes",
"WITH x AS (DELETE FROM osm_nodes RETURNING *) SELECT * FROM x", # writable CTE
"DELETE FROM osm_nodes", # not SELECT/WITH
"WITH x AS (DELETE FROM osm_nodes RETURNING *) SELECT * FROM x", # writable CTE
"DELETE FROM osm_nodes", # not SELECT/WITH
"UPDATE osm_nodes SET tags = '{}'",
"SET default_transaction_read_only = off", # top-level SET
"SELECT pg_sleep(60)", # DoS
"SELECT pg_read_file('/etc/passwd')", # file read
"SET default_transaction_read_only = off", # top-level SET
"SELECT pg_sleep(60)", # DoS
"SELECT pg_read_file('/etc/passwd')", # file read
"COPY osm_nodes TO '/tmp/x'",
" ", # empty-ish → not a read query
" ", # empty-ish → not a read query
],
)
def test_rejects_writes_and_bypasses(sql):
Expand Down
Loading
Loading