Skip to content
80 changes: 80 additions & 0 deletions .github/workflows/architecture-boundaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Architecture boundaries
on:
push:
branches: ['fix/architecture-boundaries-*']
pull_request:
paths:
- 'src/msg/core/**'
- 'src/msg/workers/**'
- 'tests/test_tool_runner_contract.py'
- '.github/workflows/architecture-boundaries.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: architecture-${{ github.ref }}
cancel-in-progress: true
jobs:
runner-contract:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest"
--health-interval 10s
--health-timeout 5s
--health-retries 5
valkey:
image: valkey/valkey:9.0.6
ports: ['6379:6379']
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
MSG_TEST_VALKEY_URL: redis://127.0.0.1:6379/0
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Record exact tested source
run: |
mkdir -p artifacts
python - <<'PY'
import json, os, pathlib, subprocess
def git(*args):
return subprocess.check_output(['git', *args], text=True).strip()
pathlib.Path('artifacts/source.json').write_text(json.dumps({
'commit': git('rev-parse', 'HEAD'), 'tree': git('rev-parse', 'HEAD^{tree}'),
'event': os.environ['GITHUB_EVENT_NAME'], 'run_id': os.environ['GITHUB_RUN_ID'],
}, indent=2) + '\n')
PY
- name: Install isolated test prerequisites
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age git-lfs
python -m pip install -e '.[dev]'
- name: Check runner ownership, real worker publication and safety fences
run: >-
python -m pytest tests/test_tool_runner_contract.py tests/test_tools.py
tests/test_effect_completion_fencing.py
--junitxml=artifacts/runner-contract.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: architecture-boundaries
path: artifacts/
85 changes: 85 additions & 0 deletions .github/workflows/executor-boundary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Executor boundary
on:
push:
branches: ['fix/executor-boundary-*']
pull_request:
paths:
- 'src/msg/core/**'
- 'src/msg/application.py'
- 'src/msg/plugins/batch.py'
- 'src/msg/plugins/communication.py'
- 'src/msg/extensions/ssh.py'
- 'tests/test_executor_boundary.py'
- '.github/workflows/executor-boundary.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: executor-boundary-${{ github.ref }}
cancel-in-progress: true
jobs:
assembly-and-transactions:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: msgtest
POSTGRES_PASSWORD: msgtest
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U msgtest -d postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
valkey:
image: valkey/valkey:9.0.6
ports: ['6379:6379']
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
MSG_TEST_POSTGRES_URL_TEMPLATE: postgresql://msgtest:msgtest@127.0.0.1:5432/{database}
MSG_TEST_VALKEY_URL: redis://127.0.0.1:6379/0
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.15'
allow-prereleases: true
cache: pip
- name: Record tested source
run: |
mkdir -p artifacts
python - <<'PY'
import json, os, pathlib, subprocess
def git(*args):
return subprocess.check_output(['git', *args], text=True).strip()
pathlib.Path('artifacts/source.json').write_text(json.dumps({
'commit': git('rev-parse', 'HEAD'), 'tree': git('rev-parse', 'HEAD^{tree}'),
'run_id': os.environ['GITHUB_RUN_ID'], 'event': os.environ['GITHUB_EVENT_NAME'],
}, indent=2) + '\n')
PY
- name: Install test prerequisites
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq age git-lfs
python -m pip install -e '.[dev]'
- name: Exercise assembly and unchanged security transaction contracts
run: >-
python -m pytest tests/test_executor_boundary.py tests/test_batch.py
tests/test_batch_secret_release.py tests/test_webhook_domain_events.py
tests/test_ssh_rss.py tests/test_security_transfer_rollback.py
tests/test_service.py::test_requested_write_projection_is_returned_without_readback
--junitxml=artifacts/executor-boundary.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: executor-boundary
path: artifacts/
34 changes: 34 additions & 0 deletions docs/ARCHITECTURE_PROGRESS_20260929.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Architecture issue progress — 2026-09-29

## Source and working boundary

- Repository: TokenNotIncluded/msg.lmm.best; coordinator: #83; architecture umbrella: #85.
- Starting main: `476e64f04ed1d9c7dcab4a41c4f3cdb72faad6a7`, tree `40ca207a3c49be347120e058e47ed328ec4e3098`.
- Main CI runs 36472507327 (complete contracts) and 36472507300 (recovery) were read as success. Shard-0 source artifact 10992168743 was downloaded and hash verified: `9abbc69988b8ffcb45f9931dc87e291145a7680f275bf7aadbedb7bed2189d10`. Baseline evidence does not validate later changes.
- Follow the authoritative design and valid completed archive linked by #83. Preserve historical schemas/signatures, current authorization, one transaction publisher, quarantine and lease fences. Never force-push another work stream or treat an old GREEN as the current combination.

