feat(runtime): multi-container v1 completion (M8 consolidation)#511
Merged
Conversation
#485) * refactor(runtime): decouple _amend_trial_metrics from log_capture (#338 stage 1) * feat(runtime): minimal trial bundle for provision-failed trials (#338 stage 2) When ProvisioningTrialExecutor catches a ProvisionError, write a minimal per-trial bundle (trajectory.yaml + metrics.yaml + grade.yaml) to <output_dir>/trials/<task>/<idx>/ by reusing the run's TrialArtifactWriter, then amend metrics.yaml with top-level error: provision_error + error_reason via the shared _amend_trial_metrics path. Bundle-write failure is best-effort and never masks the synthesized failed TrialResult.
* feat(manifest): endpoint-resolution override fields on StackPatch + EnvironmentManifest (#144 stage 1) * feat(compose): read endpoint overrides from EnvironmentManifest in resolve_env_endpoints (#144 stage 2) * docs(manifest): endpoint-resolution override fields — ADR-0009 + guide + CHANGELOG (#144 stage 3)
…on (#324) (#497) Record the Option-B decision that the built-in EngineStack path (Case A) is full_internet by construction and carries no network_policy surface: runner-net is a non-internal bridge and the runner retains LLM-provider egress for in-container LLM-as-judge grading. Only task-declared stacks (Case B/C) have an enforceable network_policy. - ADR-0018: new "Case A network posture" subsection under network-policy enforcement + repaired Case A forward-reference + dated amendment. - RUNTIME_BACKENDS.md: new "Network posture" section. - SECURITY.md: rewrite architecture overview, threat table, testing, and checklist to describe the actual runner-net (non-internal, docker-py EngineStack) model, replacing the vanished env-net / docker-compose.yaml description and the false "addressed by env-net internal:true" threat. - tests/unit/test_network_internal.py: lock the Network.internal docker-py round-trip and the EngineStack.create_networks non-internal invariant.
…idecar) (#323) (#502) * feat(manifest): limited_internet_allowlist schema + validation (#323 stage 1) * feat(runtime): limited_internet squid-sidecar enforcement transform (#323 stage 2) * test(runtime): real-docker limited_internet enforcement + NetworkPolicyError removal (#323 stage 3) * docs(runtime): limited_internet shipped — ADR-0018 + MULTI_CONTAINER_GUIDE + CHANGELOG (#323 stage 4) * fix(runtime): squid Safe_ports restriction on limited_internet proxy (#323) Restrict non-CONNECT proxied requests to ports 80 and 443 so plain-HTTP GETs to odd ports on allowlisted hosts default-deny.
Contributor
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Capability completion on the shipped multi-container substrate. Three interlocking arcs — environment-model follow-ups, observability v1, and network-policy completion — brought to their stated ship condition in a single milestone (the arcs converge at the same manifest + runtime substrate, so batching them removed cross-arc breakage windows). Ten PRs across nine sub-issues shipped: eight code+doc PRs merged into the integration branch, one issue partial-closed with a reframe + two prereqs filed, one umbrella decomposed into six workload-gated sub-issues. Additive on every compatibility surface —
metrics.yamlgainsprovisioning_duration_s,aggregate.jsongainscaptured_service_logs,EnvironmentManifest.stackgains five endpoint-override fields + alimited_internet_allowlist— all default-safe for existing packs.network_policybecomes fully three-valued (full_internet/no_internet/limited_internet) with real enforcement across every stack shape the harness materialises. Provision-failed trials now produce a minimal trial bundle so cost-aggregation and post-mortem tooling see a consistent shape.docs/SECURITY.md— actively wrong before this milestone (described a vanishedenv-net (internal:true)) — is rewritten to match the realrunner-netposture. Closes #146, #354, #339, #338, #337, #144, #324, #323, #164 (reframed), #145 (decomposed), #373 (umbrella), #374 (umbrella).Impact on existing tasks — read this first
Near-term (today):
stack.runner_port/db_service/db_port/rag_service/rag_port,stack.limited_internet_allowlist) default toNone, and their absence reproduces prior behaviour exactly.metrics.yamlgains a top-levelprovisioning_duration_s: floaton every successful trial. Additive — consumers reading unknown keys are unaffected. Noschema_versionbump.aggregate.jsongains acaptured_service_logssub-object onRunAggregate. Always emitted (zero envelope on clean runs); noschema_versionbump.<output_dir>/trials/<task>/<idx>/directory containingtrajectory.yaml+metrics.yaml(witherror: "provision_error") +grade.yaml. Prior behaviour: no directory at all. Cost-aggregation, post-mortem tooling, andtolokaforge statusnow see a consistent trial-directory shape whether the trial completed or failed to provision.SharedStackRuntimeBackendrun-level materialise failures now write<output_dir>/services/<name>.log+_capture.yamlbefore teardown. Prior behaviour: zero logs on run-level materialise failure.Longer-term commitments:
stack.limited_internet_allowlistschema is a compatibility surface: task packs declaringnetwork_policy: limited_internetMUST carry a non-empty allowlist; other policies MUST omit it. Enforced at manifest LOAD with entry-named errors. Documented indocs/PROJECTS.md+ ADR-0018.network_policyenum stays closed (full_internet/no_internet/limited_internet). No new values added; the three-value contract is now fully honoured across every stack shape._capture.yaml.capture_reasonvalues areprovision_error/materialise_error/graded_failure(lowercase enum); rollup category enum isServiceLogCaptureSource=provision_failure/trial_body/shared_stack_materialise. Task authors debugging withrg 'provision_error'still find every relevant artefact in one query; the rollup's category-level enum is one abstraction above the on-disk reason so it stays stable if a future producer varies itscapture_reasontext.Safety guards, still in force:
SecurityContext+ resource-cap defaults from earlier milestones; runner-net non-internal by construction (Case AEngineStack).edge-netdirectly under all network policies — LLM-provider egress forllm_judgegrading is preserved; the allowlist governs task-service egress only.Follow-up tickets filed and left for later:
metrics.yaml(blocks the future per_trial cost/latency study).AggregateMetricsextra="forbid"vs percentile-key drift, surfaced by runtime(observability): surface captured service logs in run-level aggregate report #337's wire-path test.Design walkthrough
flowchart LR subgraph M8["Milestone 8 — Multi-container v1 completion"] direction TB subgraph EnvModel["Environment-model follow-ups"] direction TB e144["#144: 5 endpoint-override fields<br/>on stack: (runner_port / db_service / db_port /<br/>rag_service / rag_port). Fail-loud unknown-service<br/>validation at manifest LOAD."] e146["#146: task-pack image-layering guide<br/>(SWE-bench 3-tier: base → environment → instance).<br/>Docs-only, cache-friendliness rules."] end subgraph Observability["Observability v1 (umbrella #373)"] direction TB o354["#354: provisioning_duration_s<br/>time.monotonic() around provision → await_ready → endpoints.<br/>Additive to metrics.yaml."] o338["#338: provision-fail trial bundle<br/>ProvisionError writes trajectory.yaml + metrics.yaml +<br/>grade.yaml via existing TrialArtifactWriter."] o339["#339: shared-stack materialise capture<br/>Per-service logs to output_dir/services/*.log +<br/>_capture.yaml on materialise-error branch."] o337["#337: aggregate.json rollup<br/>CapturedServiceLogsRollup across all 3 surfaces.<br/>Category enum: provision_failure / trial_body / shared_stack_materialise."] end subgraph NetPolicy["Network policy completion (umbrella #374)"] direction TB n324["#324: Case A EngineStack decision<br/>Full_internet by construction (no untrusted task code<br/>on runner-net). ADR-0018 amended;<br/>SECURITY.md rewritten."] n323["#323: limited_internet enforcement<br/>squid forward-proxy sidecar with dstdomain ACL,<br/>Safe_ports 80/443, HTTP(S)_PROXY per app service,<br/>digest-pinned image."] end subgraph Meta["Meta / scope discipline"] direction TB m164["#164: DISCOVERY-BLOCKER + reframe<br/>Not code-shippable — $12-30 real spend + interpretation.<br/>Filed #503, #504 as prereqs; ADR-0016 stays load-bearing."] m145["#145: umbrella decomposed<br/>6 workload-gated sub-issues (#505-#510).<br/>None gate M8 per the umbrella's own framing."] end end e144 -.->|manifest surface| Observability e144 -.->|manifest surface| NetPolicy o354 -.->|informs future study prereqs| m164 o337 -.->|informs future study prereqs| m164 n324 -.->|shared runner-net invariant| n323Key design choices
time.monotonic()for duration measurement, nottime.time()(#354).provisioning_duration_s/error/captured_service_logs(#354, #338).Metrics(Pydanticextra="forbid") is written by the conductor insiderun(). Adding fields there would force a default + overwrite double-write path. Post-run()read-add-write via_amend_trial_metricsis a single durable path; both existing readers useyaml.safe_load+.get(), not model round-trip.metrics.yaml, not schema break (#354, #338).schema_versionbump; consumers reading unknown keys work unchanged. Same posture as prior milestone-12 additions._amend_trial_metrics(task_id, trial_idx, updates)helper covers every amendment site (#354).provisioning_duration_s(new) andcaptured_service_logs(from #418) route through one helper; old_amend_metrics_with_captured_logsdeleted (no shim). Future per-trial metrics land in the same seam..log+_capture.yamlshape with distinctcapture_reasonvalues (provision_error/graded_failure/materialise_error). One capture pattern, three trigger sites.<output_dir>/services/mirrors<trial_dir>/services/, one level up (#339).trials/when the failure is at the run level (before any trial ran). Location signals scope.Orchestrator._construct_runtime_backendwas forwardinglog_capturetoPerTrialRuntimeBackendbut not toSharedStackRuntimeBackend(env-manifest branch). One-line forward closes it; backend attribute defaults toNone→ silent no-op when unconfigured.TrialArtifactWriter— no schema duplication for the provision-fail bundle (#338).provision_error/materialise_error/graded_failure) shared between_capture.yaml.capture_reason,metrics.yaml.error, andtrajectory.yaml.termination_reason. Task authors debugging withrg 'provision_error'find every relevant artefact in one query.log_captureparam, keys onoutput_dir(invariant:LogCaptureConfig.output_root == output_dir). Stage 2 is the feature. Reviewability: the diff of the feature commit isolates the actual behavioural change.capture_reasonstring (#337).ServiceLogCaptureSourcevalues areprovision_failure/trial_body/shared_stack_materialise— semantic categories one abstraction above on-diskcapture_reason. Keeps rollup classification stable if a future producer varies itscapture_reasontext; per-entry provenance still carries the exact literal._capture.yaml; common path ismetrics.yaml.captured_service_logs. Rolling up only the named surface would miss most captures. Architect surfaced the widening before implementation.captures: 0) from "pre-feature file" (field absent). Consumers can rely on the field's presence to gate their reader._capture.yaml/metrics.yamllog-and-skip; a corrupt artefact never breaks aggregate generation. Same posture as_collect_existing_cost.endpoint_mapdict (#144).resolve_env_endpoints' kwargs, so Stage 2 was "stop hardcoding, read the manifest."None= convention lookup; explicit non-None = override (#144).runner_portstays a requiredint(default 50051) — noNonestate because the runner always resolves; a bad port fails loud at provision._check_endpoint_services_declaredvalidator checksdb_service/rag_serviceagainstload_compose()["services"]keys.db_service: "nonexistent"raisesValidationErrorlisting the compose's actual services. Not deferred to runtime.full_internetby construction, not by config (#324).llm_judgein-container grading. Threadingnetwork_policyonto the built-in path would add a knob whose safe value is the only value.Network.internal=Trueprimitive kept as faithful docker-py serialiser (#324).Network's parity with docker-py's API.docs/SECURITY.mdrewritten in full — Rule 8 (doc-freshness) (#324).env-net/docker-compose.yamldeleted; threat table refreshed with the actual mechanisms.dstdomainmaps safely to exact +*.-suffix ACLs; tinyproxy'sFilterregex is an unanchored-substring footgun where an allowlist entryopenai.comwould matchevil-openai.com.attacker.net. Correctness of the allowlist control beats image size.Safe_ports 80 443restriction on non-CONNECT methods (#323).GET http://allowlisted:8080/was forwarded becausedstdomaindoesn't constrain the port on non-CONNECT paths.acl Safe_ports port 80 443+http_access deny !Safe_portsbefore the allow chain — plan's stated intent, missed on first cut, caught in review.no_internet— runner egress is engine-scope (LLM providers forllm_judge), independent of the task's allowlist. The allowlist governs task-service egress only.ubuntu/squid@sha256:6a097f68…— not a floating tag. Immutable across pulls per the multi-container image-pinning contract.returncode != 0(transport failure) (#323).no_internettest pattern (returncode != 0) would silently under-test here. Integration test asserts on%{http_code}explicitly.verify_network_policy_supported(interim fail-loud shim from #301) andNetworkPolicyError(only referenced by the pre-rewrite test) both deleted. Contradiction removal — those code paths are unreachable once enforcement lands.limited_internet_allowlist(#323).limited_internetrequires non-empty allowlist; other policies forbid any allowlist. Per-entry validator rejects schemes / ports / paths / IPs / multi-star / dupes with entry-named errors.--cache_levelknob; we deliberately don't. Docker's content-addressable layer cache handles most of the work automatically when task-pack authors pin images by digest/immutable tag. The pinned-image contract we already ship is the cache-key contract we need.Industry precedents
--cache_levelharness knob — Docker's content-addressable layer cache with pinned-image digests is the cache-key contract we already ship.dstdomainvs tinyproxyFilter(feat(runtime): limited_internet egress allowlist enforcement (proxy sidecar) #323). squid'sdstdomainACL matches domains exactly or via*.-suffix wildcards, both anchored — the safe, documented model for allowlist enforcement. tinyproxy'sFilteraccepts unanchored regexes, where an allowlist entryopenai.comwould matchevil-openai.com.attacker.net. Chose squid over the smaller-image tinyproxy on this correctness ground alone.Safe_portshardening (feat(runtime): limited_internet egress allowlist enforcement (proxy sidecar) #323 post-review). Standard squid deployment pattern — restrict non-CONNECT methods to canonical HTTP(S) ports before the allow chain. Reviewer flagged the omission; addedacl Safe_ports port 80 443+http_access deny !Safe_ports.Suggested review order
bc784e4.provisioning_duration_smetric (introduces the_amend_trial_metricsseam that later issues extend) —0c91c9c.6f3542a.0e42b36.captured_service_logsrollup inaggregate.json(three-commit series consuming all three surfaces from feat(runtime): per-service log capture on trial failure (Multi-container 4/5) #302/Per-service Docker logs not captured on grade-fail (only on provision/trial-body fail) #418/runtime(observability): capture shared-stack compose logs on run-level materialise failure #339) —8ab1191.EnvironmentManifest.stack(three-commit series: schema → wire → docs) —3a20d1d.EngineStackfull_internet decision (design-recording PR; runtime code diff empty;docs/SECURITY.mdfull rewrite) —9f131ab.limited_internetegress-proxy sidecar (largest single item; 4 stages + 1 post-review Safe_ports hardening) —4e245c4.Verification
Per-PR CI: each of the eight PRs shipped with
lint+test-smokelanes green on the branch (and consistent withmain) — those two lanes were the actual merge gates.hygiene-reviewis known-broken on every PR from this session (tracked as infra-side #425); its per-PR failure was not a merge gate on any of the eight.Behaviour-locking test tier per PR (as required by AGENTS.md — never mocks):
provisioning_duration_sfield is stamped on every successful trial.<output_dir>/services/*.log+_capture.yamlare written before teardown.metrics.yamlcleanly (as0.0, not skipped).CapturedServiceLogsRollupstructure across all three source enums; integration path verifies wire from_generate_reports.EngineStack.create_networks()always creates non-internal networks; theNetwork.internal=Truedocker-py serialisation round-trips.%{http_code}(403 vs 200), notreturncode; loads a reallimited_internet_allowlistthrough manifest LOAD → materialisation → provisioning.Post-merge validation run on
feat/multi-container-v1-completion:make docker-statusclean (stack was warmed once at Step 2.5 and kept warm across the milestone); targetedrun_testsagainst the affected markers on every merge; no LLM-layer changes so no capability tests were needed.Rebase: single CHANGELOG conflict on rebase (main cut
v0.9.1empty-release-marker after the integration branch started); resolved by placing the branch's## Unreleasedsection above the release marker. Subsequent 7 commits applied cleanly.Deliberately skipped: no
test-integration-fullor LLM-scale end-to-end run against real providers — the eight PRs are all substrate + observability + docs, none touching model dispatch. The eventual#164-reframed measurement study (once #503 + #504 land) is the load-bearing end-to-end validation for the multi-container substrate; running it now would be premature.What's next
Immediate follow-ups filed and left for later: #503 (per-role token/cost accounting in
metrics.yaml) + #504 (per-stage wall-clock brackets) unblock the reframed #164 measurement study — those three together will produce the interpreted cost/latency comparison the original #164 asked for. Six workload-gated sub-issues (#505-#510) decomposed from #145 stand ready to ship opportunistically as their perf value is measured (via #508's benchmark harness) or forced by a real workload. Pre-existing schema drift #486 (AggregateMetrics extra="forbid"vs percentile keys) is filed and separable from M8.