2 Commits
Author SHA1 Message Date
jhogyu 546bad4d4d feat(stage2): prepare S2_00 request before ingress
Add the fixed six-field request writer and two-task S2_00 projection. Refresh bound release metadata, regression coverage, and the S2_00 analysis; backend argument binding and live admission remain pending.
2026-10-01 16:51:57 +09:00
jhogyu 48fd0435a8 docs: record S2_00 IO analysis and request preparation plans 2026-09-30 20:07:39 +09:00
62 changed files with 15419 additions and 179 deletions
@@ -0,0 +1,103 @@
# Stage 2 - S2_00 분석
## Executive Summary
현행 배포본 `Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml`은 Stage 1 Part 1–4의 사건 자료를 받아 S2_10용 claim-neutral 문맥과 전달 계획을 만드는 비 LLM ingress 명세다. `Agent.name`은 `Stage_2_S2_00_v2`, 버전은 `1.2.0`이다. 실제 Agent task는 **request 준비**와 **deterministic ingress** 두 개이며, C00·C05·C10·C15는 두 번째 task의 inline Python 안에서 수행되는 논리 단계다. 두 task 모두 `code-executor.run_code`를 사용하고 localdocs로 파일을 읽고 쓴다.
첫 task의 `prepare_request(...)` 함수는 외부 문자열 인자 네 개를 정확히 6필드의 `s2_00_request.json`으로 만들어 고정 경로에 저장하고 read-back을 확인하도록 구현되어 있다. 그러나 배포 YAML에는 backend가 그 네 인자를 함수에 주입하는 실행 결속이 없다. 현재 inline 코드의 직접 실행 진입점은 `PREPARE_ARGUMENT_BINDING_UNVERIFIED`를 출력하고 종료 코드 2로 실패한다. 따라서 이 분석서의 DAG와 산출물은 **구현된 함수 및 선언된 계약**을 설명하며, 현재 배포본의 사건별 실실행 성공을 의미하지 않는다. 더구나 현재 release는 `DEV_FIXTURE_RELEASE / DRAFT_NOT_EXECUTABLE`이고 ingress가 DEV fixture의 실제 사건 발행을 거부한다.
## 전체 작업 DAG와 책임 경계
```text
외부 실행자: request_id, attempt_id, stage1_run_root_ref,
stage1_deployment_root_ref 제공; workspace 호출·직렬화 책임
│
▼
IN → Task_S2_00_prepare_request → Task_S2_00_deterministic_ingress → OUT
│ 6필드 request 생성·검증 │ 고정 request/Stage 1/release·자산 hydration
│ localdocs write/read-back ├─ C00: 입력 계약·출처/무결성 검증
▼ ├─ C05: review 항목 정규화·보존 검사
W/stage2_control/s2_00_request.json ├─ C10: 사건·증거·당사자·slot 문맥 구성
└─ C15: bundle/route·결과 발행
→ O/ingress/ingress_status.json 마지막 기록
```
YAML의 `task_procedure`는 `IN → prepare → ingress → OUT`의 `nexts`/`wait_until` 순서를 선언한다. `Stages[0].prevs`와 `nexts`는 빈 배열이므로 다른 Stage Agent로의 호출은 이 YAML 안에 없다. ingress 내부의 C00–C15는 별도 MCP task나 각각의 저장 파일이 아니다. 준비 task가 성공해야 ingress가 시작되는 **backend success-only gate**, 그리고 동일 workspace의 고정 request 경로에 대한 **다른 실행의 덮어쓰기 차단**은 workflow·deployment 계약에서 모두 `UNVERIFIED`다.
여기서 `W/`는 localdocs의 사용자·workspace 논리 루트, `A/`는 `W/Default_Agent/Stage_2_Clean/`, `U/`는 `W/<stage1_run_root_ref>/`, `D/`는 `W/<stage1_deployment_root_ref>/`, `O/`는 `W/stage2_runs/by-binding/<run_binding_digest>/`를 뜻한다. 이들은 Dropbox 저장소의 물리 경로가 아니다. 배포 저장소의 대응 패키지 루트는 `Default_Agent/Stage_2_Clean/`이다. `<...>` 값은 사건·release·실행 입력으로 결정되며 현재 자료에서 특정 파일명으로 확정할 수 없다.
## 실제 실행단위와 prompt 구성
| 실제 Agent task | 호출·코드 위치 | 하는 일 | LLM prompt |
|---|---|---|---|
| `Task_S2_00_prepare_request` | `code-executor.run_code`; YAML `/Agent/Stages/0/tasks/0/parameters/code` | 네 인자를 검사하고 6필드 canonical JSON을 `W/stage2_control/s2_00_request.json`에 쓰고 동일 바이트를 다시 읽어 확인한다. | 없음 |
| `Task_S2_00_deterministic_ingress` | `code-executor.run_code`; YAML `/Agent/Stages/0/tasks/1/parameters/code` | request·release·Stage 1 자료·잠금 자산을 읽고 C00–C15를 한 번의 Python 실행에서 처리한 뒤 결과를 발행한다. | 없음 |
두 task의 파라미터는 `language: python`, `requirements: httpx==0.28.1`, `network: agent-network`, `timeout: 300`이다. `localdocs` 서버는 `http://mcp-localdocs:8012/mcp`, `code-executor` 서버는 `https://code-executor.mcp.eroomai.com/mcp`로 선언되어 있다. YAML의 `description`과 주석은 실행 명세이며 system/user prompt, prompt template, 모델 호출 구성이 아니다. `run_code`에 전달되는 실제 본문은 각 task의 `parameters.code`에 내장된 Python이다. `.py`/`.txt` mirror는 빌드·검증용 대응본이지 런타임의 추가 import가 아니다.
첫 task의 `build_request`와 `prepare_request`는 호출 가능한 함수로 존재한다. ID 형식, NFC 상대경로, 정확한 필드 집합을 확인하고 UTF-8 canonical JSON과 끝의 LF를 만든다. localdocs `write_binary_file(overwrite=true)` 뒤 `read_binary_doc`를 비교하며 성공 시 SHA-256 등을 함수 반환값으로 제공한다. 다만 `__main__`은 외부 인자 결속이 없음을 명시적으로 실패 처리한다. ingress의 직접 진입점은 `run_inline_mcp()`이며 localdocs에서 필요한 바이너리를 읽어 code-executor 임시 디렉터리에 hydrate하고 core를 실행한다. 입력은 두 차례 읽어 바이트 일치를 검사하고, 원격 발행은 일반 산출물을 먼저 write/read-back한 다음 `ingress/ingress_status.json`을 마지막에 쓴다.
## In & Out 설명
모든 `.json` 입력·출력은 JSON 파일이다. `s2_00_request.json`과 발행 결과는 canonical UTF-8 JSON이며, 개별 Stage 1 원본의 바이트·스키마 검증 규칙은 release와 ingress 구현이 지배한다. 아래 `A/`, `U/`, `D/`, `O/` 표기는 앞 절의 **localdocs 논리 경로**다.
| 단계 | Input: 정확한 이름·형식·source | 역할 |
|---|---|---|
| prepare | `request_id`, `attempt_id`, `stage1_run_root_ref`, `stage1_deployment_root_ref`: 외부 문자열 인자, 파일 아님 | ID와 Stage 1 사건·배포 루트를 결속할 값. backend 공급 방식은 미검증. |
| ingress | `W/stage2_control/s2_00_request.json`: JSON, 정확히 `schema_version`, `workflow_id`, `request_id`, `attempt_id`, `stage1_run_root_ref`, `stage1_deployment_root_ref` 6필드 | prepare의 고정 출력. 상수값은 `stage2_s2_00_execution_request.v1` 및 `S2_00`. |
| ingress: Stage 1 P1 | `U/evidence_indexed.json`, `U/evidence_event_candidates.json`, `U/client_goal.json`, `U/routing/domain_screening.json`, `U/routing/domain_activation_manifest.json`, `U/quality_gates/B1_evidence_indexed_gate.json`, `U/quality_gates/B2_event_candidates_gate.json`, `U/quality_gates/stage1_part1_soft_gate_handoff.json`: 각 JSON | 증거·목표·도메인 routing 및 P1 gate/handoff. |
| ingress: Stage 1 P2 | `U/BO.json`, `U/signals/signal_manifest.json`, `U/quality_gates/stage1_part2_review_handoff.json`: 각 JSON | BO·신호 목록·P2 handoff. |
| ingress: Stage 1 P3 | `U/legal_effect_structures.json`, `U/quality_gates/stage1_part3_review_handoff.json`: 각 JSON | 법률효과 구조·P3 handoff. |
| ingress: Stage 1 P4 | `U/Fact_Ledger_base.json`, `U/stage1_tmp/fact_ledger/fact_ledger_writer_report.json`, `U/quality_gates/stage1_part4_review_handoff.json`: 각 JSON | 사실 ledger·writer report·P4 handoff. |
| ingress: 동적 신호 | `U/signals/<signal_manifest.files[i].path>`: 각 JSON | `signal_manifest.json` 행에서 확장하므로 정확한 basename·개수는 사건별 manifest가 있어야 확정된다. `U/routing/domain_activation_manifest.json`과 이름이 비슷한 신호 파일이 있더라도 별도 경로다. |
| ingress: Stage 1 배포 | `D/<release.dependency_locks.stage1.concrete_paths[i].path>`: JSON/JSON Schema, 총 55개 고정 경로 | Stage 1 runtime manifest, registry/config/schema를 release의 경로·해시로 결속한다. 정확한 55개 항목은 `A/manifest/stage2_release.json#/dependency_locks/stage1/concrete_paths`가 정의한다. |
| ingress: Stage 2 배포 | `A/manifest/stage2_release.json`, `A/manifest/module_manifest.json`, `A/schemas/ingress.schema.json`, `A/schemas/context.schema.json`, `A/schemas/review_status.schema.json` 및 `A/<release.dependency_locks.stage2_direct[i].path>` 49개: JSON·JSON Schema·YAML | release/모듈·출력 검증 기준 및 직접 의존 자산. 아래 고정 자산 표에 배포 경로를 적었다. |
| 단계·분기 | Output: 정확한 이름·형식·저장 folder | 의미 |
|---|---|---|
| prepare | `W/stage2_control/s2_00_request.json`: JSON; `W/stage2_control/` | ingress가 소비할 고정 제어 request. 성공 반환 객체는 함수 응답이지 별도 파일이 아니다. |
| ingress: 공통 | `O/ingress/stage1_input_manifest.json`, `O/ingress/intake_report.json`, `O/review/issue_ledger.base.json`, `O/ingress/ingress_status.json`: 각 JSON; `O/ingress/`·`O/review/` | 입력 출처·수용 결과·review 이슈·route/barrier. `ingress_status.json`은 마지막에 기록한다. |
| ingress: 정상 route | `O/context/case_context.json`, `evidence_inventory.json`, `object_registry.json`, `party_and_title_context.json`, `slot_crosswalk.json`, `cluster_plan.json`, `bundle_plan.json`: 각 JSON; `O/context/` | S2_10용 정규화 문맥과 cluster·bundle 계획. 공통 4개를 합쳐 고정 11개. |
| ingress: 정상 route의 가변 파일 | `O/context/cluster_slices/<cluster_id>.json`: JSON; `O/context/cluster_slices/` | 실행 가능 cluster마다 1개. 최종 ID 집합은 해당 사건의 계획으로 정해지므로 정상 출력 총수는 `11 + E`(`E`=실행 가능 cluster 수). |
| ingress: 진단 route | `O/ingress/technical_diagnostic.json`: JSON; `O/ingress/` | `TO_S2_40_STATUS_ONLY`일 때 공통 4개와 함께 총 5개. 정상 `context/` 파일은 이 분기에서 생성하지 않는다. |
ingress는 먼저 code-executor의 임시 `liti-s2-00-*` 디렉터리에 입력 및 `core_output/`을 구성한다. 최종 저장 장소는 위 `O/` localdocs 경로이며, 임시 경로는 배포 산출물 경로가 아니다. `run_binding_digest`는 입력 집합·Stage 2 release·알고리즘·release class 결속으로 정해져 `O/`를 결정한다. `attempt_id`만 바꾼다고 출력 루트가 바뀌는 계약은 아니다. `run_code`의 JSON stdout 결과도 출력 파일과 구별해야 한다.
## 작업용 고정 자산
아래 표에서 `A/<상대경로>`는 **배포 시 localdocs의 `Default_Agent/Stage_2_Clean/<상대경로>`**이고, 저장소에도 같은 상대경로의 파일이 있다. 쉼표로 열거한 basename은 앞의 배포 directory와 각각 결합한다. `R/` 표기는 혼동을 피하려고 사용하지 않는다. 런타임 hydration 자산과 빌드·결속 명세는 역할을 구분했다.
| 정확한 배포 경로 (`A/` 이하) | 형식·개수 | 역할·실행 시점 |
|---|---:|---|
| `agent_scripts/Stage_2_S2_00.yml` | YAML 1 | 이 Agent의 현행 두 task와 inline code. |
| `manifest/stage2_release.json` | JSON 1 | release class·상태, Stage 1 입력 16+동적 항목, Stage 1 배포 잠금 55개, Stage 2 직접 잠금 49개, 알고리즘·출력 계약. ingress에서 읽고 hash 검증. |
| `manifest/module_manifest.json` | JSON 1 | 모듈 고정 manifest; ingress hydration 대상. |
| `schemas/ingress.schema.json`, `schemas/context.schema.json`, `schemas/review_status.schema.json` | JSON Schema 3 | request/입력·context·review/status 형태의 ingress 검증 기준; ingress hydration 대상. |
| `agent_scripts/Stage_2_S2_10.yml`, `deployment/stage2_s2_10_llm_binding.yml`, `registry/authority/authority_registry.yml`, `manifest/authority_release.json` | YAML 3, JSON 1 | Stage 2 직접 잠금의 S2_10 Agent·LLM/authority 결속 4개. ingress가 경로·hash로 확인하나 S2_10 호출 자체는 하지 않는다. |
| `registry/substantive/EC-00_contract_general.yml`, `E-01_juristic_act_validity.yml`, `E-02_contract_money_claim.yml`, `E-03_parties_liability_succession.yml`, `E-04_unjust_enrichment.yml`, `E-05_tort_general.yml`, `E-06_professional_liability.yml`, `E-07_construction_defect.yml`, `E-08_lease_deposit.yml`, `E-09_registry_transfer_claims.yml`, `E-10_secured_registry.yml`, `E-11_possession_vindication.yml`, `E-12_co_ownership_boundary.yml`, `E-13_creditor_preservation.yml`, `E-14_execution_linked_claims.yml`, `E-15_succession_family_property.yml`, `E-16_negotiable_instruments.yml`, `E-17_labor_wage_claims.yml`, `E-18_org_resolution_status.yml`, `E-19_insurance_claims.yml`, `E-20_ip_claims.yml`, `E-21_media_personality_rights.yml` | YAML 22 | Stage 2 직접 잠금 substantive registry. 각 basename의 전체 배포 경로는 `A/registry/substantive/<basename>`이다. |
| `profiles/crosscut/E-00_residual_unrouted.yml`, `X1_notice_lifecycle.yml`, `X2_asset_identity_lineage.yml`, `X3_procedure_standing_relief.yml`, `X4_response_admission_defense.yml` | YAML 5 | Stage 2 직접 잠금 crosscut profile; 전체 배포 경로는 `A/profiles/crosscut/<basename>`. |
| `profiles/special_law/SL-AUTO_motor_vehicle.yml`, `SL-INDUSTRIAL_ACCIDENT.yml`, `SL-PRODUCT_LIABILITY.yml`, `SL-RESIDENTIAL_LEASE.yml`, `SL-COMMERCIAL_LEASE.yml`, `SL-LABOR.yml`, `SL-STATE_LIABILITY.yml`, `SL-IP-PATENT.yml`, `SL-IP-COPYRIGHT.yml`, `SL-IP-OTHER.yml`, `SL-MEDIA.yml`, `SL-TRANSPORT_MARITIME.yml`, `SL-CONSUMER_CONTRACT.yml` | YAML 13 | Stage 2 직접 잠금 특별법 profile; 전체 배포 경로는 `A/profiles/special_law/<basename>`. |
| `profiles/overlays/ACTIO-MORTGAGE.yml`, `ACTIO-MORTGAGE-CREATION.yml`, `ACTIO-ENCUMBERED-TRANSFER.yml`, `ACTIO-PRESERVED-CLAIM-BUNDLE.yml`, `ACTIO-DEFENSE-MAP.yml` | YAML 5 | Stage 2 직접 잠금 overlay; 전체 배포 경로는 `A/profiles/overlays/<basename>`. |
| `workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml` | YAML 1 | 두 task 순서·고정 request·외부 인자와 미검증 backend 항목을 선언하는 workflow 계약; ingress의 사건별 입력 파일은 아님. |
| `deployment/stage2_code_executor_binding.yml` | YAML 1 | S2_00 code-executor 결속, task 포인터·자산 hash·localdocs allowlist·live admission 상태를 선언하는 배포 계약. |
| `manifest/s2_00_inline_code_receipt.json` | JSON 1 | prepare/ingress inline code와 mirror의 정적 parity receipt. `PASS`는 live 실행 증거가 아니다. |
| `runtime/s2_00_prepare_request.py`, `runtime/s2_00_prepare_request.txt`, `runtime/s2_00_ingress.py`, `runtime/s2_00_ingress.txt` | Python 2, text 2 | inline code의 빌드·검증 mirror. YAML 직접 실행은 이 파일을 import하지 않는다. |
| `offline_build/build_s2_00_inline_projection.py`, `offline_build/build_s2_00_inline_projection.txt`, `schemas/deployment.schema.json` | Python 1, text 1, JSON Schema 1 | projection 생성·배포 계약 검사에 사용하는 offline 자산. 사건 ingress의 별도 실행 task가 아니다. |
`release.dependency_locks.stage1.concrete_paths`의 55개 파일은 위 Stage 2 패키지에 복제되는 것이 아니라 `D/`에서 참조되는 Stage 1 배포 자산이다. `stage2_direct`의 49개는 위 표의 S2_10/authority 4개 + substantive 22개 + crosscut 5개 + special_law 13개 + overlays 5개다. 이 결속은 자산 존재·hash 확인과 downstream 선택 가능성을 위한 것이며, 49개 본문 전체가 S2_10 prompt에 들어간다는 뜻은 아니다.
## Upstream/Downstream 설명
Upstream은 Stage 1의 P1–P4 사건 산출물 16개, `signal_manifest.json`에서 펼친 추가 신호 파일, Stage 1 배포 잠금 55개 및 네 외부 인자를 제공할 호출자다. 네 인자의 전달과 동일 workspace 동시 실행 제어는 외부 backend/orchestrator 경계에 있다. prepare가 고정 request를 쓰면 ingress가 그것을 다시 읽어 path/ID, release 및 원본 bytes와의 결속을 검증한다. `W/stage2_control/s2_00_request.json`은 Stage 1 고정 handoff 16개에 속하지 않는다.
Downstream에서 정상 route는 `TO_S2_10` 또는 `TO_S2_10_WITH_ISSUES`이고, 진단 route는 `TO_S2_40_STATUS_ONLY`다. S2_00은 status barrier와 bundle/cluster plan을 발행할 뿐 S2_10·S2_40 Agent를 이 YAML에서 직접 호출하지 않는다. 외부 실행자는 `O/ingress/ingress_status.json`과 해당 분기의 완전한 산출물 집합을 검증해 후속 호출을 조립해야 한다. S2_10용 선택 context·cluster case payload는 후속 handoff의 필드/구성 문제이지 S2_00이 고정 파일명으로 별도 발행하는 파일이라고 간주할 수 없다. 진단 route에는 정상 `context/` 산출물이 없으므로 S2_10으로 보내는 경로가 아니다.
## 구현과 계약 사이의 확인사항·잔여 한계
1. **네 인자·실행 환경 결속 부재:** 함수의 생성·검증·저장 구현과 offline 검증은 존재한다. 그러나 현행 task의 `__main__`은 항상 `PREPARE_ARGUMENT_BINDING_UNVERIFIED`로 종료한다. backend가 네 값을 함수에 넘기는 방식과 `{{__user_hash__}}`·`{{__workspace_hash__}}` 치환/인증의 실실행 증거가 없어, 현재 배포 YAML만으로 request 생성 완료나 두-task 실실행을 주장할 수 없다.
2. **순서와 배타성 미검증:** `task_procedure.wait_until`은 순서 선언이다. prepare 실패 시 ingress 차단을 backend가 보장하는지, request 저장부터 ingress의 두 차례 읽기와 입력 hydration 완료까지 동일 workspace의 다른 실행이 고정 경로를 덮어쓰지 못하는지는 미확인이다. 두 번 읽어 일치를 검사해도 첫 읽기 전에 이미 덮어쓴 다른 실행의 request를 식별·차단하는 증명은 아니다.
3. **현재 release 제한:** 배포 `stage2_release.json`은 `DEV_FIXTURE_RELEASE`와 `DRAFT_NOT_EXECUTABLE`이다. ingress 코드에는 `DEV_FIXTURE_REAL_RUN_FORBIDDEN` 방어가 있어 현재 release로 실제 사건 결과를 발행할 수 없다. bundle은 structural fixture 상태이며 `selected_context_refs`도 비어 있다.
4. **발행 원자성의 범위:** 비 status 파일을 원격 write/read-back한 뒤 status를 마지막에 쓰는 *논리적* barrier다. localdocs 전체 디렉터리의 원자적 rename이나 backend의 downstream barrier 소비를 입증하지 않는다. status 기록 후 예외가 생긴 경우 실패 stdout의 `FAILED_NO_BARRIER` 표현만으로 원격 status의 부재를 증명할 수도 없다.
5. **프로토콜·검증 경계:** prepare의 localdocs 응답 처리는 JSON `response.json()`에 의존하므로 서버가 SSE만 응답하는 환경과의 호환성은 확인되지 않았다. inline code receipt의 정적 parity, hash 잠금, 함수 수준 시험은 live MCP/backend 연결, Stage 1 실제 자료 완비, S2_10 전달 성공 또는 법률 내용의 타당성 검증을 대신하지 않는다.
근거 기준: 현행 `agent_scripts/Stage_2_S2_00.yml`의 두 `parameters.code`와 `task_procedure`, `manifest/stage2_release.json`, `workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml`, `deployment/stage2_code_executor_binding.yml`, `manifest/s2_00_inline_code_receipt.json`을 대조했다. 과거 분석서나 `Stage_2_S2_00_outdated_10_01.yml`의 단일 task 구조를 현행 구조로 소급하지 않았다.
@@ -12,11 +12,12 @@ Agent:
release_ref: Default_Agent/Stage_2_Clean/manifest/stage2_release.json release_ref: Default_Agent/Stage_2_Clean/manifest/stage2_release.json
workflow_contract_ref: Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml workflow_contract_ref: Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml
deployment_binding_ref: Default_Agent/Stage_2_Clean/deployment/stage2_code_executor_binding.yml deployment_binding_ref: Default_Agent/Stage_2_Clean/deployment/stage2_code_executor_binding.yml
implementation_status: S2_00_CODE_AND_FIXTURE_OFFLINE_VERIFIED_S2_10_HYBRID_REIMPLEMENTATION_AND_RESEAL_PENDING_LIVE_ADMISSION_PENDING implementation_status: S2_00_PREPARE_OFFLINE_VERIFIED_BACKEND_ARGUMENT_GATE_AND_SERIALIZATION_UNVERIFIED_LIVE_ADMISSION_PENDING
Stages: Stages:
- name: S2_00 - name: S2_00
description: >- description: >-
단일 Code Executor 호출 안에서 C00, C05, C10, C15를 순차 실행하고 첫 Code Executor task에서 고정 request를 준비하고, 다음 ingress
task의 한 호출 안에서 C00, C05, C10, C15를 순차 실행한다.
localdocs binary IO와 status-last 논리 배리어로 결과를 발행한다. localdocs binary IO와 status-last 논리 배리어로 결과를 발행한다.
prevs: [] prevs: []
nexts: [] nexts: []
@@ -29,6 +30,254 @@ Agent:
type: streamable-http type: streamable-http
url: https://code-executor.mcp.eroomai.com/mcp url: https://code-executor.mcp.eroomai.com/mcp
tasks: tasks:
- task_name: Task_S2_00_prepare_request
description: >-
네 외부 문자열 인자 request_id, attempt_id, stage1_run_root_ref,
stage1_deployment_root_ref를 검증하고 고정 localdocs request 경로에
저장·read-back한다. Backend 인자 결속 및 성공 게이트는 미검증이며
인자가 결속되지 않은 직접 실행은 실패로 종료한다.
mcp: code-executor
tool_name: run_code
parameters:
language: python
requirements: "httpx==0.28.1"
network: agent-network
timeout: 300
code: |-
#!/usr/bin/env python3
"""Prepare the fixed S2_00 control request from four explicit caller values.
The backend argument-delivery contract is not bound. Direct execution fails
closed; callers must invoke ``prepare_request`` with the four named values.
"""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import PurePosixPath
import re
import sys
import unicodedata
from typing import Any, Mapping
REQUEST_PATH = "stage2_control/s2_00_request.json"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
REQUEST_KEYS = frozenset({
"schema_version", "workflow_id", "request_id", "attempt_id",
"stage1_run_root_ref", "stage1_deployment_root_ref",
})
ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
class PrepareError(ValueError):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def canonical_json_bytes(value: Any) -> bytes:
return (json.dumps(value, ensure_ascii=False, allow_nan=False,
sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def _relative_path(value: str, *, code: str) -> str:
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
raise PrepareError(code, "logical path is empty or malformed")
if unicodedata.normalize("NFC", value) != value:
raise PrepareError(code, "logical path must already be NFC")
path = PurePosixPath(value)
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
raise PrepareError(code, "logical path must be a contained relative path")
rendered = path.as_posix()
if rendered != value:
raise PrepareError(code, "logical path is not canonical")
return rendered
def build_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
) -> bytes:
"""Return the exact six-field canonical request; do not access localdocs."""
if not isinstance(request_id, str) or ID_RE.fullmatch(request_id) is None:
raise PrepareError("REQUEST_ID_INVALID", "request_id contains forbidden characters")
if not isinstance(attempt_id, str) or ID_RE.fullmatch(attempt_id) is None:
raise PrepareError("ATTEMPT_ID_INVALID", "attempt_id contains forbidden characters")
request = {
"schema_version": "stage2_s2_00_execution_request.v1",
"workflow_id": "S2_00",
"request_id": request_id,
"attempt_id": attempt_id,
"stage1_run_root_ref": _relative_path(
stage1_run_root_ref, code="STAGE1_RUN_ROOT_REF_INVALID"),
"stage1_deployment_root_ref": _relative_path(
stage1_deployment_root_ref, code="STAGE1_DEPLOYMENT_ROOT_REF_INVALID"),
}
if set(request) != REQUEST_KEYS:
raise PrepareError("RUN_REQUEST_CLOSED_SHAPE", "request field set drifted")
return canonical_json_bytes(request)
class LocaldocsSession:
"""Small localdocs JSON-RPC session with verified binary write/read-back."""
def __init__(self, user_hash: str, workspace_hash: str, *, client: Any = None) -> None:
for name, value in (("user_hash", user_hash), ("workspace_hash", workspace_hash)):
if not isinstance(value, str) or re.fullmatch(r"[a-f0-9]{64}", value) is None:
raise PrepareError("CONTEXT_HASH_INVALID", f"{name} is not a SHA-256 digest")
self.user_hash = user_hash
self.workspace_hash = workspace_hash
if client is None:
try:
import httpx
except ImportError as exc:
raise PrepareError("HTTPX_UNAVAILABLE", "httpx==0.28.1 is required") from exc
client = httpx.Client(timeout=60)
self.client = client
self.headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
self.session_id: str | None = None
self.next_id = 10
self.initialized = False
def close(self) -> None:
self.client.close()
def _post(self, body: Mapping[str, Any], expected_id: int | None) -> Mapping[str, Any] | None:
try:
response = self.client.post(LOCALDOCS_URL, json=dict(body), headers=dict(self.headers))
response.raise_for_status()
except Exception as exc:
raise PrepareError("MCP_TRANSPORT_ERROR", "localdocs transport failed") from exc
session_id = response.headers.get("mcp-session-id")
if session_id:
if self.session_id is None and expected_id == 1:
self.session_id = session_id
elif session_id != self.session_id:
raise PrepareError("MCP_SESSION_ID_CHANGED", "localdocs session changed")
self.headers["mcp-session-id"] = session_id
if expected_id is None:
return None
try:
payload = response.json()
except Exception as exc:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response is not JSON") from exc
if not isinstance(payload, dict) or payload.get("jsonrpc") != "2.0" or payload.get("id") != expected_id:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response ID or shape mismatch")
if "error" in payload or not isinstance(payload.get("result"), dict):
raise PrepareError("MCP_TOOL_ERROR", "localdocs returned an error")
return payload["result"]
def initialize(self) -> None:
response = self._post({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}, "clientInfo": {
"name": "liti-s2-00-prepare", "version": "1.0.0",
"user_id": self.user_hash, "workspace_id": self.workspace_hash,
}}}, 1)
if response is None or response.get("protocolVersion") != MCP_PROTOCOL_VERSION or self.session_id is None:
raise PrepareError("MCP_INITIALIZE_INVALID", "localdocs initialization failed")
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, None)
self.initialized = True
def call(self, name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]:
if not self.initialized:
raise PrepareError("MCP_NOT_INITIALIZED", "localdocs is not initialized")
message_id = self.next_id
self.next_id += 1
result = self._post({"jsonrpc": "2.0", "id": message_id, "method": "tools/call",
"params": {"name": name, "arguments": dict(arguments)}}, message_id)
if result is None or result.get("isError") is True:
raise PrepareError("MCP_TOOL_ERROR", f"localdocs {name} failed")
return result
def read_binary(self, logical_path: str) -> bytes:
path = _relative_path(logical_path, code="LOCALDOCS_READ_PATH_INVALID")
result = self.call("read_binary_doc", {"doc_name": path})
content = result.get("content")
if not isinstance(content, list) or len(content) != 1 or not isinstance(content[0], dict) or content[0].get("type") != "text":
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary response")
text = content[0].get("text")
if not isinstance(text, str):
raise PrepareError("LOCALDOCS_READ_SHAPE", "missing binary envelope")
try:
envelope = json.loads(text)
if isinstance(envelope, dict) and "results" in envelope:
rows = envelope["results"]
if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0], dict):
raise ValueError("binary result cardinality mismatch")
inner = rows[0].get("content", rows[0].get("text"))
envelope = json.loads(inner) if isinstance(inner, str) else inner
encoded = envelope["content_base64"]
if not isinstance(encoded, str):
raise ValueError("binary content is not base64")
payload = base64.b64decode(encoded, validate=True)
size = envelope.get("byte_length", envelope.get("size"))
if size is not None and (not isinstance(size, int) or size != len(payload)):
raise ValueError("binary size mismatch")
digest = envelope.get("sha256")
if digest is not None and digest != hashlib.sha256(payload).hexdigest():
raise ValueError("binary hash mismatch")
return payload
except (ValueError, KeyError, TypeError, base64.binascii.Error) as exc:
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary envelope") from exc
def write_binary_verified(self, logical_path: str, payload: bytes) -> str:
path = _relative_path(logical_path, code="LOCALDOCS_WRITE_PATH_INVALID")
if path != REQUEST_PATH:
raise PrepareError("LOCALDOCS_WRITE_PATH_INVALID", "prepare may write only the fixed request path")
result = self.call("write_binary_file", {
"path": path, "content_base64": base64.b64encode(payload).decode("ascii"), "overwrite": True,
})
if not isinstance(result.get("content"), list) or result.get("isError") is True:
raise PrepareError("LOCALDOCS_WRITE_FAILED", "localdocs did not acknowledge write")
observed = self.read_binary(path)
if observed != payload:
raise PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "request read-back differs")
return hashlib.sha256(observed).hexdigest()
def prepare_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
*,
localdocs: Any = None,
) -> dict[str, Any]:
"""Validate, write, read back, and close an authenticated localdocs session."""
payload = build_request(request_id, attempt_id, stage1_run_root_ref, stage1_deployment_root_ref)
session = localdocs if localdocs is not None else LocaldocsSession(INLINE_USER_HASH, INLINE_WORKSPACE_HASH)
failure: Exception | None = None
digest: str | None = None
try:
session.initialize()
digest = session.write_binary_verified(REQUEST_PATH, payload)
except Exception as exc:
failure = exc
try:
session.close()
except Exception as exc:
if failure is None:
failure = PrepareError("LOCALDOCS_CLOSE_FAILED", "localdocs session close failed")
failure.__cause__ = exc
if failure is not None:
raise failure
return {"ok": True, "workflow_id": "S2_00", "path": REQUEST_PATH,
"request_sha256": digest}
if __name__ == "__main__":
sys.stdout.buffer.write(canonical_json_bytes({
"ok": False, "error": {"code": "PREPARE_ARGUMENT_BINDING_UNVERIFIED",
"message": "four caller values must be bound by the backend"}}))
raise SystemExit(2)
- task_name: Task_S2_00_deterministic_ingress - task_name: Task_S2_00_deterministic_ingress
description: >- description: >-
고정 request와 release를 hydration하고 S2_00 pure core를 실행한 뒤 고정 request와 release를 hydration하고 S2_00 pure core를 실행한 뒤
@@ -192,7 +441,7 @@ Agent:
# literal placeholders in the offline parity mirror and its unit tests. # literal placeholders in the offline parity mirror and its unit tests.
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}" INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json" INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json"
INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean" INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean"
INLINE_STAGE2_RELEASE_PATH = ( INLINE_STAGE2_RELEASE_PATH = (
@@ -6235,13 +6484,18 @@ Agent:
task_procedure: task_procedure:
IN: IN:
nexts: nexts:
- Task_S2_00_deterministic_ingress - Task_S2_00_prepare_request
wait_until: [] wait_until: []
Task_S2_00_prepare_request:
nexts:
- Task_S2_00_deterministic_ingress
wait_until:
- IN
Task_S2_00_deterministic_ingress: Task_S2_00_deterministic_ingress:
nexts: nexts:
- OUT - OUT
wait_until: wait_until:
- IN - Task_S2_00_prepare_request
OUT: OUT:
nexts: [] nexts: []
wait_until: wait_until:
@@ -71,7 +71,7 @@ Agent:
WORKFLOW_ID = "S2_20" WORKFLOW_ID = "S2_20"
ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0" ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp" WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
@@ -73,7 +73,7 @@ Agent:
from typing import Any, Mapping from typing import Any, Mapping
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -68,7 +68,7 @@ Agent:
WORKFLOW_ID = "S2_40" WORKFLOW_ID = "S2_40"
ALGORITHM_VERSION = "s2_40_finalizer/1.0.0" ALGORITHM_VERSION = "s2_40_finalizer/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -9,30 +9,42 @@ stage_bindings:
agent_script_ref: agent_script_ref:
asset_id: AGENT-S2_00-INLINE asset_id: AGENT-S2_00-INLINE
path: agent_scripts/Stage_2_S2_00.yml path: agent_scripts/Stage_2_S2_00.yml
sha256: 94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943 sha256: 07bc9e235d211f3d4e6886bca2faeeb1f112d525fa29fe111bd7c50c42ff9849
schema_id: liti_agent_yaml.v1 schema_id: liti_agent_yaml.v1
binding_status: BOUND binding_status: BOUND
workflow_contract_ref: workflow_contract_ref:
asset_id: WF-S2_00 asset_id: WF-S2_00
path: workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml path: workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml
sha256: 3d7ceb86b236668c8c372136f1d1b43a5d0a79d3fb05fd1ef4342bb638190f68 sha256: 9b606ef4ddc7859591d9455fd9cdf9dfe6ad2533824636deb727b82e035f503a
schema_id: stage2_workflow_contract.v1.2 schema_id: stage2_workflow_contract.v1.2
binding_status: BOUND binding_status: BOUND
inline_code_receipt_ref: inline_code_receipt_ref:
asset_id: RECEIPT-S2_00-INLINE-CODE asset_id: RECEIPT-S2_00-INLINE-CODE
path: manifest/s2_00_inline_code_receipt.json path: manifest/s2_00_inline_code_receipt.json
sha256: 36af21949e5ef53a3d39df8ea9491c64da43abcc4f5e42e13db9bb391da843b8 sha256: be044ab40ee86e7c3a1be6c421b2c5d36ee67fc2fd2870f59a3d9d911fef6c1a
schema_id: stage2_s2_00_inline_code_receipt.v2 schema_id: stage2_s2_00_inline_code_receipt.v2
binding_status: BOUND binding_status: BOUND
stage2_release_ref: stage2_release_ref:
asset_id: RELEASE-STAGE2-CLEAN asset_id: RELEASE-STAGE2-CLEAN
path: manifest/stage2_release.json path: manifest/stage2_release.json
sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
schema_id: stage2_release.v2 schema_id: stage2_release.v2
binding_status: BOUND binding_status: BOUND
expected_release_sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 expected_release_sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
agent_script_sha256: 94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943 agent_script_sha256: 07bc9e235d211f3d4e6886bca2faeeb1f112d525fa29fe111bd7c50c42ff9849
canonical_code_sha256: e3f87725d5a6496189e7c411ef940dd8a7b3a9ff5b00535803bf98fa0cc4373e canonical_code_sha256: 81e619b26560890f9a4066e4275f2f51e63910d7983ba453230d4dd6e90bfd6a
prepare_request_contract:
task_name: Task_S2_00_prepare_request
yaml_pointer: /Agent/Stages/0/tasks/0/parameters/code
required_external_arguments:
- request_id
- attempt_id
- stage1_run_root_ref
- stage1_deployment_root_ref
write_path: stage2_control/s2_00_request.json
argument_binding_status: UNVERIFIED
success_gate_status: UNVERIFIED
workspace_serialization_status: UNVERIFIED
mcp_server_id: code-executor mcp_server_id: code-executor
tool_name: run_code tool_name: run_code
language: python language: python
@@ -86,7 +98,7 @@ stage_bindings:
agent_script_ref: agent_script_ref:
asset_id: AGENT-S2_20-INLINE asset_id: AGENT-S2_20-INLINE
path: agent_scripts/Stage_2_S2_20.yml path: agent_scripts/Stage_2_S2_20.yml
sha256: 3c4e6a7404f5b6a6b2403824c3cd9e71db566d4f2865bd4de3e6fd9d07824bd6 sha256: eb82f84df10c1eb4402e5e0f87fbe97a12e58aaa04f60c9535d66076300a5fdb
schema_id: liti_agent_yaml.v1 schema_id: liti_agent_yaml.v1
binding_status: BOUND binding_status: BOUND
workflow_contract_ref: workflow_contract_ref:
@@ -98,18 +110,18 @@ stage_bindings:
inline_code_receipt_ref: inline_code_receipt_ref:
asset_id: RECEIPT-S2_20-INLINE-CODE asset_id: RECEIPT-S2_20-INLINE-CODE
path: manifest/s2_20_inline_code_receipt.json path: manifest/s2_20_inline_code_receipt.json
sha256: 65e29f10f065cdd77281aa47acaf341edb38b0595aab0aa9f04bbee3a4718608 sha256: 64fd3711bd1128b6d29d3b8ff7a6af07228764da9604e0b5506562c5420e83e9
schema_id: stage2_s2_20_inline_code_receipt.v1 schema_id: stage2_s2_20_inline_code_receipt.v1
binding_status: BOUND binding_status: BOUND
stage2_release_ref: stage2_release_ref:
asset_id: RELEASE-STAGE2-CLEAN asset_id: RELEASE-STAGE2-CLEAN
path: manifest/stage2_release.json path: manifest/stage2_release.json
sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
schema_id: stage2_release.v2 schema_id: stage2_release.v2
binding_status: BOUND binding_status: BOUND
expected_release_sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 expected_release_sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
agent_script_sha256: 3c4e6a7404f5b6a6b2403824c3cd9e71db566d4f2865bd4de3e6fd9d07824bd6 agent_script_sha256: eb82f84df10c1eb4402e5e0f87fbe97a12e58aaa04f60c9535d66076300a5fdb
canonical_code_sha256: 6f1f503378242cf64cf8f0838af2164ab1103bc7031c8a6873f4ee89be66f7fd canonical_code_sha256: 91feb9e2ced624a2a4ea36e2b83bd17ed20bb059a42206fad01660800adeb515
mcp_server_id: code-executor mcp_server_id: code-executor
tool_name: run_code tool_name: run_code
language: python language: python
@@ -237,7 +249,7 @@ stage_bindings:
agent_script_ref: agent_script_ref:
asset_id: AGENT-S2_40-INLINE asset_id: AGENT-S2_40-INLINE
path: agent_scripts/Stage_2_S2_40.yml path: agent_scripts/Stage_2_S2_40.yml
sha256: 5cbe2ac9aa73d95a5fa4e6cf71d7d8f0ad83aa05536f573c13e42505ef5d46f7 sha256: a628f7fe944e21f77b9276cdb3d048189dcec958c7ed5abdcd9638b53a909a68
schema_id: liti_agent_yaml.v1 schema_id: liti_agent_yaml.v1
binding_status: BOUND binding_status: BOUND
workflow_contract_ref: workflow_contract_ref:
@@ -249,18 +261,18 @@ stage_bindings:
inline_code_receipt_ref: inline_code_receipt_ref:
asset_id: RECEIPT-S2_40-INLINE-CODE asset_id: RECEIPT-S2_40-INLINE-CODE
path: manifest/s2_40_inline_code_receipt.json path: manifest/s2_40_inline_code_receipt.json
sha256: 9bd0cfaee8d9e78480ab80a2fa541414c7ad0565439fa0f4661701fcdfde6526 sha256: 38fa1f5d8fc930ee3f7bbff7c73e1a52efc4995d14126ae035323ff74f1abf3d
schema_id: stage2_s2_40_inline_code_receipt.v1 schema_id: stage2_s2_40_inline_code_receipt.v1
binding_status: BOUND binding_status: BOUND
stage2_release_ref: stage2_release_ref:
asset_id: RELEASE-STAGE2-CLEAN asset_id: RELEASE-STAGE2-CLEAN
path: manifest/stage2_release.json path: manifest/stage2_release.json
sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
schema_id: stage2_release.v2 schema_id: stage2_release.v2
binding_status: BOUND binding_status: BOUND
expected_release_sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 expected_release_sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
agent_script_sha256: 5cbe2ac9aa73d95a5fa4e6cf71d7d8f0ad83aa05536f573c13e42505ef5d46f7 agent_script_sha256: a628f7fe944e21f77b9276cdb3d048189dcec958c7ed5abdcd9638b53a909a68
canonical_code_sha256: e521e1a65294a769cd6bf20edb159f8720c105b60cb769885aa416c8c763dcef canonical_code_sha256: 12f6b83e1e6793ad871033c6e6b840fc1e31f9fb6695b1a15f0406978fe841f7
mcp_server_id: code-executor mcp_server_id: code-executor
tool_name: run_code tool_name: run_code
language: python language: python
@@ -323,7 +335,7 @@ embedded_task_bindings:
agent_script_ref: agent_script_ref:
asset_id: AGENT-S2_30-INLINE asset_id: AGENT-S2_30-INLINE
path: agent_scripts/Stage_2_S2_30.yml path: agent_scripts/Stage_2_S2_30.yml
sha256: d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd sha256: 8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3
schema_id: liti_agent_yaml.v1 schema_id: liti_agent_yaml.v1
binding_status: BOUND binding_status: BOUND
workflow_contract_ref: workflow_contract_ref:
@@ -335,18 +347,18 @@ embedded_task_bindings:
inline_code_receipt_ref: inline_code_receipt_ref:
asset_id: RECEIPT-S2_30-INLINE-CODE asset_id: RECEIPT-S2_30-INLINE-CODE
path: manifest/s2_30_inline_code_receipt.json path: manifest/s2_30_inline_code_receipt.json
sha256: 8d1618771cf1f72aa3c6a5a4544fe6b6e949ccc7ecfef56f166561971beee896 sha256: a5e4e3161fc16f3e44ec9ae2c05d2c4706eca02099d844520973f76475343998
schema_id: stage2_s2_30_inline_code_receipt.v1 schema_id: stage2_s2_30_inline_code_receipt.v1
binding_status: BOUND binding_status: BOUND
stage2_release_ref: stage2_release_ref:
asset_id: RELEASE-STAGE2-CLEAN asset_id: RELEASE-STAGE2-CLEAN
path: manifest/stage2_release.json path: manifest/stage2_release.json
sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
schema_id: stage2_release.v2 schema_id: stage2_release.v2
binding_status: BOUND binding_status: BOUND
expected_release_sha256: 9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53 expected_release_sha256: 2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590
agent_script_sha256: d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd agent_script_sha256: 8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3
canonical_code_sha256: 1f7f3b842afbd3f1034f094d6341288effc9804a03920ada4bf8cfd512464e4c canonical_code_sha256: 17e66a89befdeb3ac4d7901eda2dad57869db809baf5faf16434cd7a7adb067a
mcp_server_id: code-executor mcp_server_id: code-executor
tool_name: run_code tool_name: run_code
language: python language: python
@@ -2,7 +2,7 @@ schema_version: stage2_s2_30_llm_binding.v1
binding_status: PENDING_EXTERNAL_PLATFORM_BINDING binding_status: PENDING_EXTERNAL_PLATFORM_BINDING
agent_ref: agent_ref:
path: agent_scripts/Stage_2_S2_30.yml path: agent_scripts/Stage_2_S2_30.yml
sha256: d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd sha256: 8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3
workflow_ref: workflow_ref:
path: workflows/S2_30_claim_group_draft_map.yml path: workflows/S2_30_claim_group_draft_map.yml
sha256: b266e7b7ae8939b087bfdb7b34fff8b9c6011c835560ab690991d856ced0cc15 sha256: b266e7b7ae8939b087bfdb7b34fff8b9c6011c835560ab690991d856ced0cc15
@@ -13,7 +13,7 @@ schema_ref:
size_bytes: 68470 size_bytes: 68470
planner_ref: planner_ref:
path: runtime/s2_30_dispatch_planner.py path: runtime/s2_30_dispatch_planner.py
sha256: 1f7f3b842afbd3f1034f094d6341288effc9804a03920ada4bf8cfd512464e4c sha256: 17e66a89befdeb3ac4d7901eda2dad57869db809baf5faf16434cd7a7adb067a
model: model:
provider: openai provider: openai
model_id: gpt-5.6-sol model_id: gpt-5.6-sol
@@ -30,10 +30,10 @@ prompt_contract:
- S30 - S30
inline_common_prompt_sha256: a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0 inline_common_prompt_sha256: a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0
inline_static_prompt_sha256: 577efa2c3334298ec60188b9f0066f834e072dc99f785b3c6ab4d05a47fac18a inline_static_prompt_sha256: 577efa2c3334298ec60188b9f0066f834e072dc99f785b3c6ab4d05a47fac18a
projection_receipt_sha256: 1aa650d3c389ba27ce43e514295c7c2ce629583dc96220618fd97dea399d0757 projection_receipt_sha256: 00b66f3495de5d1ff1e6e508eae2939cf862fdfc799c9cd01544a7979f769c42
runtime_external_prompt_read_allowed: false runtime_external_prompt_read_allowed: false
planner_contract: planner_contract:
inline_code_receipt_sha256: 8d1618771cf1f72aa3c6a5a4544fe6b6e949ccc7ecfef56f166561971beee896 inline_code_receipt_sha256: a5e4e3161fc16f3e44ec9ae2c05d2c4706eca02099d844520973f76475343998
legal_reasoning_allowed: false legal_reasoning_allowed: false
persistent_write_allowed: false persistent_write_allowed: false
native_adapter: native_adapter:
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"authoring":{"path":"Stage_2_S2_00_v.2.yml","sha256":"94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943","size_bytes":367573,"unique_key_parse":"PASS"},"authoring_rewritten":false,"build_kind":"OFFLINE_AUTHORING_PROJECTION","canonical_code":{"ast_status":"PASS","code_sha256":"e3f87725d5a6496189e7c411ef940dd8a7b3a9ff5b00535803bf98fa0cc4373e","code_size_bytes":283196,"compile_status":"PASS","encoding":"UTF-8","external_python_source_ref_count":0,"external_url_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","argparse","base64","binascii","collections","contextlib","dataclasses","hashlib","httpx","io","itertools","json","math","os","pathlib","re","shutil","stat","sys","tempfile","typing","unicodedata"],"placeholder_count":0,"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"deployment_projection":{"byte_identical_to_authoring":true,"canonical_task_semantics_sha256":"a970635aa19b4ebca03819e30de32bb69f33ca455f7ecd6257a45636565b4e95","path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml","sha256":"94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943","size_bytes":367573},"full_code_mirrors":[{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_ingress.py","sha256":"e3f87725d5a6496189e7c411ef940dd8a7b3a9ff5b00535803bf98fa0cc4373e","size_bytes":283196},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_ingress.txt","sha256":"e3f87725d5a6496189e7c411ef940dd8a7b3a9ff5b00535803bf98fa0cc4373e","size_bytes":283196}],"parity_status":"PASS","schema_version":"stage2_s2_00_inline_code_receipt.v2","source_of_truth":"Stage_2_S2_00_v.2.yml","task_contract":{"agent":{"name":"Stage_2_S2_00_v2","version":"1.2.0"},"mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"stage":{"name":"S2_00","nexts":[],"prevs":[]},"task":{"code_sha256":"e3f87725d5a6496189e7c411ef940dd8a7b3a9ff5b00535803bf98fa0cc4373e","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_00_deterministic_ingress","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_00_deterministic_ingress"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_00_deterministic_ingress"]},"Task_S2_00_deterministic_ingress":{"nexts":["OUT"],"wait_until":["IN"]}}},"workflow_id":"S2_00"} {"authoring":{"path":"Stage_2_S2_00_v.2.yml","sha256":"07bc9e235d211f3d4e6886bca2faeeb1f112d525fa29fe111bd7c50c42ff9849","size_bytes":382438,"unique_key_parse":"PASS"},"authoring_rewritten":false,"build_kind":"OFFLINE_AUTHORING_PROJECTION","canonical_code":{"ast_status":"PASS","code_sha256":"81e619b26560890f9a4066e4275f2f51e63910d7983ba453230d4dd6e90bfd6a","code_size_bytes":283196,"compile_status":"PASS","encoding":"UTF-8","external_python_source_ref_count":0,"external_url_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","argparse","base64","binascii","collections","contextlib","dataclasses","hashlib","httpx","io","itertools","json","math","os","pathlib","re","shutil","stat","sys","tempfile","typing","unicodedata"],"placeholder_count":0,"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/1/parameters/code"},"deployment_projection":{"byte_identical_to_authoring":true,"canonical_task_semantics_sha256":"0a0f84a21f30c607b1e0a63a037e43c373623a0c190cfdda7c7576e41c3c5bfc","path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml","sha256":"07bc9e235d211f3d4e6886bca2faeeb1f112d525fa29fe111bd7c50c42ff9849","size_bytes":382438},"full_code_mirrors":[{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_prepare_request.py","sha256":"16a57fbf3840d20edee8590a80b7b8161fe0b56637f271e850a5aa6be1e1f2d4","size_bytes":11062},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_prepare_request.txt","sha256":"16a57fbf3840d20edee8590a80b7b8161fe0b56637f271e850a5aa6be1e1f2d4","size_bytes":11062},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_ingress.py","sha256":"81e619b26560890f9a4066e4275f2f51e63910d7983ba453230d4dd6e90bfd6a","size_bytes":283196},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_00_ingress.txt","sha256":"81e619b26560890f9a4066e4275f2f51e63910d7983ba453230d4dd6e90bfd6a","size_bytes":283196}],"parity_status":"PASS","prepare_code":{"ast_status":"PASS","code_sha256":"16a57fbf3840d20edee8590a80b7b8161fe0b56637f271e850a5aa6be1e1f2d4","code_size_bytes":11062,"compile_status":"PASS","encoding":"UTF-8","external_python_source_ref_count":0,"external_url_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","base64","hashlib","httpx","json","pathlib","re","sys","typing","unicodedata"],"placeholder_count":0,"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"schema_version":"stage2_s2_00_inline_code_receipt.v2","source_of_truth":"Stage_2_S2_00_v.2.yml","task_contract":{"agent":{"name":"Stage_2_S2_00_v2","version":"1.2.0"},"mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"prepare_task":{"code_sha256":"16a57fbf3840d20edee8590a80b7b8161fe0b56637f271e850a5aa6be1e1f2d4","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_00_prepare_request","tool_name":"run_code"},"stage":{"name":"S2_00","nexts":[],"prevs":[]},"task":{"code_sha256":"81e619b26560890f9a4066e4275f2f51e63910d7983ba453230d4dd6e90bfd6a","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_00_deterministic_ingress","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_00_prepare_request"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_00_deterministic_ingress"]},"Task_S2_00_deterministic_ingress":{"nexts":["OUT"],"wait_until":["Task_S2_00_prepare_request"]},"Task_S2_00_prepare_request":{"nexts":["Task_S2_00_deterministic_ingress"],"wait_until":["IN"]}}},"workflow_id":"S2_00"}
@@ -1 +1 @@
{"agent_contract":{"agent_name":"Stage_2_S2_10","agent_version":"1.1.0","deterministic_task_count":0,"inline_common_prompt_sha256":"a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0","inline_static_prompt_sha256":"894c30bedaa5f230aed117e81ef4cc06cac832928a1e29cd3ee8804cfd4dba1f","item_tokens":["{{item.selected_legal_context_json}}","{{item.cluster_case_payload_json}}"],"llm_task_count":1,"prompt_roles":["system","system","system","user"],"reduce_task_count":0,"stage_name":"S2_10","task_name":"Task_S2_10_resolve_cluster","tool_task_count":0},"authoring":{"path":"Stage_2_S2_10_v.1.yml","sha256":"122fe890efb340ebbac299afe058bf9ad0ed87d4d0ccf2456393e9e5796ec272","size_bytes":30984},"binding":{"path":"deployment/stage2_s2_10_llm_binding.yml","sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04"},"child_release":{"path":"manifest/s2_10_release.json","release_digest":"8cfc30a40ad53b35fb44c56e3c53f86605f777622399a4b1dcd0b2082eb6e197","sha256":"cb49a4501116e46d935933b739b6679b11d961ff8e36abefea35dab6b25508fb"},"deployment":{"byte_parity":"PASS","path":"agent_scripts/Stage_2_S2_10.yml","sha256":"122fe890efb340ebbac299afe058bf9ad0ed87d4d0ccf2456393e9e5796ec272","size_bytes":30984},"external_admission_status":"PENDING","inline_prompt_receipt":{"path":"manifest/s2_10_inline_prompt_projection_receipt.json","sha256":"b7fdef2ad05f251bdede9473d3b69c0f169ab7f4a059b393429ff76f739f703e","size_bytes":2659},"receipt_digest":"9bd92db101c0bf6f0c48764c71f5091e573dc65cce307ce3121b5d0271efc36c","receipt_status":"OFFLINE_AGENT_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","schema":{"path":"schemas/s2_10.schema.json","sha256":"5233afde9ddb3c5868b9101816e8b3830a90b1399e3e5e89d9d7c7bf5beacaee","size_bytes":82703},"schema_version":"stage2_s2_10_agent_receipt.v2","workflow":{"path":"workflows/S2_10_domain_relief_resolution_map.yml","sha256":"f0fba48f1760cf4e233d7d698fd19090f96ffb89cabe4b2e2cae9af7c616be0f","size_bytes":15320}} {"agent_contract":{"agent_name":"Stage_2_S2_10","agent_version":"1.1.0","deterministic_task_count":0,"inline_common_prompt_sha256":"a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0","inline_static_prompt_sha256":"894c30bedaa5f230aed117e81ef4cc06cac832928a1e29cd3ee8804cfd4dba1f","item_tokens":["{{item.selected_legal_context_json}}","{{item.cluster_case_payload_json}}"],"llm_task_count":1,"prompt_roles":["system","system","system","user"],"reduce_task_count":0,"stage_name":"S2_10","task_name":"Task_S2_10_resolve_cluster","tool_task_count":0},"authoring":{"path":"Stage_2_S2_10_v.1.yml","sha256":"122fe890efb340ebbac299afe058bf9ad0ed87d4d0ccf2456393e9e5796ec272","size_bytes":30984},"binding":{"path":"deployment/stage2_s2_10_llm_binding.yml","sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04"},"child_release":{"path":"manifest/s2_10_release.json","release_digest":"683fd83a4ffe3867c3c669f627c8ae7e719a6c9fe5f306267939e5d56b95e74a","sha256":"59cfa9d83497634e46f4f48ef691a9785a0b91228748fa65c9c23197e36fe105"},"deployment":{"byte_parity":"PASS","path":"agent_scripts/Stage_2_S2_10.yml","sha256":"122fe890efb340ebbac299afe058bf9ad0ed87d4d0ccf2456393e9e5796ec272","size_bytes":30984},"external_admission_status":"PENDING","inline_prompt_receipt":{"path":"manifest/s2_10_inline_prompt_projection_receipt.json","sha256":"b7fdef2ad05f251bdede9473d3b69c0f169ab7f4a059b393429ff76f739f703e","size_bytes":2659},"receipt_digest":"eaa6900976362c532b312759d9ba8b8823eb79d7cbc0a6ed7d979ff905c7fd17","receipt_status":"OFFLINE_AGENT_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","schema":{"path":"schemas/s2_10.schema.json","sha256":"5233afde9ddb3c5868b9101816e8b3830a90b1399e3e5e89d9d7c7bf5beacaee","size_bytes":82703},"schema_version":"stage2_s2_10_agent_receipt.v2","workflow":{"path":"workflows/S2_10_domain_relief_resolution_map.yml","sha256":"f0fba48f1760cf4e233d7d698fd19090f96ffb89cabe4b2e2cae9af7c616be0f","size_bytes":15320}}
@@ -1 +1 @@
{"binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","finding_ids":[],"inline_prompt_projection_sha256":"b7fdef2ad05f251bdede9473d3b69c0f169ab7f4a059b393429ff76f739f703e","receipt_digest":"18a29c299f5587993c17f5a40b1d2c60d03556e4b37306c23352d671c4dcbb7c","review_id":"S2_10-HYBRID-LEGAL-REVIEW-PENDING","review_scope_digest":"c8675e22ff50ba9599ab3ac3108f13718b412fc0de2425133f3f49981edf0735","reviewer_role":"KOREAN_ATTORNEY","s2_10_release_sha256":"cb49a4501116e46d935933b739b6679b11d961ff8e36abefea35dab6b25508fb","schema_version":"stage2_s2_10_legal_review_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null,"status":"PENDING_KOREAN_LAWYER_REVIEW"} {"binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","finding_ids":[],"inline_prompt_projection_sha256":"b7fdef2ad05f251bdede9473d3b69c0f169ab7f4a059b393429ff76f739f703e","receipt_digest":"f501b51861c6c5291ab3a2632cac239d362437afcada29274a57e33bc8bc1f00","review_id":"S2_10-HYBRID-LEGAL-REVIEW-PENDING","review_scope_digest":"59b98748330faaf05f894d340812f54434a547775c54403c5b3df4f7c7c15dc6","reviewer_role":"KOREAN_ATTORNEY","s2_10_release_sha256":"59cfa9d83497634e46f4f48ef691a9785a0b91228748fa65c9c23197e36fe105","schema_version":"stage2_s2_10_legal_review_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null,"status":"PENDING_KOREAN_LAWYER_REVIEW"}
@@ -1 +1 @@
{"benchmark_id":"S2_10-HYBRID-MODEL-BENCHMARK-PENDING","binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","cache_telemetry_observed":null,"endpoint":"responses","latency_p95_ms":null,"model":"gpt-5.6-sol","observed_fixture_refs":[],"reasoning_effort":"xhigh","receipt_digest":"61f34770c1ebbc4c84c53ea925b07416b92f4baf4f1740294423eacbc17895ce","s2_10_release_sha256":"cb49a4501116e46d935933b739b6679b11d961ff8e36abefea35dab6b25508fb","schema_pass_rate":null,"schema_version":"stage2_s2_10_model_benchmark_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null,"status":"PENDING_MODEL_BENCHMARK","usage_telemetry_observed":null,"verbosity":"medium"} {"benchmark_id":"S2_10-HYBRID-MODEL-BENCHMARK-PENDING","binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","cache_telemetry_observed":null,"endpoint":"responses","latency_p95_ms":null,"model":"gpt-5.6-sol","observed_fixture_refs":[],"reasoning_effort":"xhigh","receipt_digest":"8b1e3184937233eedb34f998d32a584d926407c87d3ce8300bcbc8a63498aa7e","s2_10_release_sha256":"59cfa9d83497634e46f4f48ef691a9785a0b91228748fa65c9c23197e36fe105","schema_pass_rate":null,"schema_version":"stage2_s2_10_model_benchmark_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null,"status":"PENDING_MODEL_BENCHMARK","usage_telemetry_observed":null,"verbosity":"medium"}
@@ -1 +1 @@
{"adapter_contract_version":"S2_10_NATIVE_RESULT_ADAPTER_V2","binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","capabilities":[{"activation_binding_ref":null,"capability_id":"HOST_ATOMIC_SINGLE_FLIGHT_CAS_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"AGENTBACKEND_MAP_SOURCE_ITEMS_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"AGENTBACKEND_NO_REDUCE_RESULT_ADAPTER_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_STRICT_SCHEMA_AND_REF_VALIDATOR_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_DETERMINISTIC_ID_AND_IMMUTABLE_PERSIST_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_ITEM_RETRY_CONTROLLER_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"}],"overall_status":"PENDING_EXTERNAL_PLATFORM_BINDING","parent_stage2_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","receipt_digest":"3c3a005c2ff658a53c37631e82e5dc536e038bd357cacaaefbd37453b2a99245","receipt_id":"S2_10-HYBRID-PLATFORM-ADAPTER-PENDING","s2_10_release_sha256":"cb49a4501116e46d935933b739b6679b11d961ff8e36abefea35dab6b25508fb","schema_version":"stage2_s2_10_platform_adapter_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null} {"adapter_contract_version":"S2_10_NATIVE_RESULT_ADAPTER_V2","binding_sha256":"9b5d69f1316a0b9fba7b41097ee92142cf42485c8deb0be876d9e9329c317a04","capabilities":[{"activation_binding_ref":null,"capability_id":"HOST_ATOMIC_SINGLE_FLIGHT_CAS_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"AGENTBACKEND_MAP_SOURCE_ITEMS_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"AGENTBACKEND_NO_REDUCE_RESULT_ADAPTER_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_STRICT_SCHEMA_AND_REF_VALIDATOR_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_DETERMINISTIC_ID_AND_IMMUTABLE_PERSIST_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"activation_binding_ref":null,"capability_id":"S2_10_ITEM_RETRY_CONTROLLER_V1","failure_reason_codes":["PENDING_EXTERNAL_PLATFORM_BINDING"],"handler_digest":null,"handler_version":null,"implementation_ref":null,"live_evidence_refs":[],"status":"PENDING_EXTERNAL_PLATFORM_BINDING"}],"overall_status":"PENDING_EXTERNAL_PLATFORM_BINDING","parent_stage2_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","receipt_digest":"e7f6d818b3ad2ab3093e1f9edb8ab2a06783d0e728b08bb91ddb6dc03755dd34","receipt_id":"S2_10-HYBRID-PLATFORM-ADAPTER-PENDING","s2_10_release_sha256":"59cfa9d83497634e46f4f48ef691a9785a0b91228748fa65c9c23197e36fe105","schema_version":"stage2_s2_10_platform_adapter_receipt.v2","signature_ref":null,"signed_at":null,"signed_by":null}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"authoring":{"path":"Stage_2_S2_20.yml","sha256":"3c4e6a7404f5b6a6b2403824c3cd9e71db566d4f2865bd4de3e6fd9d07824bd6","size_bytes":254756},"canonical_code":{"ast_status":"PASS","code_sha256":"6f1f503378242cf64cf8f0838af2164ab1103bc7031c8a6873f4ee89be66f7fd","code_size_bytes":200269,"compile_status":"PASS","encoding":"UTF-8","endpoint_literals":["http://mcp-localdocs:8012/mcp","https://weaviate.eroomai.com/mcp"],"expected_parent_stage2_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","external_python_source_ref_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","base64","binascii","concurrent","contextlib","datetime","decimal","hashlib","httpx","io","itertools","json","pathlib","re","sys","typing","unicodedata"],"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"deployment_projection":{"path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_20.yml","sha256":"3c4e6a7404f5b6a6b2403824c3cd9e71db566d4f2865bd4de3e6fd9d07824bd6","size_bytes":254756},"full_code_mirrors":["Default_Agent/Stage_2_Clean/runtime/s2_20_reduce.py","Default_Agent/Stage_2_Clean/runtime/s2_20_reduce.txt"],"parity_status":"PASS","schema_version":"stage2_s2_20_inline_code_receipt.v1","task_contract":{"agent":{"name":"Stage_2_S2_20","version":"1.0.0"},"authoring_rewritten":false,"canonical_task_semantics_sha256":"750bf364c9b73460349005590cbf65ae4c0235b5af0770937ccbbf669d159dd0","code_mirrors_byte_identical":true,"exactly_one_code_executor_run_code":true,"exactly_one_stage":true,"exactly_one_task":true,"expected_parent_stage2_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","internal_dag":"(C20||C21)->C25->C26->(C22||(C27->C28->C29))->C30->C35","mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"projection_byte_identical_to_authoring":true,"stage":{"name":"S2_20","nexts":[],"prevs":[],"skip_confirm":true},"task":{"code_sha256":"6f1f503378242cf64cf8f0838af2164ab1103bc7031c8a6873f4ee89be66f7fd","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_20_deterministic_relief_plan","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_20_deterministic_relief_plan"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_20_deterministic_relief_plan"]},"Task_S2_20_deterministic_relief_plan":{"nexts":["OUT"],"wait_until":["IN"]}}},"workflow_id":"S2_20"} {"authoring":{"path":"Stage_2_S2_20.yml","sha256":"eb82f84df10c1eb4402e5e0f87fbe97a12e58aaa04f60c9535d66076300a5fdb","size_bytes":254756},"canonical_code":{"ast_status":"PASS","code_sha256":"91feb9e2ced624a2a4ea36e2b83bd17ed20bb059a42206fad01660800adeb515","code_size_bytes":200269,"compile_status":"PASS","encoding":"UTF-8","endpoint_literals":["http://mcp-localdocs:8012/mcp","https://weaviate.eroomai.com/mcp"],"expected_parent_stage2_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","external_python_source_ref_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","base64","binascii","concurrent","contextlib","datetime","decimal","hashlib","httpx","io","itertools","json","pathlib","re","sys","typing","unicodedata"],"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"deployment_projection":{"path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_20.yml","sha256":"eb82f84df10c1eb4402e5e0f87fbe97a12e58aaa04f60c9535d66076300a5fdb","size_bytes":254756},"full_code_mirrors":["Default_Agent/Stage_2_Clean/runtime/s2_20_reduce.py","Default_Agent/Stage_2_Clean/runtime/s2_20_reduce.txt"],"parity_status":"PASS","schema_version":"stage2_s2_20_inline_code_receipt.v1","task_contract":{"agent":{"name":"Stage_2_S2_20","version":"1.0.0"},"authoring_rewritten":false,"canonical_task_semantics_sha256":"32f3abd5bf05c546f200284339abcaee222912b698bebe6ffdae44ac511a987c","code_mirrors_byte_identical":true,"exactly_one_code_executor_run_code":true,"exactly_one_stage":true,"exactly_one_task":true,"expected_parent_stage2_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","internal_dag":"(C20||C21)->C25->C26->(C22||(C27->C28->C29))->C30->C35","mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"projection_byte_identical_to_authoring":true,"stage":{"name":"S2_20","nexts":[],"prevs":[],"skip_confirm":true},"task":{"code_sha256":"91feb9e2ced624a2a4ea36e2b83bd17ed20bb059a42206fad01660800adeb515","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_20_deterministic_relief_plan","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_20_deterministic_relief_plan"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_20_deterministic_relief_plan"]},"Task_S2_20_deterministic_relief_plan":{"nexts":["OUT"],"wait_until":["IN"]}}},"workflow_id":"S2_20"}
@@ -1 +1 @@
{"authoring_sha256":"d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd","binding_sha256":"7cf19e7dd8d3ad58251e20edc56e11fde3bffb3936997538eb09301da06189dc","byte_parity":"PASS","child_release_sha256":"e6dd6abe44b39de462ea01f5e78aa39c70bbf47e0b75e08d224615a297d2135f","deployment_sha256":"d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd","deterministic_helper_task_count":1,"llm_task_template_count":1,"receipt_digest":"fac154c1a47a050c1d8cac98a6df7abd92b0410e024c9cb26664dcef4006f13d","receipt_status":"OFFLINE_AGENT_PROJECTION_VERIFIED","reduce_task_count":0,"schema_version":"stage2_s2_30_agent_receipt.v1","task_template_count":2} {"authoring_sha256":"8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3","binding_sha256":"2a479395e3c4d829500097c296089781ebce13a2bc18aaaea0f4bf42b5fc494a","byte_parity":"PASS","child_release_sha256":"5ca1bdb3499794a770e41612d20890b002ab0edaf7bdd916c211efefc96f10f4","deployment_sha256":"8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3","deterministic_helper_task_count":1,"llm_task_template_count":1,"receipt_digest":"5ba94db0f02a029eef813bd2f71bb84896cb1ced75e1a02431393c50cbecd1b5","receipt_status":"OFFLINE_AGENT_PROJECTION_VERIFIED","reduce_task_count":0,"schema_version":"stage2_s2_30_agent_receipt.v1","task_template_count":2}
@@ -1 +1 @@
{"ast_policy_status":"PASS","authoring_agent_sha256":"d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd","compile_status":"PASS","inline_code_sha256":"1f7f3b842afbd3f1034f094d6341288effc9804a03920ada4bf8cfd512464e4c","inline_code_size_bytes":67777,"planner_mirror_paths":["runtime/s2_30_dispatch_planner.py","runtime/s2_30_dispatch_planner.txt"],"receipt_digest":"fe592796579be30899bde4006a0a3ba1d51b97b51a5b7049a3f67539158c71c9","receipt_status":"OFFLINE_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","runtime_external_python_import_count":0,"schema_version":"stage2_s2_30_inline_code_receipt.v1"} {"ast_policy_status":"PASS","authoring_agent_sha256":"8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3","compile_status":"PASS","inline_code_sha256":"17e66a89befdeb3ac4d7901eda2dad57869db809baf5faf16434cd7a7adb067a","inline_code_size_bytes":67777,"planner_mirror_paths":["runtime/s2_30_dispatch_planner.py","runtime/s2_30_dispatch_planner.txt"],"receipt_digest":"dcc0a29b9753fb8afd1ace9a0da1a34f1bd14b880b253d7d590e1ad03fff40ff","receipt_status":"OFFLINE_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","runtime_external_python_import_count":0,"schema_version":"stage2_s2_30_inline_code_receipt.v1"}
@@ -1 +1 @@
{"authoring_agent":{"path":"Stage_2_S2_30.yml","sha256":"d0804526941207395c9b64a314b124ff8e919c6852e828d829dc45a9feb3ccdd","size_bytes":117060},"dynamic_item_tokens":["{{item.p32_common_authority_json}}","{{item.p31_rule_and_pack_json}}","{{item.s30_group_slice_json}}","{{item.compile_mode}}","{{item.s30_group_slice_sha256}}"],"inline_scalars":[{"parity":"PASS","raw_scalar_sha256":"a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0","source":{"path":"prompts/P00_system_and_safety_contract.md","sha256":"c82cfc0b61adb1af7759fd321f78bcf7bebf7e5d2fc3a9c01533a80736c6d80e","size_bytes":7304},"wrapper":"stage_2_common_cache_prefix","yaml_pointer":"/Agent/Stages/0/tasks/1/prompts/0/content"},{"parity":"PASS","raw_scalar_sha256":"577efa2c3334298ec60188b9f0066f834e072dc99f785b3c6ab4d05a47fac18a","source":{"path":"prompts/P30_joint_drafting_contract.md","sha256":"5d790fccdacd1b7ce27e25cd52e4c301dbce24ba276def18bca608b5b16362aa","size_bytes":13681},"wrapper":"s2_30_joint_drafting_static_prompt","yaml_pointer":"/Agent/Stages/0/tasks/1/prompts/1/content"}],"message_order":["INLINE_COMMON_SYSTEM","INLINE_S2_30_STATIC_SYSTEM","P32_COMMON_AUTHORITY_SYSTEM_DATA","P31_RULE_AND_PACK_SYSTEM_DATA","S30_GROUP_CASE_USER_DATA"],"receipt_digest":"13b89691392616f6f70b2cb1a92a24ee4d891480b56c8c1b41b5bdabe88bbad1","receipt_status":"OFFLINE_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","schema_version":"stage2_s2_30_inline_prompt_projection_receipt.v1"} {"authoring_agent":{"path":"Stage_2_S2_30.yml","sha256":"8ff81d9a9db6b80216bf453634b93881f1c2ef7220962f077e44f703fb1457c3","size_bytes":117060},"dynamic_item_tokens":["{{item.p32_common_authority_json}}","{{item.p31_rule_and_pack_json}}","{{item.s30_group_slice_json}}","{{item.compile_mode}}","{{item.s30_group_slice_sha256}}"],"inline_scalars":[{"parity":"PASS","raw_scalar_sha256":"a90ac95f0b24ea938a16699f34ef7f17eb870edcbd8964ba4f6709e36e1bbcc0","source":{"path":"prompts/P00_system_and_safety_contract.md","sha256":"c82cfc0b61adb1af7759fd321f78bcf7bebf7e5d2fc3a9c01533a80736c6d80e","size_bytes":7304},"wrapper":"stage_2_common_cache_prefix","yaml_pointer":"/Agent/Stages/0/tasks/1/prompts/0/content"},{"parity":"PASS","raw_scalar_sha256":"577efa2c3334298ec60188b9f0066f834e072dc99f785b3c6ab4d05a47fac18a","source":{"path":"prompts/P30_joint_drafting_contract.md","sha256":"5d790fccdacd1b7ce27e25cd52e4c301dbce24ba276def18bca608b5b16362aa","size_bytes":13681},"wrapper":"s2_30_joint_drafting_static_prompt","yaml_pointer":"/Agent/Stages/0/tasks/1/prompts/1/content"}],"message_order":["INLINE_COMMON_SYSTEM","INLINE_S2_30_STATIC_SYSTEM","P32_COMMON_AUTHORITY_SYSTEM_DATA","P31_RULE_AND_PACK_SYSTEM_DATA","S30_GROUP_CASE_USER_DATA"],"receipt_digest":"172d342643993049c83b928a8445bd4954d0e9449aeab91917dec8f7cd4b1e4b","receipt_status":"OFFLINE_PROJECTION_VERIFIED","runtime_admission_effect":"NONE","schema_version":"stage2_s2_30_inline_prompt_projection_receipt.v1"}
@@ -1 +1 @@
{"binding_sha256":"7cf19e7dd8d3ad58251e20edc56e11fde3bffb3936997538eb09301da06189dc","child_release_sha256":"e6dd6abe44b39de462ea01f5e78aa39c70bbf47e0b75e08d224615a297d2135f","evidence":[],"live_execution_attested":false,"parent_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","receipt_digest":"6fc276d58d8613c0f7a2f4cb31dce0fc12c04e0239168a969f77e8a3b85437cb","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_legal_review_receipt.v1","status":"PENDING_KOREAN_LAWYER_REVIEW"} {"binding_sha256":"2a479395e3c4d829500097c296089781ebce13a2bc18aaaea0f4bf42b5fc494a","child_release_sha256":"5ca1bdb3499794a770e41612d20890b002ab0edaf7bdd916c211efefc96f10f4","evidence":[],"live_execution_attested":false,"parent_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","receipt_digest":"0eb5248b6a0068e5b6c02473d251fb2efa1993106ec2eda84af5a123c17b7891","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_legal_review_receipt.v1","status":"PENDING_KOREAN_LAWYER_REVIEW"}
@@ -1 +1 @@
{"binding_sha256":"7cf19e7dd8d3ad58251e20edc56e11fde3bffb3936997538eb09301da06189dc","child_release_sha256":"e6dd6abe44b39de462ea01f5e78aa39c70bbf47e0b75e08d224615a297d2135f","evidence":[],"live_execution_attested":false,"parent_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","receipt_digest":"d8d231359c086ae13fec794d233ebf0ba5854c207da569923ed72d8c6ca90abc","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_model_benchmark_receipt.v1","status":"PENDING_MODEL_BENCHMARK"} {"binding_sha256":"2a479395e3c4d829500097c296089781ebce13a2bc18aaaea0f4bf42b5fc494a","child_release_sha256":"5ca1bdb3499794a770e41612d20890b002ab0edaf7bdd916c211efefc96f10f4","evidence":[],"live_execution_attested":false,"parent_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","receipt_digest":"37dc500196b6282805fc8a70207a300a3a86832119b4e6595f52071fecce89f6","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_model_benchmark_receipt.v1","status":"PENDING_MODEL_BENCHMARK"}
@@ -1 +1 @@
{"binding_sha256":"7cf19e7dd8d3ad58251e20edc56e11fde3bffb3936997538eb09301da06189dc","capabilities":[{"capability_id":"HOST_ATOMIC_GROUP_DISPATCH_CAS_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"AGENTBACKEND_TASK_PROCEDURE_WILDCARD_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"AGENTBACKEND_WILDCARD_RESULT_ADAPTER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CONTEXT_MATERIALIZER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CACHE_OWNER_SEQUENCE_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_STRICT_SCHEMA_REF_PROVENANCE_VALIDATOR_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_DETERMINISTIC_ATOM_ID_TWO_PASS_PERSIST_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_GROUP_RETRY_CONTROLLER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_GROUP_BARRIER_COORDINATOR_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CANARY_AUTHORIZATION_SIGNATURE_VERIFY_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"}],"child_release_sha256":"e6dd6abe44b39de462ea01f5e78aa39c70bbf47e0b75e08d224615a297d2135f","evidence":[],"live_execution_attested":false,"overall_status":"PENDING_EXTERNAL_PLATFORM_BINDING","parent_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","receipt_digest":"31b81bdbef3bea6712c6ca2921af4e6b55e264f69b2ed341e1d83c770d3df1c5","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_platform_adapter_receipt.v1"} {"binding_sha256":"2a479395e3c4d829500097c296089781ebce13a2bc18aaaea0f4bf42b5fc494a","capabilities":[{"capability_id":"HOST_ATOMIC_GROUP_DISPATCH_CAS_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"AGENTBACKEND_TASK_PROCEDURE_WILDCARD_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"AGENTBACKEND_WILDCARD_RESULT_ADAPTER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CONTEXT_MATERIALIZER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CACHE_OWNER_SEQUENCE_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_STRICT_SCHEMA_REF_PROVENANCE_VALIDATOR_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_DETERMINISTIC_ATOM_ID_TWO_PASS_PERSIST_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_GROUP_RETRY_CONTROLLER_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_GROUP_BARRIER_COORDINATOR_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"},{"capability_id":"S2_30_CANARY_AUTHORIZATION_SIGNATURE_VERIFY_V1","status":"PENDING_EXTERNAL_PLATFORM_BINDING"}],"child_release_sha256":"5ca1bdb3499794a770e41612d20890b002ab0edaf7bdd916c211efefc96f10f4","evidence":[],"live_execution_attested":false,"overall_status":"PENDING_EXTERNAL_PLATFORM_BINDING","parent_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","receipt_digest":"dbcbe59cd79d6a368b235fc4c038434cbe6acff500defdf41f5b5ac6630f2434","runtime_admission_effect":"BLOCK_PRODUCTION","schema_version":"stage2_s2_30_platform_adapter_receipt.v1"}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"authoring":{"path":"Stage_2_S2_40.yml","sha256":"5cbe2ac9aa73d95a5fa4e6cf71d7d8f0ad83aa05536f573c13e42505ef5d46f7","size_bytes":249967,"unique_key_parse":"PASS"},"authoring_rewritten":false,"build_kind":"OFFLINE_AUTHORING_PROJECTION","canonical_code":{"ast_status":"PASS","code_sha256":"e521e1a65294a769cd6bf20edb159f8720c105b60cb769885aa416c8c763dcef","code_size_bytes":200946,"compile_status":"PASS","encoding":"UTF-8","expected_parent_stage2_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","external_python_source_ref_count":0,"external_url_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","base64","binascii","contextlib","datetime","hashlib","httpx","io","itertools","json","pathlib","re","sys","typing","unicodedata"],"placeholder_count":0,"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"deployment_projection":{"byte_identical_to_authoring":true,"canonical_task_semantics_sha256":"a8d6450b5a40308b3c2af5227fd3bc0c4f575dd8ea17b10b5bb7d6687ebc9d9e","path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_40.yml","sha256":"5cbe2ac9aa73d95a5fa4e6cf71d7d8f0ad83aa05536f573c13e42505ef5d46f7","size_bytes":249967},"expected_parent_stage2_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","full_code_mirrors":[{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_40_commit.py","sha256":"e521e1a65294a769cd6bf20edb159f8720c105b60cb769885aa416c8c763dcef","size_bytes":200946},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_40_commit.txt","sha256":"e521e1a65294a769cd6bf20edb159f8720c105b60cb769885aa416c8c763dcef","size_bytes":200946}],"parity_status":"PASS","schema_version":"stage2_s2_40_inline_code_receipt.v1","source_of_truth":"Stage_2_S2_40.yml","task_contract":{"agent":{"name":"Stage_2_S2_40","version":"1.0.0"},"authoring_rewritten":false,"canonical_task_semantics_sha256":"a8d6450b5a40308b3c2af5227fd3bc0c4f575dd8ea17b10b5bb7d6687ebc9d9e","code_mirrors_byte_identical":true,"exactly_one_code_executor_run_code":true,"exactly_one_stage":true,"exactly_one_task":true,"mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"projection_byte_identical_to_authoring":true,"stage":{"name":"S2_40","nexts":[],"prevs":[]},"task":{"code_sha256":"e521e1a65294a769cd6bf20edb159f8720c105b60cb769885aa416c8c763dcef","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_40_deterministic_finalizer","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_40_deterministic_finalizer"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_40_deterministic_finalizer"]},"Task_S2_40_deterministic_finalizer":{"nexts":["OUT"],"wait_until":["IN"]}}},"workflow_id":"S2_40"} {"authoring":{"path":"Stage_2_S2_40.yml","sha256":"a628f7fe944e21f77b9276cdb3d048189dcec958c7ed5abdcd9638b53a909a68","size_bytes":249967,"unique_key_parse":"PASS"},"authoring_rewritten":false,"build_kind":"OFFLINE_AUTHORING_PROJECTION","canonical_code":{"ast_status":"PASS","code_sha256":"12f6b83e1e6793ad871033c6e6b840fc1e31f9fb6695b1a15f0406978fe841f7","code_size_bytes":200946,"compile_status":"PASS","encoding":"UTF-8","expected_parent_stage2_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","external_python_source_ref_count":0,"external_url_count":0,"extraction_transform":"NONE","forbidden_dynamic_call_count":0,"forbidden_import_count":0,"imports":["__future__","base64","binascii","contextlib","datetime","hashlib","httpx","io","itertools","json","pathlib","re","sys","typing","unicodedata"],"placeholder_count":0,"plaintext_secret_count":0,"yaml_pointer":"/Agent/Stages/0/tasks/0/parameters/code"},"deployment_projection":{"byte_identical_to_authoring":true,"canonical_task_semantics_sha256":"de643093e087151fcc80b35be5518890ab64563f4c872260e4c56cbd00d09548","path":"Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_40.yml","sha256":"a628f7fe944e21f77b9276cdb3d048189dcec958c7ed5abdcd9638b53a909a68","size_bytes":249967},"expected_parent_stage2_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","full_code_mirrors":[{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_40_commit.py","sha256":"12f6b83e1e6793ad871033c6e6b840fc1e31f9fb6695b1a15f0406978fe841f7","size_bytes":200946},{"byte_identical_to_canonical_code":true,"path":"Default_Agent/Stage_2_Clean/runtime/s2_40_commit.txt","sha256":"12f6b83e1e6793ad871033c6e6b840fc1e31f9fb6695b1a15f0406978fe841f7","size_bytes":200946}],"parity_status":"PASS","schema_version":"stage2_s2_40_inline_code_receipt.v1","source_of_truth":"Stage_2_S2_40.yml","task_contract":{"agent":{"name":"Stage_2_S2_40","version":"1.0.0"},"authoring_rewritten":false,"canonical_task_semantics_sha256":"de643093e087151fcc80b35be5518890ab64563f4c872260e4c56cbd00d09548","code_mirrors_byte_identical":true,"exactly_one_code_executor_run_code":true,"exactly_one_stage":true,"exactly_one_task":true,"mcp_servers":{"code-executor":{"type":"streamable-http","url":"https://code-executor.mcp.eroomai.com/mcp"},"localdocs":{"type":"streamable-http","url":"http://mcp-localdocs:8012/mcp"}},"projection_byte_identical_to_authoring":true,"stage":{"name":"S2_40","nexts":[],"prevs":[]},"task":{"code_sha256":"12f6b83e1e6793ad871033c6e6b840fc1e31f9fb6695b1a15f0406978fe841f7","mcp":"code-executor","parameters":{"language":"python","network":"agent-network","requirements":"httpx==0.28.1","timeout":300},"task_name":"Task_S2_40_deterministic_finalizer","tool_name":"run_code"},"task_procedure":{"IN":{"nexts":["Task_S2_40_deterministic_finalizer"],"wait_until":[]},"OUT":{"nexts":[],"wait_until":["Task_S2_40_deterministic_finalizer"]},"Task_S2_40_deterministic_finalizer":{"nexts":["OUT"],"wait_until":["IN"]}}},"workflow_id":"S2_40"}
@@ -1 +1 @@
{"admission_status":"PENDING","backend_capability_receipt_sha256":"PENDING_SEQUENTIAL_BIND","embedded_task_admissions":[{"admission_scope":"CODE_EXECUTOR_HELPER_ONLY","binding_id":"S2-BINDING-S2_30-DISPATCH-PLANNER-V1","execution_unit_id":"S2_30::Task_S2_30_dispatch_planner","host_stage_id":"S2_30","inline_code_receipt_ref":{"asset_id":"RECEIPT-S2_30-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_30_inline_code_receipt.json","schema_id":"stage2_s2_30_inline_code_receipt.v1","sha256":"8d1618771cf1f72aa3c6a5a4544fe6b6e949ccc7ecfef56f166561971beee896"},"task_name":"Task_S2_30_dispatch_planner"}],"executor_binding_sha256":"a1062e9db242747c75a4362fb95eeba6c65f78ac647d4571ed774d2ee3af40f7","parent_release_sha256":"9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53","present_deterministic_stage_ids":["S2_00","S2_20","S2_40"],"schema_version":"stage2_deterministic_admission_receipt.v1","signature":"PENDING_EXTERNAL_SIGNATURE","signature_verification_status":"PENDING_EXTERNAL_SIGNATURE","signed_payload_sha256":"PENDING_SEQUENTIAL_BIND","stage_receipts":[{"asset_id":"RECEIPT-S2_00-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_00_inline_code_receipt.json","schema_id":"stage2_s2_00_inline_code_receipt.v2","sha256":"36af21949e5ef53a3d39df8ea9491c64da43abcc4f5e42e13db9bb391da843b8"},{"asset_id":"RECEIPT-S2_20-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_20_inline_code_receipt.json","schema_id":"stage2_s2_20_inline_code_receipt.v1","sha256":"65e29f10f065cdd77281aa47acaf341edb38b0595aab0aa9f04bbee3a4718608"},{"asset_id":"RECEIPT-S2_40-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_40_inline_code_receipt.json","schema_id":"stage2_s2_40_inline_code_receipt.v1","sha256":"9bd0cfaee8d9e78480ab80a2fa541414c7ad0565439fa0f4661701fcdfde6526"}]} {"admission_status":"PENDING","backend_capability_receipt_sha256":"PENDING_SEQUENTIAL_BIND","embedded_task_admissions":[{"admission_scope":"CODE_EXECUTOR_HELPER_ONLY","binding_id":"S2-BINDING-S2_30-DISPATCH-PLANNER-V1","execution_unit_id":"S2_30::Task_S2_30_dispatch_planner","host_stage_id":"S2_30","inline_code_receipt_ref":{"asset_id":"RECEIPT-S2_30-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_30_inline_code_receipt.json","schema_id":"stage2_s2_30_inline_code_receipt.v1","sha256":"a5e4e3161fc16f3e44ec9ae2c05d2c4706eca02099d844520973f76475343998"},"task_name":"Task_S2_30_dispatch_planner"}],"executor_binding_sha256":"bc9aad7a5b817b5f4933bb74cd54782f3d777397a077f64fb9cd9ade55e4c40d","parent_release_sha256":"2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590","present_deterministic_stage_ids":["S2_00","S2_20","S2_40"],"schema_version":"stage2_deterministic_admission_receipt.v1","signature":"PENDING_EXTERNAL_SIGNATURE","signature_verification_status":"PENDING_EXTERNAL_SIGNATURE","signed_payload_sha256":"PENDING_SEQUENTIAL_BIND","stage_receipts":[{"asset_id":"RECEIPT-S2_00-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_00_inline_code_receipt.json","schema_id":"stage2_s2_00_inline_code_receipt.v2","sha256":"be044ab40ee86e7c3a1be6c421b2c5d36ee67fc2fd2870f59a3d9d911fef6c1a"},{"asset_id":"RECEIPT-S2_20-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_20_inline_code_receipt.json","schema_id":"stage2_s2_20_inline_code_receipt.v1","sha256":"64fd3711bd1128b6d29d3b8ff7a6af07228764da9604e0b5506562c5420e83e9"},{"asset_id":"RECEIPT-S2_40-INLINE-CODE","binding_status":"BOUND","path":"manifest/s2_40_inline_code_receipt.json","schema_id":"stage2_s2_40_inline_code_receipt.v1","sha256":"38fa1f5d8fc930ee3f7bbff7c73e1a52efc4995d14126ae035323ff74f1abf3d"}]}
File diff suppressed because one or more lines are too long
@@ -33,6 +33,8 @@ FORBIDDEN_PARENT_MEMBERS = frozenset(
"agent_scripts/Stage_2_S2_00.yml", "agent_scripts/Stage_2_S2_00.yml",
"runtime/s2_00_ingress.py", "runtime/s2_00_ingress.py",
"runtime/s2_00_ingress.txt", "runtime/s2_00_ingress.txt",
"runtime/s2_00_prepare_request.py",
"runtime/s2_00_prepare_request.txt",
"manifest/s2_00_inline_code_receipt.json", "manifest/s2_00_inline_code_receipt.json",
"agent_scripts/Stage_2_S2_20.yml", "agent_scripts/Stage_2_S2_20.yml",
"runtime/s2_20_reduce.py", "runtime/s2_20_reduce.py",
@@ -33,6 +33,8 @@ FORBIDDEN_PARENT_MEMBERS = frozenset(
"agent_scripts/Stage_2_S2_00.yml", "agent_scripts/Stage_2_S2_00.yml",
"runtime/s2_00_ingress.py", "runtime/s2_00_ingress.py",
"runtime/s2_00_ingress.txt", "runtime/s2_00_ingress.txt",
"runtime/s2_00_prepare_request.py",
"runtime/s2_00_prepare_request.txt",
"manifest/s2_00_inline_code_receipt.json", "manifest/s2_00_inline_code_receipt.json",
"agent_scripts/Stage_2_S2_20.yml", "agent_scripts/Stage_2_S2_20.yml",
"runtime/s2_20_reduce.py", "runtime/s2_20_reduce.py",
@@ -32,10 +32,13 @@ AUTHORING_PATH = MAIN_WORKING_DIRECTORY / "Stage_2_S2_00_v.2.yml"
PROJECTION_PATH = DEPLOYMENT_ROOT / "agent_scripts" / "Stage_2_S2_00.yml" PROJECTION_PATH = DEPLOYMENT_ROOT / "agent_scripts" / "Stage_2_S2_00.yml"
MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.py" MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.py"
MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.txt" MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.txt"
PREPARE_MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_prepare_request.py"
PREPARE_MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_prepare_request.txt"
RECEIPT_PATH = DEPLOYMENT_ROOT / "manifest" / "s2_00_inline_code_receipt.json" RECEIPT_PATH = DEPLOYMENT_ROOT / "manifest" / "s2_00_inline_code_receipt.json"
BINDING_PATH = DEPLOYMENT_ROOT / "deployment" / "stage2_code_executor_binding.yml" BINDING_PATH = DEPLOYMENT_ROOT / "deployment" / "stage2_code_executor_binding.yml"
EXPECTED_TASK_NAME = "Task_S2_00_deterministic_ingress" EXPECTED_TASK_NAME = "Task_S2_00_deterministic_ingress"
EXPECTED_PREPARE_TASK_NAME = "Task_S2_00_prepare_request"
EXPECTED_AGENT_NAME = "Stage_2_S2_00_v2" EXPECTED_AGENT_NAME = "Stage_2_S2_00_v2"
EXPECTED_AGENT_VERSION = "1.2.0" EXPECTED_AGENT_VERSION = "1.2.0"
EXPECTED_PARAMETERS = { EXPECTED_PARAMETERS = {
@@ -252,6 +255,20 @@ def select_s2_00_binding(document: Mapping[str, Any]) -> dict[str, Any]:
"expected": None, "expected": None,
"observed": row.get("external_mcp_contract"), "observed": row.get("external_mcp_contract"),
} }
expected_prepare_contract = {
"task_name": EXPECTED_PREPARE_TASK_NAME,
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code",
"required_external_arguments": ["request_id", "attempt_id", "stage1_run_root_ref", "stage1_deployment_root_ref"],
"write_path": "stage2_control/s2_00_request.json",
"argument_binding_status": "UNVERIFIED",
"success_gate_status": "UNVERIFIED",
"workspace_serialization_status": "UNVERIFIED",
}
if row.get("prepare_request_contract") != expected_prepare_contract:
drift["prepare_request_contract"] = {
"expected": expected_prepare_contract,
"observed": row.get("prepare_request_contract"),
}
if drift: if drift:
raise ProjectionError( raise ProjectionError(
"S2_00_BINDING_SEMANTICS_DRIFT", "S2_00_BINDING_SEMANTICS_DRIFT",
@@ -280,7 +297,7 @@ def _require_list(value: Any, code: str) -> list[Any]:
return value return value
def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: def extract_run_code_tasks(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
agent = _require_mapping(document.get("Agent"), "AGENT_OBJECT_REQUIRED") agent = _require_mapping(document.get("Agent"), "AGENT_OBJECT_REQUIRED")
if agent.get("name") != EXPECTED_AGENT_NAME or agent.get("version") != EXPECTED_AGENT_VERSION: if agent.get("name") != EXPECTED_AGENT_NAME or agent.get("version") != EXPECTED_AGENT_VERSION:
raise ProjectionError( raise ProjectionError(
@@ -324,9 +341,10 @@ def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any],
) )
tasks = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED") tasks = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED")
if len(tasks) != 1: if len(tasks) != 2:
raise ProjectionError("EXACTLY_ONE_TASK_REQUIRED", str(len(tasks))) raise ProjectionError("EXACTLY_TWO_TASKS_REQUIRED", str(len(tasks)))
task = _require_mapping(tasks[0], "TASK_OBJECT_REQUIRED") prepare_task = _require_mapping(tasks[0], "PREPARE_TASK_OBJECT_REQUIRED")
task = _require_mapping(tasks[1], "INGRESS_TASK_OBJECT_REQUIRED")
run_code_tasks = [ run_code_tasks = [
row row
for row in tasks for row in tasks
@@ -334,43 +352,54 @@ def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any],
and row.get("mcp") == "code-executor" and row.get("mcp") == "code-executor"
and row.get("tool_name") == "run_code" and row.get("tool_name") == "run_code"
] ]
if len(run_code_tasks) != 1: if len(run_code_tasks) != 2:
raise ProjectionError("EXACTLY_ONE_RUN_CODE_REQUIRED", str(len(run_code_tasks))) raise ProjectionError("EXACTLY_TWO_RUN_CODE_REQUIRED", str(len(run_code_tasks)))
if prepare_task.get("task_name") != EXPECTED_PREPARE_TASK_NAME:
raise ProjectionError("PREPARE_TASK_NAME_MISMATCH", repr(prepare_task.get("task_name")))
if task.get("task_name") != EXPECTED_TASK_NAME: if task.get("task_name") != EXPECTED_TASK_NAME:
raise ProjectionError("TASK_NAME_MISMATCH", repr(task.get("task_name"))) raise ProjectionError("TASK_NAME_MISMATCH", repr(task.get("task_name")))
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") for current in (prepare_task, task):
expected_parameter_keys = set(EXPECTED_PARAMETERS) | {"code"} if current.get("mcp") != "code-executor" or current.get("tool_name") != "run_code":
if set(parameters) != expected_parameter_keys: raise ProjectionError("RUN_CODE_TOOL_MISMATCH", repr(current.get("task_name")))
raise ProjectionError( parameters = _require_mapping(current.get("parameters"), "TASK_PARAMETERS_REQUIRED")
"RUN_CODE_PARAMETER_SET_MISMATCH", expected_parameter_keys = set(EXPECTED_PARAMETERS) | {"code"}
repr(sorted(parameters)), if set(parameters) != expected_parameter_keys:
)
for key, expected in EXPECTED_PARAMETERS.items():
if parameters.get(key) != expected:
raise ProjectionError( raise ProjectionError(
"RUN_CODE_PARAMETER_MISMATCH", "RUN_CODE_PARAMETER_SET_MISMATCH", repr(sorted(parameters)),
f"{key}: expected {expected!r}, observed {parameters.get(key)!r}", )
for key, expected in EXPECTED_PARAMETERS.items():
if parameters.get(key) != expected:
raise ProjectionError(
"RUN_CODE_PARAMETER_MISMATCH",
f"{key}: expected {expected!r}, observed {parameters.get(key)!r}",
)
code = parameters.get("code")
if not isinstance(code, str) or not code:
raise ProjectionError("INLINE_CODE_REQUIRED", repr(code)[:100])
if code.endswith(("\n", "\r")):
raise ProjectionError(
"INLINE_CODE_TRAILING_NEWLINE_FORBIDDEN",
"authoring YAML must use code: |- so parser-returned code has no trailing newline",
) )
code = parameters.get("code")
if not isinstance(code, str) or not code:
raise ProjectionError("INLINE_CODE_REQUIRED", repr(code)[:100])
if code.endswith(("\n", "\r")):
raise ProjectionError(
"INLINE_CODE_TRAILING_NEWLINE_FORBIDDEN",
"authoring YAML must use code: |- so parser-returned code has no trailing newline",
)
procedure = _require_mapping(stage.get("task_procedure"), "TASK_PROCEDURE_REQUIRED") procedure = _require_mapping(stage.get("task_procedure"), "TASK_PROCEDURE_REQUIRED")
expected_procedure = { expected_procedure = {
"IN": {"nexts": [EXPECTED_TASK_NAME], "wait_until": []}, "IN": {"nexts": [EXPECTED_PREPARE_TASK_NAME], "wait_until": []},
EXPECTED_TASK_NAME: {"nexts": ["OUT"], "wait_until": ["IN"]}, EXPECTED_PREPARE_TASK_NAME: {"nexts": [EXPECTED_TASK_NAME], "wait_until": ["IN"]},
EXPECTED_TASK_NAME: {"nexts": ["OUT"], "wait_until": [EXPECTED_PREPARE_TASK_NAME]},
"OUT": {"nexts": [], "wait_until": [EXPECTED_TASK_NAME]}, "OUT": {"nexts": [], "wait_until": [EXPECTED_TASK_NAME]},
} }
if procedure != expected_procedure: if procedure != expected_procedure:
raise ProjectionError("TASK_PROCEDURE_MISMATCH", repr(procedure)[:500]) raise ProjectionError("TASK_PROCEDURE_MISMATCH", repr(procedure)[:500])
if stage.get("prevs") != [] or stage.get("nexts") != []: if stage.get("prevs") != [] or stage.get("nexts") != []:
raise ProjectionError("STANDALONE_STAGE_EDGES_MUST_BE_EMPTY", repr(stage)) raise ProjectionError("STANDALONE_STAGE_EDGES_MUST_BE_EMPTY", repr(stage))
return stage, task return stage, prepare_task, task
def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
"""Compatibility accessor for the ingress task; validates the complete DAG."""
stage, _prepare, ingress = extract_run_code_tasks(document)
return stage, ingress
def _import_root(name: str | None) -> str: def _import_root(name: str | None) -> str:
@@ -435,7 +464,7 @@ def _is_localdocs_endpoint(node: ast.expr) -> bool:
) )
def validate_inline_code(code: str) -> dict[str, Any]: def validate_inline_code(code: str, *, prepare: bool = False) -> dict[str, Any]:
code_bytes = code.encode("utf-8") code_bytes = code.encode("utf-8")
try: try:
compile(code, "<Stage_2_S2_00.parameters.code>", "exec", dont_inherit=True) compile(code, "<Stage_2_S2_00.parameters.code>", "exec", dont_inherit=True)
@@ -443,12 +472,19 @@ def validate_inline_code(code: str) -> dict[str, Any]:
except (SyntaxError, ValueError, UnicodeError) as exc: except (SyntaxError, ValueError, UnicodeError) as exc:
raise ProjectionError("INLINE_CODE_COMPILE_FAILED", str(exc)) from exc raise ProjectionError("INLINE_CODE_COMPILE_FAILED", str(exc)) from exc
missing_tokens = [token for token in REQUIRED_CODE_TOKENS if token not in code] required_tokens = (
missing_tokens.extend( (EXPECTED_LOCALDOCS_URL, "{{__user_hash__}}", "{{__workspace_hash__}}",
"|".join(alternatives) "build_request", "prepare_request", "write_binary_verified",
for alternatives in REQUIRED_CODE_TOKEN_ALTERNATIVES "read_binary_doc", "stage2_control/s2_00_request.json")
if not any(token in code for token in alternatives) if prepare else REQUIRED_CODE_TOKENS
) )
missing_tokens = [token for token in required_tokens if token not in code]
if not prepare:
missing_tokens.extend(
"|".join(alternatives)
for alternatives in REQUIRED_CODE_TOKEN_ALTERNATIVES
if not any(token in code for token in alternatives)
)
if missing_tokens: if missing_tokens:
raise ProjectionError("INLINE_CODE_REQUIRED_TOKEN_MISSING", ",".join(missing_tokens)) raise ProjectionError("INLINE_CODE_REQUIRED_TOKEN_MISSING", ",".join(missing_tokens))
if PLACEHOLDER_COMMENT_RE.search(code): if PLACEHOLDER_COMMENT_RE.search(code):
@@ -651,6 +687,8 @@ def _canonical_task_semantics(document: Mapping[str, Any], stage: Mapping[str, A
"MCP_SERVERS_OBJECT_REQUIRED", "MCP_SERVERS_OBJECT_REQUIRED",
) )
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED")
prepare_task = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED")[0]
prepare_parameters = _require_mapping(prepare_task.get("parameters"), "PREPARE_PARAMETERS_REQUIRED")
return { return {
"agent": {"name": agent.get("name"), "version": agent.get("version")}, "agent": {"name": agent.get("name"), "version": agent.get("version")},
"stage": { "stage": {
@@ -663,6 +701,16 @@ def _canonical_task_semantics(document: Mapping[str, Any], stage: Mapping[str, A
for name, value in sorted(servers.items()) for name, value in sorted(servers.items())
if isinstance(value, dict) if isinstance(value, dict)
}, },
"prepare_task": {
"task_name": prepare_task.get("task_name"),
"mcp": prepare_task.get("mcp"),
"tool_name": prepare_task.get("tool_name"),
"parameters": {
key: prepare_parameters.get(key)
for key in ("language", "requirements", "network", "timeout")
},
"code_sha256": sha256_bytes(prepare_parameters["code"].encode("utf-8")),
},
"task": { "task": {
"task_name": task.get("task_name"), "task_name": task.get("task_name"),
"mcp": task.get("mcp"), "mcp": task.get("mcp"),
@@ -681,7 +729,10 @@ def expected_outputs(
authoring_raw: bytes, authoring_raw: bytes,
document: Mapping[str, Any], document: Mapping[str, Any],
) -> tuple[dict[Path, bytes], dict[str, Any]]: ) -> tuple[dict[Path, bytes], dict[str, Any]]:
stage, task = extract_run_code_task(document) stage, prepare_task, task = extract_run_code_tasks(document)
prepare_code = _require_mapping(prepare_task.get("parameters"), "PREPARE_PARAMETERS_REQUIRED")["code"]
prepare_validation = validate_inline_code(prepare_code, prepare=True)
prepare_bytes = prepare_code.encode("utf-8")
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED")
code = parameters["code"] code = parameters["code"]
validation = validate_inline_code(code) validation = validate_inline_code(code)
@@ -709,12 +760,30 @@ def expected_outputs(
"canonical_task_semantics_sha256": semantics_sha256, "canonical_task_semantics_sha256": semantics_sha256,
}, },
"canonical_code": { "canonical_code": {
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code", "yaml_pointer": "/Agent/Stages/0/tasks/1/parameters/code",
"encoding": "UTF-8", "encoding": "UTF-8",
"extraction_transform": "NONE", "extraction_transform": "NONE",
**validation, **validation,
}, },
"prepare_code": {
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code",
"encoding": "UTF-8",
"extraction_transform": "NONE",
**prepare_validation,
},
"full_code_mirrors": [ "full_code_mirrors": [
{
"path": _logical_path(PREPARE_MIRROR_PY_PATH),
"sha256": prepare_validation["code_sha256"],
"size_bytes": len(prepare_bytes),
"byte_identical_to_canonical_code": True,
},
{
"path": _logical_path(PREPARE_MIRROR_TXT_PATH),
"sha256": prepare_validation["code_sha256"],
"size_bytes": len(prepare_bytes),
"byte_identical_to_canonical_code": True,
},
{ {
"path": _logical_path(MIRROR_PY_PATH), "path": _logical_path(MIRROR_PY_PATH),
"sha256": validation["code_sha256"], "sha256": validation["code_sha256"],
@@ -733,6 +802,8 @@ def expected_outputs(
} }
outputs = { outputs = {
PROJECTION_PATH: authoring_raw, PROJECTION_PATH: authoring_raw,
PREPARE_MIRROR_PY_PATH: prepare_bytes,
PREPARE_MIRROR_TXT_PATH: prepare_bytes,
MIRROR_PY_PATH: code_bytes, MIRROR_PY_PATH: code_bytes,
MIRROR_TXT_PATH: code_bytes, MIRROR_TXT_PATH: code_bytes,
RECEIPT_PATH: canonical_json_bytes(receipt), RECEIPT_PATH: canonical_json_bytes(receipt),
@@ -32,10 +32,13 @@ AUTHORING_PATH = MAIN_WORKING_DIRECTORY / "Stage_2_S2_00_v.2.yml"
PROJECTION_PATH = DEPLOYMENT_ROOT / "agent_scripts" / "Stage_2_S2_00.yml" PROJECTION_PATH = DEPLOYMENT_ROOT / "agent_scripts" / "Stage_2_S2_00.yml"
MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.py" MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.py"
MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.txt" MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_ingress.txt"
PREPARE_MIRROR_PY_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_prepare_request.py"
PREPARE_MIRROR_TXT_PATH = DEPLOYMENT_ROOT / "runtime" / "s2_00_prepare_request.txt"
RECEIPT_PATH = DEPLOYMENT_ROOT / "manifest" / "s2_00_inline_code_receipt.json" RECEIPT_PATH = DEPLOYMENT_ROOT / "manifest" / "s2_00_inline_code_receipt.json"
BINDING_PATH = DEPLOYMENT_ROOT / "deployment" / "stage2_code_executor_binding.yml" BINDING_PATH = DEPLOYMENT_ROOT / "deployment" / "stage2_code_executor_binding.yml"
EXPECTED_TASK_NAME = "Task_S2_00_deterministic_ingress" EXPECTED_TASK_NAME = "Task_S2_00_deterministic_ingress"
EXPECTED_PREPARE_TASK_NAME = "Task_S2_00_prepare_request"
EXPECTED_AGENT_NAME = "Stage_2_S2_00_v2" EXPECTED_AGENT_NAME = "Stage_2_S2_00_v2"
EXPECTED_AGENT_VERSION = "1.2.0" EXPECTED_AGENT_VERSION = "1.2.0"
EXPECTED_PARAMETERS = { EXPECTED_PARAMETERS = {
@@ -252,6 +255,20 @@ def select_s2_00_binding(document: Mapping[str, Any]) -> dict[str, Any]:
"expected": None, "expected": None,
"observed": row.get("external_mcp_contract"), "observed": row.get("external_mcp_contract"),
} }
expected_prepare_contract = {
"task_name": EXPECTED_PREPARE_TASK_NAME,
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code",
"required_external_arguments": ["request_id", "attempt_id", "stage1_run_root_ref", "stage1_deployment_root_ref"],
"write_path": "stage2_control/s2_00_request.json",
"argument_binding_status": "UNVERIFIED",
"success_gate_status": "UNVERIFIED",
"workspace_serialization_status": "UNVERIFIED",
}
if row.get("prepare_request_contract") != expected_prepare_contract:
drift["prepare_request_contract"] = {
"expected": expected_prepare_contract,
"observed": row.get("prepare_request_contract"),
}
if drift: if drift:
raise ProjectionError( raise ProjectionError(
"S2_00_BINDING_SEMANTICS_DRIFT", "S2_00_BINDING_SEMANTICS_DRIFT",
@@ -280,7 +297,7 @@ def _require_list(value: Any, code: str) -> list[Any]:
return value return value
def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: def extract_run_code_tasks(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
agent = _require_mapping(document.get("Agent"), "AGENT_OBJECT_REQUIRED") agent = _require_mapping(document.get("Agent"), "AGENT_OBJECT_REQUIRED")
if agent.get("name") != EXPECTED_AGENT_NAME or agent.get("version") != EXPECTED_AGENT_VERSION: if agent.get("name") != EXPECTED_AGENT_NAME or agent.get("version") != EXPECTED_AGENT_VERSION:
raise ProjectionError( raise ProjectionError(
@@ -324,9 +341,10 @@ def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any],
) )
tasks = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED") tasks = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED")
if len(tasks) != 1: if len(tasks) != 2:
raise ProjectionError("EXACTLY_ONE_TASK_REQUIRED", str(len(tasks))) raise ProjectionError("EXACTLY_TWO_TASKS_REQUIRED", str(len(tasks)))
task = _require_mapping(tasks[0], "TASK_OBJECT_REQUIRED") prepare_task = _require_mapping(tasks[0], "PREPARE_TASK_OBJECT_REQUIRED")
task = _require_mapping(tasks[1], "INGRESS_TASK_OBJECT_REQUIRED")
run_code_tasks = [ run_code_tasks = [
row row
for row in tasks for row in tasks
@@ -334,43 +352,54 @@ def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any],
and row.get("mcp") == "code-executor" and row.get("mcp") == "code-executor"
and row.get("tool_name") == "run_code" and row.get("tool_name") == "run_code"
] ]
if len(run_code_tasks) != 1: if len(run_code_tasks) != 2:
raise ProjectionError("EXACTLY_ONE_RUN_CODE_REQUIRED", str(len(run_code_tasks))) raise ProjectionError("EXACTLY_TWO_RUN_CODE_REQUIRED", str(len(run_code_tasks)))
if prepare_task.get("task_name") != EXPECTED_PREPARE_TASK_NAME:
raise ProjectionError("PREPARE_TASK_NAME_MISMATCH", repr(prepare_task.get("task_name")))
if task.get("task_name") != EXPECTED_TASK_NAME: if task.get("task_name") != EXPECTED_TASK_NAME:
raise ProjectionError("TASK_NAME_MISMATCH", repr(task.get("task_name"))) raise ProjectionError("TASK_NAME_MISMATCH", repr(task.get("task_name")))
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") for current in (prepare_task, task):
expected_parameter_keys = set(EXPECTED_PARAMETERS) | {"code"} if current.get("mcp") != "code-executor" or current.get("tool_name") != "run_code":
if set(parameters) != expected_parameter_keys: raise ProjectionError("RUN_CODE_TOOL_MISMATCH", repr(current.get("task_name")))
raise ProjectionError( parameters = _require_mapping(current.get("parameters"), "TASK_PARAMETERS_REQUIRED")
"RUN_CODE_PARAMETER_SET_MISMATCH", expected_parameter_keys = set(EXPECTED_PARAMETERS) | {"code"}
repr(sorted(parameters)), if set(parameters) != expected_parameter_keys:
)
for key, expected in EXPECTED_PARAMETERS.items():
if parameters.get(key) != expected:
raise ProjectionError( raise ProjectionError(
"RUN_CODE_PARAMETER_MISMATCH", "RUN_CODE_PARAMETER_SET_MISMATCH", repr(sorted(parameters)),
f"{key}: expected {expected!r}, observed {parameters.get(key)!r}", )
for key, expected in EXPECTED_PARAMETERS.items():
if parameters.get(key) != expected:
raise ProjectionError(
"RUN_CODE_PARAMETER_MISMATCH",
f"{key}: expected {expected!r}, observed {parameters.get(key)!r}",
)
code = parameters.get("code")
if not isinstance(code, str) or not code:
raise ProjectionError("INLINE_CODE_REQUIRED", repr(code)[:100])
if code.endswith(("\n", "\r")):
raise ProjectionError(
"INLINE_CODE_TRAILING_NEWLINE_FORBIDDEN",
"authoring YAML must use code: |- so parser-returned code has no trailing newline",
) )
code = parameters.get("code")
if not isinstance(code, str) or not code:
raise ProjectionError("INLINE_CODE_REQUIRED", repr(code)[:100])
if code.endswith(("\n", "\r")):
raise ProjectionError(
"INLINE_CODE_TRAILING_NEWLINE_FORBIDDEN",
"authoring YAML must use code: |- so parser-returned code has no trailing newline",
)
procedure = _require_mapping(stage.get("task_procedure"), "TASK_PROCEDURE_REQUIRED") procedure = _require_mapping(stage.get("task_procedure"), "TASK_PROCEDURE_REQUIRED")
expected_procedure = { expected_procedure = {
"IN": {"nexts": [EXPECTED_TASK_NAME], "wait_until": []}, "IN": {"nexts": [EXPECTED_PREPARE_TASK_NAME], "wait_until": []},
EXPECTED_TASK_NAME: {"nexts": ["OUT"], "wait_until": ["IN"]}, EXPECTED_PREPARE_TASK_NAME: {"nexts": [EXPECTED_TASK_NAME], "wait_until": ["IN"]},
EXPECTED_TASK_NAME: {"nexts": ["OUT"], "wait_until": [EXPECTED_PREPARE_TASK_NAME]},
"OUT": {"nexts": [], "wait_until": [EXPECTED_TASK_NAME]}, "OUT": {"nexts": [], "wait_until": [EXPECTED_TASK_NAME]},
} }
if procedure != expected_procedure: if procedure != expected_procedure:
raise ProjectionError("TASK_PROCEDURE_MISMATCH", repr(procedure)[:500]) raise ProjectionError("TASK_PROCEDURE_MISMATCH", repr(procedure)[:500])
if stage.get("prevs") != [] or stage.get("nexts") != []: if stage.get("prevs") != [] or stage.get("nexts") != []:
raise ProjectionError("STANDALONE_STAGE_EDGES_MUST_BE_EMPTY", repr(stage)) raise ProjectionError("STANDALONE_STAGE_EDGES_MUST_BE_EMPTY", repr(stage))
return stage, task return stage, prepare_task, task
def extract_run_code_task(document: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
"""Compatibility accessor for the ingress task; validates the complete DAG."""
stage, _prepare, ingress = extract_run_code_tasks(document)
return stage, ingress
def _import_root(name: str | None) -> str: def _import_root(name: str | None) -> str:
@@ -435,7 +464,7 @@ def _is_localdocs_endpoint(node: ast.expr) -> bool:
) )
def validate_inline_code(code: str) -> dict[str, Any]: def validate_inline_code(code: str, *, prepare: bool = False) -> dict[str, Any]:
code_bytes = code.encode("utf-8") code_bytes = code.encode("utf-8")
try: try:
compile(code, "<Stage_2_S2_00.parameters.code>", "exec", dont_inherit=True) compile(code, "<Stage_2_S2_00.parameters.code>", "exec", dont_inherit=True)
@@ -443,12 +472,19 @@ def validate_inline_code(code: str) -> dict[str, Any]:
except (SyntaxError, ValueError, UnicodeError) as exc: except (SyntaxError, ValueError, UnicodeError) as exc:
raise ProjectionError("INLINE_CODE_COMPILE_FAILED", str(exc)) from exc raise ProjectionError("INLINE_CODE_COMPILE_FAILED", str(exc)) from exc
missing_tokens = [token for token in REQUIRED_CODE_TOKENS if token not in code] required_tokens = (
missing_tokens.extend( (EXPECTED_LOCALDOCS_URL, "{{__user_hash__}}", "{{__workspace_hash__}}",
"|".join(alternatives) "build_request", "prepare_request", "write_binary_verified",
for alternatives in REQUIRED_CODE_TOKEN_ALTERNATIVES "read_binary_doc", "stage2_control/s2_00_request.json")
if not any(token in code for token in alternatives) if prepare else REQUIRED_CODE_TOKENS
) )
missing_tokens = [token for token in required_tokens if token not in code]
if not prepare:
missing_tokens.extend(
"|".join(alternatives)
for alternatives in REQUIRED_CODE_TOKEN_ALTERNATIVES
if not any(token in code for token in alternatives)
)
if missing_tokens: if missing_tokens:
raise ProjectionError("INLINE_CODE_REQUIRED_TOKEN_MISSING", ",".join(missing_tokens)) raise ProjectionError("INLINE_CODE_REQUIRED_TOKEN_MISSING", ",".join(missing_tokens))
if PLACEHOLDER_COMMENT_RE.search(code): if PLACEHOLDER_COMMENT_RE.search(code):
@@ -651,6 +687,8 @@ def _canonical_task_semantics(document: Mapping[str, Any], stage: Mapping[str, A
"MCP_SERVERS_OBJECT_REQUIRED", "MCP_SERVERS_OBJECT_REQUIRED",
) )
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED")
prepare_task = _require_list(stage.get("tasks"), "TASKS_ARRAY_REQUIRED")[0]
prepare_parameters = _require_mapping(prepare_task.get("parameters"), "PREPARE_PARAMETERS_REQUIRED")
return { return {
"agent": {"name": agent.get("name"), "version": agent.get("version")}, "agent": {"name": agent.get("name"), "version": agent.get("version")},
"stage": { "stage": {
@@ -663,6 +701,16 @@ def _canonical_task_semantics(document: Mapping[str, Any], stage: Mapping[str, A
for name, value in sorted(servers.items()) for name, value in sorted(servers.items())
if isinstance(value, dict) if isinstance(value, dict)
}, },
"prepare_task": {
"task_name": prepare_task.get("task_name"),
"mcp": prepare_task.get("mcp"),
"tool_name": prepare_task.get("tool_name"),
"parameters": {
key: prepare_parameters.get(key)
for key in ("language", "requirements", "network", "timeout")
},
"code_sha256": sha256_bytes(prepare_parameters["code"].encode("utf-8")),
},
"task": { "task": {
"task_name": task.get("task_name"), "task_name": task.get("task_name"),
"mcp": task.get("mcp"), "mcp": task.get("mcp"),
@@ -681,7 +729,10 @@ def expected_outputs(
authoring_raw: bytes, authoring_raw: bytes,
document: Mapping[str, Any], document: Mapping[str, Any],
) -> tuple[dict[Path, bytes], dict[str, Any]]: ) -> tuple[dict[Path, bytes], dict[str, Any]]:
stage, task = extract_run_code_task(document) stage, prepare_task, task = extract_run_code_tasks(document)
prepare_code = _require_mapping(prepare_task.get("parameters"), "PREPARE_PARAMETERS_REQUIRED")["code"]
prepare_validation = validate_inline_code(prepare_code, prepare=True)
prepare_bytes = prepare_code.encode("utf-8")
parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED") parameters = _require_mapping(task.get("parameters"), "TASK_PARAMETERS_REQUIRED")
code = parameters["code"] code = parameters["code"]
validation = validate_inline_code(code) validation = validate_inline_code(code)
@@ -709,12 +760,30 @@ def expected_outputs(
"canonical_task_semantics_sha256": semantics_sha256, "canonical_task_semantics_sha256": semantics_sha256,
}, },
"canonical_code": { "canonical_code": {
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code", "yaml_pointer": "/Agent/Stages/0/tasks/1/parameters/code",
"encoding": "UTF-8", "encoding": "UTF-8",
"extraction_transform": "NONE", "extraction_transform": "NONE",
**validation, **validation,
}, },
"prepare_code": {
"yaml_pointer": "/Agent/Stages/0/tasks/0/parameters/code",
"encoding": "UTF-8",
"extraction_transform": "NONE",
**prepare_validation,
},
"full_code_mirrors": [ "full_code_mirrors": [
{
"path": _logical_path(PREPARE_MIRROR_PY_PATH),
"sha256": prepare_validation["code_sha256"],
"size_bytes": len(prepare_bytes),
"byte_identical_to_canonical_code": True,
},
{
"path": _logical_path(PREPARE_MIRROR_TXT_PATH),
"sha256": prepare_validation["code_sha256"],
"size_bytes": len(prepare_bytes),
"byte_identical_to_canonical_code": True,
},
{ {
"path": _logical_path(MIRROR_PY_PATH), "path": _logical_path(MIRROR_PY_PATH),
"sha256": validation["code_sha256"], "sha256": validation["code_sha256"],
@@ -733,6 +802,8 @@ def expected_outputs(
} }
outputs = { outputs = {
PROJECTION_PATH: authoring_raw, PROJECTION_PATH: authoring_raw,
PREPARE_MIRROR_PY_PATH: prepare_bytes,
PREPARE_MIRROR_TXT_PATH: prepare_bytes,
MIRROR_PY_PATH: code_bytes, MIRROR_PY_PATH: code_bytes,
MIRROR_TXT_PATH: code_bytes, MIRROR_TXT_PATH: code_bytes,
RECEIPT_PATH: canonical_json_bytes(receipt), RECEIPT_PATH: canonical_json_bytes(receipt),
@@ -58,6 +58,8 @@ FORBIDDEN_PARENT_MEMBERS = frozenset(
"agent_scripts/Stage_2_S2_00.yml", "agent_scripts/Stage_2_S2_00.yml",
"runtime/s2_00_ingress.py", "runtime/s2_00_ingress.py",
"runtime/s2_00_ingress.txt", "runtime/s2_00_ingress.txt",
"runtime/s2_00_prepare_request.py",
"runtime/s2_00_prepare_request.txt",
"manifest/s2_00_inline_code_receipt.json", "manifest/s2_00_inline_code_receipt.json",
"agent_scripts/Stage_2_S2_20.yml", "agent_scripts/Stage_2_S2_20.yml",
"runtime/s2_20_reduce.py", "runtime/s2_20_reduce.py",
@@ -58,6 +58,8 @@ FORBIDDEN_PARENT_MEMBERS = frozenset(
"agent_scripts/Stage_2_S2_00.yml", "agent_scripts/Stage_2_S2_00.yml",
"runtime/s2_00_ingress.py", "runtime/s2_00_ingress.py",
"runtime/s2_00_ingress.txt", "runtime/s2_00_ingress.txt",
"runtime/s2_00_prepare_request.py",
"runtime/s2_00_prepare_request.txt",
"manifest/s2_00_inline_code_receipt.json", "manifest/s2_00_inline_code_receipt.json",
"agent_scripts/Stage_2_S2_20.yml", "agent_scripts/Stage_2_S2_20.yml",
"runtime/s2_20_reduce.py", "runtime/s2_20_reduce.py",
@@ -149,7 +149,7 @@ _RAW_VALUE_UNSET = object()
# literal placeholders in the offline parity mirror and its unit tests. # literal placeholders in the offline parity mirror and its unit tests.
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}" INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json" INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json"
INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean" INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean"
INLINE_STAGE2_RELEASE_PATH = ( INLINE_STAGE2_RELEASE_PATH = (
@@ -149,7 +149,7 @@ _RAW_VALUE_UNSET = object()
# literal placeholders in the offline parity mirror and its unit tests. # literal placeholders in the offline parity mirror and its unit tests.
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}" INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json" INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json"
INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean" INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean"
INLINE_STAGE2_RELEASE_PATH = ( INLINE_STAGE2_RELEASE_PATH = (
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Prepare the fixed S2_00 control request from four explicit caller values.
The backend argument-delivery contract is not bound. Direct execution fails
closed; callers must invoke ``prepare_request`` with the four named values.
"""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import PurePosixPath
import re
import sys
import unicodedata
from typing import Any, Mapping
REQUEST_PATH = "stage2_control/s2_00_request.json"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
REQUEST_KEYS = frozenset({
"schema_version", "workflow_id", "request_id", "attempt_id",
"stage1_run_root_ref", "stage1_deployment_root_ref",
})
ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
class PrepareError(ValueError):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def canonical_json_bytes(value: Any) -> bytes:
return (json.dumps(value, ensure_ascii=False, allow_nan=False,
sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def _relative_path(value: str, *, code: str) -> str:
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
raise PrepareError(code, "logical path is empty or malformed")
if unicodedata.normalize("NFC", value) != value:
raise PrepareError(code, "logical path must already be NFC")
path = PurePosixPath(value)
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
raise PrepareError(code, "logical path must be a contained relative path")
rendered = path.as_posix()
if rendered != value:
raise PrepareError(code, "logical path is not canonical")
return rendered
def build_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
) -> bytes:
"""Return the exact six-field canonical request; do not access localdocs."""
if not isinstance(request_id, str) or ID_RE.fullmatch(request_id) is None:
raise PrepareError("REQUEST_ID_INVALID", "request_id contains forbidden characters")
if not isinstance(attempt_id, str) or ID_RE.fullmatch(attempt_id) is None:
raise PrepareError("ATTEMPT_ID_INVALID", "attempt_id contains forbidden characters")
request = {
"schema_version": "stage2_s2_00_execution_request.v1",
"workflow_id": "S2_00",
"request_id": request_id,
"attempt_id": attempt_id,
"stage1_run_root_ref": _relative_path(
stage1_run_root_ref, code="STAGE1_RUN_ROOT_REF_INVALID"),
"stage1_deployment_root_ref": _relative_path(
stage1_deployment_root_ref, code="STAGE1_DEPLOYMENT_ROOT_REF_INVALID"),
}
if set(request) != REQUEST_KEYS:
raise PrepareError("RUN_REQUEST_CLOSED_SHAPE", "request field set drifted")
return canonical_json_bytes(request)
class LocaldocsSession:
"""Small localdocs JSON-RPC session with verified binary write/read-back."""
def __init__(self, user_hash: str, workspace_hash: str, *, client: Any = None) -> None:
for name, value in (("user_hash", user_hash), ("workspace_hash", workspace_hash)):
if not isinstance(value, str) or re.fullmatch(r"[a-f0-9]{64}", value) is None:
raise PrepareError("CONTEXT_HASH_INVALID", f"{name} is not a SHA-256 digest")
self.user_hash = user_hash
self.workspace_hash = workspace_hash
if client is None:
try:
import httpx
except ImportError as exc:
raise PrepareError("HTTPX_UNAVAILABLE", "httpx==0.28.1 is required") from exc
client = httpx.Client(timeout=60)
self.client = client
self.headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
self.session_id: str | None = None
self.next_id = 10
self.initialized = False
def close(self) -> None:
self.client.close()
def _post(self, body: Mapping[str, Any], expected_id: int | None) -> Mapping[str, Any] | None:
try:
response = self.client.post(LOCALDOCS_URL, json=dict(body), headers=dict(self.headers))
response.raise_for_status()
except Exception as exc:
raise PrepareError("MCP_TRANSPORT_ERROR", "localdocs transport failed") from exc
session_id = response.headers.get("mcp-session-id")
if session_id:
if self.session_id is None and expected_id == 1:
self.session_id = session_id
elif session_id != self.session_id:
raise PrepareError("MCP_SESSION_ID_CHANGED", "localdocs session changed")
self.headers["mcp-session-id"] = session_id
if expected_id is None:
return None
try:
payload = response.json()
except Exception as exc:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response is not JSON") from exc
if not isinstance(payload, dict) or payload.get("jsonrpc") != "2.0" or payload.get("id") != expected_id:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response ID or shape mismatch")
if "error" in payload or not isinstance(payload.get("result"), dict):
raise PrepareError("MCP_TOOL_ERROR", "localdocs returned an error")
return payload["result"]
def initialize(self) -> None:
response = self._post({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}, "clientInfo": {
"name": "liti-s2-00-prepare", "version": "1.0.0",
"user_id": self.user_hash, "workspace_id": self.workspace_hash,
}}}, 1)
if response is None or response.get("protocolVersion") != MCP_PROTOCOL_VERSION or self.session_id is None:
raise PrepareError("MCP_INITIALIZE_INVALID", "localdocs initialization failed")
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, None)
self.initialized = True
def call(self, name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]:
if not self.initialized:
raise PrepareError("MCP_NOT_INITIALIZED", "localdocs is not initialized")
message_id = self.next_id
self.next_id += 1
result = self._post({"jsonrpc": "2.0", "id": message_id, "method": "tools/call",
"params": {"name": name, "arguments": dict(arguments)}}, message_id)
if result is None or result.get("isError") is True:
raise PrepareError("MCP_TOOL_ERROR", f"localdocs {name} failed")
return result
def read_binary(self, logical_path: str) -> bytes:
path = _relative_path(logical_path, code="LOCALDOCS_READ_PATH_INVALID")
result = self.call("read_binary_doc", {"doc_name": path})
content = result.get("content")
if not isinstance(content, list) or len(content) != 1 or not isinstance(content[0], dict) or content[0].get("type") != "text":
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary response")
text = content[0].get("text")
if not isinstance(text, str):
raise PrepareError("LOCALDOCS_READ_SHAPE", "missing binary envelope")
try:
envelope = json.loads(text)
if isinstance(envelope, dict) and "results" in envelope:
rows = envelope["results"]
if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0], dict):
raise ValueError("binary result cardinality mismatch")
inner = rows[0].get("content", rows[0].get("text"))
envelope = json.loads(inner) if isinstance(inner, str) else inner
encoded = envelope["content_base64"]
if not isinstance(encoded, str):
raise ValueError("binary content is not base64")
payload = base64.b64decode(encoded, validate=True)
size = envelope.get("byte_length", envelope.get("size"))
if size is not None and (not isinstance(size, int) or size != len(payload)):
raise ValueError("binary size mismatch")
digest = envelope.get("sha256")
if digest is not None and digest != hashlib.sha256(payload).hexdigest():
raise ValueError("binary hash mismatch")
return payload
except (ValueError, KeyError, TypeError, base64.binascii.Error) as exc:
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary envelope") from exc
def write_binary_verified(self, logical_path: str, payload: bytes) -> str:
path = _relative_path(logical_path, code="LOCALDOCS_WRITE_PATH_INVALID")
if path != REQUEST_PATH:
raise PrepareError("LOCALDOCS_WRITE_PATH_INVALID", "prepare may write only the fixed request path")
result = self.call("write_binary_file", {
"path": path, "content_base64": base64.b64encode(payload).decode("ascii"), "overwrite": True,
})
if not isinstance(result.get("content"), list) or result.get("isError") is True:
raise PrepareError("LOCALDOCS_WRITE_FAILED", "localdocs did not acknowledge write")
observed = self.read_binary(path)
if observed != payload:
raise PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "request read-back differs")
return hashlib.sha256(observed).hexdigest()
def prepare_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
*,
localdocs: Any = None,
) -> dict[str, Any]:
"""Validate, write, read back, and close an authenticated localdocs session."""
payload = build_request(request_id, attempt_id, stage1_run_root_ref, stage1_deployment_root_ref)
session = localdocs if localdocs is not None else LocaldocsSession(INLINE_USER_HASH, INLINE_WORKSPACE_HASH)
failure: Exception | None = None
digest: str | None = None
try:
session.initialize()
digest = session.write_binary_verified(REQUEST_PATH, payload)
except Exception as exc:
failure = exc
try:
session.close()
except Exception as exc:
if failure is None:
failure = PrepareError("LOCALDOCS_CLOSE_FAILED", "localdocs session close failed")
failure.__cause__ = exc
if failure is not None:
raise failure
return {"ok": True, "workflow_id": "S2_00", "path": REQUEST_PATH,
"request_sha256": digest}
if __name__ == "__main__":
sys.stdout.buffer.write(canonical_json_bytes({
"ok": False, "error": {"code": "PREPARE_ARGUMENT_BINDING_UNVERIFIED",
"message": "four caller values must be bound by the backend"}}))
raise SystemExit(2)
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Prepare the fixed S2_00 control request from four explicit caller values.
The backend argument-delivery contract is not bound. Direct execution fails
closed; callers must invoke ``prepare_request`` with the four named values.
"""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import PurePosixPath
import re
import sys
import unicodedata
from typing import Any, Mapping
REQUEST_PATH = "stage2_control/s2_00_request.json"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
REQUEST_KEYS = frozenset({
"schema_version", "workflow_id", "request_id", "attempt_id",
"stage1_run_root_ref", "stage1_deployment_root_ref",
})
ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
class PrepareError(ValueError):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def canonical_json_bytes(value: Any) -> bytes:
return (json.dumps(value, ensure_ascii=False, allow_nan=False,
sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def _relative_path(value: str, *, code: str) -> str:
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
raise PrepareError(code, "logical path is empty or malformed")
if unicodedata.normalize("NFC", value) != value:
raise PrepareError(code, "logical path must already be NFC")
path = PurePosixPath(value)
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
raise PrepareError(code, "logical path must be a contained relative path")
rendered = path.as_posix()
if rendered != value:
raise PrepareError(code, "logical path is not canonical")
return rendered
def build_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
) -> bytes:
"""Return the exact six-field canonical request; do not access localdocs."""
if not isinstance(request_id, str) or ID_RE.fullmatch(request_id) is None:
raise PrepareError("REQUEST_ID_INVALID", "request_id contains forbidden characters")
if not isinstance(attempt_id, str) or ID_RE.fullmatch(attempt_id) is None:
raise PrepareError("ATTEMPT_ID_INVALID", "attempt_id contains forbidden characters")
request = {
"schema_version": "stage2_s2_00_execution_request.v1",
"workflow_id": "S2_00",
"request_id": request_id,
"attempt_id": attempt_id,
"stage1_run_root_ref": _relative_path(
stage1_run_root_ref, code="STAGE1_RUN_ROOT_REF_INVALID"),
"stage1_deployment_root_ref": _relative_path(
stage1_deployment_root_ref, code="STAGE1_DEPLOYMENT_ROOT_REF_INVALID"),
}
if set(request) != REQUEST_KEYS:
raise PrepareError("RUN_REQUEST_CLOSED_SHAPE", "request field set drifted")
return canonical_json_bytes(request)
class LocaldocsSession:
"""Small localdocs JSON-RPC session with verified binary write/read-back."""
def __init__(self, user_hash: str, workspace_hash: str, *, client: Any = None) -> None:
for name, value in (("user_hash", user_hash), ("workspace_hash", workspace_hash)):
if not isinstance(value, str) or re.fullmatch(r"[a-f0-9]{64}", value) is None:
raise PrepareError("CONTEXT_HASH_INVALID", f"{name} is not a SHA-256 digest")
self.user_hash = user_hash
self.workspace_hash = workspace_hash
if client is None:
try:
import httpx
except ImportError as exc:
raise PrepareError("HTTPX_UNAVAILABLE", "httpx==0.28.1 is required") from exc
client = httpx.Client(timeout=60)
self.client = client
self.headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
self.session_id: str | None = None
self.next_id = 10
self.initialized = False
def close(self) -> None:
self.client.close()
def _post(self, body: Mapping[str, Any], expected_id: int | None) -> Mapping[str, Any] | None:
try:
response = self.client.post(LOCALDOCS_URL, json=dict(body), headers=dict(self.headers))
response.raise_for_status()
except Exception as exc:
raise PrepareError("MCP_TRANSPORT_ERROR", "localdocs transport failed") from exc
session_id = response.headers.get("mcp-session-id")
if session_id:
if self.session_id is None and expected_id == 1:
self.session_id = session_id
elif session_id != self.session_id:
raise PrepareError("MCP_SESSION_ID_CHANGED", "localdocs session changed")
self.headers["mcp-session-id"] = session_id
if expected_id is None:
return None
try:
payload = response.json()
except Exception as exc:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response is not JSON") from exc
if not isinstance(payload, dict) or payload.get("jsonrpc") != "2.0" or payload.get("id") != expected_id:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response ID or shape mismatch")
if "error" in payload or not isinstance(payload.get("result"), dict):
raise PrepareError("MCP_TOOL_ERROR", "localdocs returned an error")
return payload["result"]
def initialize(self) -> None:
response = self._post({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}, "clientInfo": {
"name": "liti-s2-00-prepare", "version": "1.0.0",
"user_id": self.user_hash, "workspace_id": self.workspace_hash,
}}}, 1)
if response is None or response.get("protocolVersion") != MCP_PROTOCOL_VERSION or self.session_id is None:
raise PrepareError("MCP_INITIALIZE_INVALID", "localdocs initialization failed")
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, None)
self.initialized = True
def call(self, name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]:
if not self.initialized:
raise PrepareError("MCP_NOT_INITIALIZED", "localdocs is not initialized")
message_id = self.next_id
self.next_id += 1
result = self._post({"jsonrpc": "2.0", "id": message_id, "method": "tools/call",
"params": {"name": name, "arguments": dict(arguments)}}, message_id)
if result is None or result.get("isError") is True:
raise PrepareError("MCP_TOOL_ERROR", f"localdocs {name} failed")
return result
def read_binary(self, logical_path: str) -> bytes:
path = _relative_path(logical_path, code="LOCALDOCS_READ_PATH_INVALID")
result = self.call("read_binary_doc", {"doc_name": path})
content = result.get("content")
if not isinstance(content, list) or len(content) != 1 or not isinstance(content[0], dict) or content[0].get("type") != "text":
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary response")
text = content[0].get("text")
if not isinstance(text, str):
raise PrepareError("LOCALDOCS_READ_SHAPE", "missing binary envelope")
try:
envelope = json.loads(text)
if isinstance(envelope, dict) and "results" in envelope:
rows = envelope["results"]
if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0], dict):
raise ValueError("binary result cardinality mismatch")
inner = rows[0].get("content", rows[0].get("text"))
envelope = json.loads(inner) if isinstance(inner, str) else inner
encoded = envelope["content_base64"]
if not isinstance(encoded, str):
raise ValueError("binary content is not base64")
payload = base64.b64decode(encoded, validate=True)
size = envelope.get("byte_length", envelope.get("size"))
if size is not None and (not isinstance(size, int) or size != len(payload)):
raise ValueError("binary size mismatch")
digest = envelope.get("sha256")
if digest is not None and digest != hashlib.sha256(payload).hexdigest():
raise ValueError("binary hash mismatch")
return payload
except (ValueError, KeyError, TypeError, base64.binascii.Error) as exc:
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary envelope") from exc
def write_binary_verified(self, logical_path: str, payload: bytes) -> str:
path = _relative_path(logical_path, code="LOCALDOCS_WRITE_PATH_INVALID")
if path != REQUEST_PATH:
raise PrepareError("LOCALDOCS_WRITE_PATH_INVALID", "prepare may write only the fixed request path")
result = self.call("write_binary_file", {
"path": path, "content_base64": base64.b64encode(payload).decode("ascii"), "overwrite": True,
})
if not isinstance(result.get("content"), list) or result.get("isError") is True:
raise PrepareError("LOCALDOCS_WRITE_FAILED", "localdocs did not acknowledge write")
observed = self.read_binary(path)
if observed != payload:
raise PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "request read-back differs")
return hashlib.sha256(observed).hexdigest()
def prepare_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
*,
localdocs: Any = None,
) -> dict[str, Any]:
"""Validate, write, read back, and close an authenticated localdocs session."""
payload = build_request(request_id, attempt_id, stage1_run_root_ref, stage1_deployment_root_ref)
session = localdocs if localdocs is not None else LocaldocsSession(INLINE_USER_HASH, INLINE_WORKSPACE_HASH)
failure: Exception | None = None
digest: str | None = None
try:
session.initialize()
digest = session.write_binary_verified(REQUEST_PATH, payload)
except Exception as exc:
failure = exc
try:
session.close()
except Exception as exc:
if failure is None:
failure = PrepareError("LOCALDOCS_CLOSE_FAILED", "localdocs session close failed")
failure.__cause__ = exc
if failure is not None:
raise failure
return {"ok": True, "workflow_id": "S2_00", "path": REQUEST_PATH,
"request_sha256": digest}
if __name__ == "__main__":
sys.stdout.buffer.write(canonical_json_bytes({
"ok": False, "error": {"code": "PREPARE_ARGUMENT_BINDING_UNVERIFIED",
"message": "four caller values must be bound by the backend"}}))
raise SystemExit(2)
@@ -27,7 +27,7 @@ from typing import Any, Iterable, Mapping, Sequence
WORKFLOW_ID = "S2_20" WORKFLOW_ID = "S2_20"
ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0" ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp" WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
@@ -27,7 +27,7 @@ from typing import Any, Iterable, Mapping, Sequence
WORKFLOW_ID = "S2_20" WORKFLOW_ID = "S2_20"
ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0" ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp" WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
@@ -18,7 +18,7 @@ import sys
from typing import Any, Mapping from typing import Any, Mapping
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -18,7 +18,7 @@ import sys
from typing import Any, Mapping from typing import Any, Mapping
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -25,7 +25,7 @@ from typing import Any, Iterable, Mapping, Sequence
WORKFLOW_ID = "S2_40" WORKFLOW_ID = "S2_40"
ALGORITHM_VERSION = "s2_40_finalizer/1.0.0" ALGORITHM_VERSION = "s2_40_finalizer/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -25,7 +25,7 @@ from typing import Any, Iterable, Mapping, Sequence
WORKFLOW_ID = "S2_40" WORKFLOW_ID = "S2_40"
ALGORITHM_VERSION = "s2_40_finalizer/1.0.0" ALGORITHM_VERSION = "s2_40_finalizer/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -186,6 +186,20 @@
"expected_release_sha256": {"$ref": "#/$defs/bindable_sha256"}, "expected_release_sha256": {"$ref": "#/$defs/bindable_sha256"},
"agent_script_sha256": {"$ref": "#/$defs/bindable_sha256"}, "agent_script_sha256": {"$ref": "#/$defs/bindable_sha256"},
"canonical_code_sha256": {"$ref": "#/$defs/bindable_sha256"}, "canonical_code_sha256": {"$ref": "#/$defs/bindable_sha256"},
"prepare_request_contract": {
"type": "object",
"additionalProperties": false,
"required": ["task_name", "yaml_pointer", "required_external_arguments", "write_path", "argument_binding_status", "success_gate_status", "workspace_serialization_status"],
"properties": {
"task_name": {"const": "Task_S2_00_prepare_request"},
"yaml_pointer": {"const": "/Agent/Stages/0/tasks/0/parameters/code"},
"required_external_arguments": {"const": ["request_id", "attempt_id", "stage1_run_root_ref", "stage1_deployment_root_ref"]},
"write_path": {"const": "stage2_control/s2_00_request.json"},
"argument_binding_status": {"const": "UNVERIFIED"},
"success_gate_status": {"const": "UNVERIFIED"},
"workspace_serialization_status": {"const": "UNVERIFIED"}
}
},
"mcp_server_id": {"const": "code-executor"}, "mcp_server_id": {"const": "code-executor"},
"tool_name": {"const": "run_code"}, "tool_name": {"const": "run_code"},
"language": {"const": "python"}, "language": {"const": "python"},
@@ -692,6 +706,7 @@
"authoring", "authoring",
"deployment_projection", "deployment_projection",
"canonical_code", "canonical_code",
"prepare_code",
"full_code_mirrors", "full_code_mirrors",
"task_contract", "task_contract",
"parity_status" "parity_status"
@@ -768,12 +783,55 @@
}, },
"placeholder_count": {"const": 0}, "placeholder_count": {"const": 0},
"plaintext_secret_count": {"const": 0}, "plaintext_secret_count": {"const": 0},
"yaml_pointer": {"const": "/Agent/Stages/0/tasks/1/parameters/code"}
}
},
"prepare_code": {
"type": "object",
"additionalProperties": false,
"required": ["ast_status", "code_sha256", "code_size_bytes", "compile_status", "encoding", "external_python_source_ref_count", "external_url_count", "extraction_transform", "forbidden_dynamic_call_count", "forbidden_import_count", "imports", "placeholder_count", "plaintext_secret_count", "yaml_pointer"],
"properties": {
"ast_status": {"const": "PASS"},
"code_sha256": {"$ref": "#/$defs/sha256"},
"code_size_bytes": {"type": "integer", "minimum": 1},
"compile_status": {"const": "PASS"},
"encoding": {"const": "UTF-8"},
"external_python_source_ref_count": {"const": 0},
"external_url_count": {"const": 0},
"extraction_transform": {"const": "NONE"},
"forbidden_dynamic_call_count": {"const": 0},
"forbidden_import_count": {"const": 0},
"imports": {"type": "array", "items": {"$ref": "#/$defs/nonempty_string"}, "uniqueItems": true},
"placeholder_count": {"const": 0},
"plaintext_secret_count": {"const": 0},
"yaml_pointer": {"const": "/Agent/Stages/0/tasks/0/parameters/code"} "yaml_pointer": {"const": "/Agent/Stages/0/tasks/0/parameters/code"}
} }
}, },
"full_code_mirrors": { "full_code_mirrors": {
"type": "array", "type": "array",
"prefixItems": [ "prefixItems": [
{
"type": "object",
"additionalProperties": false,
"required": ["path", "sha256", "size_bytes", "byte_identical_to_canonical_code"],
"properties": {
"path": {"const": "Default_Agent/Stage_2_Clean/runtime/s2_00_prepare_request.py"},
"sha256": {"$ref": "#/$defs/sha256"},
"size_bytes": {"type": "integer", "minimum": 1},
"byte_identical_to_canonical_code": {"const": true}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["path", "sha256", "size_bytes", "byte_identical_to_canonical_code"],
"properties": {
"path": {"const": "Default_Agent/Stage_2_Clean/runtime/s2_00_prepare_request.txt"},
"sha256": {"$ref": "#/$defs/sha256"},
"size_bytes": {"type": "integer", "minimum": 1},
"byte_identical_to_canonical_code": {"const": true}
}
},
{ {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -798,13 +856,13 @@
} }
], ],
"items": false, "items": false,
"minItems": 2, "minItems": 4,
"maxItems": 2 "maxItems": 4
}, },
"task_contract": { "task_contract": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["agent", "mcp_servers", "stage", "task", "task_procedure"], "required": ["agent", "mcp_servers", "stage", "prepare_task", "task", "task_procedure"],
"properties": { "properties": {
"agent": {"const": {"name": "Stage_2_S2_00_v2", "version": "1.2.0"}}, "agent": {"const": {"name": "Stage_2_S2_00_v2", "version": "1.2.0"}},
"mcp_servers": { "mcp_servers": {
@@ -814,6 +872,18 @@
} }
}, },
"stage": {"const": {"name": "S2_00", "nexts": [], "prevs": []}}, "stage": {"const": {"name": "S2_00", "nexts": [], "prevs": []}},
"prepare_task": {
"type": "object",
"additionalProperties": false,
"required": ["task_name", "mcp", "tool_name", "parameters", "code_sha256"],
"properties": {
"task_name": {"const": "Task_S2_00_prepare_request"},
"mcp": {"const": "code-executor"},
"tool_name": {"const": "run_code"},
"parameters": {"const": {"language": "python", "requirements": "httpx==0.28.1", "network": "agent-network", "timeout": 300}},
"code_sha256": {"$ref": "#/$defs/sha256"}
}
},
"task": { "task": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -835,8 +905,9 @@
}, },
"task_procedure": { "task_procedure": {
"const": { "const": {
"IN": {"nexts": ["Task_S2_00_deterministic_ingress"], "wait_until": []}, "IN": {"nexts": ["Task_S2_00_prepare_request"], "wait_until": []},
"Task_S2_00_deterministic_ingress": {"nexts": ["OUT"], "wait_until": ["IN"]}, "Task_S2_00_prepare_request": {"nexts": ["Task_S2_00_deterministic_ingress"], "wait_until": ["IN"]},
"Task_S2_00_deterministic_ingress": {"nexts": ["OUT"], "wait_until": ["Task_S2_00_prepare_request"]},
"OUT": {"nexts": [], "wait_until": ["Task_S2_00_deterministic_ingress"]} "OUT": {"nexts": [], "wait_until": ["Task_S2_00_deterministic_ingress"]}
} }
} }
@@ -1313,7 +1313,7 @@
}, },
{ {
"path": "tests/s2_00/test_cluster_bundle_compile.py", "path": "tests/s2_00/test_cluster_bundle_compile.py",
"sha256": "b643d2d4018ebe3349c64304063edf7ba0194fc92fb1fe9337eca3f384ae794a", "sha256": "1b7a58ae53e7465cd39f0c911c4fab8e96b9e89df707eb9423e8a22fabc0a54b",
"status": "DEV_HASH_BOUND" "status": "DEV_HASH_BOUND"
}, },
{ {
@@ -1328,7 +1328,12 @@
}, },
{ {
"path": "tests/s2_00/test_inline_code_parity.py", "path": "tests/s2_00/test_inline_code_parity.py",
"sha256": "8f0f55e8e1b970de572129946ab8bf34967c7fd84e316a9193509cab07a455ed", "sha256": "325e0ed4dc7a4562b87946f2ff68693865997bd33b55ce5d0a6c558920404a18",
"status": "DEV_HASH_BOUND"
},
{
"path": "tests/s2_00/test_prepare_request.py",
"sha256": "db76d064f742c2003eacfcb4814ba0b9d0b36984622147485cbab1139d5deaeb",
"status": "DEV_HASH_BOUND" "status": "DEV_HASH_BOUND"
}, },
{ {
@@ -643,7 +643,10 @@ class ClusterAndBundleTests(unittest.TestCase):
self.assertEqual(binding["expected_release_sha256"], binding["stage2_release_ref"]["sha256"]) self.assertEqual(binding["expected_release_sha256"], binding["stage2_release_ref"]["sha256"])
self.assertEqual(binding["expected_release_sha256"], hashlib.sha256(release_path.read_bytes()).hexdigest()) self.assertEqual(binding["expected_release_sha256"], hashlib.sha256(release_path.read_bytes()).hexdigest())
agent = yaml.safe_load(agent_path.read_text(encoding="utf-8")) agent = yaml.safe_load(agent_path.read_text(encoding="utf-8"))
code = agent["Agent"]["Stages"][0]["tasks"][0]["parameters"]["code"] tasks = agent["Agent"]["Stages"][0]["tasks"]
self.assertEqual(tasks[0]["task_name"], "Task_S2_00_prepare_request")
self.assertEqual(tasks[1]["task_name"], "Task_S2_00_deterministic_ingress")
code = tasks[1]["parameters"]["code"]
self.assertEqual(hashlib.sha256(code.encode("utf-8")).hexdigest(), binding["canonical_code_sha256"]) self.assertEqual(hashlib.sha256(code.encode("utf-8")).hexdigest(), binding["canonical_code_sha256"])
self.assertEqual(binding["agent_script_sha256"], binding["agent_script_ref"]["sha256"]) self.assertEqual(binding["agent_script_sha256"], binding["agent_script_ref"]["sha256"])
@@ -643,7 +643,10 @@ class ClusterAndBundleTests(unittest.TestCase):
self.assertEqual(binding["expected_release_sha256"], binding["stage2_release_ref"]["sha256"]) self.assertEqual(binding["expected_release_sha256"], binding["stage2_release_ref"]["sha256"])
self.assertEqual(binding["expected_release_sha256"], hashlib.sha256(release_path.read_bytes()).hexdigest()) self.assertEqual(binding["expected_release_sha256"], hashlib.sha256(release_path.read_bytes()).hexdigest())
agent = yaml.safe_load(agent_path.read_text(encoding="utf-8")) agent = yaml.safe_load(agent_path.read_text(encoding="utf-8"))
code = agent["Agent"]["Stages"][0]["tasks"][0]["parameters"]["code"] tasks = agent["Agent"]["Stages"][0]["tasks"]
self.assertEqual(tasks[0]["task_name"], "Task_S2_00_prepare_request")
self.assertEqual(tasks[1]["task_name"], "Task_S2_00_deterministic_ingress")
code = tasks[1]["parameters"]["code"]
self.assertEqual(hashlib.sha256(code.encode("utf-8")).hexdigest(), binding["canonical_code_sha256"]) self.assertEqual(hashlib.sha256(code.encode("utf-8")).hexdigest(), binding["canonical_code_sha256"])
self.assertEqual(binding["agent_script_sha256"], binding["agent_script_ref"]["sha256"]) self.assertEqual(binding["agent_script_sha256"], binding["agent_script_ref"]["sha256"])
@@ -38,6 +38,18 @@ def valid_inline_code() -> str:
def authoring_yaml(code: str | None = None) -> bytes: def authoring_yaml(code: str | None = None) -> bytes:
source = valid_inline_code() if code is None else code source = valid_inline_code() if code is None else code
indented = "\n".join(f" {line}" for line in source.split("\n")) indented = "\n".join(f" {line}" for line in source.split("\n"))
prepare_source = "\n".join(
f" {line}" for line in (
"from __future__ import annotations\n"
"LOCALDOCS_URL = 'http://mcp-localdocs:8012/mcp'\n"
"USER_HASH = '{{__user_hash__}}'\n"
"WORKSPACE_HASH = '{{__workspace_hash__}}'\n"
"READ_TOOL = 'read_binary_doc'\n"
"REQUEST_PATH = 'stage2_control/s2_00_request.json'\n"
"def build_request():\n return b'{}'\n"
"def prepare_request():\n return 'write_binary_verified'"
).split("\n")
)
return ( return (
"Agent:\n" "Agent:\n"
" name: Stage_2_S2_00_v2\n" " name: Stage_2_S2_00_v2\n"
@@ -53,6 +65,16 @@ def authoring_yaml(code: str | None = None) -> bytes:
" type: streamable-http\n" " type: streamable-http\n"
" url: https://code-executor.mcp.eroomai.com/mcp\n" " url: https://code-executor.mcp.eroomai.com/mcp\n"
" tasks:\n" " tasks:\n"
" - task_name: Task_S2_00_prepare_request\n"
" mcp: code-executor\n"
" tool_name: run_code\n"
" parameters:\n"
" language: python\n"
" requirements: 'httpx==0.28.1'\n"
" network: agent-network\n"
" timeout: 300\n"
" code: |-\n"
f"{prepare_source}\n"
" - task_name: Task_S2_00_deterministic_ingress\n" " - task_name: Task_S2_00_deterministic_ingress\n"
" mcp: code-executor\n" " mcp: code-executor\n"
" tool_name: run_code\n" " tool_name: run_code\n"
@@ -65,11 +87,14 @@ def authoring_yaml(code: str | None = None) -> bytes:
f"{indented}\n" f"{indented}\n"
" task_procedure:\n" " task_procedure:\n"
" IN:\n" " IN:\n"
" nexts: [Task_S2_00_deterministic_ingress]\n" " nexts: [Task_S2_00_prepare_request]\n"
" wait_until: []\n" " wait_until: []\n"
" Task_S2_00_prepare_request:\n"
" nexts: [Task_S2_00_deterministic_ingress]\n"
" wait_until: [IN]\n"
" Task_S2_00_deterministic_ingress:\n" " Task_S2_00_deterministic_ingress:\n"
" nexts: [OUT]\n" " nexts: [OUT]\n"
" wait_until: [IN]\n" " wait_until: [Task_S2_00_prepare_request]\n"
" OUT:\n" " OUT:\n"
" nexts: []\n" " nexts: []\n"
" wait_until: [Task_S2_00_deterministic_ingress]\n" " wait_until: [Task_S2_00_deterministic_ingress]\n"
@@ -129,6 +154,7 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
self.assertEqual(outputs[builder.PROJECTION_PATH], raw) self.assertEqual(outputs[builder.PROJECTION_PATH], raw)
self.assertEqual(outputs[builder.MIRROR_PY_PATH], valid_inline_code().encode("utf-8")) self.assertEqual(outputs[builder.MIRROR_PY_PATH], valid_inline_code().encode("utf-8"))
self.assertEqual(outputs[builder.MIRROR_TXT_PATH], outputs[builder.MIRROR_PY_PATH]) self.assertEqual(outputs[builder.MIRROR_TXT_PATH], outputs[builder.MIRROR_PY_PATH])
self.assertEqual(outputs[builder.PREPARE_MIRROR_PY_PATH], outputs[builder.PREPARE_MIRROR_TXT_PATH])
parsed_receipt = json.loads(outputs[builder.RECEIPT_PATH]) parsed_receipt = json.loads(outputs[builder.RECEIPT_PATH])
self.assertEqual(parsed_receipt, receipt) self.assertEqual(parsed_receipt, receipt)
self.assertEqual(parsed_receipt["parity_status"], "PASS") self.assertEqual(parsed_receipt["parity_status"], "PASS")
@@ -233,6 +259,8 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
projection = root / "agent_scripts" / "Stage_2_S2_00.yml" projection = root / "agent_scripts" / "Stage_2_S2_00.yml"
mirror_py = root / "runtime" / "s2_00_ingress.py" mirror_py = root / "runtime" / "s2_00_ingress.py"
mirror_txt = root / "runtime" / "s2_00_ingress.txt" mirror_txt = root / "runtime" / "s2_00_ingress.txt"
prepare_py = root / "runtime" / "s2_00_prepare_request.py"
prepare_txt = root / "runtime" / "s2_00_prepare_request.txt"
receipt = root / "manifest" / "s2_00_inline_code_receipt.json" receipt = root / "manifest" / "s2_00_inline_code_receipt.json"
root.mkdir(parents=True) root.mkdir(parents=True)
original = authoring_yaml() original = authoring_yaml()
@@ -244,6 +272,8 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
mock.patch.object(builder, "PROJECTION_PATH", projection), mock.patch.object(builder, "PROJECTION_PATH", projection),
mock.patch.object(builder, "MIRROR_PY_PATH", mirror_py), mock.patch.object(builder, "MIRROR_PY_PATH", mirror_py),
mock.patch.object(builder, "MIRROR_TXT_PATH", mirror_txt), mock.patch.object(builder, "MIRROR_TXT_PATH", mirror_txt),
mock.patch.object(builder, "PREPARE_MIRROR_PY_PATH", prepare_py),
mock.patch.object(builder, "PREPARE_MIRROR_TXT_PATH", prepare_txt),
mock.patch.object(builder, "RECEIPT_PATH", receipt), mock.patch.object(builder, "RECEIPT_PATH", receipt),
): ):
status, payload = builder.run(check=False) status, payload = builder.run(check=False)
@@ -253,6 +283,7 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
self.assertEqual(projection.read_bytes(), original) self.assertEqual(projection.read_bytes(), original)
self.assertEqual(mirror_py.read_bytes(), valid_inline_code().encode("utf-8")) self.assertEqual(mirror_py.read_bytes(), valid_inline_code().encode("utf-8"))
self.assertEqual(mirror_py.read_bytes(), mirror_txt.read_bytes()) self.assertEqual(mirror_py.read_bytes(), mirror_txt.read_bytes())
self.assertEqual(prepare_py.read_bytes(), prepare_txt.read_bytes())
receipt_before = receipt.read_bytes() receipt_before = receipt.read_bytes()
status, payload = builder.run(check=True) status, payload = builder.run(check=True)
self.assertEqual(status, 0) self.assertEqual(status, 0)
@@ -38,6 +38,18 @@ def valid_inline_code() -> str:
def authoring_yaml(code: str | None = None) -> bytes: def authoring_yaml(code: str | None = None) -> bytes:
source = valid_inline_code() if code is None else code source = valid_inline_code() if code is None else code
indented = "\n".join(f" {line}" for line in source.split("\n")) indented = "\n".join(f" {line}" for line in source.split("\n"))
prepare_source = "\n".join(
f" {line}" for line in (
"from __future__ import annotations\n"
"LOCALDOCS_URL = 'http://mcp-localdocs:8012/mcp'\n"
"USER_HASH = '{{__user_hash__}}'\n"
"WORKSPACE_HASH = '{{__workspace_hash__}}'\n"
"READ_TOOL = 'read_binary_doc'\n"
"REQUEST_PATH = 'stage2_control/s2_00_request.json'\n"
"def build_request():\n return b'{}'\n"
"def prepare_request():\n return 'write_binary_verified'"
).split("\n")
)
return ( return (
"Agent:\n" "Agent:\n"
" name: Stage_2_S2_00_v2\n" " name: Stage_2_S2_00_v2\n"
@@ -53,6 +65,16 @@ def authoring_yaml(code: str | None = None) -> bytes:
" type: streamable-http\n" " type: streamable-http\n"
" url: https://code-executor.mcp.eroomai.com/mcp\n" " url: https://code-executor.mcp.eroomai.com/mcp\n"
" tasks:\n" " tasks:\n"
" - task_name: Task_S2_00_prepare_request\n"
" mcp: code-executor\n"
" tool_name: run_code\n"
" parameters:\n"
" language: python\n"
" requirements: 'httpx==0.28.1'\n"
" network: agent-network\n"
" timeout: 300\n"
" code: |-\n"
f"{prepare_source}\n"
" - task_name: Task_S2_00_deterministic_ingress\n" " - task_name: Task_S2_00_deterministic_ingress\n"
" mcp: code-executor\n" " mcp: code-executor\n"
" tool_name: run_code\n" " tool_name: run_code\n"
@@ -65,11 +87,14 @@ def authoring_yaml(code: str | None = None) -> bytes:
f"{indented}\n" f"{indented}\n"
" task_procedure:\n" " task_procedure:\n"
" IN:\n" " IN:\n"
" nexts: [Task_S2_00_deterministic_ingress]\n" " nexts: [Task_S2_00_prepare_request]\n"
" wait_until: []\n" " wait_until: []\n"
" Task_S2_00_prepare_request:\n"
" nexts: [Task_S2_00_deterministic_ingress]\n"
" wait_until: [IN]\n"
" Task_S2_00_deterministic_ingress:\n" " Task_S2_00_deterministic_ingress:\n"
" nexts: [OUT]\n" " nexts: [OUT]\n"
" wait_until: [IN]\n" " wait_until: [Task_S2_00_prepare_request]\n"
" OUT:\n" " OUT:\n"
" nexts: []\n" " nexts: []\n"
" wait_until: [Task_S2_00_deterministic_ingress]\n" " wait_until: [Task_S2_00_deterministic_ingress]\n"
@@ -129,6 +154,7 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
self.assertEqual(outputs[builder.PROJECTION_PATH], raw) self.assertEqual(outputs[builder.PROJECTION_PATH], raw)
self.assertEqual(outputs[builder.MIRROR_PY_PATH], valid_inline_code().encode("utf-8")) self.assertEqual(outputs[builder.MIRROR_PY_PATH], valid_inline_code().encode("utf-8"))
self.assertEqual(outputs[builder.MIRROR_TXT_PATH], outputs[builder.MIRROR_PY_PATH]) self.assertEqual(outputs[builder.MIRROR_TXT_PATH], outputs[builder.MIRROR_PY_PATH])
self.assertEqual(outputs[builder.PREPARE_MIRROR_PY_PATH], outputs[builder.PREPARE_MIRROR_TXT_PATH])
parsed_receipt = json.loads(outputs[builder.RECEIPT_PATH]) parsed_receipt = json.loads(outputs[builder.RECEIPT_PATH])
self.assertEqual(parsed_receipt, receipt) self.assertEqual(parsed_receipt, receipt)
self.assertEqual(parsed_receipt["parity_status"], "PASS") self.assertEqual(parsed_receipt["parity_status"], "PASS")
@@ -233,6 +259,8 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
projection = root / "agent_scripts" / "Stage_2_S2_00.yml" projection = root / "agent_scripts" / "Stage_2_S2_00.yml"
mirror_py = root / "runtime" / "s2_00_ingress.py" mirror_py = root / "runtime" / "s2_00_ingress.py"
mirror_txt = root / "runtime" / "s2_00_ingress.txt" mirror_txt = root / "runtime" / "s2_00_ingress.txt"
prepare_py = root / "runtime" / "s2_00_prepare_request.py"
prepare_txt = root / "runtime" / "s2_00_prepare_request.txt"
receipt = root / "manifest" / "s2_00_inline_code_receipt.json" receipt = root / "manifest" / "s2_00_inline_code_receipt.json"
root.mkdir(parents=True) root.mkdir(parents=True)
original = authoring_yaml() original = authoring_yaml()
@@ -244,6 +272,8 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
mock.patch.object(builder, "PROJECTION_PATH", projection), mock.patch.object(builder, "PROJECTION_PATH", projection),
mock.patch.object(builder, "MIRROR_PY_PATH", mirror_py), mock.patch.object(builder, "MIRROR_PY_PATH", mirror_py),
mock.patch.object(builder, "MIRROR_TXT_PATH", mirror_txt), mock.patch.object(builder, "MIRROR_TXT_PATH", mirror_txt),
mock.patch.object(builder, "PREPARE_MIRROR_PY_PATH", prepare_py),
mock.patch.object(builder, "PREPARE_MIRROR_TXT_PATH", prepare_txt),
mock.patch.object(builder, "RECEIPT_PATH", receipt), mock.patch.object(builder, "RECEIPT_PATH", receipt),
): ):
status, payload = builder.run(check=False) status, payload = builder.run(check=False)
@@ -253,6 +283,7 @@ class InlineCodeProjectionUnitTests(unittest.TestCase):
self.assertEqual(projection.read_bytes(), original) self.assertEqual(projection.read_bytes(), original)
self.assertEqual(mirror_py.read_bytes(), valid_inline_code().encode("utf-8")) self.assertEqual(mirror_py.read_bytes(), valid_inline_code().encode("utf-8"))
self.assertEqual(mirror_py.read_bytes(), mirror_txt.read_bytes()) self.assertEqual(mirror_py.read_bytes(), mirror_txt.read_bytes())
self.assertEqual(prepare_py.read_bytes(), prepare_txt.read_bytes())
receipt_before = receipt.read_bytes() receipt_before = receipt.read_bytes()
status, payload = builder.run(check=True) status, payload = builder.run(check=True)
self.assertEqual(status, 0) self.assertEqual(status, 0)
@@ -0,0 +1,171 @@
from __future__ import annotations
import json
from pathlib import Path
import sys
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from runtime import s2_00_ingress as ingress # noqa: E402
from runtime import s2_00_prepare_request as prepare # noqa: E402
ARGS = ("request-1", "attempt.1", "stage1/runs/matter-1", "stage1/deployment")
class FakeLocaldocs:
def __init__(self, *, fail_at: str | None = None) -> None:
self.fail_at = fail_at
self.initialized = False
self.closed = False
self.writes: list[tuple[str, bytes]] = []
self.saved: bytes | None = None
def initialize(self) -> None:
if self.fail_at == "initialize":
raise RuntimeError("initialize failed")
self.initialized = True
def write_binary_verified(self, path: str, payload: bytes) -> str:
if self.fail_at == "write":
raise RuntimeError("write failed")
self.writes.append((path, payload))
self.saved = payload
if self.fail_at == "readback":
raise prepare.PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "read-back differs")
return prepare.hashlib.sha256(payload).hexdigest()
def close(self) -> None:
self.closed = True
if self.fail_at == "close":
raise RuntimeError("close failed")
class FakeResponse:
def __init__(self, payload: dict, *, session_id: str = "session-1") -> None:
self.payload = payload
self.headers = {"mcp-session-id": session_id}
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return self.payload
class FakeMcpClient:
def __init__(self, *, corrupt_read: bool = False) -> None:
self.corrupt_read = corrupt_read
self.saved: bytes | None = None
self.closed = False
self.calls: list[str] = []
def post(self, url: str, *, json: dict, headers: dict) -> FakeResponse:
if url != prepare.LOCALDOCS_URL:
raise RuntimeError("unexpected endpoint")
method = json["method"]
self.calls.append(method)
if method == "initialize":
result = {"protocolVersion": prepare.MCP_PROTOCOL_VERSION}
elif method == "notifications/initialized":
return FakeResponse({})
else:
name = json["params"]["name"]
arguments = json["params"]["arguments"]
if name == "write_binary_file":
if arguments["path"] != prepare.REQUEST_PATH:
raise RuntimeError("wrong write path")
self.saved = prepare.base64.b64decode(arguments["content_base64"])
text = "{}"
elif name == "read_binary_doc":
payload = b"corrupt" if self.corrupt_read else self.saved
text = prepare.json.dumps({"content_base64": prepare.base64.b64encode(payload).decode("ascii")})
else:
raise RuntimeError("wrong tool")
result = {"content": [{"type": "text", "text": text}]}
return FakeResponse({"jsonrpc": "2.0", "id": json["id"], "result": result})
def close(self) -> None:
self.closed = True
class PrepareRequestTests(unittest.TestCase):
def test_exact_canonical_six_field_request_matches_existing_ingress(self) -> None:
payload = prepare.build_request(*ARGS)
value = json.loads(payload)
self.assertEqual(set(value), prepare.REQUEST_KEYS)
self.assertEqual(payload, ingress.canonical_json_bytes(value))
self.assertEqual(ingress._inline_validate_request(payload), value)
self.assertTrue(payload.endswith(b"\n"))
def test_invalid_inputs_never_write(self) -> None:
variants = [
(123, *ARGS[1:]),
("bad/id", *ARGS[1:]),
(ARGS[0], "bad id", *ARGS[2:]),
(*ARGS[:2], "../escape", ARGS[3]),
(*ARGS[:3], "/absolute"),
(*ARGS[:2], "stage1//run", ARGS[3]),
(*ARGS[:2], "stage1/e\u0301", ARGS[3]),
]
for values in variants:
with self.subTest(values=values):
fake = FakeLocaldocs()
with self.assertRaises(prepare.PrepareError):
prepare.prepare_request(*values, localdocs=fake)
self.assertEqual(fake.writes, [])
def test_verified_write_and_close(self) -> None:
fake = FakeLocaldocs()
result = prepare.prepare_request(*ARGS, localdocs=fake)
self.assertEqual(fake.writes, [(prepare.REQUEST_PATH, prepare.build_request(*ARGS))])
self.assertTrue(fake.initialized)
self.assertTrue(fake.closed)
self.assertEqual(result["request_sha256"], prepare.hashlib.sha256(fake.saved).hexdigest())
def test_io_and_close_failures_do_not_return_success(self) -> None:
for failure in ("initialize", "write", "readback", "close"):
with self.subTest(failure=failure):
fake = FakeLocaldocs(fail_at=failure)
with self.assertRaises(Exception):
prepare.prepare_request(*ARGS, localdocs=fake)
self.assertTrue(fake.closed)
def test_localdocs_write_path_is_fixed(self) -> None:
session = object.__new__(prepare.LocaldocsSession)
with self.assertRaises(prepare.PrepareError):
session.write_binary_verified("other/request.json", b"{}\n")
def test_real_session_adapter_writes_and_reads_back_through_mcp(self) -> None:
client = FakeMcpClient()
session = prepare.LocaldocsSession("a" * 64, "b" * 64, client=client)
result = prepare.prepare_request(*ARGS, localdocs=session)
self.assertEqual(client.saved, prepare.build_request(*ARGS))
self.assertEqual(result["request_sha256"], prepare.hashlib.sha256(client.saved).hexdigest())
self.assertEqual(client.calls, ["initialize", "notifications/initialized", "tools/call", "tools/call"])
self.assertTrue(client.closed)
def test_real_session_adapter_rejects_readback_mismatch(self) -> None:
client = FakeMcpClient(corrupt_read=True)
session = prepare.LocaldocsSession("a" * 64, "b" * 64, client=client)
with self.assertRaises(prepare.PrepareError) as raised:
prepare.prepare_request(*ARGS, localdocs=session)
self.assertEqual(raised.exception.code, "LOCALDOCS_WRITE_READBACK_MISMATCH")
self.assertTrue(client.closed)
def test_ingress_is_not_called_after_prepare_failure_in_local_flow(self) -> None:
calls: list[str] = []
def local_flow(fake: FakeLocaldocs) -> None:
prepare.prepare_request(*ARGS, localdocs=fake)
calls.append("ingress")
with self.assertRaises(RuntimeError):
local_flow(FakeLocaldocs(fail_at="write"))
self.assertEqual(calls, [])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,171 @@
from __future__ import annotations
import json
from pathlib import Path
import sys
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from runtime import s2_00_ingress as ingress # noqa: E402
from runtime import s2_00_prepare_request as prepare # noqa: E402
ARGS = ("request-1", "attempt.1", "stage1/runs/matter-1", "stage1/deployment")
class FakeLocaldocs:
def __init__(self, *, fail_at: str | None = None) -> None:
self.fail_at = fail_at
self.initialized = False
self.closed = False
self.writes: list[tuple[str, bytes]] = []
self.saved: bytes | None = None
def initialize(self) -> None:
if self.fail_at == "initialize":
raise RuntimeError("initialize failed")
self.initialized = True
def write_binary_verified(self, path: str, payload: bytes) -> str:
if self.fail_at == "write":
raise RuntimeError("write failed")
self.writes.append((path, payload))
self.saved = payload
if self.fail_at == "readback":
raise prepare.PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "read-back differs")
return prepare.hashlib.sha256(payload).hexdigest()
def close(self) -> None:
self.closed = True
if self.fail_at == "close":
raise RuntimeError("close failed")
class FakeResponse:
def __init__(self, payload: dict, *, session_id: str = "session-1") -> None:
self.payload = payload
self.headers = {"mcp-session-id": session_id}
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return self.payload
class FakeMcpClient:
def __init__(self, *, corrupt_read: bool = False) -> None:
self.corrupt_read = corrupt_read
self.saved: bytes | None = None
self.closed = False
self.calls: list[str] = []
def post(self, url: str, *, json: dict, headers: dict) -> FakeResponse:
if url != prepare.LOCALDOCS_URL:
raise RuntimeError("unexpected endpoint")
method = json["method"]
self.calls.append(method)
if method == "initialize":
result = {"protocolVersion": prepare.MCP_PROTOCOL_VERSION}
elif method == "notifications/initialized":
return FakeResponse({})
else:
name = json["params"]["name"]
arguments = json["params"]["arguments"]
if name == "write_binary_file":
if arguments["path"] != prepare.REQUEST_PATH:
raise RuntimeError("wrong write path")
self.saved = prepare.base64.b64decode(arguments["content_base64"])
text = "{}"
elif name == "read_binary_doc":
payload = b"corrupt" if self.corrupt_read else self.saved
text = prepare.json.dumps({"content_base64": prepare.base64.b64encode(payload).decode("ascii")})
else:
raise RuntimeError("wrong tool")
result = {"content": [{"type": "text", "text": text}]}
return FakeResponse({"jsonrpc": "2.0", "id": json["id"], "result": result})
def close(self) -> None:
self.closed = True
class PrepareRequestTests(unittest.TestCase):
def test_exact_canonical_six_field_request_matches_existing_ingress(self) -> None:
payload = prepare.build_request(*ARGS)
value = json.loads(payload)
self.assertEqual(set(value), prepare.REQUEST_KEYS)
self.assertEqual(payload, ingress.canonical_json_bytes(value))
self.assertEqual(ingress._inline_validate_request(payload), value)
self.assertTrue(payload.endswith(b"\n"))
def test_invalid_inputs_never_write(self) -> None:
variants = [
(123, *ARGS[1:]),
("bad/id", *ARGS[1:]),
(ARGS[0], "bad id", *ARGS[2:]),
(*ARGS[:2], "../escape", ARGS[3]),
(*ARGS[:3], "/absolute"),
(*ARGS[:2], "stage1//run", ARGS[3]),
(*ARGS[:2], "stage1/e\u0301", ARGS[3]),
]
for values in variants:
with self.subTest(values=values):
fake = FakeLocaldocs()
with self.assertRaises(prepare.PrepareError):
prepare.prepare_request(*values, localdocs=fake)
self.assertEqual(fake.writes, [])
def test_verified_write_and_close(self) -> None:
fake = FakeLocaldocs()
result = prepare.prepare_request(*ARGS, localdocs=fake)
self.assertEqual(fake.writes, [(prepare.REQUEST_PATH, prepare.build_request(*ARGS))])
self.assertTrue(fake.initialized)
self.assertTrue(fake.closed)
self.assertEqual(result["request_sha256"], prepare.hashlib.sha256(fake.saved).hexdigest())
def test_io_and_close_failures_do_not_return_success(self) -> None:
for failure in ("initialize", "write", "readback", "close"):
with self.subTest(failure=failure):
fake = FakeLocaldocs(fail_at=failure)
with self.assertRaises(Exception):
prepare.prepare_request(*ARGS, localdocs=fake)
self.assertTrue(fake.closed)
def test_localdocs_write_path_is_fixed(self) -> None:
session = object.__new__(prepare.LocaldocsSession)
with self.assertRaises(prepare.PrepareError):
session.write_binary_verified("other/request.json", b"{}\n")
def test_real_session_adapter_writes_and_reads_back_through_mcp(self) -> None:
client = FakeMcpClient()
session = prepare.LocaldocsSession("a" * 64, "b" * 64, client=client)
result = prepare.prepare_request(*ARGS, localdocs=session)
self.assertEqual(client.saved, prepare.build_request(*ARGS))
self.assertEqual(result["request_sha256"], prepare.hashlib.sha256(client.saved).hexdigest())
self.assertEqual(client.calls, ["initialize", "notifications/initialized", "tools/call", "tools/call"])
self.assertTrue(client.closed)
def test_real_session_adapter_rejects_readback_mismatch(self) -> None:
client = FakeMcpClient(corrupt_read=True)
session = prepare.LocaldocsSession("a" * 64, "b" * 64, client=client)
with self.assertRaises(prepare.PrepareError) as raised:
prepare.prepare_request(*ARGS, localdocs=session)
self.assertEqual(raised.exception.code, "LOCALDOCS_WRITE_READBACK_MISMATCH")
self.assertTrue(client.closed)
def test_ingress_is_not_called_after_prepare_failure_in_local_flow(self) -> None:
calls: list[str] = []
def local_flow(fake: FakeLocaldocs) -> None:
prepare.prepare_request(*ARGS, localdocs=fake)
calls.append("ingress")
with self.assertRaises(RuntimeError):
local_flow(FakeLocaldocs(fail_at="write"))
self.assertEqual(calls, [])
if __name__ == "__main__":
unittest.main()
@@ -22,8 +22,8 @@ Agent:
- name: S2_00 - name: S2_00
description: >- description: >-
C00, C05, C10, C15의 IO·순서·분기·배리어를 선언하는 NON-LLM 계약. C00, C05, C10, C15의 IO·순서·분기·배리어를 선언하는 NON-LLM 계약.
실행 Python은 이 문서가 아니라 Agent YAML의 exactly-one 실행 Python은 Agent YAML의 request 준비와 ingress 두
code-executor.run_code task에만 존재한다. code-executor.run_code task에 존재한다.
prevs: [] prevs: []
nexts: nexts:
- S2_10 - S2_10
@@ -102,8 +102,13 @@ Agent:
requirements: "httpx==0.28.1" requirements: "httpx==0.28.1"
network_profile: agent-network network_profile: agent-network
timeout_seconds: 300 timeout_seconds: 300
inline_task_count: 1 inline_task_count: 2
executable_task_owner_ref: agent_scripts/Stage_2_S2_00.yml#/Agent/Stages/0/tasks/0 prepare_task_owner_ref: agent_scripts/Stage_2_S2_00.yml#/Agent/Stages/0/tasks/0
executable_task_owner_ref: agent_scripts/Stage_2_S2_00.yml#/Agent/Stages/0/tasks/1
task_order: [Task_S2_00_prepare_request, Task_S2_00_deterministic_ingress]
backend_argument_binding_status: UNVERIFIED
backend_success_gate_status: UNVERIFIED
workspace_serialization_status: UNVERIFIED
retry_policy_id: S2-RETRY-TRANSIENT-READ-V1 retry_policy_id: S2-RETRY-TRANSIENT-READ-V1
retry_policy_ref: manifest/stage2_release.json#/retry_policies/0 retry_policy_ref: manifest/stage2_release.json#/retry_policies/0
idempotence_key_material: idempotence_key_material:
@@ -122,6 +127,14 @@ Agent:
request_contract: request_contract:
fixed_path: stage2_control/s2_00_request.json fixed_path: stage2_control/s2_00_request.json
producer_task: Task_S2_00_prepare_request
producer_write_path: stage2_control/s2_00_request.json
external_argument_delivery_status: UNVERIFIED
required_external_arguments:
- request_id
- attempt_id
- stage1_run_root_ref
- stage1_deployment_root_ref
schema_ref: schemas/ingress.schema.json#/$defs/execution_request schema_ref: schemas/ingress.schema.json#/$defs/execution_request
directory_scan_allowed: false directory_scan_allowed: false
alternate_request_path_allowed: false alternate_request_path_allowed: false
@@ -383,7 +396,7 @@ Agent:
llm_calls_allowed: false llm_calls_allowed: false
llm_configuration_present: false llm_configuration_present: false
inline_python_allowed_in_workflow_contract: false inline_python_allowed_in_workflow_contract: false
executable_inline_python_owner: agent_scripts/Stage_2_S2_00.yml#/Agent/Stages/0/tasks/0/parameters/code executable_inline_python_owner: agent_scripts/Stage_2_S2_00.yml#/Agent/Stages/0/tasks/1/parameters/code
inline_schema_allowed: false inline_schema_allowed: false
external_internet_access_allowed: false external_internet_access_allowed: false
mcp_endpoint_allowlist: mcp_endpoint_allowlist:
@@ -16,6 +16,34 @@
- 하위 에이전트(subagents)를 사용하여 핵심 테마와 교훈을 식별하고 MEMORY.md에 섹션으로 저장하라. - 하위 에이전트(subagents)를 사용하여 핵심 테마와 교훈을 식별하고 MEMORY.md에 섹션으로 저장하라.
- 향후 세션 시작 시 MEMORY.md의 이전 섹션을 참조하라. - 향후 세션 시작 시 MEMORY.md의 이전 섹션을 참조하라.
## 2026-10-01 — 현행 S2_00 배포 YAML 분석
한 줄 요약: `Default_Agent/Stage_2_Clean/agent_scripts/Analysis_Stage_2_S2_00.md`에 현행 두-task DAG, C00–C15의 단일 ingress 실행 경계, 고정·동적 입력과 분기별 출력, 배포 자산·후속 route를 기록했다. prepare 함수의 request 저장 구현과 배포 YAML의 직접 실행 가능성을 구분하는 것이 핵심이다. 현재 네 외부 인자 결속·실패 차단·workspace 직렬화는 미검증이고, 직접 실행은 fail closed하며 DEV fixture release는 실제 사건 발행을 금지한다.
## 2026-10-01 — S2_00 request 준비 코드·두 task YAML
한 줄 요약: 기존 배포 YAML을 `Stage_2_S2_00_outdated_10_01.yml`로 byte-identical 보존하고, 현행 S2_00 앞단에 네 인자→정확히 6필드 canonical JSON 생성·검증·localdocs 고정 경로 write/read-back을 수행하는 `Task_S2_00_prepare_request`를 추가했다. authoring/deployment YAML, 두 code mirror와 receipt, builder·workflow·binding·schema·회귀시험 및 영향받은 parent/child release hash 참조를 갱신했다. 함수·가짜 localdocs 및 정적 projection/release 검증은 통과했지만 backend의 네 인자 주입·prepare 실패 후 ingress 차단·동일 workspace 직렬화는 미확인이다. 직접 `run_code`는 인자 미결속 오류로 fail closed하며 DEV release도 계속 `DRAFT_NOT_EXECUTABLE`이다. S2_20 전체 시험의 case catalog 1건 실패는 별도 미추적 `Case_02_Comparison_Research/case_kinds.md`와 기존 registry의 불일치로 남겼고 해당 자료는 변경하지 않았다.
## 2026-10-01 — S2_00 request 준비 계획 v2
한 줄 요약: `plans/s2-00-request-preparation_v2.md`를 작성해, 네 인자 함수의 6필드 JSON 생성·검증·localdocs 저장/read-back과 Stage 2 작업명세서 첫 task 배치를 backend 확인 전에 구현·offline 시험하도록 순서를 개정했다. 실제 인자 주입·prepare 실패 차단·workspace 직렬화는 운영 연결 검증으로 남기며, v2 작성만으로 YAML·request 생성이나 live-ready 상태를 주장하지 않는다.
## 2026-09-30 — S2_00 IO 분석·request 준비 계획 및 구현 경계 정정
한 줄 요약: S2_00 입출력 문서와 request 준비 계획·단순화 v1을 작성하고 원본 YAML을 보존했으며, request 생성 코드 구현과 backend 운영 연결 검증을 분리해야 한다는 판단으로 정정했다.
- `Stage_2_00_IO_info.md`: 배포 YAML·분석서·release를 대조하여 DAG, 단계별 파일명·형식·경로, Stage 1 고정 입력 16개·배포 의존 55개·Stage 2 직접 의존 49개, 정상 11+E/diagnostic 5개 출력 및 임시/최종 저장을 문서화했다. 사건별 root·signal 목록·digest·cluster ID는 실제 값이 아닌 결정 규칙으로 구분했다.
- `../../plans/s2-00-request-preparation.md` 및 `_v1.md`: 두-task request 준비 계획을 작성하고, v1은 동일 불변 외부 인자에서 request를 재구성하여 대조하는 방식으로 별도 준비 receipt 전달·중복 검증을 줄였다. 구현 범위는 v1의 Objective 이하이며, 기존 6필드·canonical JSON·검증·localdocs write/read-back·후행 실패 차단·동시 실행 통제·관련 hash 일관성을 유지한다.
- `Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_9_09.yml`에 기존 배포본을 byte-identical로 보존했다(SHA-256 `94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943`). 기존 builder `--check`는 `PARITY_PASS`, mismatches=[]였으며 변경 전 기준선만 검증했다. 활성 YAML·authoring/runtime/binding은 변경하지 않았고 request writer 및 신규 YAML 구현·live 실행은 미완료다.
- Stage 1 Default_Agent 자산, v.8 YAML 4개 및 분석 보고서 4개를 확인했다. task 의존·동적 확장·workspace 치환에 관한 단서는 있으나 S2_00 외부 인자 공급·success-only·workspace 직렬화의 backend 구현 전체를 입증하지는 않는다.
- **정정·후속 원칙:** backend 정보 미확인을 이유로 생성 코드 구현까지 중단한 것은 과도했다. 기존 Stage 2 방식과 일관된 생성·검증·저장 함수를 네 외부 인자를 받도록 구현하고 offline 시험하는 데 backend 전체 구현·명세는 필요하지 않다. 실제 인자 주입·실패 차단·동시 실행의 운영 연결은 필요한 backend 정보만 확보하여 별도로 완성·검증한다. 미지원 템플릿·환경변수·잠금 기능을 임의로 가정하거나 offline 성공을 live-ready로 표현하지 않는다.
## 2026-09-17 — Stage 2 외부 규칙 문서·Weaviate 절차에 관한 의견 평가
외부 문서 접근과 임베딩 검색 위임이 가능하다는 가정 아래 현행 배포 YAML 5종·분석서·관련 자산 및 Stage 1 인계 자료를 대조하여 `Evaluaton_on_hogyus_opinion.md`를 작성하였다. C25/C26의 사건종류·규칙 선택과 Markdown 원문 적재·해시 검증, C27~C29의 Weaviate `search_hybrid` 호출·요건사실 팩 구성은 존재하므로 “절차 부재”라는 평가는 정정하되, 승인된 규칙·검색 범위의 미충족, 규칙·청크 본문의 S2_30 전달 공백, 고정된 요건별 gap 자료, V12 필드와 S2_30→40 해시 대상 불일치는 접근 가능성만으로 해결되지 않음을 구분하였다. 외부 공용 자산의 부재를 추정하지 않았으며, 절차·출처 참조의 존재와 실제 내용 활용·요건 충족·실행 및 법률 검증을 분리하는 것이 핵심 교훈이다. 핵심 경로 독립 검토와 문서 링크·해시 확인을 수행하고 해시 전사 오류 1건을 수정하였으며, 실행 자산 변경·live LLM/MCP/Weaviate·E2E·법률검수·기존 회귀시험 재실행은 수행하지 않았다.
## 2026-09-09 — Stage 2 배포 YAML 5종 전문 분석과 단계 간 계약 차이 기록 ## 2026-09-09 — Stage 2 배포 YAML 5종 전문 분석과 단계 간 계약 차이 기록
한 줄 요약: 배포 YAML 00/10/20/30/40 총 16,100행을 독립 병렬 분담해 전문 분석하고 각 분석서에 정확히 2회 검증·증분수정을 수행했으며, “구현/봉인/과거 offline PASS”와 “현재 의미 인계·live 실행·법률 승인”을 구별해야 한다는 교훈을 확인했다. 한 줄 요약: 배포 YAML 00/10/20/30/40 총 16,100행을 독립 병렬 분담해 전문 분석하고 각 분석서에 정확히 2회 검증·증분수정을 수행했으며, “구현/봉인/과거 offline PASS”와 “현재 의미 인계·live 실행·법률 승인”을 구별해야 한다는 교훈을 확인했다.
@@ -1,10 +1,17 @@
# Stage 2_00 작업분석서 # Stage 2_00 작업분석서
## 2026-10-01 현행 구현 보충
아래 2026-09-09 분석과 `Y:행번호`는 [보존된 당시 배포본](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml)에 관한 기록이다. [현행 배포본](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml)은 `IN → Task_S2_00_prepare_request → Task_S2_00_deterministic_ingress → OUT`의 두 task DAG로 바뀌었다. 첫 task의 `build_request(request_id, attempt_id, stage1_run_root_ref, stage1_deployment_root_ref)`는 기존 ID·NFC 상대경로 규칙으로 정확히 6필드의 canonical UTF-8 JSON bytes를 만들고, `prepare_request`는 localdocs의 `stage2_control/s2_00_request.json`에 binary write/read-back한 후 세션을 닫는다. ingress의 C00–C15 및 기존 출력 계약은 유지된다. 준비 code의 별도 `.py/.txt` mirror와 두 task pointer/hash는 inline receipt에 기록된다.
현재 inline `run_code`에 네 인자를 주입하는 backend 문법이 확인되지 않아 준비 task의 직접 실행은 `PREPARE_ARGUMENT_BINDING_UNVERIFIED`와 exit 2로 실패한다. YAML의 edge가 backend의 성공 조건·동일 workspace 직렬화를 실제로 강제하는지도 미검증이다. 생성 함수·격리 localdocs 시험과 배포본의 정적 결속은 완료했으나 실사건 Stage 2 실행 적격성은 입증하지 않았다. 아래 본문에서 “단일 task”, 외부 request producer, 행번호라고 적힌 부분은 **보존본 분석 시점의 사실**로 읽어야 한다.
## 0. 분석 범위와 결론 ## 0. 분석 범위와 결론
분석 기준일은 2026-09-09이다. 분석 정본은 [배포 Stage_2_S2_00.yml](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml) **1–6,248행 전체**, SHA-256 `94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943`이다. 이 문서에서 `Y:번호`는 이 배포 YAML의 행번호이며, 각 절의 링크와 함께 읽는다. 실제 구현을 우선하고 [S2_00_SOW_v.2.md](S2_00_SOW_v.2.md), [S2_00_assets_v.2.md](S2_00_assets_v.2.md), [workflow 계약](Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml), [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json)는 의도·배포계약과의 대조에 사용했다. JSON이 한 행으로 저장된 release는 행번호 대신 JSON Pointer로 특정한다. 분석 기준일은 2026-09-09이다. 분석 정본은 [배포 Stage_2_S2_00.yml](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml) **1–6,248행 전체**, SHA-256 `94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943`이다. 이 문서에서 `Y:번호`는 이 배포 YAML의 행번호이며, 각 절의 링크와 함께 읽는다. 실제 구현을 우선하고 [S2_00_SOW_v.2.md](S2_00_SOW_v.2.md), [S2_00_assets_v.2.md](S2_00_assets_v.2.md), [workflow 계약](Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml), [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json)는 의도·배포계약과의 대조에 사용했다. JSON이 한 행으로 저장된 release는 행번호 대신 JSON Pointer로 특정한다.
S2_00은 **Stage 1 자료를 받아 후속 법률판단에 사용할 입력을 구성하는 단일 deterministic ingress task**다. Stage 1의 사실·증거·구조·signal·review를 검사하고, context·claim-neutral cluster·slice·S2_10용 structured-context handoff를 만든다. 청구권의 성립, 청구취지, 청구원인 문안을 LLM으로 판단하지 않는다. C00·C05·C10·C15는 한 Python 호출 안의 논리적 구성요소이며 네 개 Agent task가 아니다. `Agent.Stages[0].prevs`와 `nexts`는 모두 빈 배열이므로 전체 Stage 2 연결은 YAML 내부 stage edge가 아니라 **출력 barrier의 route와 외부 orchestrator**로 성립한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:1–43, 6018–6103, 6230–6248. S2_00은 **Stage 1 자료를 받아 후속 법률판단에 사용할 입력을 구성하는 단일 deterministic ingress task**다. Stage 1의 사실·증거·구조·signal·review를 검사하고, context·claim-neutral cluster·slice·S2_10용 structured-context handoff를 만든다. 청구권의 성립, 청구취지, 청구원인 문안을 LLM으로 판단하지 않는다. C00·C05·C10·C15는 한 Python 호출 안의 논리적 구성요소이며 네 개 Agent task가 아니다. `Agent.Stages[0].prevs`와 `nexts`는 모두 빈 배열이므로 전체 Stage 2 연결은 YAML 내부 stage edge가 아니라 **출력 barrier의 route와 외부 orchestrator**로 성립한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:1–43, 6018–6103, 6230–6248.
다만 현재 코드와 배포계약만으로 “Stage 1 자료가 충분히 보존된 실행 가능한 S2_10 입력이 완성된다”고 단정할 수 없다. 현재 release는 `DEV_FIXTURE_RELEASE / DRAFT_NOT_EXECUTABLE`, bundle은 `STRUCTURAL_FIXTURE`, selected context는 빈 배열이다. 또한 source-family adapter 불일치, provenance pointer 불일치, 일부 자료의 slice 미편입 및 core와 MCP 진입경로의 실패처리 차이가 존재한다. 이들은 §8에 실제 구현의 제한으로 별도 기재했다. MEMORY의 240/240 등은 이전 작업 기록이며 본 분석에서 새로 실행한 시험 결과가 아니다. 다만 현재 코드와 배포계약만으로 “Stage 1 자료가 충분히 보존된 실행 가능한 S2_10 입력이 완성된다”고 단정할 수 없다. 현재 release는 `DEV_FIXTURE_RELEASE / DRAFT_NOT_EXECUTABLE`, bundle은 `STRUCTURAL_FIXTURE`, selected context는 빈 배열이다. 또한 source-family adapter 불일치, provenance pointer 불일치, 일부 자료의 slice 미편입 및 core와 MCP 진입경로의 실패처리 차이가 존재한다. 이들은 §8에 실제 구현의 제한으로 별도 기재했다. MEMORY의 240/240 등은 이전 작업 기록이며 본 분석에서 새로 실행한 시험 결과가 아니다.
@@ -20,7 +27,7 @@ S2_00은 **Stage 1 자료를 받아 후속 법률판단에 사용할 입력을
| `O/` | `W/stage2_runs/by-binding/<run_binding_digest>/` | | `O/` | `W/stage2_runs/by-binding/<run_binding_digest>/` |
| `T/` | Code Executor에서 `TemporaryDirectory(prefix="liti-s2-00-")`로 만드는 임시 root | | `T/` | Code Executor에서 `TemporaryDirectory(prefix="liti-s2-00-")`로 만드는 임시 root |
`W/Default_Agent/Stage_2_Clean`가 live 자산 root다. 저장소의 `R/`는 그 배포 패키지의 로컬 위치다. request가 임의 output root나 release path를 전달할 수는 없다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:194–205, 5548–5591, 5957–5959. `W/Default_Agent/Stage_2_Clean`가 live 자산 root다. 저장소의 `R/`는 그 배포 패키지의 로컬 위치다. request가 임의 output root나 release path를 전달할 수는 없다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:194–205, 5548–5591, 5957–5959.
## 1. 전체 작업 DAG ## 1. 전체 작업 DAG
@@ -95,13 +102,13 @@ stdout 단일 JSON inner receipt + exit 0
(저장 이후 read-back/close 실패도 같은 label; 저장상태 재확인 필요) (저장 이후 read-back/close 실패도 같은 label; 저장상태 재확인 필요)
``` ```
위 도식의 C00–C15는 작업 책임을 묶은 것이다. `execute_ingress`는 마지막에 manifest/intake/issue/status를 조립해 일괄 발행하므로 C00 또는 C10 진입 직후 해당 파일이 이미 외부에 저장되는 것은 아니다. 로컬 `os.rename`과 원격 `STATUS_LAST_LOGICAL_COMMIT`은 서로 다른 보장이다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:4015–4108, 4801–5243, 5896–6103. 위 도식의 C00–C15는 작업 책임을 묶은 것이다. `execute_ingress`는 마지막에 manifest/intake/issue/status를 조립해 일괄 발행하므로 C00 또는 C10 진입 직후 해당 파일이 이미 외부에 저장되는 것은 아니다. 로컬 `os.rename`과 원격 `STATUS_LAST_LOGICAL_COMMIT`은 서로 다른 보장이다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:4015–4108, 4801–5243, 5896–6103.
## 2. IO 구조 ## 2. IO 구조
### 2.1 외부 제어 입력과 Stage 1 사건 입력 ### 2.1 외부 제어 입력과 Stage 1 사건 입력
모든 아래 파일은 JSON이다. 이 표의 producer는 현 parent release `/stage1_sources`에 선언된 expected producer이며, 실제 Stage 1 사건 실행 사실을 확인했다는 의미가 아니다. raw source는 strict UTF-8, duplicate JSON key 및 NaN/Infinity 거부, 기본 파일당 32 MiB/run 256 MiB/depth 96/items 1,000,000 제한을 적용한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:83–87, 211–227, 299–329, 1084–1392. 모든 아래 파일은 JSON이다. 이 표의 producer는 현 parent release `/stage1_sources`에 선언된 expected producer이며, 실제 Stage 1 사건 실행 사실을 확인했다는 의미가 아니다. raw source는 strict UTF-8, duplicate JSON key 및 NaN/Infinity 거부, 기본 파일당 32 MiB/run 256 MiB/depth 96/items 1,000,000 제한을 적용한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:83–87, 211–227, 299–329, 1084–1392.
| 입력 exact path | producer/공급자 | C00 이후의 사용 | | 입력 exact path | producer/공급자 | C00 이후의 사용 |
|---|---|---| |---|---|---|
@@ -125,13 +132,13 @@ stdout 단일 JSON inner receipt + exit 0
| `U/quality_gates/stage1_part4_review_handoff.json` | `Task_C_FL_F2_final_fact_ledger_gate_and_writer` | wrapped review 정규화·count | | `U/quality_gates/stage1_part4_review_handoff.json` | `Task_C_FL_F2_final_fact_ledger_gate_and_writer` | wrapped review 정규화·count |
| `U/signals/<signal_manifest.files[i].path>` | signal compiler, alias `PA-SG-COMPILER-001` | 유일한 manifest-expanded 가변 input family. `signals/` 중복 prefix 금지 | | `U/signals/<signal_manifest.files[i].path>` | signal compiler, alias `PA-SG-COMPILER-001` | 유일한 manifest-expanded 가변 input family. `signals/` 중복 prefix 금지 |
Stage 1의 16개 고정 입력과 signal family를 구분한다. manifest row의 `canonical`·`domain_signal`은 의미 입력, `compatibility_view`는 파일 무결성만 검사하는 입력이다. signal ID 하나로 deduplicate하지 않고 `(transaction, file_path, record_ordinal, signal_id)` 발생 단위를 보존한다. `USED/UNUSED/UNMAPPED` 판정은 source의 명시적 ID/ref 존재 여부에 따른다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:1420–1591, 1612–1669. Stage 1의 16개 고정 입력과 signal family를 구분한다. manifest row의 `canonical`·`domain_signal`은 의미 입력, `compatibility_view`는 파일 무결성만 검사하는 입력이다. signal ID 하나로 deduplicate하지 않고 `(transaction, file_path, record_ordinal, signal_id)` 발생 단위를 보존한다. `USED/UNUSED/UNMAPPED` 판정은 source의 명시적 ID/ref 존재 여부에 따른다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:1420–1591, 1612–1669.
조건부 추가 입력은 release `/dependency_locks/stage1/contract_manifest_ref`가 지정한 `D/<path>`와 `/completion_seal_ref`가 지정한 `U/<path>`다. 현재는 contract path가 null이고 completion ref도 없다. 따라서 새 Stage 1 completion 파일의 상시 생성을 요구하는 구조로 해석하지 않는다. core resolver는 release-bound `path_overrides`만 인정하지만 실제 inline hydration는 먼저 release의 원래 fixed path를 읽으므로 relocation 실행 호환성에는 §8의 제한이 있다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:791–820, 4141–4161, 5700–5720, 6162–6183. 조건부 추가 입력은 release `/dependency_locks/stage1/contract_manifest_ref`가 지정한 `D/<path>`와 `/completion_seal_ref`가 지정한 `U/<path>`다. 현재는 contract path가 null이고 completion ref도 없다. 따라서 새 Stage 1 completion 파일의 상시 생성을 요구하는 구조로 해석하지 않는다. core resolver는 release-bound `path_overrides`만 인정하지만 실제 inline hydration는 먼저 release의 원래 fixed path를 읽으므로 relocation 실행 호환성에는 §8의 제한이 있다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:791–820, 4141–4161, 5700–5720, 6162–6183.
### 2.2 출력 파일과 소비자 ### 2.2 출력 파일과 소비자
모든 persisted 출력은 canonical UTF-8 JSON이며 S2_00이 단독 writer다. `E`는 **최종 executable cluster 수**이다. 정상 branch는 아래 공통 4개 + context 7개 + slice E개, 즉 **11 + E개**다. diagnostic branch는 공통 4개 + technical diagnostic 1개, 정확히 5개다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:172–186, 5109–5243. 모든 persisted 출력은 canonical UTF-8 JSON이며 S2_00이 단독 writer다. `E`는 **최종 executable cluster 수**이다. 정상 branch는 아래 공통 4개 + context 7개 + slice E개, 즉 **11 + E개**다. diagnostic branch는 공통 4개 + technical diagnostic 1개, 정확히 5개다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:172–186, 5109–5243.
| 출력 경로 | branch | 내용 및 직접/후속 소비 | | 출력 경로 | branch | 내용 및 직접/후속 소비 |
|---|---|---| |---|---|---|
@@ -151,7 +158,7 @@ Stage 1의 16개 고정 입력과 signal family를 구분한다. manifest row의
`context_materialization_receipt`, `run_binding_receipt`, `hydration_stability_receipt`, `output_barrier`는 위 파일 안의 **embedded object**이며 별도 고정 파일이 아니다. `selected_legal_context_json`과 `cluster_case_payload_json`은 S2_00 output file이 아니라 **외부 orchestrator가 생성할 S2_10 item의 문자열 필드**다. `context_materialization_receipt`, `run_binding_receipt`, `hydration_stability_receipt`, `output_barrier`는 위 파일 안의 **embedded object**이며 별도 고정 파일이 아니다. `selected_legal_context_json`과 `cluster_case_payload_json`은 S2_00 output file이 아니라 **외부 orchestrator가 생성할 S2_10 item의 문자열 필드**다.
stdout에는 `stage2_s2_00_inner_receipt.v1` 한 객체만 출력한다. 성공은 `ok:true`, `SUCCEEDED` 또는 `DIAGNOSTIC_PUBLISHED`, route/run/hash와 `logical_publish_receipt`를 담는다. 예외는 `ok:false`, `FAILED_NO_BARRIER`, error와 exit 2다. stdout receipt를 `control/run_status.json`이나 최종 소송문안으로 오인하면 안 된다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:6018–6103. stdout에는 `stage2_s2_00_inner_receipt.v1` 한 객체만 출력한다. 성공은 `ok:true`, `SUCCEEDED` 또는 `DIAGNOSTIC_PUBLISHED`, route/run/hash와 `logical_publish_receipt`를 담는다. 예외는 `ok:false`, `FAILED_NO_BARRIER`, error와 exit 2다. stdout receipt를 `control/run_status.json`이나 최종 소송문안으로 오인하면 안 된다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:6018–6103.
## 3. 작업용 자산과 정확한 배포 위치 ## 3. 작업용 자산과 정확한 배포 위치
@@ -177,11 +184,11 @@ stdout에는 `stage2_s2_00_inner_receipt.v1` 한 객체만 출력한다. 성공
| `R/release_ops/stage2_loader.py` 및 `R/release_ops/stage2_loader.txt` | Python/TXT | 과거 loader 보존본. S2_00 live 실행 dependency가 아님 | | `R/release_ops/stage2_loader.py` 및 `R/release_ops/stage2_loader.txt` | Python/TXT | 과거 loader 보존본. S2_00 live 실행 dependency가 아님 |
| `R/manifest/stage2_deterministic_admission_receipt.json` | JSON | 후속 구현과 공유하는 detached admission 증거. S2_00 inline에서 직접 검증하지 않으므로 외부 admission 책임과 구분 | | `R/manifest/stage2_deterministic_admission_receipt.json` | JSON | 후속 구현과 공유하는 detached admission 증거. S2_00 inline에서 직접 검증하지 않으므로 외부 admission 책임과 구분 |
[근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:8–15, 194–208, 637–645, 5721–5753; [자산 inventory](S2_00_assets_v.2.md) §1.2·§3·§7; [workflow 계약](Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml) `orchestration_contract`. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:8–15, 194–208, 637–645, 5721–5753; [자산 inventory](S2_00_assets_v.2.md) §1.2·§3·§7; [workflow 계약](Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml) `orchestration_contract`.
### 3.2 Stage 1 고정 배포 closure 55개 ### 3.2 Stage 1 고정 배포 closure 55개
각 경로는 `D/` 기준이며 모두 JSON이다. source-of-truth는 [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json) `/dependency_locks/stage1/concrete_paths`이다. 전체 Stage 1 YAML/runtime를 import하는 것이 아니라 **열거된 55개 source 계약 파일만** raw hash로 결속해 읽는다. `expected_concrete_path_count=55`이며 선언 개수·중복 경로·hash·strict JSON을 검사한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:4164–4242, 5670–5698. 각 경로는 `D/` 기준이며 모두 JSON이다. source-of-truth는 [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json) `/dependency_locks/stage1/concrete_paths`이다. 전체 Stage 1 YAML/runtime를 import하는 것이 아니라 **열거된 55개 source 계약 파일만** raw hash로 결속해 읽는다. `expected_concrete_path_count=55`이며 선언 개수·중복 경로·hash·strict JSON을 검사한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:4164–4242, 5670–5698.
| lock ID 범위 | exact path 또는 닫힌 전개 규칙 | 개수·사용 | | lock ID 범위 | exact path 또는 닫힌 전개 규칙 | 개수·사용 |
|---|---|---| |---|---|---|
@@ -218,7 +225,7 @@ signal_manifest.schema.json
### 3.3 Stage 2 direct closure 49개 ### 3.3 Stage 2 direct closure 49개
현재 [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json) `/dependency_locks/stage2_direct`에는 아래 **49개**가 있다. inline hydration는 이 목록 전부를 읽고 raw hash 검증한다. selected context의 의미적 선택은 `/bundle/selected_context_refs`에 별도로 기록되며 현재 빈 배열이다. 따라서 “활성 profile만 원격 읽는다”와 “읽은 모든 profile을 LLM에게 준다”는 설명 모두 실제 코드와 다르다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:3283–3475, 5741–5753. 현재 [parent release](Default_Agent/Stage_2_Clean/manifest/stage2_release.json) `/dependency_locks/stage2_direct`에는 아래 **49개**가 있다. inline hydration는 이 목록 전부를 읽고 raw hash 검증한다. selected context의 의미적 선택은 `/bundle/selected_context_refs`에 별도로 기록되며 현재 빈 배열이다. 따라서 “활성 profile만 원격 읽는다”와 “읽은 모든 profile을 LLM에게 준다”는 설명 모두 실제 코드와 다르다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:3283–3475, 5741–5753.
| path prefix | exact basename | 개수·역할 | | path prefix | exact basename | 개수·역할 |
|---|---|---| |---|---|---|
@@ -330,7 +337,7 @@ ACTIO-DEFENSE-MAP.yml
### 4.1 C05 검사 세부 목록 ### 4.1 C05 검사 세부 목록
`check_conservation`은 `BO_FACT_MULTISET`, `FACT_ID_SEQUENCE`, `CURRENT_V8_LEDGER_EXTENSIONS`, `LES_BO_JOIN`, `LES_DECLARED_ACTUAL_COUNT`, `LES_REVERSE_INDEX`, `EVIDENCE_REFERENCE_CONSERVATION`, `EVENT_REFERENCE_CONSERVATION`, `EVENT_DISPOSITION_CONSERVATION`, `FACT_LEDGER_WRITER_REPORT_CONNECTION`, `SIGNAL_FILE_ROW_CONSERVATION`, `SIGNAL_RECORD_OCCURRENCE_CONSERVATION`, `REVIEW_OCCURRENCE_CONSERVATION`을 구성한다. BO와 source_bo_id의 Counter 동일성, fact 배열의 `F-001..F-N` 순서·유일성, LES attach/reverse index, 참조 universe, writer-report 실측 재계산을 구별한다. 비교 자료가 없는 일부 검사에는 `UNEVALUABLE`이 가능하며 이를 PASS와 동일시하지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:2068–2436. `check_conservation`은 `BO_FACT_MULTISET`, `FACT_ID_SEQUENCE`, `CURRENT_V8_LEDGER_EXTENSIONS`, `LES_BO_JOIN`, `LES_DECLARED_ACTUAL_COUNT`, `LES_REVERSE_INDEX`, `EVIDENCE_REFERENCE_CONSERVATION`, `EVENT_REFERENCE_CONSERVATION`, `EVENT_DISPOSITION_CONSERVATION`, `FACT_LEDGER_WRITER_REPORT_CONNECTION`, `SIGNAL_FILE_ROW_CONSERVATION`, `SIGNAL_RECORD_OCCURRENCE_CONSERVATION`, `REVIEW_OCCURRENCE_CONSERVATION`을 구성한다. BO와 source_bo_id의 Counter 동일성, fact 배열의 `F-001..F-N` 순서·유일성, LES attach/reverse index, 참조 universe, writer-report 실측 재계산을 구별한다. 비교 자료가 없는 일부 검사에는 `UNEVALUABLE`이 가능하며 이를 PASS와 동일시하지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:2068–2436.
### 4.2 cluster·slice·bundle의 서로 다른 의미 ### 4.2 cluster·slice·bundle의 서로 다른 의미
@@ -338,7 +345,7 @@ cluster는 청구권 또는 claim group의 확정값이 아니다. 같은 BO, LE
slice는 cluster별 content/ref snapshot이다. projection당 최대 262,144 UTF-8 bytes, slice content 합계 최대 2,097,152 bytes이며 넘으면 잘라내지 않고 exception을 발생시킨다. 모든 projection은 source ID에 결속된 하나의 raw source hash를 요구한다. `slice_digest`는 그 필드를 넣기 전 body의 canonical digest, bundle의 `member_slices[].sha256`는 digest 필드까지 포함한 최종 slice body의 canonical digest다. 두 hash를 같은 값으로 취급하면 안 된다. slice는 cluster별 content/ref snapshot이다. projection당 최대 262,144 UTF-8 bytes, slice content 합계 최대 2,097,152 bytes이며 넘으면 잘라내지 않고 exception을 발생시킨다. 모든 projection은 source ID에 결속된 하나의 raw source hash를 요구한다. `slice_digest`는 그 필드를 넣기 전 body의 canonical digest, bundle의 `member_slices[].sha256`는 digest 필드까지 포함한 최종 slice body의 canonical digest다. 두 hash를 같은 값으로 취급하면 안 된다.
cohort는 현재 compiler에서 동일 release-selected context를 쓰는 valid slice 전체를 묶는다. cluster별 profile을 별도로 법률추론하여 골라주는 알고리즘이 아니다. missing slice는 별도의 non-executable cohort로 보존한다. selected context 허용 kind는 `ACTIVE_PROFILE`, `APPROVED_COMMON_AUTHORITY`, `LAW_VALUE_TOKEN`, `AUTHORITY_PROPOSITION`; 여기의 법률 context에 주민번호·이메일·한국 휴대전화 패턴이 있으면 PII 오류를 남긴다. 이 제한을 사건별 dynamic payload의 PII 전면 금지로 확대 해석하지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:2877–3279, 3283–3906. cohort는 현재 compiler에서 동일 release-selected context를 쓰는 valid slice 전체를 묶는다. cluster별 profile을 별도로 법률추론하여 골라주는 알고리즘이 아니다. missing slice는 별도의 non-executable cohort로 보존한다. selected context 허용 kind는 `ACTIVE_PROFILE`, `APPROVED_COMMON_AUTHORITY`, `LAW_VALUE_TOKEN`, `AUTHORITY_PROPOSITION`; 여기의 법률 context에 주민번호·이메일·한국 휴대전화 패턴이 있으면 PII 오류를 남긴다. 이 제한을 사건별 dynamic payload의 PII 전면 금지로 확대 해석하지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:2877–3279, 3283–3906.
## 5. upstream–downstream 자료 의존 ## 5. upstream–downstream 자료 의존
@@ -354,13 +361,13 @@ cohort는 현재 compiler에서 동일 release-selected context를 쓰는 valid
| S2_00 → S2_40 status-only | 법률판단 입력이 성립하지 않는 run의 상태 보존 | 정확히 status·technical_diagnostic·manifest·intake·base issue 5개. 청구취지·청구원인 생성을 우회 | | S2_00 → S2_40 status-only | 법률판단 입력이 성립하지 않는 run의 상태 보존 | 정확히 status·technical_diagnostic·manifest·intake·base issue 5개. 청구취지·청구원인 생성을 우회 |
| S2_00 → S2_40 정상 검증 | 최종 문안이 원래 자료·run에 연결되는지 대조 | S2_00 barrier/manifest/source digest를 상류 검증 기준으로 사용. 최종 상태 writer는 S2_40 | | S2_00 → S2_40 정상 검증 | 최종 문안이 원래 자료·run에 연결되는지 대조 | S2_00 barrier/manifest/source digest를 상류 검증 기준으로 사용. 최종 상태 writer는 S2_40 |
[근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:211–227, 4801–5243; [SOW](S2_00_SOW_v.2.md) §8.3; [S2_10 Agent](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_10.yml), [S2_40 Agent](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_40.yml)는 후단 소비 명세의 위치이며 해당 전체 분석은 각각의 별도 작업분석서를 참조한다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:211–227, 4801–5243; [SOW](S2_00_SOW_v.2.md) §8.3; [S2_10 Agent](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_10.yml), [S2_40 Agent](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_40.yml)는 후단 소비 명세의 위치이며 해당 전체 분석은 각각의 별도 작업분석서를 참조한다.
## 6. 결정성·발행·실패의 의미 ## 6. 결정성·발행·실패의 의미
`input_set_digest`는 fixed snapshot과 signal/deployment closure의 logical ID/path/raw hash로 계산한다. `run_binding_digest`는 `input_set_digest + parent release raw hash + ALGORITHM_SEMANTIC_DIGEST + release_class`의 canonical JSON digest다. `run_id=S2RUN-<digest>`, output root는 `by-binding/<digest>`로 파생한다. algorithm digest는 `ALGORITHM_VERSION` 문자열에 namespace를 붙여 해시한 의미 버전 digest이며 **Python 전체 source hash와는 다르다**. actual source hash는 projection/receipt 체계가 별도로 관리한다. attempt ID는 canonical binding에 포함되지 않는다. request ID와 user/workspace hash는 receipt에 보존되지만 binding formula에는 포함되지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:79–82, 4544–4612. `input_set_digest`는 fixed snapshot과 signal/deployment closure의 logical ID/path/raw hash로 계산한다. `run_binding_digest`는 `input_set_digest + parent release raw hash + ALGORITHM_SEMANTIC_DIGEST + release_class`의 canonical JSON digest다. `run_id=S2RUN-<digest>`, output root는 `by-binding/<digest>`로 파생한다. algorithm digest는 `ALGORITHM_VERSION` 문자열에 namespace를 붙여 해시한 의미 버전 digest이며 **Python 전체 source hash와는 다르다**. actual source hash는 projection/receipt 체계가 별도로 관리한다. attempt ID는 canonical binding에 포함되지 않는다. request ID와 user/workspace hash는 receipt에 보존되지만 binding formula에는 포함되지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:79–82, 4544–4612.
로컬 publish는 전 출력 schema 검사 → non-status write/fsync → status에 exact artifact list/digest 삽입 → status write/fsync → staging directory rename 순서다. 원격은 localdocs `write_binary_file(overwrite:true)`와 매 파일 read-back을 사용하고 barrier를 마지막에 쓴다. 원격 directory rename·OS fsync·단일 transaction이 보장된다는 뜻은 아니다. 기존 remote barrier가 있으면 run binding과 **status 전체 bytes** 및 모든 artifact bytes까지 동일해야 idempotent success다. 다른 request ID가 같은 input binding을 공유하면 status bytes가 달라질 수 있으므로 “input이 같으면 request ID와 무관하게 무조건 재사용”이라고 쓰지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:4015–4108, 5934–6008. 로컬 publish는 전 출력 schema 검사 → non-status write/fsync → status에 exact artifact list/digest 삽입 → status write/fsync → staging directory rename 순서다. 원격은 localdocs `write_binary_file(overwrite:true)`와 매 파일 read-back을 사용하고 barrier를 마지막에 쓴다. 원격 directory rename·OS fsync·단일 transaction이 보장된다는 뜻은 아니다. 기존 remote barrier가 있으면 run binding과 **status 전체 bytes** 및 모든 artifact bytes까지 동일해야 idempotent success다. 다른 request ID가 같은 input binding을 공유하면 status bytes가 달라질 수 있으므로 “input이 같으면 request ID와 무관하게 무조건 재사용”이라고 쓰지 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:4015–4108, 5934–6008.
| 실제 조건 | 구현 결과 | | 실제 조건 | 구현 결과 |
|---|---| |---|---|
@@ -371,7 +378,7 @@ cohort는 현재 compiler에서 동일 release-selected context를 쓰는 valid
| hydration/transport/예외 등 core 정상 조립 이전 또는 도중 fatal 오류 | `FAILED_NO_BARRIER`, exit 2. diagnostic 5파일 자동 발행이 아님 | | hydration/transport/예외 등 core 정상 조립 이전 또는 도중 fatal 오류 | `FAILED_NO_BARRIER`, exit 2. diagnostic 5파일 자동 발행이 아님 |
| `release_class=DEV_FIXTURE_RELEASE`로 `execute_ingress` 진입 | `DEV_FIXTURE_REAL_RUN_FORBIDDEN`; STRUCTURAL_FIXTURE는 별개 bundle mode | | `release_class=DEV_FIXTURE_RELEASE`로 `execute_ingress` 진입 | `DEV_FIXTURE_REAL_RUN_FORBIDDEN`; STRUCTURAL_FIXTURE는 별개 bundle mode |
core diagnostic code 집합은 `RELEASE_SOURCE_CONTRACT_MISSING`, `RELEASE_SOURCE_PATH_MISMATCH`, `RAW_HASH_MISMATCH`, `SCHEMA_HASH_MISMATCH`, `SCHEMA_ID_MISMATCH`, `RUN_IDENTITY_CONFLICT`, `TRANSACTION_IDENTITY_CONFLICT`, `BO_FACT_CONSERVATION_FAILED`, `FACT_ID_CONSERVATION_FAILED`, `CURRENT_V8_LEDGER_EXTENSION_MISSING`다. 모든 ERROR를 동일하게 전역 차단하지 않으며 모든 exception을 status-only로 회복하지도 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml), Y:4770–4830, 6087–6103. core diagnostic code 집합은 `RELEASE_SOURCE_CONTRACT_MISSING`, `RELEASE_SOURCE_PATH_MISMATCH`, `RAW_HASH_MISMATCH`, `SCHEMA_HASH_MISMATCH`, `SCHEMA_ID_MISMATCH`, `RUN_IDENTITY_CONFLICT`, `TRANSACTION_IDENTITY_CONFLICT`, `BO_FACT_CONSERVATION_FAILED`, `FACT_ID_CONSERVATION_FAILED`, `CURRENT_V8_LEDGER_EXTENSION_MISSING`다. 모든 ERROR를 동일하게 전역 차단하지 않으며 모든 exception을 status-only로 회복하지도 않는다. [근거: 배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml), Y:4770–4830, 6087–6103.
`FAILED_NO_BARRIER`는 반환 label이지 실제 barrier가 없다는 확인 결과가 아니다. Y:5989의 status write 뒤 5990의 read-back 실패, 또는 publish 성공 후 Y:6084–6086의 localdocs.close 실패도 generic catch에서 이 label을 반환한다. 이미 쓴 non-status 파일과 barrier를 rollback하지 않으므로 외부 실행 관리자는 실제 상태/hash를 재확인해야 한다. `FAILED_NO_BARRIER`는 반환 label이지 실제 barrier가 없다는 확인 결과가 아니다. Y:5989의 status write 뒤 5990의 read-back 실패, 또는 publish 성공 후 Y:6084–6086의 localdocs.close 실패도 generic catch에서 이 label을 반환한다. 이미 쓴 non-status 파일과 barrier를 rollback하지 않으므로 외부 실행 관리자는 실제 상태/hash를 재확인해야 한다.
@@ -0,0 +1,344 @@
# Stage_2_S2_00.yml 단계별 Input / Output 파일 정보
## 2026-10-01 현행 IO 변경
아래 표와 `Y:행번호`는 [보존된 기존 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml)의 정적 분석이다. [현행 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00.yml)의 Agent DAG는 `IN → Task_S2_00_prepare_request → Task_S2_00_deterministic_ingress → OUT`이다.
| 단계 | 입력 파일·형식·source folder | 출력 파일·형식·저장 folder | 상태 |
|---|---|---|---|
| `Task_S2_00_prepare_request` | 고정 입력 파일 없음. 호출자가 공급하는 네 문자열 `request_id`, `attempt_id`, `stage1_run_root_ref`, `stage1_deployment_root_ref` 및 인증된 localdocs workspace 세션 | `s2_00_request.json` · canonical UTF-8 JSON, 정확히 6필드 · `W/stage2_control/` | 함수의 생성·검증·write/read-back은 offline 시험 완료. backend 인자 전달·성공 게이트·workspace 직렬화는 미검증이며 직접 `run_code` 실행은 fail closed |
| `Task_S2_00_deterministic_ingress` | `W/stage2_control/s2_00_request.json` 및 아래 §3의 기존 Stage 1·release 입력 | 아래 §4의 기존 `O/` 정상 11+E 또는 diagnostic 5개 JSON | 기존 ingress 검증과 두 read-pass 유지. 실사건 실행은 미검증 |
`W/`는 동일 user/workspace의 localdocs 논리 root다. 첫 task의 성공 뒤에도 다른 실행이 고정 request 경로를 덮어쓰지 못한다는 보장은 backend 통합 전까지 없다. 아래 “외부 orchestrator가 request를 공급”이라는 producer 표기는 보존본 기준이며, 현행 코드상 producer는 새 prepare task다.
## 1. 범위·기준 및 경로 표기
작성 기준일: 2026-09-30. 사용자 지정 [Stage_2_00_Analysis_v1.md](Stage_2_00_Analysis_v1.md)의 DAG·IO·자산 분석을 기준으로, [배포 YAML](Default_Agent/Stage_2_Clean/agent_scripts/Stage_2_S2_00_outdated_10_01.yml)과 해당 YAML이 참조하는 release/module manifest를 대조하였다. main 폴더 바로 아래의 동명 YAML은 분석 대상이 아니다. `Y:행번호`는 배포 YAML의 행번호이다.
이 문서는 **파일 계약에 대한 정적 분석**이다. 사건 run이나 MCP 실행을 수행하여 파일 생성 성공을 확인한 문서가 아니다. 현재 release는 `DEV_FIXTURE_RELEASE / DRAFT_NOT_EXECUTABLE`이며 core의 DEV guard가 실제 run을 거절한다. 아래 출력은 해당 실행 경로가 정상적으로 완료되는 경우의 코드상 산출물이다.
| 기호 | 정확한 위치·결정 방식 |
|---|---|
| `M/` | `prompt_updates_sequential/Case_02_Comparison_Research/YAML_Prompts/2. Stage_2/` — 이 문서 저장 폴더 |
| `R/` | `M/Default_Agent/Stage_2_Clean/` — 로컬 배포 패키지 |
| `W/` | localdocs가 user/workspace session에 결속하는 논리 workspace root. 로컬 Dropbox 절대경로와 동일하다고 가정하지 않음 |
| `A/` | `W/Default_Agent/Stage_2_Clean/` — 실행 시 Stage 2 자산 source root. 로컬 대응 위치는 `R/` |
| `U/` | `W/<s2_00_request.json의 stage1_run_root_ref>/` — Stage 1 사건 입력 root |
| `D/` | `W/<s2_00_request.json의 stage1_deployment_root_ref>/` — Stage 1 고정 배포 source root |
| `O/` | `W/stage2_runs/by-binding/<run_binding_digest>/` — 최종 파일 저장 root |
| `T/` | Code Executor의 `TemporaryDirectory(prefix="liti-s2-00-")`로 생성한 임시 root |
`U/`, `D/`, `T/`, `<run_binding_digest>`, `<cluster_id>`의 실제 값은 실행 입력·환경·내용에 따라 결정된다. 분석 자료만으로 실제 사건 폴더명이나 digest를 확정할 수 없으므로, 임의 값 대신 정확한 결정식을 기재한다. 각 표의 **folder + file name**을 이어 붙이면 전체 경로가 된다. 대소문자와 점·밑줄은 원문대로 유지하였다.
근거: 분석서 §0.1·§2·§6, Y:194–227, 4544–4612, 5548–5591, 5957–5959, 6032–6059.
분석 YAML SHA-256: `94c2744d71d222cfb5cc1b2a0d33a6cda20079439823de431eef378a2fa5c943`. 분석서에 기록된 YAML hash와 일치한다. 분석서 SHA-256: `905abf5d93d67b7ab7f9f55e0197e320a69c2d909859dcba145e173a13b65253`.
## 2. 작업 DAG structure
```mermaid
flowchart TD
IN[Agent IN] --> TASK[Task_S2_00_deterministic_ingress: 단일 code-executor.run_code]
TASK --> H0[MCP 세션 초기화 · request/release 읽기]
H0 --> H1[exact input closure 2회 읽기 · hash/bytes 대조 · 임시 파일 hydration]
H1 --> C00[C00: source/schema/producer/identity/seal · signal ALL 검증]
C00 --> C05[C05: review 정규화 · 보존검사]
C05 --> B[run binding · artifact header]
B --> C10[C10: case/evidence/object/party/slot context]
C10 --> C15A[C15: membership · cluster · SCC · scheduling wave]
C15A --> C15B[C15: slice · bundle/cohort · invariant]
C15B --> ROUTE[executable/residual 분할 · route · manifest/intake/issue/status 조립]
ROUTE --> N[정상: 공통 4 + context 7 + slice E]
ROUTE --> D[diagnostic: 공통 4 + technical diagnostic 1]
N --> L[원본 snapshot 재검사 결과 반영 · output schema 검사 · 임시 tree 저장/rename]
D --> L
L --> V[exact artifact set/hash 재검사]
V --> P[원격 non-status write/read-back · ingress_status 마지막 write/read-back]
P --> OUT[stdout JSON receipt · Agent OUT]
OUT --> EXT[외부 orchestrator: barrier 검증]
EXT --> S10[S2_10 또는 S2_10_WITH_ISSUES]
EXT --> S40[S2_40_STATUS_ONLY]
```
원본 snapshot 재검사는 실제 코드에서 route 결정 직후, manifest/intake/status 최종 조립 이전에 수행된다(Y:5114–5117). 도식의 저장 노드는 그 재검사 결과를 전제로 한다.
Agent의 실제 task edge는 `IN → Task_S2_00_deterministic_ingress → OUT` 하나다. C00·C05·C10·C15는 단일 Python 호출 내부의 논리 단계이다. **C00–C15 사이에는 중간 JSON 파일을 저장하고 다음 단계가 다시 읽는 구조가 없다.** 메모리 객체를 전달하고 마지막에 선택된 출력 집합을 일괄 저장한다. S2_10/S2_40 호출은 YAML 외부 orchestrator의 작업이다.
예외가 발생하면 `ok:false / FAILED_NO_BARRIER` receipt 및 exit 2로 빠질 수 있다. 이는 diagnostic 5파일이 자동 생성된다는 뜻도, 이미 저장한 원격 파일이 없다는 보장도 아니다.
근거: 분석서 §1·§4·§6, Y:4801–5243, 5896–6103, 6230–6248.
## 3. 전체 입력 파일 목록
### 3.1 제어·자산 메타데이터 및 출력 검증 schema
| ID | 정확한 file name | file format | source folder | 사용 단계 |
|---|---|---|---|---|
| CTRL-1 | `s2_00_request.json` | JSON, UTF-8 | `W/stage2_control/` | H0/H1: 6개 request 필드와 source root 결정 |
| CTRL-2 | `stage2_release.json` | JSON | `A/manifest/` | H0/H1 및 C00–C15: raw hash pin·입력 및 bundle 계약 |
| META-1 | `module_manifest.json` | JSON | `A/manifest/` | H1: 아래 schema 3개 선택·hash 검증 |
| SCHEMA-1 | `ingress.schema.json` | JSON Schema (`.json`) | `A/schemas/` | hydration, manifest/intake/status/diagnostic 출력 검증 |
| SCHEMA-2 | `context.schema.json` | JSON Schema (`.json`) | `A/schemas/` | hydration, header 및 context/slice/bundle 검증 |
| SCHEMA-3 | `review_status.schema.json` | JSON Schema (`.json`) | `A/schemas/` | hydration, issue ledger 검증 |
request는 정확히 `schema_version`, `workflow_id`, `request_id`, `attempt_id`, `stage1_run_root_ref`, `stage1_deployment_root_ref`의 6필드이다. 임의 output root를 받지 않는다. YAML 실행 정의 자체는 `R/agent_scripts/Stage_2_S2_00.yml`(YAML)이며 backend가 그 안의 Python code를 실행한다. 별도 `.py` 파일을 사건 입력으로 읽는 방식이 아니다.
근거: Y:194–208, 637–682, 5548–5591, 5721–5753.
### 3.2 Stage 1 사건 고정 입력 16개
아래 모두 strict UTF-8 JSON이다. H1에서 전부 필수 읽기 및 임시 복제하고 C00에서 계약 검증한다. 단계별 의미 사용은 §5에 정리한다. source folder는 원격 `U/` 기준이며 core가 읽는 대응 폴더는 `T/stage1_run/`이다.
| ID | logical input ID | 정확한 file name | file format | source folder |
|---|---|---|---|---|
| I01 | `evidence_indexed` | `evidence_indexed.json` | JSON | `U/` |
| I02 | `evidence_event_candidates` | `evidence_event_candidates.json` | JSON | `U/` |
| I03 | `client_goal` | `client_goal.json` | JSON | `U/` |
| I04 | `domain_screening` | `domain_screening.json` | JSON | `U/routing/` |
| I05 | `domain_activation_manifest` | `domain_activation_manifest.json` | JSON | `U/routing/` |
| I06 | `b1_evidence_indexed_gate` | `B1_evidence_indexed_gate.json` | JSON | `U/quality_gates/` |
| I07 | `b2_event_candidates_gate` | `B2_event_candidates_gate.json` | JSON | `U/quality_gates/` |
| I08 | `stage1_part1_soft_gate_handoff` | `stage1_part1_soft_gate_handoff.json` | JSON | `U/quality_gates/` |
| I09 | `bo` | `BO.json` | JSON | `U/` |
| I10 | `signal_manifest` | `signal_manifest.json` | JSON | `U/signals/` |
| I11 | `stage1_part2_review_handoff` | `stage1_part2_review_handoff.json` | JSON | `U/quality_gates/` |
| I12 | `legal_effect_structures` | `legal_effect_structures.json` | JSON | `U/` |
| I13 | `stage1_part3_review_handoff` | `stage1_part3_review_handoff.json` | JSON | `U/quality_gates/` |
| I14 | `fact_ledger_base` | `Fact_Ledger_base.json` | JSON | `U/` |
| I15 | `fact_ledger_writer_report` | `fact_ledger_writer_report.json` | JSON | `U/stage1_tmp/fact_ledger/` |
| I16 | `stage1_part4_review_handoff` | `stage1_part4_review_handoff.json` | JSON | `U/quality_gates/` |
### 3.3 manifest 전개 입력·조건부 입력
| 입력 | 정확한 file name / path 결정 규칙 | file format | source folder | 현재 확정 범위 |
|---|---|---|---|---|
| signal payload family | `basename(signal_manifest.files[i].path)` | JSON | `U/signals/<dirname(files[i].path)>/`(dirname이 비어 있으면 `U/signals/`) | `U/signals/signal_manifest.json`의 모든 `files[]`를 전개. 전체 파일명 확정에는 실제 사건 manifest 필요 |
| SG-01 activation | `domain_activation_manifest.json` | JSON | `U/signals/` | 위 family 내부에서 core가 명시적으로 찾는 파일. `U/routing/`의 동명 파일과 별개 |
| contract manifest | `basename(release.dependency_locks.stage1.contract_manifest_ref.path)` | JSON | `D/<dirname(path)>/` | 현 release는 path=null. 현재 고정 입력에는 추가하지 않음 |
| completion seal | `basename(release.dependency_locks.stage1.completion_seal_ref.path)` | JSON | `U/<dirname(path)>/` | 현 release는 ref 없음. 현재 고정 입력에는 추가하지 않음 |
signal의 정확한 전체 경로는 `U/signals/<files[i].path>`. `files[i].path` 자체에 `signals/` prefix를 포함하면 금지된다. canonical/domain_signal은 의미 처리, compatibility_view는 무결성 검증 대상이다. 디렉터리 scan이나 추측한 고정 파일 목록으로 대체하지 않는다.
근거: 분석서 §2.1, Y:1420–1591, 4895–4914, 5635–5720, 6162–6183.
### 3.4 Stage 1 고정 배포 입력 55개
source-of-truth: [stage2_release.json](Default_Agent/Stage_2_Clean/manifest/stage2_release.json)의 `/dependency_locks/stage1/concrete_paths`. H1에서 **전부** raw hash를 검증하고 `T/stage1_deployment/`의 같은 상대경로로 복제한다. C00 검증과 C10 slot 구성에서 사용한다. schema 파일도 직렬화 형식은 JSON이다. 근거: 분석서 §3.2, Y:4164–4242, 5670–5698.
| 번호 | 정확한 file name | file format | source folder |
|---|---|---|---|
| D01 | `runtime_manifest.json` | JSON | `D/` |
| D02 | `_registry_index.json` | JSON | `D/domains/` |
| D03 | `signal_registry.v2.json` | JSON | `D/signals/` |
| D04 | `domain_config.json` | JSON | `D/domains/E-00/` |
| D05 | `domain_config.json` | JSON | `D/domains/E-01/` |
| D06 | `domain_config.json` | JSON | `D/domains/E-02/` |
| D07 | `domain_config.json` | JSON | `D/domains/E-03/` |
| D08 | `domain_config.json` | JSON | `D/domains/E-04/` |
| D09 | `domain_config.json` | JSON | `D/domains/E-05/` |
| D10 | `domain_config.json` | JSON | `D/domains/E-06/` |
| D11 | `domain_config.json` | JSON | `D/domains/E-07/` |
| D12 | `domain_config.json` | JSON | `D/domains/E-08/` |
| D13 | `domain_config.json` | JSON | `D/domains/E-09/` |
| D14 | `domain_config.json` | JSON | `D/domains/E-10/` |
| D15 | `domain_config.json` | JSON | `D/domains/E-11/` |
| D16 | `domain_config.json` | JSON | `D/domains/E-12/` |
| D17 | `domain_config.json` | JSON | `D/domains/E-13/` |
| D18 | `domain_config.json` | JSON | `D/domains/E-14/` |
| D19 | `domain_config.json` | JSON | `D/domains/E-15/` |
| D20 | `domain_config.json` | JSON | `D/domains/E-16/` |
| D21 | `domain_config.json` | JSON | `D/domains/E-17/` |
| D22 | `domain_config.json` | JSON | `D/domains/E-18/` |
| D23 | `domain_config.json` | JSON | `D/domains/E-19/` |
| D24 | `domain_config.json` | JSON | `D/domains/E-20/` |
| D25 | `domain_config.json` | JSON | `D/domains/E-21/` |
| D26 | `domain_config.json` | JSON | `D/domains/EC-00/` |
| D27 | `domain_config.json` | JSON | `D/domains/X1/` |
| D28 | `domain_config.json` | JSON | `D/domains/X2/` |
| D29 | `domain_config.json` | JSON | `D/domains/X3/` |
| D30 | `client_goal_domain_profiles.schema.json` | JSON Schema | `D/platform/schemas/` |
| D31 | `domain_fanout_plan.schema.json` | JSON Schema | `D/platform/schemas/` |
| D32 | `domain_seed_output.schema.v3.json` | JSON Schema | `D/platform/schemas/` |
| D33 | `domain_slice.schema.v2.json` | JSON Schema | `D/platform/schemas/` |
| D34 | `fact_exception_pack.schema.json` | JSON Schema | `D/platform/schemas/` |
| D35 | `fact_ledger_base.schema.json` | JSON Schema | `D/platform/schemas/` |
| D36 | `fact_ledger_candidate_bundle.schema.json` | JSON Schema | `D/platform/schemas/` |
| D37 | `legal_effect_structures.schema.json` | JSON Schema | `D/platform/schemas/` |
| D38 | `structure_seed_bundle.schema.json` | JSON Schema | `D/platform/schemas/` |
| D39 | `evidence_slot_status.schema.json` | JSON Schema | `D/signals/_common/` |
| D40 | `signal_item.schema.json` | JSON Schema | `D/signals/_common/` |
| D41 | `domain_activation_manifest.schema.json` | JSON Schema | `D/signals/schemas/` |
| D42 | `procedural_posture_relief_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D43 | `party_capacity_standing_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D44 | `governing_law_version_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D45 | `legal_relation_lifecycle_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D46 | `timeline_notice_condition_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D47 | `asset_right_state_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D48 | `liability_causation_damage_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D49 | `defense_exception_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D50 | `evidence_proof_conflict_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D51 | `calculation_requirements.schema.json` | JSON Schema | `D/signals/schemas/` |
| D52 | `remedy_enforcement_signals.schema.json` | JSON Schema | `D/signals/schemas/` |
| D53 | `legal_effect_routes.schema.json` | JSON Schema | `D/signals/schemas/` |
| D54 | `domain_signal_envelope.schema.v2.json` | JSON Schema | `D/signals/schemas/` |
| D55 | `signal_manifest.schema.json` | JSON Schema | `D/signals/schemas/` |
### 3.5 Stage 2 직접 의존 입력 49개
source-of-truth: release `/dependency_locks/stage2_direct`. H1이 아래 **49개 전부를 읽고** `T/stage2_asset/`의 같은 상대경로에 복제한다. C15가 S2_10 Agent/binding의 opaque hash와 release가 선택한 context를 결속한다. 읽은 모든 profile을 후속 LLM에 제공한다는 뜻은 아니다. 현 release의 `/bundle/selected_context_refs`는 빈 배열이다. 근거: 분석서 §3.3, Y:3283–3475, 5741–5753.
| 번호 | 정확한 file name | file format | source folder |
|---|---|---|---|
| A01 | `Stage_2_S2_10.yml` | YAML | `A/agent_scripts/` |
| A02 | `stage2_s2_10_llm_binding.yml` | YAML | `A/deployment/` |
| A03 | `authority_registry.yml` | YAML | `A/registry/authority/` |
| A04 | `authority_release.json` | JSON | `A/manifest/` |
| A05 | `EC-00_contract_general.yml` | YAML | `A/registry/substantive/` |
| A06 | `E-01_juristic_act_validity.yml` | YAML | `A/registry/substantive/` |
| A07 | `E-02_contract_money_claim.yml` | YAML | `A/registry/substantive/` |
| A08 | `E-03_parties_liability_succession.yml` | YAML | `A/registry/substantive/` |
| A09 | `E-04_unjust_enrichment.yml` | YAML | `A/registry/substantive/` |
| A10 | `E-05_tort_general.yml` | YAML | `A/registry/substantive/` |
| A11 | `E-06_professional_liability.yml` | YAML | `A/registry/substantive/` |
| A12 | `E-07_construction_defect.yml` | YAML | `A/registry/substantive/` |
| A13 | `E-08_lease_deposit.yml` | YAML | `A/registry/substantive/` |
| A14 | `E-09_registry_transfer_claims.yml` | YAML | `A/registry/substantive/` |
| A15 | `E-10_secured_registry.yml` | YAML | `A/registry/substantive/` |
| A16 | `E-11_possession_vindication.yml` | YAML | `A/registry/substantive/` |
| A17 | `E-12_co_ownership_boundary.yml` | YAML | `A/registry/substantive/` |
| A18 | `E-13_creditor_preservation.yml` | YAML | `A/registry/substantive/` |
| A19 | `E-14_execution_linked_claims.yml` | YAML | `A/registry/substantive/` |
| A20 | `E-15_succession_family_property.yml` | YAML | `A/registry/substantive/` |
| A21 | `E-16_negotiable_instruments.yml` | YAML | `A/registry/substantive/` |
| A22 | `E-17_labor_wage_claims.yml` | YAML | `A/registry/substantive/` |
| A23 | `E-18_org_resolution_status.yml` | YAML | `A/registry/substantive/` |
| A24 | `E-19_insurance_claims.yml` | YAML | `A/registry/substantive/` |
| A25 | `E-20_ip_claims.yml` | YAML | `A/registry/substantive/` |
| A26 | `E-21_media_personality_rights.yml` | YAML | `A/registry/substantive/` |
| A27 | `E-00_residual_unrouted.yml` | YAML | `A/profiles/crosscut/` |
| A28 | `X1_notice_lifecycle.yml` | YAML | `A/profiles/crosscut/` |
| A29 | `X2_asset_identity_lineage.yml` | YAML | `A/profiles/crosscut/` |
| A30 | `X3_procedure_standing_relief.yml` | YAML | `A/profiles/crosscut/` |
| A31 | `X4_response_admission_defense.yml` | YAML | `A/profiles/crosscut/` |
| A32 | `SL-AUTO_motor_vehicle.yml` | YAML | `A/profiles/special_law/` |
| A33 | `SL-INDUSTRIAL_ACCIDENT.yml` | YAML | `A/profiles/special_law/` |
| A34 | `SL-PRODUCT_LIABILITY.yml` | YAML | `A/profiles/special_law/` |
| A35 | `SL-RESIDENTIAL_LEASE.yml` | YAML | `A/profiles/special_law/` |
| A36 | `SL-COMMERCIAL_LEASE.yml` | YAML | `A/profiles/special_law/` |
| A37 | `SL-LABOR.yml` | YAML | `A/profiles/special_law/` |
| A38 | `SL-STATE_LIABILITY.yml` | YAML | `A/profiles/special_law/` |
| A39 | `SL-IP-PATENT.yml` | YAML | `A/profiles/special_law/` |
| A40 | `SL-IP-COPYRIGHT.yml` | YAML | `A/profiles/special_law/` |
| A41 | `SL-IP-OTHER.yml` | YAML | `A/profiles/special_law/` |
| A42 | `SL-MEDIA.yml` | YAML | `A/profiles/special_law/` |
| A43 | `SL-TRANSPORT_MARITIME.yml` | YAML | `A/profiles/special_law/` |
| A44 | `SL-CONSUMER_CONTRACT.yml` | YAML | `A/profiles/special_law/` |
| A45 | `ACTIO-MORTGAGE.yml` | YAML | `A/profiles/overlays/` |
| A46 | `ACTIO-MORTGAGE-CREATION.yml` | YAML | `A/profiles/overlays/` |
| A47 | `ACTIO-ENCUMBERED-TRANSFER.yml` | YAML | `A/profiles/overlays/` |
| A48 | `ACTIO-PRESERVED-CLAIM-BUNDLE.yml` | YAML | `A/profiles/overlays/` |
| A49 | `ACTIO-DEFENSE-MAP.yml` | YAML | `A/profiles/overlays/` |
## 4. 최종 출력 파일 목록
모든 출력 형식은 **canonical UTF-8 JSON**이다. 표의 `O/`는 최종 원격 저장 root이며, 같은 상대경로가 먼저 `T/core_output/` 아래에 저장된다. `E`는 cohort 검증 이후의 **최종 executable cluster 수**이다. 정상 branch(`TO_S2_10`, `TO_S2_10_WITH_ISSUES`)는 11+E개, diagnostic branch(`TO_S2_40_STATUS_ONLY`)는 정확히 5개다.
| ID | 정확한 file name | file format | 최종 저장 folder | branch | 생성 책임·내용 |
|---|---|---|---|---|---|
| O01 | `stage1_input_manifest.json` | JSON | `O/ingress/` | 공통 | C00 검사 결과·source snapshot·hydration receipt를 마지막에 조립 |
| O02 | `intake_report.json` | JSON | `O/ingress/` | 공통 | C00/C05 결과·source count·compact review/conservation report |
| O03 | `issue_ledger.base.json` | JSON | `O/review/` | 공통 | 누적 technical issue와 UNMAPPED review를 조립 |
| O04 | `ingress_status.json` | JSON | `O/ingress/` | 공통·마지막 write | route·run binding·exact artifact hash 목록·barrier |
| O05 | `case_context.json` | JSON | `O/context/` | 정상 | C10: fact/BO/LES/evidence/event/signal 등의 참조 context |
| O06 | `evidence_inventory.json` | JSON | `O/context/` | 정상 | C10: evidence-event-fact 연결·provenance |
| O07 | `object_registry.json` | JSON | `O/context/` | 정상 | C10: BO object occurrence·ID·label·lineage |
| O08 | `party_and_title_context.json` | JSON | `O/context/` | 정상 | C10: party occurrence와 title/role 등 context |
| O09 | `slot_crosswalk.json` | JSON | `O/context/` | 정상 | C10: domain config의 slot skeleton |
| O10 | `cluster_plan.json` | JSON | `O/context/` | 정상 | C15: cluster·SCC·wave·executable/residual |
| O11 | `bundle_plan.json` | JSON | `O/context/` | 정상 | C15: selected context·cohort·slice·S2_10 binding |
| O12 | `<cluster_id>.json` | JSON | `O/context/cluster_slices/` | 정상·E개 | C15: 최종 executable cluster별 immutable slice |
| O13 | `technical_diagnostic.json` | JSON | `O/ingress/` | diagnostic만 | route 조립: reason·source ref·context_published=false |
O12의 실제 basename은 코드가 계산한 `cluster_id`에 `.json`을 붙인 값이다. residual/non-executable cluster의 slice는 최종 출력 집합에 포함하지 않는다. diagnostic branch에는 O05–O12가 없다.
근거: 분석서 §2.2, Y:5180–5243.
## 5. DAG 각 단계별 Input / Output 연결
아래 ID는 §3·§4의 정확한 이름·형식·폴더 행을 참조한다. C00 이후의 '입력'은 원칙적으로 hydration된 파일을 파싱한 메모리 객체이다. '출력 Oxx'는 그 단계에서 준비하는 데이터의 **최종 파일 대응**이며 해당 단계 즉시 저장을 뜻하지 않는다.
| 단계·작업명 | Input file / 전달 객체 | Output file / 전달 객체와 저장 위치 | 근거 Y |
|---|---|---|---|
| Agent IN·단일 executor 호출 | `R/agent_scripts/Stage_2_S2_00.yml`(YAML); user/workspace hash 및 인증·실행환경은 파일이 아닌 외부 주입 값 | Python 실행 시작. 별도 업무 출력 파일 없음 | 1–43, 6230–6248 |
| H0 MCP 초기화·제어 확정 | CTRL-1, CTRL-2 | request/release 메모리 객체. 별도 request 복사 파일 없음 | 5548–5591, 5757–5824 |
| H1 exact closure 2회 읽기·hydration | CTRL-1/2, META-1, SCHEMA-1–3, I01–I16, signal family, D01–D55, A01–A49; 조건이 충족되면 contract/completion | §6.1에 명시한 임시 파일 복제. `hydration_stability_receipt`는 메모리 객체, 이후 O01에 포함 | 5602–5893 |
| C00 source/schema/producer/identity | I01–I16, release, D01–D55 및 조건부 contract/completion | source contract rows·snapshots·issues. 이후 O01/O02/O03/O04로 조립, 이 단계 저장 없음 | 1084–1392, 4801–4888 |
| C00 signal ALL·activation | I10 `signal_manifest.json`, manifest payload 전부, I05 routing activation, `D/signals/signal_registry.v2.json`, 사건 documents | signal occurrence·semantic/integrity rows·activation 비교 결과. 별도 signal output 파일 없음 | 1420–1726, 4881–4923 |
| C00 cross-artifact seal | 사건 documents/snapshots, 특히 I04/I06/I07/I08 등의 P1 guard와 `D/domains/_registry_index.json`, P2–P4 및 ledger/report | seal 검사 결과·issues. 이후 O01/O02/O03에 반영 | 1740–1782, 4924–4931 |
| C05 review 정규화 | I08/I11/I13/I16(4개 review handoff), CTRL-2의 review mapping | normalized review occurrence·issues; 독립 `normalized_reviews.json`은 없음. compact receipt→O02, UNMAPPED issue→O03 | 1897–2065, 4932–4934 |
| C05 보존검사 | I01–I16 파싱 documents와 snapshots, signal ALL 결과, normalized reviews | conservation checks·issues. compact checks→O02, issue→O03 | 2068–2436, 4935–4941 |
| run binding·artifact header | source/signal/deployment snapshots, release, request/context 값, SCHEMA-2 | binding/header 메모리 객체. O04 내 run_binding_receipt 및 context header 등에 포함 | 4942–4971 |
| C10 case context | 사건 documents(주요 I05/I09/I12/I14 및 evidence/event/source refs), signal ALL, binding/header/issues | O05용 `case_context` 객체 → 최종 `O/context/case_context.json` | 2523–2630, 4988–4994 |
| C10 evidence inventory | I01 `evidence_indexed.json`, I02 `evidence_event_candidates.json` 등 documents·header | O06용 객체 → 최종 `O/context/evidence_inventory.json` | 2633–2685, 4995–4999 |
| C10 object registry | I09 `BO.json`, header | O07용 객체 → 최종 `O/context/object_registry.json` | 2701–2731, 5000–5004 |
| C10 party/title context | I09 `BO.json`, header | O08용 객체 → 최종 `O/context/party_and_title_context.json` | 2734–2779, 5005–5009 |
| C10 slot crosswalk | 사건 documents, D/ 아래 domain config 26개(E-00–E-21, EC-00, X1–X3), header | O09용 객체 → 최종 `O/context/slot_crosswalk.json`; 현재 fact/evidence 배열은 빈 skeleton | 2782–2831, 4972–4977, 5010–5015 |
| C15 membership·cluster·SCC·wave | I09 BO, I14 ledger, I12 LES, I01 evidence, I02 event의 명시 member/relation | O10용 `cluster_plan` 객체. 최종 executable/wave는 cohort 검사 후 갱신 | 2834–3129, 4339–4527, 5016–5017 |
| C15 bounded slice | cluster_plan 및 C10 context 객체, EVENT/LES 원문 row, signal/review/domain-config projection source map | O12용 slice 객체들. 최종 executable만 `O/context/cluster_slices/<cluster_id>.json` | 3132–3269, 5018–5054 |
| C15 bundle/cohort·invariant | cluster_plan·slice 객체, CTRL-2 bundle 계약, `A/agent_scripts/Stage_2_S2_10.yml`, `A/deployment/stage2_s2_10_llm_binding.yml`, release-selected context refs(현재 없음) | O11용 bundle_plan, O10 executable/residual/wave 갱신. 입력 Agent/binding은 hash 결속 대상 | 3283–3906, 5055–5113 |
| route·snapshot 재검사·최종 조립 | source rows, C00/C05 검사·review·issue, C10/C15 객체, 원본 임시 snapshot | 정상 O01–O11+O12 E개 또는 diagnostic O01–O04+O13를 메모리 artifact map으로 확정 | 5114–5243 |
| output schema 검사·로컬 tree 발행 | 선택된 artifact map, `T/stage2_asset/schemas/`의 SCHEMA-1–3 | 모든 선택 파일을 `T/.staging/core_output.<attempt_id>/`에 저장/fsync, status 마지막 저장, `T/core_output/`로 rename | 4015–4108, 5235–5242 |
| exact artifact set/hash 재검사 | `T/core_output/ingress/ingress_status.json` 및 해당 tree의 전체 파일 | 파일 bytes map. 새로운 별도 출력 파일 없음 | 5896–5931 |
| 원격 status-last 발행 | 검증된 local 파일 bytes; 기존 `O/ingress/ingress_status.json`이 있으면 기존 O 전체 파일도 읽어 대조 | 선택된 동일 파일 집합을 O에 저장/read-back; O04 마지막. 기존 집합과 bytes 동일하면 재사용 | 5934–6008 |
| 단일 receipt·Agent OUT | core 결과·logical publication receipt 또는 예외 | stdout JSON 객체 1개. 고정 파일명·저장 폴더 없음 | 6018–6103 |
| 외부 downstream dispatch | O04 barrier 및 O10/O11/O12 등 | S2_10 동적 item 또는 S2_40 status-only 호출. S2_00이 추가 파일을 쓰지 않음 | 분석서 §1·§2.2·§5 |
C15 표의 projection source map에 signal/review/profile이 준비된다는 것과 실제 slice에 모두 포함된다는 것은 다르다. 현 구현의 member 생성은 BO/FACT/LES/EVIDENCE/EVENT 중심이며, 이 문서는 전체 내용의 무손실 인계를 보증하지 않는다(분석서 §8).
## 6. 임시 저장·최종 저장·파일이 아닌 출력의 구별
### 6.1 hydration 파일의 source → 임시 output
| 원격 Input | 임시 Output 경로 | file name / format |
|---|---|---|
| `U/<상대경로>`: I01–I16 및 signal family, 조건부 completion | `T/stage1_run/<동일 상대경로>` | 원본 basename 및 JSON bytes 그대로 |
| `D/<상대경로>`: D01–D55, 조건부 contract manifest | `T/stage1_deployment/<동일 상대경로>` | 원본 basename 및 JSON bytes 그대로 |
| `A/<상대경로>`: release/module manifest·schema 3개·direct 49개 | `T/stage2_asset/<동일 상대경로>` | 원본 basename 및 JSON/YAML bytes 그대로 |
| `W/stage2_control/s2_00_request.json` | 임시 파일로 저장하지 않음 | 메모리 request 및 두 read-pass 비교에 사용 |
두 read-pass는 임시 복제 전에 같은 원격 경로의 bytes를 비교한다. 임시 폴더는 `TemporaryDirectory` 수명 내에서만 사용한다. 여기의 input 복제는 최종 업무 output 개수(11+E 또는 5)에 포함하지 않는다. 근거: Y:5602–5753, 5825–5893.
### 6.2 최종 저장 순서
1. 선택된 모든 output에 schema 검사를 수행한다.
2. `T/.staging/core_output.<attempt_id>/<출력 상대경로>`에 non-status를 저장/fsync하고 `ingress/ingress_status.json`을 마지막에 저장한다.
3. staging tree를 `T/core_output/`으로 rename한다.
4. exact file set와 raw hash를 다시 검사한다.
5. 기존 원격 barrier가 있으면 binding·status 및 전체 파일 bytes 일치를 검사한다. 일치하면 `IDEMPOTENT_SUCCESS`이다.
6. 기존 barrier가 없으면 `O/<출력 상대경로>`에 non-status 파일별 write/read-back을 마친 뒤 `O/ingress/ingress_status.json`을 마지막에 write/read-back한다.
로컬 rename과 원격 `STATUS_LAST_LOGICAL_COMMIT`은 서로 다른 저장 보장이다. 저장 후 read-back 또는 close 실패가 발생하면 실패 receipt와 일부 원격 파일이 함께 남을 수 있다. 근거: Y:4015–4108, 5896–6103.
### 6.3 별도 파일로 오인하면 안 되는 값
| 이름 | 실제 형태·위치 |
|---|---|
| `hydration_stability_receipt` | 메모리 객체 및 O01 내부 객체 |
| `run_binding_receipt` | O04 등의 내부 객체 |
| `context_materialization_receipt` | bundle 관련 내부 객체 |
| `output_barrier` | O04 내부 객체 |
| `logical_publish_receipt` | stdout inner receipt 내부 객체 |
| `stage2_s2_00_inner_receipt.v1` | stdout 객체의 schema_version; basename이 아님 |
| `selected_legal_context_json`, `cluster_case_payload_json` | 외부 orchestrator가 만드는 S2_10 item의 문자열 필드 |
S2_00은 `control/run_status.json`이나 최종 청구취지·청구원인 문안을 생성하지 않는다. 또한 YAML 안에 보존된 별도 `main(argv)` CLI의 `project_root/stage2_runs/<run_id>` 경로를 현재 inline MCP 실행의 O 경로와 혼동하면 안 된다. 실제 `__main__`은 `run_inline_mcp()`를 호출한다(Y:6190–6232).
## 7. 식별 결과와 검증 범위
- 16개 Stage 1 고정 사건 입력, 55개 Stage 1 배포 입력, 49개 Stage 2 직접 의존 입력의 이름·형식·source folder를 현재 release와 대조하였다.
- 제어 request/release, module manifest 및 출력 검증 schema 3개를 별도로 식별하였다.
- 정상·diagnostic 분기의 출력 목록과 개수, 임시 staging/core_output 및 최종 O 경로를 YAML의 artifact map·publication 코드와 대조하였다.
- signal family의 모든 basename, 조건부 입력의 실제 경로, U/D의 실제 사건 경로, output digest 및 cluster별 basename은 실제 request/manifest/run이 없으므로 값 자체는 미확정이다. 코드가 정한 결정 규칙을 명시하였다.
- `runtime/s2_00_ingress.py`, `.txt` mirror, workflow YAML, offline build/test 파일은 실행·검증 자산이지만 이 inline task가 별도 업무 입력으로 import/실행하는 파일이 아니다. `deployment.schema.json`, prompt cache policy, P00/P10 Markdown 등도 위 실제 hydration 집합에 포함하지 않았다(분석서 §3.1·§3.3).
- 원본 YAML·분석서·release 및 실행 자산은 수정하지 않았다. 문서의 경로·집합 대조는 정적 검증이며, MCP 실행 성공·후속 Agent 실행·법률적 적정성 검증을 뜻하지 않는다.
@@ -12,11 +12,12 @@ Agent:
release_ref: Default_Agent/Stage_2_Clean/manifest/stage2_release.json release_ref: Default_Agent/Stage_2_Clean/manifest/stage2_release.json
workflow_contract_ref: Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml workflow_contract_ref: Default_Agent/Stage_2_Clean/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml
deployment_binding_ref: Default_Agent/Stage_2_Clean/deployment/stage2_code_executor_binding.yml deployment_binding_ref: Default_Agent/Stage_2_Clean/deployment/stage2_code_executor_binding.yml
implementation_status: S2_00_CODE_AND_FIXTURE_OFFLINE_VERIFIED_S2_10_HYBRID_REIMPLEMENTATION_AND_RESEAL_PENDING_LIVE_ADMISSION_PENDING implementation_status: S2_00_PREPARE_OFFLINE_VERIFIED_BACKEND_ARGUMENT_GATE_AND_SERIALIZATION_UNVERIFIED_LIVE_ADMISSION_PENDING
Stages: Stages:
- name: S2_00 - name: S2_00
description: >- description: >-
단일 Code Executor 호출 안에서 C00, C05, C10, C15를 순차 실행하고 첫 Code Executor task에서 고정 request를 준비하고, 다음 ingress
task의 한 호출 안에서 C00, C05, C10, C15를 순차 실행한다.
localdocs binary IO와 status-last 논리 배리어로 결과를 발행한다. localdocs binary IO와 status-last 논리 배리어로 결과를 발행한다.
prevs: [] prevs: []
nexts: [] nexts: []
@@ -29,6 +30,254 @@ Agent:
type: streamable-http type: streamable-http
url: https://code-executor.mcp.eroomai.com/mcp url: https://code-executor.mcp.eroomai.com/mcp
tasks: tasks:
- task_name: Task_S2_00_prepare_request
description: >-
네 외부 문자열 인자 request_id, attempt_id, stage1_run_root_ref,
stage1_deployment_root_ref를 검증하고 고정 localdocs request 경로에
저장·read-back한다. Backend 인자 결속 및 성공 게이트는 미검증이며
인자가 결속되지 않은 직접 실행은 실패로 종료한다.
mcp: code-executor
tool_name: run_code
parameters:
language: python
requirements: "httpx==0.28.1"
network: agent-network
timeout: 300
code: |-
#!/usr/bin/env python3
"""Prepare the fixed S2_00 control request from four explicit caller values.
The backend argument-delivery contract is not bound. Direct execution fails
closed; callers must invoke ``prepare_request`` with the four named values.
"""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import PurePosixPath
import re
import sys
import unicodedata
from typing import Any, Mapping
REQUEST_PATH = "stage2_control/s2_00_request.json"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
REQUEST_KEYS = frozenset({
"schema_version", "workflow_id", "request_id", "attempt_id",
"stage1_run_root_ref", "stage1_deployment_root_ref",
})
ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
class PrepareError(ValueError):
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def canonical_json_bytes(value: Any) -> bytes:
return (json.dumps(value, ensure_ascii=False, allow_nan=False,
sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def _relative_path(value: str, *, code: str) -> str:
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
raise PrepareError(code, "logical path is empty or malformed")
if unicodedata.normalize("NFC", value) != value:
raise PrepareError(code, "logical path must already be NFC")
path = PurePosixPath(value)
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
raise PrepareError(code, "logical path must be a contained relative path")
rendered = path.as_posix()
if rendered != value:
raise PrepareError(code, "logical path is not canonical")
return rendered
def build_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
) -> bytes:
"""Return the exact six-field canonical request; do not access localdocs."""
if not isinstance(request_id, str) or ID_RE.fullmatch(request_id) is None:
raise PrepareError("REQUEST_ID_INVALID", "request_id contains forbidden characters")
if not isinstance(attempt_id, str) or ID_RE.fullmatch(attempt_id) is None:
raise PrepareError("ATTEMPT_ID_INVALID", "attempt_id contains forbidden characters")
request = {
"schema_version": "stage2_s2_00_execution_request.v1",
"workflow_id": "S2_00",
"request_id": request_id,
"attempt_id": attempt_id,
"stage1_run_root_ref": _relative_path(
stage1_run_root_ref, code="STAGE1_RUN_ROOT_REF_INVALID"),
"stage1_deployment_root_ref": _relative_path(
stage1_deployment_root_ref, code="STAGE1_DEPLOYMENT_ROOT_REF_INVALID"),
}
if set(request) != REQUEST_KEYS:
raise PrepareError("RUN_REQUEST_CLOSED_SHAPE", "request field set drifted")
return canonical_json_bytes(request)
class LocaldocsSession:
"""Small localdocs JSON-RPC session with verified binary write/read-back."""
def __init__(self, user_hash: str, workspace_hash: str, *, client: Any = None) -> None:
for name, value in (("user_hash", user_hash), ("workspace_hash", workspace_hash)):
if not isinstance(value, str) or re.fullmatch(r"[a-f0-9]{64}", value) is None:
raise PrepareError("CONTEXT_HASH_INVALID", f"{name} is not a SHA-256 digest")
self.user_hash = user_hash
self.workspace_hash = workspace_hash
if client is None:
try:
import httpx
except ImportError as exc:
raise PrepareError("HTTPX_UNAVAILABLE", "httpx==0.28.1 is required") from exc
client = httpx.Client(timeout=60)
self.client = client
self.headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
self.session_id: str | None = None
self.next_id = 10
self.initialized = False
def close(self) -> None:
self.client.close()
def _post(self, body: Mapping[str, Any], expected_id: int | None) -> Mapping[str, Any] | None:
try:
response = self.client.post(LOCALDOCS_URL, json=dict(body), headers=dict(self.headers))
response.raise_for_status()
except Exception as exc:
raise PrepareError("MCP_TRANSPORT_ERROR", "localdocs transport failed") from exc
session_id = response.headers.get("mcp-session-id")
if session_id:
if self.session_id is None and expected_id == 1:
self.session_id = session_id
elif session_id != self.session_id:
raise PrepareError("MCP_SESSION_ID_CHANGED", "localdocs session changed")
self.headers["mcp-session-id"] = session_id
if expected_id is None:
return None
try:
payload = response.json()
except Exception as exc:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response is not JSON") from exc
if not isinstance(payload, dict) or payload.get("jsonrpc") != "2.0" or payload.get("id") != expected_id:
raise PrepareError("MCP_RESPONSE_INVALID", "localdocs response ID or shape mismatch")
if "error" in payload or not isinstance(payload.get("result"), dict):
raise PrepareError("MCP_TOOL_ERROR", "localdocs returned an error")
return payload["result"]
def initialize(self) -> None:
response = self._post({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}, "clientInfo": {
"name": "liti-s2-00-prepare", "version": "1.0.0",
"user_id": self.user_hash, "workspace_id": self.workspace_hash,
}}}, 1)
if response is None or response.get("protocolVersion") != MCP_PROTOCOL_VERSION or self.session_id is None:
raise PrepareError("MCP_INITIALIZE_INVALID", "localdocs initialization failed")
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, None)
self.initialized = True
def call(self, name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]:
if not self.initialized:
raise PrepareError("MCP_NOT_INITIALIZED", "localdocs is not initialized")
message_id = self.next_id
self.next_id += 1
result = self._post({"jsonrpc": "2.0", "id": message_id, "method": "tools/call",
"params": {"name": name, "arguments": dict(arguments)}}, message_id)
if result is None or result.get("isError") is True:
raise PrepareError("MCP_TOOL_ERROR", f"localdocs {name} failed")
return result
def read_binary(self, logical_path: str) -> bytes:
path = _relative_path(logical_path, code="LOCALDOCS_READ_PATH_INVALID")
result = self.call("read_binary_doc", {"doc_name": path})
content = result.get("content")
if not isinstance(content, list) or len(content) != 1 or not isinstance(content[0], dict) or content[0].get("type") != "text":
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary response")
text = content[0].get("text")
if not isinstance(text, str):
raise PrepareError("LOCALDOCS_READ_SHAPE", "missing binary envelope")
try:
envelope = json.loads(text)
if isinstance(envelope, dict) and "results" in envelope:
rows = envelope["results"]
if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0], dict):
raise ValueError("binary result cardinality mismatch")
inner = rows[0].get("content", rows[0].get("text"))
envelope = json.loads(inner) if isinstance(inner, str) else inner
encoded = envelope["content_base64"]
if not isinstance(encoded, str):
raise ValueError("binary content is not base64")
payload = base64.b64decode(encoded, validate=True)
size = envelope.get("byte_length", envelope.get("size"))
if size is not None and (not isinstance(size, int) or size != len(payload)):
raise ValueError("binary size mismatch")
digest = envelope.get("sha256")
if digest is not None and digest != hashlib.sha256(payload).hexdigest():
raise ValueError("binary hash mismatch")
return payload
except (ValueError, KeyError, TypeError, base64.binascii.Error) as exc:
raise PrepareError("LOCALDOCS_READ_SHAPE", "invalid binary envelope") from exc
def write_binary_verified(self, logical_path: str, payload: bytes) -> str:
path = _relative_path(logical_path, code="LOCALDOCS_WRITE_PATH_INVALID")
if path != REQUEST_PATH:
raise PrepareError("LOCALDOCS_WRITE_PATH_INVALID", "prepare may write only the fixed request path")
result = self.call("write_binary_file", {
"path": path, "content_base64": base64.b64encode(payload).decode("ascii"), "overwrite": True,
})
if not isinstance(result.get("content"), list) or result.get("isError") is True:
raise PrepareError("LOCALDOCS_WRITE_FAILED", "localdocs did not acknowledge write")
observed = self.read_binary(path)
if observed != payload:
raise PrepareError("LOCALDOCS_WRITE_READBACK_MISMATCH", "request read-back differs")
return hashlib.sha256(observed).hexdigest()
def prepare_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
*,
localdocs: Any = None,
) -> dict[str, Any]:
"""Validate, write, read back, and close an authenticated localdocs session."""
payload = build_request(request_id, attempt_id, stage1_run_root_ref, stage1_deployment_root_ref)
session = localdocs if localdocs is not None else LocaldocsSession(INLINE_USER_HASH, INLINE_WORKSPACE_HASH)
failure: Exception | None = None
digest: str | None = None
try:
session.initialize()
digest = session.write_binary_verified(REQUEST_PATH, payload)
except Exception as exc:
failure = exc
try:
session.close()
except Exception as exc:
if failure is None:
failure = PrepareError("LOCALDOCS_CLOSE_FAILED", "localdocs session close failed")
failure.__cause__ = exc
if failure is not None:
raise failure
return {"ok": True, "workflow_id": "S2_00", "path": REQUEST_PATH,
"request_sha256": digest}
if __name__ == "__main__":
sys.stdout.buffer.write(canonical_json_bytes({
"ok": False, "error": {"code": "PREPARE_ARGUMENT_BINDING_UNVERIFIED",
"message": "four caller values must be bound by the backend"}}))
raise SystemExit(2)
- task_name: Task_S2_00_deterministic_ingress - task_name: Task_S2_00_deterministic_ingress
description: >- description: >-
고정 request와 release를 hydration하고 S2_00 pure core를 실행한 뒤 고정 request와 release를 hydration하고 S2_00 pure core를 실행한 뒤
@@ -192,7 +441,7 @@ Agent:
# literal placeholders in the offline parity mirror and its unit tests. # literal placeholders in the offline parity mirror and its unit tests.
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}" INLINE_WORKSPACE_HASH = "{{__workspace_hash__}}"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json" INLINE_REQUEST_PATH = "stage2_control/s2_00_request.json"
INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean" INLINE_STAGE2_ASSET_ROOT = "Default_Agent/Stage_2_Clean"
INLINE_STAGE2_RELEASE_PATH = ( INLINE_STAGE2_RELEASE_PATH = (
@@ -6235,13 +6484,18 @@ Agent:
task_procedure: task_procedure:
IN: IN:
nexts: nexts:
- Task_S2_00_deterministic_ingress - Task_S2_00_prepare_request
wait_until: [] wait_until: []
Task_S2_00_prepare_request:
nexts:
- Task_S2_00_deterministic_ingress
wait_until:
- IN
Task_S2_00_deterministic_ingress: Task_S2_00_deterministic_ingress:
nexts: nexts:
- OUT - OUT
wait_until: wait_until:
- IN - Task_S2_00_prepare_request
OUT: OUT:
nexts: [] nexts: []
wait_until: wait_until:
@@ -71,7 +71,7 @@ Agent:
WORKFLOW_ID = "S2_20" WORKFLOW_ID = "S2_20"
ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0" ALGORITHM_VERSION = "s2_20_relief_plan/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp" WEAVIATE_MCP_URL = "https://weaviate.eroomai.com/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
@@ -73,7 +73,7 @@ Agent:
from typing import Any, Mapping from typing import Any, Mapping
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -68,7 +68,7 @@ Agent:
WORKFLOW_ID = "S2_40" WORKFLOW_ID = "S2_40"
ALGORITHM_VERSION = "s2_40_finalizer/1.0.0" ALGORITHM_VERSION = "s2_40_finalizer/1.0.0"
EXPECTED_STAGE2_RELEASE_SHA256 = "9fa85bb94c5f14f675dcdaa0cf94d06745b21675d97797539ffc14015dcc9f53" EXPECTED_STAGE2_RELEASE_SHA256 = "2363f1166ee8a3ea2b38350cde61fccfc9852a413c6b9f14a6ed6606af15a590"
LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
MCP_PROTOCOL_VERSION = "2025-03-26" MCP_PROTOCOL_VERSION = "2025-03-26"
INLINE_USER_HASH = "{{__user_hash__}}" INLINE_USER_HASH = "{{__user_hash__}}"
@@ -0,0 +1,262 @@
# S2_00 request 생성·검증·저장 선행 task 신설 계획
## Objective
`Stage_2_S2_00.yml`의 맨 앞에 `Task_S2_00_prepare_request`를 신설하여 외부 실행 인자를 검증하고 `stage2_control/s2_00_request.json`을 저장·read-back한 뒤, 성공한 동일 실행만 기존 `Task_S2_00_deterministic_ingress`로 진행하도록 한다. workspace 단위 동시 실행 통제, 두 task 간 request identity 결속, 빌드·배포·hash 계약 갱신을 하나의 변경으로 다룬다.
작성일: 2026-09-30. 상태: **구현 전 계획**. 이 문서 작성 과정에서는 실행 YAML·runtime·배포 자산을 변경하지 않았다.
## Deliverables
| 산출물 | 내용 |
|---|---|
| 외부 실행 인자·host 계약 | 인자 전달 방식, workspace 권한·경로 결속, 직렬화, task 실패 차단, receipt 전달 |
| 2-task authoring 및 배포 YAML | prepare → ingress 순서와 success-only 실행 |
| prepare 구현 및 runtime mirror | 입력 검증, request 생성, write/read-back, 준비 receipt |
| ingress 보강 | host가 전달한 준비 receipt의 request hash·실행 identity와 실제 입력 대조 |
| workflow/schema/binding/builder 갱신 | task별 쓰기 권한, 2-task 추출·검증·receipt·mirror 계약 |
| 회귀·통합 검증 증거 | mock, 동시 실행, 실제 backend를 구분한 결과 |
| 분석·IO 문서 개정 | 새로운 선행 단계, 외부 인자 및 실패·잠금 수명 반영 |
## Scope and Non-Scope
- 범위: request 준비와 기존 ingress를 연결하는 구조 및 이를 배포 가능한 상태로 검증하는 데 필요한 계약 변경.
- 유지: request 파일의 기존 6필드, 고정 경로, 기존 C00/C05/C10/C15의 업무 책임, 정상/diagnostic 산출물 집합.
- 제외: Stage 1 사건 데이터 수정, S2_10 이후 업무 로직 변경, 기존 DEV release의 자동 production 승격, 기존 보존·projection 결함의 일괄 수정.
- 계획 작성이 구현 또는 live 배포 승인을 뜻하지 않는다. 실행 제한 해제는 request 생성 구현과 별개의 작업이다.
## Known Inputs
`M = Case_02_Comparison_Research/YAML_Prompts/2. Stage_2/`, `R = M/Default_Agent/Stage_2_Clean/`, `W = localdocs가 결속한 논리 workspace root`.
| 확인한 현 상태 | 근거 |
|---|---|
| 현재 IN → ingress → OUT, 단일 task | `R/agent_scripts/Stage_2_S2_00.yml`: 17–43, 6230–6248 |
| request는 읽기 입력이며 6필드 폐쇄형 검증 | 같은 YAML: 5548–5591, 5757 이후 |
| 입력 closure를 두 번 읽고 bytes 비교 후 temp hydration | 같은 YAML: 5825–5893 |
| 빌더가 정확히 1 task·1 run_code·고정 DAG를 강제 | `R/offline_build/build_s2_00_inline_projection.py`: 283–378 |
| authoring 정본은 `M/Stage_2_S2_00_v.2.yml`; 배포 YAML과 mirror는 파생본 | 같은 builder: 30–38, 680–755 |
| 단일 code pointer와 code hash·mirror를 receipt에 기록 | 같은 builder: 680–755 |
| 현재 S2_00 쓰기 root는 `stage2_runs/by-binding/<run_binding_digest>/` | `R/deployment/stage2_code_executor_binding.yml`: 47–66 |
| localdocs 계약상 허용 도구는 read_binary_doc/write_binary_file | 같은 binding: 47–66. CAS·분산 잠금 기능이 있다는 근거는 없음 |
| 외부 인자 주입 및 성공 receipt의 task 간 전달에 필요한 backend 구체 API | 현재 확인 범위에서 미확정. 구현 1단계에서 확인해야 함 |
사용자 지정 기존 분석 자료: [Stage_2_00_Analysis_v1.md](../YAML_Prompts/2.%20Stage_2/Stage_2_00_Analysis_v1.md), [Stage_2_00_IO_info.md](../YAML_Prompts/2.%20Stage_2/Stage_2_00_IO_info.md).
## Material Assumptions
1. request는 LLM이 추론하거나 폴더를 scan해서 만드는 값이 아니다. host가 확정한 네 값과 코드상 두 상수로 생성한다.
2. task 프로세스·임시 디렉터리·메모리는 서로 공유된다고 가정하지 않는다. backend가 인증된 실행 context와 선행 receipt를 후행 task에 전달해야 한다.
3. `wait_until`은 실행 순서를 기술하지만, 실패한 선행 task의 후속 실행까지 자동 차단한다는 보장은 별도로 검증한다.
4. 파일 읽기→없으면 lock 파일 쓰기는 원자적 잠금이 아니다. read-back·hash 비교도 상호 배제를 대신하지 않는다.
5. 권고 기본안은 **host가 같은 user/workspace의 전체 S2_00 실행을 직렬화**하는 것이다. 최소 요구 구간인 request 저장 전부터 hydration 완료까지보다 길게 잠금을 유지하여, 초기 구현에서 중간 해제 프로토콜을 추가하지 않는다.
## Questions That Could Change the Outcome
구현 착수 시 아래를 backend 문서·코드·시험으로 먼저 확인한다. 답을 추측해 YAML 문법이나 도구 기능을 만들지 않는다.
| 확인 사항 | 결정 기준·미지원 시 처리 |
|---|---|
| 외부 structured 인자 전달 방식 | 실제 지원하는 executor 입력 채널 사용. 고정된 템플릿 문법·미지원 run_code parameter를 임의 추가하지 않음 |
| 선행 stdout receipt를 후행 입력으로 전달하는 방식 | backend가 실행 identity에 결속해 전달해야 함. 동일 고정 파일에 별도 receipt를 쓴다는 방식만으로 신뢰 경계를 대신하지 않음 |
| task 성공 판정 | transport 성공뿐 아니라 exit code·receipt schema·ok·실행 ID 일치 확인 가능해야 함 |
| workspace 직렬화와 취소·worker 종료 보장 | 여러 backend worker에 걸친 전역 보장 필요. 한 프로세스의 mutex만 있으면 불충분 |
| 저장 권한 강제 | prepare에 정확한 request 경로만 쓰기 허용; ingress 결과 쓰기 권한과 분리 가능한지 확인 |
미지원 항목이 있으면 해당 backend 기능을 선행 구현 대상으로 기록한다. 로컬/mock 작업은 계속할 수 있으나 backend 증거 없이 live-ready로 표시하지 않는다.
## Workstreams and Dependencies
### 1. 기준선 및 변경 영향 확정
- 관련 authoring·배포 YAML, runtime mirror, workflow, schema, binding, builder·tests의 hash와 Git 변경 상태를 기록한다. 기존 사용자 변경은 보존한다.
- 문자열뿐 아니라 task ID·code pointer·code hash를 참조하는 소비자를 찾아 변경 목록을 닫는다.
- 기존 검사 결과를 기준선으로 기록하여 이번 변경의 회귀와 기존 실패를 구분한다.
- 위 backend 확인 사항을 검증하고 실제 인자·receipt 주입 방식 및 잠금 구현 위치를 확정한다.
완료 기준: 정확한 변경 파일 목록, backend 연동 계약, 기존 실패 목록이 기록되어야 한다.
### 2. 외부 인자 및 request 계약 확정
request 출력은 다음 6필드로 고정한다.
| 필드 | 공급자·형식 | 검증 |
|---|---|---|
| `schema_version` | prepare 코드 상수 | `stage2_s2_00_execution_request.v1` |
| `workflow_id` | prepare 코드 상수 | `S2_00` |
| `request_id` | host 문자열 | `[A-Za-z0-9][A-Za-z0-9._-]{0,127}` |
| `attempt_id` | host 문자열 | 위와 동일. 재시도 정책에 따라 새 시도 식별자 부여 |
| `stage1_run_root_ref` | host 문자열 | W 아래 승인된 Stage 1 사건 root에 결속한 canonical 상대경로 |
| `stage1_deployment_root_ref` | host 문자열 | W 아래 승인된 Stage 1 배포 root에 결속한 canonical 상대경로 |
- 누락·추가 필드, 빈 값, 잘못된 타입, absolute path, `..`, 역슬래시, NUL, 비정규 NFC 경로 등을 거절한다. 기존 `_inline_relative_path`와 검증 의미를 맞춘다.
- 문법적으로 올바른 경로라는 사실과 해당 실행이 접근할 권한이 있다는 사실을 구별한다. host가 승인한 user/workspace 및 Stage 1 완료 실행과 두 root를 결속한다.
- request 값은 다른 업무 run에서 남은 파일, 작업 디렉터리 추측, LLM 판단으로 채우지 않는다.
- user/workspace·host execution ID·lock ownership·준비 receipt는 별도의 실행 envelope에 담는다. 기존 request JSON에 7번째 필드를 추가하지 않는다.
- structured 전달을 우선한다. 코드 템플릿에 전달해야 하는 환경이라면 검증된 직렬화·안전한 encoding을 사용하고, 입력 문자열을 Python source에 직접 삽입하지 않는다.
완료 기준: 허용·거절 예제와 인자 전달 계약이 양쪽 task 및 backend 검증에서 일치한다.
### 3. host 직렬화 및 실패 수명 구현
잠금 key는 인증된 `(user, workspace, S2_00 request 고정 경로)`로 정한다. 서로 다른 request_id라도 동일 workspace의 고정 파일을 공유하면 같은 key를 사용한다.
```text
host admission → workspace 실행 소유권 획득
→ prepare 시작 → request 저장/read-back
→ 성공 receipt를 동일 실행 ingress에 전달
→ ingress request 1차/2차 읽기 → temp hydration
→ core 실행·원격 출력 발행 → 종료/취소 정리
→ 두 task와 미완료 I/O의 종료 확인 → 소유권 해제
```
- 기본안은 전체 S2_00 종료까지 직렬화한다. 따라서 최소 보호 구간인 hydration 완료까지는 확실히 포함된다.
- prepare에서 획득한 프로세스 로컬 lock을 task 종료 때 해제하는 구조는 금지한다. 소유권은 host 실행 전체에 귀속된다.
- 모든 request writer와 재시도 경로가 같은 통제에 참여해야 한다. 통제 밖 writer는 권한으로 차단한다.
- timeout·취소·host 재시작 시 오래된 worker와 진행 중 쓰기가 끝났는지 확인한 후 다음 실행을 허용한다. TTL 만료만으로 새 소유자를 실행하지 않는다.
- lease 방식이 불가피하다면 stale owner의 쓰기/읽기를 실제로 거절하는 fencing 또는 동등한 host 보장을 구현한다. JSON에 token만 넣는 것은 fencing이 아니다.
- 이후 성능 최적화로 hydration 직후 해제하려면 authenticated hydration-complete 신호, 이후 request 재읽기 없음, 원본 Stage 1 run의 불변성 등을 별도 입증한다. 초기 버전에는 적용하지 않는다.
완료 기준: 두 host worker의 경쟁·지연·취소 시험에서도 request 교차 소비가 없어야 한다.
### 4. 선행 task 구현
신규 task ID: `Task_S2_00_prepare_request`.
1. 인증된 host envelope, 실행 소유권, 네 외부 인자를 검증한다.
2. 고정 두 상수를 추가하여 6필드 request 객체를 만든다.
3. strict JSON/schema 및 경로·ID 검증을 수행한다. schema 자산을 읽는 경우 release/module manifest의 hash 결속된 정확한 closure를 사용한다.
4. 기존 canonical JSON 규칙(UTF-8, 정렬 key, 고정 구분자, NaN 금지, 종료 newline)으로 직렬화하고 raw SHA-256을 계산한다.
5. 소유권을 유지한 상태에서 `W/stage2_control/s2_00_request.json`에 binary write를 수행한다. 현재 계약에 없는 원격 rename·원자적 파일 교체 기능을 가정하지 않는다.
6. 같은 경로를 binary read-back하여 bytes가 동일한지 확인하고, 다시 parse/schema 검증한다.
7. 성공일 때만 stdout 준비 receipt를 반환한다. 권고 schema는 신규 `stage2_s2_00_prepare_request_receipt.v1`이며 `ok`, request path/hash/byte length, request_id/attempt_id, host execution binding을 포함한다. 이것은 **신규 설계**이며 기존 파일이라고 표현하지 않는다.
8. 오류·write 응답 유실·read-back 불일치·close 실패 등은 성공 receipt로 취급하지 않는다. ingress는 실행하지 않는다. 부분 파일이 남아 있어도 성공한 것으로 간주하지 않는다.
동일 시도 재실행은 같은 bytes 및 host identity가 확인되면 재사용 가능하다. 새 시도/다른 request로 교체할 때에는 이전 실행 종료 및 새 소유권이 먼저 확인되어야 한다. 성공 후 무조건 request를 삭제하는 정리 코드는 두지 않는다.
완료 기준: 실제 저장 bytes와 receipt hash가 일치하고, 모든 실패 경로가 후행 실행을 차단한다.
### 5. ingress 결속 및 DAG 변경
```text
IN
→ Task_S2_00_prepare_request
→ [host: exit=0 + 유효한 ok:true receipt + 동일 실행 binding 확인]
→ Task_S2_00_deterministic_ingress
→ OUT
```
- YAML `task_procedure`에 prepare의 `wait_until: [IN]`, ingress의 `wait_until: [Task_S2_00_prepare_request]`를 설정하고 nexts를 대응시킨다. stage prevs/nexts는 기존 독립 stage 구조를 유지한다.
- host의 success-only 조건은 YAML edge와 별도로 계약 및 시험에 포함한다. 실패한 prepare가 '완료'되었다는 이유로 ingress를 시작하지 않는다.
- ingress는 host가 제공한 준비 receipt가 없거나 잘못되면 시작을 거절한다. 최초 request bytes와 expected hash, request_id/attempt_id를 대조한다.
- 기존 두 read-pass의 동등성 검사를 유지한다. 두 번째 request bytes도 준비 receipt hash와 일치해야 한다. 동일하게 바뀐 다른 request를 두 번 읽어 통과하는 상황까지 차단한다.
- 신뢰할 수 없는 stale receipt·다른 workspace receipt·다른 attempt의 receipt를 거절한다.
- 기존 C00–C15, output barrier·route 계약은 유지한다. wrapper 실패와 core diagnostic 분기를 혼동하지 않는다.
완료 기준: prepare와 ingress가 같은 request를 사용했다는 검증 가능한 연결이 생긴다.
### 6. authoring·빌드·배포 계약 갱신
| 파일·영역 | 작업 |
|---|---|
| `M/Stage_2_S2_00_v.2.yml` | authoring 정본에서 2-task 구조·설명·version·inline source 갱신 |
| `R/agent_scripts/Stage_2_S2_00.yml` | 수정한 builder로 배포본 생성. 배포본만 수작업 수정하지 않음 |
| `R/offline_build/build_s2_00_inline_projection.py/.txt` | '정확히 1개' 검증을 **정확히 두 개의 지정 task와 지정 순서**로 변경; 추가 임의 task는 거절 |
| 같은 builder의 code 검사 | 공통 보안·endpoint·import 검사는 유지; ingress 전용 token과 prepare 전용 token을 분리 |
| `R/runtime/s2_00_ingress.py/.txt` | ingress task code의 파생 mirror로 유지 |
| 신규 `R/runtime/s2_00_prepare_request.py/.txt` | prepare code의 파생 mirror 제안. 두 task를 결합한 실행 파일로 만들지 않음 |
| `R/manifest/s2_00_inline_code_receipt.json` | task별 code pointer·hash·mirror, 전체 task semantics hash로 확장; schema version/소비자 동시 갱신 |
| `R/workflows/S2_00_stage1_ingress_normalize_and_bundle_compile.yml` | 선행 단계, 외부 인자, receipt·success gate·직렬화 계약 반영 |
| `R/deployment/stage2_code_executor_binding.yml` | task별 executor 및 권한: prepare는 request 고정 경로, ingress는 기존 결과 root. schema/release 읽기 경로도 task별 명시 |
| `R/schemas/ingress.schema.json` | 기존 6필드 execution_request 유지, 신규 prepare/envelope receipt 검증 정의 |
| `R/schemas/deployment.schema.json` 및 관련 schema | 두 task·task별 code binding·receipt 계약과 실제 소비자 호환성 반영 |
| `R/manifest/module_manifest.json`, `stage2_release.json`, release validator | 변경 자산의 등록·hash와 추가된 계약 검증 반영 |
| `M/Stage_2_00_Analysis_v1.md`, `Stage_2_00_IO_info.md` | 구현 완료 후 실제 구조·IO·검증 상태로 개정 |
`task index=0`에 ingress가 있다고 가정하는 코드를 모두 확인한다. task_name으로 추출하고 receipt에는 실제 YAML pointer도 남긴다. version 값은 관련 schema와 소비자 호환성을 확인해 함께 올린다.
### 7. hash 재결속 및 패키지 확정
- 변경 자산 → module/release → inline release pin → authoring projection/mirror/receipt → executor binding/detached admission의 실제 참조 그래프를 먼저 만든다.
- 현재 builder는 projection 이후 binding 재결속을 전제로 한다. 기존 detached 설계를 유지하고, parent release에 자신을 포함한 code hash를 되먹이는 순환 의존을 새로 만들지 않는다.
- 참조 그래프의 위상 순서대로 재생성한다. 순환이 발견되면 hash를 반복 덮어쓰는 대신 detached boundary를 설계·검증한다.
- 현재 `expected_release_sha256`, task별 code hash, Agent raw hash, workflow/schema/module hash와 receipt pointer를 함께 대조한다.
- 재생성 뒤 빌더 `--check`, release validator, mirror parity를 실행한다. 두 번째 빌드에서 bytes 차이가 없어야 한다.
- 테스트용 release와 실제 배포 release를 분리한다. DEV guard를 삭제하거나 production 표기를 붙여 검사를 통과시키지 않는다.
완료 기준: 참조된 변경 자산의 hash가 모두 일치하고, 반복 빌드가 추가 변경을 만들지 않는다.
## Source and Tool Plan
로컬 authoring·builder·runtime·tests를 우선 읽는다. backend 관련 문서·코드는 필요한 범위에서 확인하고 지원 여부를 증거로 남긴다. 신규 의존성 설치나 서비스 호출은 이 계획 작성에 포함하지 않는다. 구현 시 기존 도구·검증 runner를 확인해 사용하고, 라이브 시험은 별도 격리 workspace와 비실사건 fixture로 수행한다.
## Validation Plan
| 시험 | 통과 기준 |
|---|---|
| 유효한 인자 | request 정확히 6필드, canonical bytes/hash/read-back 일치 |
| 누락·추가·타입·ID·경로 오류 | 저장 전 거절, ingress 호출 0회 |
| 권한 밖 경로·다른 workspace | host 결속 검증 실패, 업무 입력 읽기 불가 |
| 저장 실패·응답 유실·read-back mismatch | 성공 receipt 없음, ingress 호출 0회 |
| 선행 task exit 0이나 ok:false/깨진 receipt | host가 후행 task를 실행하지 않음 |
| 준비 후 request 변조 | 첫 읽기 expected hash 검증 또는 두 pass 비교에서 거절 |
| 두 pass 모두 동일한 타 요청으로 교체 | 준비 receipt hash/identity 검증에서 거절 |
| 같은 workspace 두 요청·두 worker | 단일 소유자만 진입, 다른 요청 bytes를 소비하지 않음 |
| 다른 workspace 병렬 실행 | 불필요한 전역 직렬화 없이 독립 진행 |
| cancel·timeout·host crash·오래된 worker 재개 | 이전 쓰기 가능성이 사라지기 전 새 owner 쓰기 금지; stale owner 차단 |
| 동일 시도 retry / 새 attempt | 정의한 재사용/교체 정책 준수, 기존 output idempotency 계약 유지 |
| task shape / builder | 지정 두 task와 순서만 허용; task 누락·추가·우회 edge 거절 |
| 두 code mirror·receipt pointer·hash | authoring에서 추출한 bytes와 정확히 일치 |
| 기존 core 정상·diagnostic 회귀 | fixture 기준 11+E/5 출력 및 barrier 계약 보존 |
| 실제 backend 통합 | 인자 주입·receipt 전달·failure gate·workspace 격리를 실제 환경에서 확인 |
mock 통과는 실제 host 잠금·권한 강제 증거가 아니다. `STATIC_PASS`, `OFFLINE_TEST_PASS`, `BACKEND_INTEGRATION_PASS`, `LIVE_ADMISSION_PENDING`을 구분해 보고한다. 현재 DEV release로 정상 core 성공을 주장하지 않는다.
## Approval Boundaries
현재 요청은 작업 계획 작성이다. 이 단계에서는 계획 문서만 생성한다. 이후 구현 요청 시 범위 내 로컬 수정·시험을 수행하고, 실제 서비스 배포·외부 쓰기·live activation은 그때의 승인 범위를 확인한다. backend 기능 미지원은 기술적 미완료 조건으로 기록하며 이를 단순 승인 문제로 대체하지 않는다.
## Progress
- [x] 현재 단일 task·request read·builder 제약 확인
- [x] 현재 write root와 localdocs 도구 계약 확인
- [x] 작업 순서·의존성·동시 실행·검증 계획 작성
- [ ] backend 전달·직렬화 기능 확인 및 연동 방식 확정
- [ ] schema·prepare·ingress·DAG 구현
- [ ] builder·binding·hash 재결속
- [ ] offline·동시 실행·backend 통합 검증
- [ ] 분석·IO 문서 갱신 및 배포 적격성 판정
## Decision Log
| 날짜 | 결정 | 근거 | 결과 |
|---|---|---|---|
| 2026-09-30 | request의 기존 6필드 유지 | 현재 strict validator와 사용자 요구 | 실행 보조 정보는 host envelope로 분리 |
| 2026-09-30 | 첫 버전은 전체 S2_00 host 직렬화 | 두 독립 task 사이 lock 수명 및 고정 request 경로 | hydration 완료 전 해제 위험과 중간 callback 의존 감소 |
| 2026-09-30 | 준비 receipt hash를 ingress에 전달 | 두 read-pass 비교만으로 다른 요청 교체를 모두 검출할 수 없음 | 소비할 request를 선행 실행에 결속 |
| 2026-09-30 | authoring부터 재생성 | builder가 정본 및 mirror parity를 관리 | 배포 YAML 단독 변경 방지 |
## Evidence Ledger
| 명제 | 확인 근거 | 상태 |
|---|---|---|
| 기존 builder는 두 task를 거절한다 | extract_run_code_task의 EXACTLY_ONE_TASK_REQUIRED·EXACTLY_ONE_RUN_CODE_REQUIRED | 확인 |
| 현재 request 저장은 쓰기 허용 root 밖이다 | S2_00 binding의 write_root_rule | 계약에서 확인; live 강제 여부는 미확인 |
| localdocs 원자적 lock/CAS 지원 | 현재 allowlist에 read/write만 있음 | 지원 근거 없음, 사용 가능하다고 가정하지 않음 |
| 선행 task를 현재 backend가 어떻게 인자 결속하는가 | 구체 backend 구현을 아직 확인하지 않음 | 미확정 |
| request writer의 생성 필요 | 현재 소비 코드 및 이전 폴더 전체 검색 결과 | 현 패키지에 실사용 writer 없음 |
## Risks and Failure Modes
- YAML만 바꾸면 builder·code pointer·권한·hash 계약에서 실패한다.
- `wait_until`만 추가하면 prepare 실패 후 ingress가 진행될 수 있다.
- 고정 request 파일에 write/read-back만 구현하면 동시 요청이 서로 덮어쓸 수 있다.
- lock TTL 이후 오래된 writer가 살아 있으면 새 요청을 오염시킬 수 있다.
- 기존 release 제한을 이번 변경의 실패로 혼동하거나, 이를 제거해 정상 실행을 과장할 수 있다.
- 다른 request가 같은 run binding으로 귀결될 때 기존 status bytes 일치 요건은 여전히 적용된다. 기존 출력 충돌을 자동 덮어쓰기로 해소하지 않는다.
롤백은 두 task 실행을 먼저 정지하고 진행 중 I/O 종료를 확인한 뒤, authoring·배포본·mirror·schema·binding·receipt를 검증된 동일 버전 묶음으로 복원한다. 이전 버전도 외부 request 공급 없이는 실행되지 않으므로 롤백 자체를 서비스 정상화로 보지 않는다. 사건 입력·이미 발행된 결과는 일괄 삭제하지 않는다.
## Results and Residual Uncertainty
계획은 작성 완료하였고, 구현은 미착수다. 가장 중요한 미확정 항목은 backend의 structured 인자/receipt 전달과 workspace 단위 직렬화·worker 종료 통제이다. 이 기능이 확인되거나 구현되어야 안전한 2-task 실행이 완성된다. 완료 판정은 request 생성 성공뿐 아니라 **동일 요청의 후행 소비, 실패 시 차단, 동시 실행 안전성, 빌드·hash 일치**를 모두 통과하는 것으로 한다. 실제 backend 시험 전에는 live-ready로 판정하지 않는다.
@@ -0,0 +1,206 @@
# S2_00 request 준비 계획 v1 — 필수 조건을 유지한 단순화
작성일: 2026-09-30. 상태: **분석·계획 작성 완료 / 구현 미착수**.
원본: [s2-00-request-preparation.md](s2-00-request-preparation.md). 원본은 보존한다.
## 1. Occam's Razor 분석
**기존 계획은 안전성에 필요한 항목과 선택적인 구현을 함께 필수화하여, 가장 단순한 계획이라고 보기는 어렵다.** 같은 요구를 충족하면서 새 프로토콜·검증 중복·변경 범위를 줄일 수 있다. 다만 backend 기능이 미확정이므로 '절대적으로 가장 효율적'이라고 단정하지 않고, 확인된 코드와 사용자 제약 아래의 권고안을 제시한다. 여기서 효율성은 우선 구현·검증·유지보수 비용이며, 실행시간 개선 수치는 측정하지 않았다.
### 1.1 제약을 지키는 단순화와 구조 변경의 구분
| 대안 | 장점·비용 | 판단 |
|---|---|---|
| 원본의 독립 2-task + 준비 receipt 전달 | 별도 receipt schema·실행 envelope·후행 전달·task별 검증 필요 | 요청 identity 확인은 타당하지만 구체 수단을 줄일 수 있음 |
| **2-task + 동일한 불변 host 인자 공급** | 두 task가 같은 값으로 request를 재구성; 별도 준비 receipt 전달 불필요. 2-task 빌드 변경은 여전히 필요 | **v1 채택**: 기존 '선행 task 신설' 요구 유지 |
| 기존 단일 task 맨 앞의 준비 함수 | task 간 전달·실패 gate·2-task builder 변경을 제거할 수 있어 구조적으로 더 단순 | 독립 선행 task 요구를 완화하는 대안. v1에 몰래 적용하지 않음 |
| 외부 host에서 request 생성 | YAML 구조 변경이 작음 | 'YAML 맨 앞의 선행 task' 요구와 다르므로 기본안 제외 |
v1은 요청 목적을 바꾸지 않고 독립 선행 task를 유지한다. 단일 task 내부 함수 방식은 추가 구조를 가장 크게 줄이지만, 기존에 명시한 두-task 조건과 구별해야 한다.
### 1.2 줄일 수 있는 항목과 유지할 항목
| 원본 계획 | v1 결정 | 이유·적용 조건 |
|---|---|---|
| 신규 prepare receipt schema와 host→ingress receipt 전달 | **제거**. host가 확정한 동일 네 인자를 두 task에 제공하고 ingress가 expected bytes를 재구성 | 별도 파생 자료를 전달하지 않아도 요청 일치 검증 가능. host 인자는 실행 중 불변이어야 함 |
| 새로운 execution envelope·lock token을 task마다 검증 | **별도 형식 신설 안 함**. 기존 인증된 실행 context와 host 직렬화 사용 | 기존 기능의 실제 지원 여부부터 확인. 없는 기능을 있다고 가정하지 않음 |
| 검증한 bytes를 write/read-back한 뒤 동일 bytes를 다시 parse/schema 검사 | **반복 검사 제거** | 처음 검증한 canonical bytes와 read-back bytes의 완전 일치가 확인되면 같은 내용을 다시 검증할 필요 없음 |
| 선행 단계에서 schema/release closure 별도 hydration | **기본안에서 제외** | 이미 존재하는 폐쇄형 request validator와 schema의 일치를 offline test로 확인. ingress의 기존 release 검증은 유지 |
| 신규 분산 lock·lease·fencing 체계 가능성까지 초기 구현에 포함 | **기존 host 직렬화 재사용 우선**, 별도 체계는 실제 미지원일 때만 재설계 | 잠금 안전성은 필수이나 새 잠금 서비스를 만드는 것이 필수는 아님 |
| 동일 attempt 재사용 최적화 | **초기 제외**. 소유권 획득 후 전체 준비 작업 재실행 | 작은 JSON의 재쓰기보다 별도 재사용 분기의 관리 비용이 클 수 있음. 기존 결과 idempotency는 유지 |
| schema·manifest 전체 개정 | **실제 구조·hash 참조가 바뀌는 항목만 갱신** | 파일을 더 적게 수정하려고 필요한 hash/권한 갱신을 생략하지 않음 |
| 정확히 2 task·순서 및 두 code hash 검증 | **유지** | 현 builder가 1 task를 강제하므로 피할 수 없는 변경 |
| 저장 실패 시 차단·request 2회 읽기·host 직렬화 | **유지** | 요구된 실행 안전성. bytes 비교는 상호 배제를 대신하지 않음 |
특히 runtime의 `write_binary_verified()`에는 이미 write 후 read-back bytes 비교가 있다. 같은 일을 하는 새 IO 계층을 만들 필요가 없다. `_inline_validate_request()`도 이미 정확히 6필드·ID·경로 검증을 수행한다. 근거는 아래 Evidence Ledger에 기록한다.
## Objective
`IN → Task_S2_00_prepare_request → Task_S2_00_deterministic_ingress → OUT`을 구현하되, 새 준비 receipt 전달 프로토콜 없이 동일한 외부 인자와 기존 검증·IO 기능으로 request 생성 및 소비를 연결한다.
## Deliverables
- 기존 authoring을 정본으로 하는 2-task YAML·파생 배포본 및 필요한 code mirror/빌드 receipt 변경.
- 최소 준비 함수, ingress의 expected request bytes 대조, 외부 인자·직렬화·success-only 계약.
- 실제 변경된 binding·schema·manifest/hash, 관련 시험 및 분석·IO 문서 개정.
## Scope and Non-Scope
request의 6필드·고정 경로와 기존 ingress의 C00–C15·출력 계약을 유지한다. 신규 receipt 파일·범용 workflow framework·독립 lock 서비스·release 전체 재설계는 기본 범위에 넣지 않는다. 기존 DEV release 제한과 core 결함 수정은 별개다. 이 문서는 구현 계획이며 실행 자산을 변경하지 않는다.
## Known Inputs
`M = Case_02_Comparison_Research/YAML_Prompts/2. Stage_2/`, `R = M/Default_Agent/Stage_2_Clean/`, `W = 인증된 localdocs workspace`.
현재 authoring은 `M/Stage_2_S2_00_v.2.yml`, 배포본은 `R/agent_scripts/Stage_2_S2_00.yml`이다. 빌더는 1 task·1 code pointer를 전제로 하고, S2_00 binding은 결과 root만 쓰기 허용한다. request writer를 추가하려면 이 두 계약 변경은 불가피하다. backend의 실제 인자 전달·직렬화 구현은 **UNVERIFIED**다.
## Material Assumptions
- host가 권한을 확인한 네 인자를 실행 시작 때 확정하고 두 task에 동일하게 공급할 수 있어야 한다. YAML 문법·환경변수 이름·run_code parameter를 임의로 발명하지 않는다.
- 같은 `(user, workspace)`의 S2_00 실행은 host가 전역 직렬화한다. 단일 worker의 로컬 mutex만으로 다중 worker 안전성을 주장하지 않는다.
- task 간 메모리 공유는 가정하지 않는다. request bytes는 각 task가 동일한 규칙으로 재구성한다.
- 동일한 bytes의 request가 남아 있다는 사실만으로 prepare 성공을 대신하지 않는다. 현재 host 실행에서 prepare 성공 판정이 먼저 있어야 한다.
## Questions That Could Change the Outcome
구현 전에 **세 가지**만 먼저 확인한다: ① 동일 불변 인자를 두 task에 공급하는 실제 방식, ② exit code와 task 결과에 따른 success-only 후행 실행, ③ 취소·worker 장애·진행 중 I/O까지 포함한 workspace 직렬화. 세 조건이 지원되면 아래 최소안을 진행한다. 미지원이면 필요한 backend 보강을 명시하며, receipt나 lock 파일을 추가하는 것으로 문제가 해결되었다고 간주하지 않는다. 기존 지원만으로 가능하다는 현재의 실증 근거는 없다.
## Workstreams and Dependencies
### 1단계 — 외부 입력과 host 경계 확정
외부에서 공급하는 값은 다음 네 문자열이다.
| 필드 | 검증 |
|---|---|
| `request_id` | `[A-Za-z0-9][A-Za-z0-9._-]{0,127}` |
| `attempt_id` | 위와 동일; host 재시도 정책으로 결정 |
| `stage1_run_root_ref` | W 아래 승인된 Stage 1 사건 root의 NFC canonical 상대경로 |
| `stage1_deployment_root_ref` | W 아래 승인된 Stage 1 배포 root의 NFC canonical 상대경로 |
준비 task가 `schema_version=stage2_s2_00_execution_request.v1`, `workflow_id=S2_00`을 추가하여 정확히 6필드를 만든다. 누락·추가 외부 키와 잘못된 타입을 거절한다. 기존 path validator로 absolute path·`..`·역슬래시·NUL·비정규 경로를 거절하고, 접근 권한과 Stage 1 run 선택은 host가 결속한다. shell/Python source에 원문 인자를 직접 삽입하지 않는다.
host는 준비 task 시작 전 실행 소유권을 획득하여 **S2_00 전체 종료 및 미완료 I/O 종료까지** 유지한다. 이는 hydration 완료까지의 최소 보호 요구를 충족한다. 중간 해제 callback은 추가하지 않는다. 모든 writer·retry가 같은 통제에 참여해야 하며, 종료 여부가 불명확한 경우 다음 실행을 보류한다. TTL 만료만으로 takeover하지 않는다. host에 이 보장이 없으면 live-ready로 판정하지 않는다.
### 2단계 — 최소 준비 함수와 ingress 비교 구현
아래는 의미를 설명하는 pseudocode이며 현재 실행 가능한 API가 아니다.
```python
# 양쪽 task에 host가 동일하게 공급한 네 값
expected = build_request(immutable_host_args) # 두 상수 추가, 정확한 외부 키 확인
expected_raw = canonical_json_bytes(expected)
_inline_validate_request(expected_raw)
# prepare task: 인증된 localdocs 세션 및 host 실행 소유권 아래 수행
localdocs.write_binary_verified(INLINE_REQUEST_PATH, expected_raw)
# 세션 종료까지 성공한 후 task 성공을 반환한다.
# ingress task: 현재 실행의 host_args로 expected_raw를 독립 재구성
actual_raw = localdocs.read_binary(INLINE_REQUEST_PATH) # 기존 첫 읽기에 통합
if actual_raw != expected_raw:
raise IngressError("RUN_REQUEST_INPUT_MISMATCH", "request differs from host input")
# 이후 기존 strict validation 및 2회 읽기/hydration을 계속한다.
```
- `build_request`는 네 값에 두 상수를 추가하는 작은 함수로 제한한다. 별도의 범용 serializer나 validator framework를 만들지 않는다.
- prepare는 기존 validator·canonical serializer·MCP IO helper의 동작을 재사용한다. 두 독립 inline payload에 필요한 helper는 authoring/build 시 포함한다. 외부 `.py` runtime import나 ingress 전체 실행 코드를 복제해 실행하는 방식은 피한다. 중복 포함된 helper의 일치는 좁은 parity 검사로 확인하며 새 공통 모듈 배포 체계까지 만들지 않는다.
- `write_binary_verified`가 bytes 일치를 확인하므로 추가 read-back/parse를 중복 수행하지 않는다. task 성공 결과에는 backend가 요구하는 최소 `ok`/error만 사용하며 새 버전 receipt schema·추가 파일은 만들지 않는다.
- ingress의 expected bytes 검사는 기존 첫 request 읽기에 결합한다. 이미 첫 읽기가 expected와 같고 두 번째가 첫 번째와 같은지 검사하므로 두 번째에서 동일한 expected hash를 다시 계산할 필요는 없다.
- 이 대조는 request ID만이 아니라 여섯 필드 전체를 확인한다. 서로 다른 요청으로 두 pass 모두 교체되더라도 거절한다. 다만 상호 배제는 여전히 host 책임이다.
- 실패 시 준비 작업 전체를 다시 실행한다. 남아 있는 파일을 신뢰해 ingress만 우회 실행하지 않는다. 정리 목적으로 request를 무조건 삭제하지 않는다.
### 3단계 — 두 task의 실행 순서와 실패 차단
```text
host: 동일 workspace 실행 소유권 확보, 네 인자 확정
→ IN
→ prepare: 검증 → 저장/read-back → 세션 종료 → 성공
→ host: 현재 실행의 prepare 성공일 때만 ingress 시작
→ ingress: expected bytes 일치 → 기존 2회 읽기/hydration → 기존 core/발행
→ OUT 또는 실패 정리
→ host: task와 미완료 I/O 종료 확인 후 소유권 해제
```
YAML의 nexts/wait_until은 위 순서를 정확히 기술한다. transport 응답 성공만으로 업무 성공을 판정하지 않는다. exit code 및 backend가 해석하는 task 결과에서 실패·잘못된 결과·timeout이면 ingress를 호출하지 않는다. 후행에 준비 receipt를 전달할 필요는 없지만, host의 task 성공 판정 자체는 생략할 수 없다. 직접 ingress 호출은 host admission으로 차단한다.
### 4단계 — 실제 영향 범위만 빌드·배포 갱신
| 항목 | 최소 변경 |
|---|---|
| `M/Stage_2_S2_00_v.2.yml` | prepare task와 지정 DAG, ingress expected bytes 검사 추가 |
| `R/offline_build/build_s2_00_inline_projection.py/.txt` | 정확히 두 task를 ID로 추출·검증. 0번 task가 ingress라는 가정 제거. 공통 코드 검사와 task별 필수 검사 분리 |
| `R/agent_scripts/Stage_2_S2_00.yml`, runtime mirror, `R/manifest/s2_00_inline_code_receipt.json` | 기존 파생 절차로 재생성. 두 code pointer/hash를 결속. 별도 runtime 준비 receipt와 혼동하지 않음 |
| prepare code mirror | 기존 mirror 정책을 유지하는 데 필요한 `runtime/s2_00_prepare_request.py/.txt`만 파생 생성. 새로운 독립 서비스는 아님 |
| workflow 및 executor binding | 외부 네 인자, success-only, 직렬화 책임과 prepare의 정확한 request 쓰기 경로 추가. ingress의 결과 root 쓰기 범위 유지 |
| 관련 schema | 2-task binding/빌드 receipt 표현이 바뀌어 기존 schema와 충돌하는 부분만 수정. 기존 execution_request의 6필드 schema는 유지 |
| module/release·code pin·Agent/workflow hash | 실제 변경 자산 및 이를 참조하는 hash만 재결속. 순환 결속을 새로 만들지 않고 기존 detached 절차 유지 |
| 기존 분석서·IO 문서 | 구현 완료 후 실제 두-task 구조, 인자 및 request 저장 시점을 반영 |
스키마를 안 바꾼다는 이유로 필요한 code hash를 누락하거나, 단순화를 이유로 기존 검증기를 무력화하지 않는다. 두 task 구성이 요구되는 한 builder·mirror·권한·hash 비용은 남는다. 별도의 준비 receipt schema와 전달 소비자만 제거하는 것이다. 빌드 재실행 시 동일 bytes인지 확인한다.
### 5단계 — 필요한 시험과 완료 판정
아래 Validation Plan을 실행하여 기준선에 있던 실패와 이번 변경의 회귀를 구분한다. host 지원 여부가 불확실하면 offline 구현·시험과 실제 운영 적격성을 별도로 보고한다. DEV guard를 해제해서 성공 결과를 만들지 않는다.
## Source and Tool Plan
원본 계획, 현재 authoring/runtime/builder/binding을 우선 근거로 삼는다. 외부 제품 권고나 일반론이 아닌 로컬 코드의 중복·의존성을 분석한다. 구현 시 backend 코드·문서로 세 선행 조건을 확인하고 기존 테스트 runner 및 빌드 `--check`를 사용한다. 실제 backend 통합 시험은 격리된 fixture 환경에서 수행한다.
## Validation Plan
| 시험 묶음 | 통과 기준 |
|---|---|
| 정상·잘못된 외부 입력 | 정상은 정확한 6필드 canonical bytes; 누락/추가/ID/path/type 오류는 저장 전 거절. 기존 request schema와 validator 수용 범위 일치 |
| 저장·read-back·종료 실패 | 후행 ingress 호출 0회. `ok:false`·비정상 exit·잘못된 성공 결과도 차단 |
| request 교체·잘못된 host 입력 결속 | 첫 읽기의 전체 bytes 대조 및 기존 두 pass 비교로 거절. stale request·다른 attempt·다른 roots 포함 |
| 경쟁·취소·재시도 | 같은 workspace의 두 worker는 직렬 실행; 이전 writer/I/O가 남은 동안 다음 실행 금지. 다른 workspace는 독립 실행 |
| 2-task 빌드·회귀 | 지정 두 task/순서만 허용, code/mirror/receipt/hash 일치, 반복 빌드 동일. 기존 정상 11+E/diagnostic 5파일 fixture 계약 유지 |
| backend 통합 | 동일 불변 인자 공급, success-only 실행, 전역 직렬화·권한 강제가 실제 작동 |
별도 준비 receipt, TTL/fencing 프로토콜, 재사용 최적화를 만들지 않으므로 이들의 전용 시험도 만들지 않는다. 단, worker 장애 이후 교차 쓰기가 없다는 기능 시험은 필수로 남는다. 신규 기능의 fixture 회귀와 실제 MCP/backend 실행 증거를 구분한다.
## Approval Boundaries
이번 요청 범위는 분석과 v1 계획 파일 작성이다. 원본 계획과 실행 자산을 보존한다. 이 문서 작성만으로 구현·배포·live 검증을 수행한 것으로 표현하지 않는다.
## Progress
- [x] 원본 계획과 현재 validator·IO·builder·binding 대조
- [x] 제약 유지안과 제약 완화 대안 구별
- [x] 중복 프로토콜·검증·최적화 제거 및 v1 작성
- [ ] backend 세 조건 확인
- [ ] 준비 task·ingress·빌드/배포 계약 구현
- [ ] 회귀·경쟁·backend 통합 검증 및 문서 갱신
## Decision Log
| 결정 | 근거·효과 |
|---|---|
| 독립 선행 task 유지 | 앞선 사용자 요구를 바꾸지 않음. 절대 최소 구조와 제약 내 최소안을 구분 |
| 동일 host 인자로 expected bytes 재구성 | 별도 준비 receipt·schema·전달을 제거하면서 전체 request 일치 검증 유지 |
| 기존 verified write/validator 재사용 | 현재 존재하는 동작을 새 계층으로 중복 구현하지 않음 |
| 전체 실행 직렬화 유지 | 중간 해제 신호보다 구현이 단순함. 동일 workspace 처리량 최적화는 이번 목표에서 제외 |
| 조건부 backend 확인 | 미확정 host 기능을 기정사실로 삼는 것이 가장 큰 숨은 복잡성이므로 먼저 검증 |
## Evidence Ledger
행번호는 2026-09-30 확인본 기준이다. runtime mirror를 구현 대조 자료로 사용했으며 authoring의 정본 지위를 바꾸지 않는다.
| 분석 명제 | 현재 확인 근거 | 판정 |
|---|---|---|
| 중복 없는 writer/validator 재사용 가능 | `R/runtime/s2_00_ingress.py`의 `write_binary_verified`(5492행), `_inline_validate_request`(5505행) | write/read-back 및 6필드 검증 존재 확인 |
| ingress 첫 읽기에 expected 대조 추가 가능 | 같은 runtime `_inline_hydrate`(5712행 이후) | 기존 첫 읽기 및 검증 순서 확인; 변경은 제안 |
| 2-task 빌드 변경은 생략 불가 | `R/offline_build/build_s2_00_inline_projection.py`: 326–378 | 단일 task·DAG 제약 확인 |
| 준비 receipt 제거의 전제 | 두 task가 동일한 인증된 불변 host 입력을 받는다는 새 계약 | 설계 제안; backend 실제 지원 UNVERIFIED |
| request 쓰기 권한 추가 필요 | `R/deployment/stage2_code_executor_binding.yml`: 47–66 | 기존 write root는 결과 폴더 |
## Risks and Failure Modes
직렬화가 실제로 보장되지 않으면 receipt를 제거하든 유지하든 고정 request 경로의 안전한 실행은 완성되지 않는다. task별 입력이 서로 다르거나 조작될 수 있다면 expected bytes 방식의 전제가 무너진다. 이 경우 host 입력 결속을 먼저 수정해야 한다. 인증된 인자·권한·잠금은 JSON을 단순하게 만드는 것과 별개의 필수 조건이다.
실패한 변경의 롤백은 진행 중 실행과 I/O 종료 후 검증된 기존 authoring·배포본·mirror·binding/hash 묶음으로 복원한다. 사건 자료·발행 결과는 삭제하지 않는다. 기존 버전도 request 외부 공급이 필요하므로 롤백이 자동 정상화를 뜻하지 않는다.
## Results and Residual Uncertainty
**기존 계획은 단순화할 수 있다.** v1은 두-task 요구를 유지하면서 신규 준비 receipt/전달 schema·독립 실행 envelope 설계·중복 read-back 검증·불필요한 사전 closure 읽기·재사용 최적화를 기본 범위에서 제거했다. 필수 인자 검증·저장 확인·후행 차단·동일 요청 소비·동시 실행 통제·hash 일치는 유지했다. 이는 코드 근거에 따른 설계 평가이며 성능 벤치마크 결과가 아니다. backend 세 조건의 실제 지원과 구현 완료 여부는 미확정·미착수다.
@@ -0,0 +1,157 @@
# S2_00 request 생성·저장 계획 v2
작성일: 2026-10-01. 상태: **계획 작성 완료 / 구현 미착수**. [v1](s2-00-request-preparation_v1.md)의 두 task 구상과 기존 request 계약을 계승한다. 이 문서는 `s2_00_request.json`의 생성 코드를 먼저 구현·시험할 수 있도록 작업 순서를 개정한 계획이다.
## Objective
`Stage_2_S2_00.yml` 작업명세서의 맨 앞에 `Task_S2_00_prepare_request`를 배치한다. 이 작업의 핵심 함수를 네 외부 인자를 받는 명시적 인터페이스로 구현하여, 기존 6필드 검증·canonical JSON 직렬화·localdocs binary 저장/read-back 방식으로 `stage2_control/s2_00_request.json`을 만든다. 함수와 task의 offline 검증은 backend의 인자 주입·스케줄링·직렬화 구현 정보가 없어도 진행한다. 실제 운영 연결은 확인된 backend 인터페이스에만 결속한다.
```text
IN → Task_S2_00_prepare_request → Task_S2_00_deterministic_ingress → OUT
4개 인자 검증 기존 request 읽기·2회 hydration
6필드 JSON 구성
localdocs 저장·read-back
```
위 DAG는 작업명세서에서 의도하는 순서다. `nexts`·`wait_until` 선언만으로 prepare 실패 시 ingress가 실제로 차단되거나 두 실행 사이의 파일 경합이 해소되었다고 주장하지 않는다.
## Deliverables
1. 네 인자를 받아 정확히 6필드 request bytes를 생성·검증하는 작은 함수와, 검증된 bytes를 고정 경로에 write/read-back하는 준비 함수.
2. 준비 task를 기존 ingress 앞에 둔 authoring YAML 및 빌더로 생성한 배포 YAML, 두 task의 code mirror·빌드 receipt.
3. 변경으로 실제 영향을 받는 workflow·executor binding·schema·manifest/release·hash 참조와 정적·offline 시험.
4. 구현 후 실제 상태에 맞춘 S2_00 분석서·IO 문서. 운영 연결이 검증되지 않았다면 그 상태를 명시한다.
## Scope and Non-Scope
현재 request의 **6필드와 고정 경로를 유지**한다. 기존 C00–C15의 자료 처리 및 정상/diagnostic 출력 계약은 변경하지 않는다. 새 request receipt 파일, 범용 실행 프레임워크, 임의 backend 변수·템플릿, 독립 잠금 서비스를 생성 코드의 선행 조건으로 두지 않는다. 기존 DEV release 제한이나 다른 Stage 2 결함의 해소는 이 작업의 완료 기준에 포함하지 않는다.
이 계획의 1차 완료는 **생성·검증·저장 코드와 offline 시험**이다. 같은 YAML이 실제 사건 run에서 네 인자를 어떻게 받는지, 실패 시 다음 task 호출을 어떻게 막는지, 고정 request 경로의 동시 쓰기를 어떻게 막는지는 별도의 운영 통합 완료 조건이다. 이 구분 때문에 운영 정보가 없어도 코드 작성을 중단하지 않으며, offline 결과를 live 실행 성공으로 표시하지 않는다.
## Known Inputs
`M/ = Case_02_Comparison_Research/YAML_Prompts/2. Stage_2/`, `R/ = M/Default_Agent/Stage_2_Clean/`, `W/ = localdocs가 결속한 논리 workspace root`.
- 정본: `M/Stage_2_S2_00_v.2.yml`; 현재 배포본: `R/agent_scripts/Stage_2_S2_00.yml`.
- 기존 request 소비 코드는 `stage2_control/s2_00_request.json`을 먼저 읽고, 정확히 6필드·ID·canonical 상대경로를 검증하며, hydration에서 같은 원격 경로의 bytes를 두 번 비교한다.
- 기존 runtime의 `canonical_json_bytes`, `_inline_validate_request`, `_inline_relative_path`, `write_binary_verified`가 각각 직렬화·검증·경로 검증·write/read-back 기준이다. 독립 task가 runtime `.py`를 외부 import한다고 가정하지 않는다.
- 현재 S2_00 빌더는 정확히 1 task 및 1 code pointer를 요구하고 executor binding은 결과 root 쓰기를 명시한다. 준비 task 추가 시 관련 계약 변경이 필요하다.
- backend의 네 인자 주입·성공 조건·workspace 직렬화 구현은 현재 확인되지 않았다. **코드 구현의 선행 차단 조건이 아니라 운영 연결의 미확인 항목**으로 둔다.
## Material Assumptions
- 네 값은 호출자가 지정한 문자열이며, 함수는 위치를 추측하거나 사건 폴더를 검색하지 않는다.
- `request_id`와 `attempt_id`는 기존 정규식에 맞고, 두 root ref는 NFC canonical workspace 상대경로여야 한다. 문법 검증과 실제 Stage 1 run 선택·접근 권한 검증은 구별한다.
- `write_binary_verified`의 성공은 그 호출 직후 read-back한 bytes의 일치를 뜻한다. 다른 실행의 동시 덮어쓰기까지 막는 보장은 아니다.
- 독립 task 사이에 메모리 공유가 있다고 가정하지 않는다. 같은 인자를 후행 task에 공급할 수 있음이 확인되면 v1의 전체 expected-bytes 비교를 적용한다. 확인 전에는 그 비교를 운영상 완료했다고 주장하지 않는다.
## Questions That Could Change the Outcome
다음 사항은 **운영 연결 단계에서만** 확인한다: 실제 backend가 네 값을 task에 전달하는 형식, 실패한 prepare 이후 ingress 시작을 막는 판정 방식, 동일 workspace의 고정 경로를 보호하는 직렬화·취소 처리. 코드 작성 시 이 형식을 추측한 placeholder를 executable로 넣지 않는다. 운영 기능이 없다면 필요한 backend 변경을 기록하고 생성 코드·offline 검증 결과와 분리한다.
## Workstreams and Dependencies
### 1. 기존 계약에 맞는 순수 request 생성 함수
```python
def build_request(
request_id: str,
attempt_id: str,
stage1_run_root_ref: str,
stage1_deployment_root_ref: str,
) -> bytes:
# 설명용 서명: 실제 함수명과 코드는 구현 시 기존 inline 규약에 맞춘다.
...
```
1. 네 인자의 타입·값을 검사한다. ID는 `[A-Za-z0-9][A-Za-z0-9._-]{0,127}`를, root ref는 기존 `_inline_relative_path`의 NFC·상대경로·구성요소 규칙을 따른다.
2. `schema_version = "stage2_s2_00_execution_request.v1"`, `workflow_id = "S2_00"`을 코드 상수로 넣어 **정확히 여섯 필드**를 구성한다. caller가 추가 키를 request에 넣는 우회 경로를 만들지 않는다.
3. 기존 `canonical_json_bytes`와 동일한 규칙으로 UTF-8 bytes를 만든다. 기존 `_inline_validate_request`의 수용 여부와 byte 직렬화 규칙을 대조한다. 별도 schema framework를 만들지 않는다.
4. 이 함수는 localdocs를 호출하지 않아 정상·오류 입력의 단위 시험이 가능해야 한다. 함수가 root의 실재나 권한을 확인했다고 표시하지 않는다.
### 2. localdocs 저장 함수와 선행 task 배치
- 준비 함수는 위 bytes를 받아 인증된 localdocs 세션에서 **고정 경로** `stage2_control/s2_00_request.json`에 `write_binary_verified` 방식으로 쓴다. write 응답과 같은 경로의 read-back bytes가 일치해야 성공한다.
- session 초기화·종료 및 오류 응답은 기존 S2_00 inline MCP 패턴을 따른다. 종료 실패까지 성공으로 표시하지 않는다. 독립 task 코드가 필요한 helper를 포함할 때 기존 구현과 parity를 검사한다.
- 새 task를 authoring YAML의 첫 task로 놓고 `task_procedure`를 `IN → prepare → ingress → OUT`으로 지정한다. prepare의 입력 계약에는 네 문자열 인자와 localdocs 세션을 명시한다. **실제 backend가 지원하는 전달 문법을 확인하기 전에는 임의 `run_code` parameter·환경변수·치환 토큰을 작성하지 않는다.**
- prepare는 성공 시 최소한의 기계 판독 가능 성공/실패 결과를 내도록 설계한다. 신규 저장 receipt나 별도 버전 schema는 만들지 않는다. 오류·read-back 불일치·session 종료 실패 시 실패를 반환한다.
- 기존 ingress는 request의 strict 검증·두 read-pass를 유지한다. 같은 네 인자를 후행에도 공급하는 연결이 확인되면 첫 읽기의 전체 bytes를 재구성한 expected bytes와 비교하는 검사를 추가한다. 이 연결이 없는 offline 단계에서는 준비 함수와 ingress validator의 호환성을 직접 시험하고, 런타임의 동일 요청 소비는 미검증으로 표시한다.
### 3. 변경된 task 구조에 필요한 자산만 갱신
| 자산 | 필요한 변경 |
|---|---|
| `M/Stage_2_S2_00_v.2.yml` | 준비 task를 맨 앞에 배치하고 순서·입력·성공/실패 계약 기록 |
| `R/offline_build/build_s2_00_inline_projection.py/.txt` | 정확한 두 task ID·순서·code pointer 검증; `tasks[0]`이 ingress라는 전제 제거 |
| `R/agent_scripts/Stage_2_S2_00.yml` 및 runtime `.py/.txt` mirror | 정본으로부터 배포본과 두 task의 code mirror 재생성·parity 확인. 기존 파일 덮어쓰기 전 원본 보존 조건 적용 |
| `R/manifest/s2_00_inline_code_receipt.json` | 두 code pointer·hash와 task DAG 결속; 빌드 receipt에만 반영, 새 실행 receipt 파일 없음 |
| workflow·executor binding | 준비 단계의 네 인자 계약·고정 request 쓰기 경로와 기존 ingress 결과 경로를 구별. 실제 backend 강제 여부는 별도 표시 |
| 관련 schema·module/release/hash | task 구조·참조가 실제 바뀐 파일만 갱신하고 기존 6필드 request schema는 유지. hash 참조 순서를 확인하여 순환 결속을 만들지 않음 |
| `M/Stage_2_00_Analysis_v1.md`, `M/Stage_2_00_IO_info.md` | 구현·검증 후 선행 task와 생성 시점, offline/운영 상태의 경계를 반영 |
기존 `Stage_2_S2_00_outdated_9_09.yml` 보존본과 원본 v1 계획은 변경하지 않는다. 새 task를 추가했으므로 기존 단일 task 빌드 검사를 끄거나 결과만 수동 수정하는 방식은 사용하지 않는다.
### 4. offline 시험과 운영 연결의 순서
1. 순수 함수의 정상·오류 입력과 기존 validator 호환성을 시험한다.
2. fake localdocs 또는 격리 fixture에서 write/read-back 일치, 쓰기·읽기·session 종료 실패, 고정 경로 외 쓰기 방지를 시험한다. 실패 결과 뒤 ingress를 호출하지 않는 **로컬 호출 흐름**도 확인한다.
3. builder `--check`, 두 code mirror/hash, 변경 schema·release 참조, 기존 S2_00 정상/diagnostic fixture 회귀를 검증한다. 같은 입력으로 반복 빌드해 bytes 차이가 없는지 확인한다.
4. 실제 backend 인자 전달·실패 차단·workspace 직렬화가 확인되면 YAML의 실행 연결을 결속하고, 동일 요청 소비·경쟁·취소 시험을 수행한다. 이 단계가 끝나기 전에는 실제 S2_00 run 적격성을 주장하지 않는다.
## Source and Tool Plan
현재 authoring YAML·배포 코드·request schema·builder·binding을 1차 근거로 사용한다. 구현의 초기 시험은 직접 함수 호출과 격리된 localdocs 대역으로 진행한다. 운영 연결에 필요한 정보만 실제 backend 코드·명세에서 확인한다. 이 문서 작성 자체는 코드를 실행·수정하지 않는다.
## Validation Plan
| 검증 | 통과 기준 | 상태 구분 |
|---|---|---|
| 4인자·6필드·canonical bytes | 정상 입력의 정확한 bytes 및 기존 validator 일치; 타입·ID·경로 오류는 저장 전 거절 | 함수 단위 |
| 저장·read-back | 저장 경로 한정, bytes 일치; 오류·응답 불일치·close 실패는 실패 | offline IO |
| task 구조·빌드 | prepare가 첫 task, 지정 DAG, 두 code/mirror/hash 일치, 반복 빌드 동일 | 정적·offline |
| ingress 회귀 | 기존 strict request·두 read-pass 및 정상 11+E/diagnostic 5파일 계약 유지 | offline 회귀 |
| 실제 인자 공급·실패 차단·동시 실행 | 같은 인자의 전체 bytes 대조, prepare 실패 시 ingress 미호출, workspace 실행 경합 없음 | **backend 통합 후에만** |
테스트용 fixture 통과를 실사건 실행, downstream handoff 완성, 법률 검증으로 해석하지 않는다. DEV release guard를 시험 편의상 제거하지 않는다.
## Approval Boundaries
이 요청은 **v2 계획 문서 작성**이다. 구현·배포·원격 실행은 수행하지 않는다. 이후 구현 요청에서는 승인된 범위의 로컬 수정과 offline 시험을 진행하고, 실제 운영 호출·외부 쓰기가 필요한 단계의 권한을 그때 확인한다.
## Progress
- [x] v1의 Objective 이하 검토 및 backend 비의존 구현 순서로 개정
- [x] 새 task의 첫 배치, 네 인자 함수, 저장/read-back·검증 계획 작성
- [ ] 생성·저장 함수와 준비 task 구현
- [ ] builder·binding·hash 갱신 및 offline 검증
- [ ] backend 연동·동시 실행 검증
- [ ] 구현 상태에 맞춘 분석·IO 문서 개정
## Decision Log
| 결정 | 근거 | 결과 |
|---|---|---|
| 생성 코드와 운영 연결을 별도 완료 조건으로 둠 | 함수는 명시적 인자와 localdocs 대역으로 검증 가능 | backend 정보 미확인 상태에서도 구현 진행 |
| 두 task 구조와 첫 task 배치 유지 | 사용자가 지정한 Stage 2 작업명세서 순서 | 기존 단일 task 빌드 계약은 실제 구현 시 갱신 |
| 기존 validator·serializer·verified write 재사용 | 6필드·bytes·경로 계약이 이미 존재 | 중복 구현과 새 receipt 제거 |
| 후행 expected bytes 대조는 인자 결속 확인 후 적용 | 별개 task의 인자 동일성을 가정할 수 없음 | offline 호환성과 실제 동일 요청 소비를 구별 |
## Evidence Ledger
| 명제 | 확인된 자료 | 상태 |
|---|---|---|
| 기존 request는 6필드 고정 계약 | `R/runtime/s2_00_ingress.py`의 `_inline_validate_request`; `R/schemas/ingress.schema.json` | 기존 구현 확인 |
| 기존 직렬화·저장/read-back helper가 있음 | 같은 runtime의 `canonical_json_bytes`, `_InlineLocaldocs.write_binary_verified` | 기존 구현 확인 |
| 단일 task 빌더·고정 output 쓰기 root 변경 필요 | `R/offline_build/build_s2_00_inline_projection.py`, `R/deployment/stage2_code_executor_binding.yml` | 기존 제약 확인 |
| 실제 backend 인자 공급·성공 게이트·직렬화 | 현재 v1 및 관련 자료에서 확인되지 않음 | **UNVERIFIED; 생성 함수 구현을 막지 않음** |
## Risks and Failure Modes
- 두 task를 YAML에 나열하기만 해서는 외부 인자 공급이나 실패 시 후행 차단이 구현되지 않는다. backend 연결 확인 전에는 offline artifact로 표시한다.
- write/read-back은 그 순간의 bytes를 확인하지만 고정 경로의 동시 덮어쓰기를 막지 못한다. 실제 운영에는 workspace 단위 통제가 별도로 필요하다.
- 정본·배포본·mirror·receipt·binding의 hash를 따로 갱신하면 참조 불일치가 생긴다. 변경된 참조만 함께 재결속하고 반복 빌드로 확인한다.
- 현 release의 `DEV_FIXTURE_RELEASE / DRAFT_NOT_EXECUTABLE` 상태는 request 생성 기능의 구현만으로 바뀌지 않는다.
## Results and Residual Uncertainty
v2는 backend 기능 확인을 **생성 코드 구현의 선행 조건에서 운영 연결의 완료 조건으로 이동**시켰다. 실행 가능한 함수와 offline 저장 시험을 먼저 만들고, Stage 2 작업명세서에서 이를 첫 task로 배치하는 변경을 계획한다. 실제 인자 주입·후행 차단·동시 실행 통제의 구현 여부는 아직 확인되지 않았고, 이 계획 작성으로 request 파일이나 새 YAML이 생성된 것은 아니다.