## #160 — tool runner boundary, PR #165

Implemented on `fix/architecture-boundaries-20260929` at **f14fd1533f70983b046df4b4c813993e8a536d12**, tree **8a62150d8273a2c75775872694749cb529835966**.

The neutral `core.tool_execution` owns callable ToolRunner and local ToolResult. EffectWorker and BubblewrapRunner share the actual contract. The effects.ToolResult export remains the same type; ToolExecutor is an explicit compatibility alias, not a second unused invoke-to-ResourceRef contract. Existing worker authorization, mandatory sandbox, output and completion fences are unchanged. See `TOOL_RUNNER_BOUNDARY.md`.

- RED commit b553c59b484a4e5a383df3500ad9e4cab9d51272; run **36480982432**: **7 failed / 31 passed**. Old tools/fence tests passed.
- Focused GREEN PR run **36481370662**: downloaded artifact **10996748629**, SHA-256 `6a86edb23bac1f6463a65ca9b1118a9c54b0b25c274b81539defdc8ee0e03e9f`. Independently checked JUnit **38 tests / 0 failures / 0 errors / 0 skips**. Synthetic merge 0af4b1910bd31615e0203ae1cce5571b8a048142 has the exact implementation tree above.
- Recovery safety run **36481370672** was read as success. Full run **36481370643** still in progress at this checkpoint. No merge or closure is claimed by this note; refresh final PR/base/CI before acting.

## #156 — executor boundary

Separate stacked branch `fix/executor-boundary-20260929`. RED run **36481661656** independently verified **9 failed / 25 passed / 0 errors / 0 skips**. Implementation applied at c639a520ab69e22cccaabb07999d7cb54b8be4ca and final-head verification remains pending. See `EXECUTOR_PROGRESS_20260929.md` for exact artifacts and ownership. This removes all three categories of core-to-plugin dependency, not just one renamed import.

## Other queue and parallel work

- #157 is being handled separately by PR **#166**, metadata-session invariants versus concrete SQLite/PostgreSQL adapters. Do not duplicate or overwrite that branch.
- #158 now has independent branch `fix/client-boundary-20260929` with regression-first client/wheel/import tests at 6ea463d6fa8b256cb07d0ede2d7795a5336eda4f. Minimal packaging and actual four transport calls must be proved; no client-boundary success is claimed here.
- #159: market/plugin helper ownership remains open; preserve published order versions and sole protected ledger path.
- New #161–#163 need their own source/overlap review; do not infer they are resolved by the above module moves.
- #155 recovery head last read as 0e4dbe777053e354bcf4845b851a0c9b86ab8310 while body retained an older failure. Refresh actual diff/review/CI; do not infer success or rewrite its active branch.
- Functional umbrellas #68–#83/#85 remain open unless every remaining acceptance item has exact evidence. Do not reconstruct completed functionality from stale issue bodies.
- #64/#65/#84 need authorized production logs/configuration, real protected snapshots and field/capacity evidence. #68/#69/#70 also retain external trust/retirement/console requirements. Code CI cannot supply those facts.

No deployment, production Root/PIN, real money, outbound notification, key destruction or recovery promotion is authorized or performed. All project execution/testing in this work stream is cloud-only; local work is source inspection/editing and artifact verification.
23 changes: 23 additions & 0 deletions docs/EXECUTOR_PROGRESS_20260929.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Executor architecture progress — 2026-09-29

Refs #156/#83/#85. Branch `fix/executor-boundary-20260929`, PR #169. Its prerequisite #165 was merged as 08c58928ee3f6c6cb350d9e9fba75d072bd78f08 with exact tested tool tree 8a62150d8273a2c75775872694749cb529835966; #160 is closed. Do not overwrite #155/#166 or other work streams.

## Evidence and current state

- RED: 1953beeda08cb34570f2c56535c4e4b1249546eb, tree 28cd98e8d3cb1e54df9714efc29ea06a41107898, cloud run36481661656. Artifact10996698881 SHA-256 `0dfce2e01becb21a5660017ddc017d2ad3102cdc7cb98b99c86a42bdf1f1e8e9`; independently inspected source/JUnit: **9 failed / 25 passed / 0 errors / 0 skips**.
- Reviewed implementation patch SHA-256 `0389f11f5609abc7c54ecf67159b6aae75245d81c94611c8a4ecf7017f3ed3dc` was applied by cloud run36482413751 only to its nine enumerated source paths. Generated commit c639a520ab69e22cccaabb07999d7cb54b8be4ca/tree07ad9c86bf26b7d9e3b294200ebd9709f7e5f888. Artifact10998370091 SHA-256 `9914220c3ff71d1dcca02491a3cada0dd95e4babf06b1838f9b09298a827a4e9` matched source, paths and patch. The temporary patch publisher and patch are absent from the final tree; it did not execute project code with write credentials.
- First implementation/note head f7f8a890d91e4eda5d3da5f3c2ac4d8988e70848/treea9e18de677098798cc9f518cdd9ccb4c1a82c336: focused run36483396339 produced **2 failed / 32 passed / 0 errors / 0 skips**. Artifact10997282312 SHA-256 `d726cd26b800c6415ef7f0edc8c94f5a39f7509036fa66c8590f0cf9d79c26ca`, source/JUnit independently inspected. Recovery36483396239 and tool-boundary36483396136 succeeded; full gate was still queued. That head is **not GREEN**.
- Two precise corrections follow: the new standalone test compares the entire wire representation because models freeze nested lists/maps; the composition root uses a live module binding for Event projection rather than capturing a function with partial, retaining the existing post-load fault-injection/instrumentation contract. Existing webhook rollback assertions are untouched. This commit requires a new final-head cloud run; success is not yet claimed.

## Implemented ownership

- `core.batch.BatchPolicy` owns the single child-admission algorithm and consumes explicit bounded `PacketDecoder`. Atomic/independent batch size, idempotency and secret-delivery rules stay unchanged.
- `core.events.event_id` owns the unchanged deterministic digest; communication has a compatibility import of that exact function.
- `OperationExecutor` has explicit decoder/byte-limit, `ProjectionReader` and in-transaction `EventProjector` dependencies. It has no Application/settings backreference or business-plugin/transport implementation imports.
- `Application.make_executor` composes normal and forced-SSH execution with their respective authenticator and existing recovery/secret-release fences. It exposes finite trusted services, not a resource-configured hook engine.
- Missing decoder/projection fails closed with stable errors. Event callback failure rolls back business state, Event and request result in the existing transaction. No second writer/transaction is introduced.
- The issue_78_80 test-only harness supplies its decoder/projection explicitly. No original behavioral assertion was deleted or weakened.

## Remaining gates

Read corrected focused/full final-head artifacts, review the full diff and exact current-main merge tree, then decide #156 closure. All project execution/tests are cloud-only. No production/log/snapshot/Root/PIN/funds/outbound/recovery-promotion work was performed; production-blocked issues remain blocked.
23 changes: 23 additions & 0 deletions docs/TOOL_RUNNER_BOUNDARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Tool execution boundary

Refs #160, #82, #85. This is a module-boundary change, not a new tool authority or scheduler.

Before: EffectWorker injected an untyped callable; BubblewrapRunner imported ToolResult from EffectWorker's implementation module. The unused core ToolExecutor.invoke declaration instead claimed to return a published ResourceRef.

After:

```text
EffectWorker --> core.tool_execution.ToolRunner / ToolResult
BubblewrapRunner --> core.tool_execution.ToolResult
EffectWorker -- trusted default construction --> BubblewrapRunner
runner -- local ToolResult --> worker validation/current authority/attempt fence
worker -- existing storage transaction --> published ResourceRef
```

ToolRunner is the actual asynchronous callable port: ToolSpec, arguments, the tuple of already intersected NetworkPolicy alternatives and a per-attempt output directory. ToolResult is only a frozen local-artifact record (path, media_type, metadata). It is not proof of authorization, successful publication, or exactly-once execution. Runtime validation, mandatory isolation, output bounds, quarantine, current credential/certificate checks and attempt/state/deadline fences remain unchanged in their existing owners.

EffectWorker's constructor and BubblewrapRunner's callable now share explicit input/output annotations; the assembly tests compare the annotations and exercise a correctly injected runner in real PostgreSQL. Returning an existing ResourceRef instead of a local artifact cannot bypass the publisher; the attempt becomes uncertain and no output resource is created.

Compatibility: `msg.workers.effects.ToolResult` still exports the exact same class, not a copy or adapter. `msg.core.contracts.ToolExecutor` remains an import alias for ToolRunner. Its unused `invoke(...) -> ResourceRef` declaration is retired; no in-repository implementation or caller used it. This preserves the old import spelling, not that never-implemented invocation interface. New callers should use ToolRunner. No SMTP/Webhook/Git backend is made to implement this tool-specific port.

Evidence lives in `tests/test_tool_runner_contract.py`, existing `tests/test_tools.py` and `tests/test_effect_completion_fencing.py`, the focused cloud workflow and the full CI gate. A missing-bwrap refusal test is not a claim of successful real bubblewrap or production acceptance.
32 changes: 29 additions & 3 deletions src/msg/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,41 @@ async def load(self):
self.authenticator=AuthenticationService(self.registry,self.certificates,self.settings.service_url,self.clock,
self.primary_ceiling,self.temporary_ceiling)
self.authorizer=AuthorizationService(self.registry,self.certificates)
self.executor=OperationExecutor(self.registry,self.metadata,self.contents,self.authenticator,self.authorizer,
self.clock,self.receipt_signer)
self.executor.application=self
self.executor=self.make_executor()
self.executor.recovery_drill_marker=marker
self.executor.recovery_quarantined=quarantined
self.executor.response_hook=self._secrets_for_caller
self._loaded=True
return self

def make_executor(self, *, authenticator=None):
"""Compose the same finite services for network and SSH execution."""
from msg.plugins.discovery import read_projection
from msg.plugins import communication
from msg.transports.packet import decode_packet

async def projection(context, request, tx, ref):
return await read_projection(self,context,request,tx,ref.id,
revision=ref.revision,fields=request.return_fields)

async def project_event(tx, event):
# Keep the installed adapter's live binding for instrumentation and
# fault injection; the executor still knows only its narrow port.
await communication.enqueue_domain_webhooks(self,tx,event)

executor=OperationExecutor(self.registry,self.metadata,self.contents,
self.authenticator if authenticator is None else authenticator,
self.authorizer,self.clock,self.receipt_signer,
max_request_bytes=self.settings.server.limits.max_request_bytes,
packet_decoder=decode_packet,projection_reader=projection,
event_projector=project_event)
executor.response_hook=self._secrets_for_caller
executor.recovery_drill_marker=self.settings.config_dir/'recovery-drill.json'
if self.executor is not None:
executor.recovery_drill_marker=self.executor.recovery_drill_marker
executor.recovery_quarantined=self.executor.recovery_quarantined
return executor

async def online_issuer(self,tx):
issuer=tx.setting('online_ca_certificate')
require(issuer is not None,'issuer_not_ready')
Expand Down
34 changes: 34 additions & 0 deletions src/msg/core/batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""One bounded child-admission policy for both named batch primitives."""
from __future__ import annotations

from .codec import canonical
from .errors import require
from .execution_ports import PacketDecoder
from .requests import SECRET_DELIVERY_MIN_VERSION

# Response-only credentials require their own non-batched delivery call.
NO_BATCH = frozenset(SECRET_DELIVERY_MIN_VERSION) | {'identity.register', 'identity.upgrade'}


class BatchPolicy:
def __init__(self, registry, decoder: PacketDecoder | None, max_request_bytes: int):
self.registry = registry
self.decoder = decoder
self.max_request_bytes = max_request_bytes

def packets(self, request, subject, *, bounded=True):
require(self.decoder is not None, 'batch_decoder_not_configured')
# Independent batches historically use the validated parent envelope size.
max_bytes = self.max_request_bytes if bounded else len(canonical(request))
values = []
for value in request.arguments['requests']:
packet = self.decoder(value, max_bytes)
spec = self.registry.operation(packet.operation, packet.contract_version)
require(spec.effect in {'read', 'transaction'} and not packet.operation.startswith('batch.')
and packet.operation not in NO_BATCH and 'network' in spec.entries,
'operation_not_batchable')
require(packet.subject == subject, 'batch_subject_mismatch')
values.append(packet)
require(len({p.request_id for p in values}) == len(values), 'duplicate_batch_request_id')
require(request.request_id not in {p.request_id for p in values}, 'recursive_request_id')
return values
Loading
Loading