diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b1fd32478..ea265fdc2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -67,7 +67,7 @@ These are separate ownership domains. Noema does not duplicate their internal au This revision adds a candidate Tool / Capability admission port at `src/tool-capability/external-extension-admission.ts` for external Claude community plugins. It is not an HTTP route and does not change `/health`, `/ready`, or `/exchange`. Marketplace discovery, Anthropic review, and plugin packaging are not runtime authority. The port stays a local fail-closed ACL until an immutable `context-graph-contracts` artifact contract exists. -Candidate PR #574 adds a durable lifecycle aggregate behind that admission boundary without expanding foreign ownership. `src/tool-capability/external-extension-lifecycle-store.ts` partitions one event stream by the complete exact extension/source/artifact identity and persists Noema lifecycle transition/version/head authority plus immutable Policy / Approval, AppGuardrail, quarantine/isolation, and Egress references/digests. It does not persist mutable scanner verdicts, quarantine runtime truth, outbound policy bodies, provider routing, raw secrets, product payloads, or hidden reasoning. +Protected source includes the durable lifecycle aggregate behind that admission boundary without expanding foreign ownership. `src/tool-capability/external-extension-lifecycle-store.ts` partitions one event stream by the complete exact extension/source/artifact identity and persists Noema lifecycle transition/version/head authority plus immutable Policy / Approval, AppGuardrail, quarantine/isolation, and Egress references/digests. It does not persist mutable scanner verdicts, quarantine runtime truth, outbound policy bodies, provider routing, raw secrets, product payloads, or hidden reasoning. ```mermaid flowchart LR @@ -90,9 +90,11 @@ The arrows from foreign owners carry immutable evidence identities only. Noema d Protected source includes a library-only Agent Runtime aggregate for bounded procedural guidance. `src/agent-runtime/procedural-graph.ts` admits one immutable tenant/task/graph snapshot, canonicalizes nodes and directed relationships, computes local content/structure digests, and pins a module-admitted session to one canonical execution identity. `src/agent-runtime/procedural-evolution.ts` screens a direct child graph against paired held-out evidence but always returns `activationAuthorized: false`; eligibility is evidence for a later independent approval boundary, not permission to publish or execute a graph. `src/agent-runtime/procedural-execution.ts` additionally projects guidance only when its caller supplies a fresh authenticated `running` lifecycle snapshot for the same execution identity; it does not itself become durable lifecycle or revocation authority. +On this active branch, candidate `src/agent-runtime/procedural-current-lifecycle.ts` adds a workflow-backed current-state ACL over the existing execution-scoped Workflow / Task Execution Durable Object. The ACL re-admits the workflow plan and validates the locally admitted procedural session against the same canonical execution identity before it selects or reads any Durable Object. It then performs the existing private workflow-state `read`, validates current execution/plan/task/cancellation evidence, and projects only a conservative Agent Runtime lifecycle state into the already-protected running-only procedural gate. Current cancellation, terminal work and pre-start evidence suppress guidance. The ACL cannot mutate workflow state, create lifecycle transitions or retries, grant tools or Policy / Approval, or authorize graph activation. + The aggregate deliberately owns only Noema runtime mechanics. Procedural text is inert advisory data and is not tool authority, Policy / Approval, a prompt-injection verdict, a secret/PII scrubber, or product-domain truth. Graph/session WeakSet admission prevents structural lookalikes from becoming local runtime capabilities. Unknown procedures and context-budget overflow abstain without a hidden full-graph fallback. Execution identities reuse the canonical Agent Runtime grammar rather than defining a second identity domain. -Cross-product ownership remains outside this protected advisory boundary: released wire contracts belong to `context-graph-contracts`, enterprise adoption/decision records to `enterprise-architecture-core`, model routing to `contextual-orchestrator`, credentials to Keyverse, and graph content/evaluation truth to the owning product. Mutable sibling PR heads are not consumed. ADR 0017 remains `Proposed`; protected source integration alone does not establish authenticated evaluation receipts, durable graph history, current-lifecycle revocation, Policy / Approval CAS, canary/rollback evidence, production activation, or organization-wide self-evolution. +Cross-product ownership remains outside this protected advisory boundary: released wire contracts belong to `context-graph-contracts`, enterprise adoption/decision records to `enterprise-architecture-core`, model routing to `contextual-orchestrator`, credentials to Keyverse, and graph content/evaluation truth to the owning product. Mutable sibling PR heads are not consumed. ADR 0017 remains `Proposed`; protected source integration plus the active workflow-backed ACL candidate do not establish authenticated evaluation receipts, durable graph history, non-workflow current-lifecycle revocation, Policy / Approval CAS, deployed Durable Object behavior, canary/rollback evidence, production activation, or organization-wide self-evolution. ## 5. Evidence and authority separation @@ -138,9 +140,9 @@ Model-facing automation uses the `NOEMA_LLM_*` gateway contract where applicable Durable Object alarms are at-least-once. Handlers reread current deadline/expiry state and **reschedule** from current state so delayed alarms cannot delete newer state. Storage-class, binding-name, or lifecycle changes require migration/rollback analysis. -Candidate #574 adds separate Durable Object storage semantics for external-extension lifecycle evidence. The event log is append-only and is not the bounded Workflow / Task receipt ledger. Event/request digests are computed outside the short transaction; the transaction revalidates expected version, prior state, and prior head digest before atomically writing event + idempotency index + compact head. `readCurrent()` verifies only the head and exact tail for the latency-sensitive path, whereas `readAudit()` verifies every retained version/hash link and final head/tail identity. Corrupt or truncated durable state is a conflict, never an empty stream. Recovery and rollback must preserve acknowledged history and follow `docs/external-extension-lifecycle-recovery.md`. +Protected source includes separate Durable Object storage semantics for external-extension lifecycle evidence. The event log is append-only and is not the bounded Workflow / Task receipt ledger. Event/request digests are computed outside the short transaction; the transaction revalidates expected version, prior state, and prior head digest before atomically writing event + idempotency index + compact head. `readCurrent()` verifies only the head and exact tail for the latency-sensitive path, whereas `readAudit()` verifies every retained version/hash link and final head/tail identity. Corrupt or truncated durable state is a conflict, never an empty stream. Recovery and rollback must preserve acknowledged history and follow `docs/external-extension-lifecycle-recovery.md`. -The protected procedural-graph source is intentionally non-durable. A graph digest or successful held-out screen is not retained activation authority. Durable history, authenticated evaluation receipts, current-lifecycle revocation, approval CAS, canary state, rollback and recovery require a later owner-approved boundary; adding those concerns directly to the library aggregate would collapse State / Checkpoint, Policy / Approval, and product-domain ownership. +The protected procedural-graph source itself remains intentionally non-durable. A graph digest or successful held-out screen is not retained activation authority. Candidate #589 reuses the existing durable Workflow / Task Execution state only as current task/cancellation evidence for workflow-backed advisory gating; it does not persist graph history, approval, or activation state and does not replace Agent Runtime lifecycle ownership. Durable graph/rejection history, authenticated evaluation receipts, non-workflow current-lifecycle revocation, approval CAS, canary state, rollback and recovery require later owner-approved boundaries; adding those concerns directly to the library aggregate would collapse State / Checkpoint, Policy / Approval, and product-domain ownership. ## 9. Standalone and modular MSA contract @@ -159,7 +161,7 @@ The protected procedural-graph source is intentionally non-durable. A graph dige | OIDC/GitHub App | issuer/audience/repository/workflow-ref, immutable workflow-source SHA when configured, malformed token/JWKS, replay, redirect/egress, secret non-disclosure regressions | | Durable Objects | cross-instance semantics, delayed/retried alarm, current-state reschedule, malformed backend/storage-failure tests | | External-extension lifecycle | legal-edge validation; restart/replay/CAS races; exact Policy / Approval and foreign-owner reference binding; corruption/truncation/cross-stream rejection; >128-transition auditability; O(1) verified current projection; full audit/recovery rehearsal; actual Durable Object p95/contention/storage-growth evidence before runtime acceptance | -| Procedural graph guidance | exact schema/identity bounds; graph/session local admission; canonical digest behavior; cycle-safe bounded neighborhood extraction; unknown/budget abstention; paired holdout separation and exact candidate/base/context binding; safety and measured-score non-regression; `activationAuthorized: false`; fresh authenticated lifecycle snapshot on execution projection; later authenticated receipt/durable revocation/approval/canary evidence before activation | +| Procedural graph guidance | exact schema/identity bounds; graph/session local admission; canonical digest behavior; cycle-safe bounded neighborhood extraction; unknown/budget abstention; paired holdout separation and exact candidate/base/context binding; safety and measured-score non-regression; `activationAuthorized: false`; pure same-execution fresh lifecycle projection; workflow-backed plan/session identity rejection before durable lookup plus fresh current Workflow / Task Execution read per decision; later authenticated receipt/durable graph/non-workflow revocation/approval/canary/deployed-DO evidence before activation | | GitHub Actions/control plane | least privilege, exact-head/live-base binding, full pagination, stale-head refusal, evidence-class separation | | LLM integration | gateway contract, provider-key isolation, deterministic gates independent of model judgement | | release/acquisition | protected source, CI/security/coverage, package/SBOM/provenance/reproducibility, licensing/NOTICE, rollback/recovery, later operational/buyer evidence | @@ -170,9 +172,9 @@ Owned production remains subject to exact 100% statement/branch/function/line co Repository source/docs cannot fabricate stronger live `main` governance than the current ruleset, independent approval, App provisioning, reviewer staffing, protected production approval, immutable release/signing/provenance, 30-day KPI evidence, customer/revenue evidence, or legal transfer authority. These remain separate evidence classes and fail closed when required but absent. -Candidate lifecycle source also cannot establish actual Durable Object p95, contention/partition behavior, backup/restore success, production recovery, or deployed invocation enforcement by documentation alone. Those remain later exact operational evidence. +Protected external-extension lifecycle source cannot establish actual Durable Object p95, contention/partition behavior, backup/restore success, production recovery, or deployed invocation enforcement by documentation alone. Those remain later exact operational evidence. -Protected procedural-graph source cannot establish authenticated evaluation provenance, cross-language/released digest semantics, durable version history, current-lifecycle revocation, approval, canary operation, rollback success, or production outcome improvement. Those remain later contract, Policy / Approval, operational, and product-owner evidence. +Protected procedural-graph source and candidate #589 cannot establish authenticated evaluation provenance, cross-language/released digest semantics, durable graph version history, non-workflow current-lifecycle revocation, approval, deployed workflow-state ACL behavior, canary operation, rollback success, or production outcome improvement. Those remain later contract, Policy / Approval, operational, and product-owner evidence. ## 12. Canonical documentation graph @@ -197,6 +199,6 @@ The default shape is **small credential-exchange service + explicit state coordi The external-extension lifecycle remains a bounded Tool Capability / State / Checkpoint aggregate rather than a new scanner, quarantine runtime, egress engine, identity provider, or model router. Its synchronous projection path and full audit/recovery path are deliberately separate so buyer/runtime latency does not require scanning retained history while recovery still verifies the complete chain. -The protected procedural-graph advisory remains a bounded Agent Runtime aggregate rather than an execution engine or autonomous policy plane. It may select localized procedural context and screen a direct child candidate, and its execution adapter may project that context only against a caller-supplied fresh authenticated `running` lifecycle snapshot. Any future persistence, release, approval, current-lifecycle revocation, canary, or activation path must cross explicit versioned owner contracts and retain `activationAuthorized: false` until those independent authorities are proven. +The protected procedural-graph advisory remains a bounded Agent Runtime aggregate rather than an execution engine or autonomous policy plane. Its pure execution adapter may project localized context against a caller-supplied fresh authenticated same-execution `running` lifecycle snapshot. On this active branch, candidate #589 adds only a workflow-backed current-state ACL: it reuses the existing canonical Workflow / Task Execution durable owner for fresh task/cancellation evidence and rejects plan/session execution mismatch before durable lookup. Any future graph persistence, release, approval, non-workflow current-lifecycle revocation, canary, or activation path must cross explicit versioned owner contracts and retain `activationAuthorized: false` until those independent authorities are proven. Architecture changes must keep source behavior, realistic regression tests, canonical documentation, traceability, and CHANGELOG semantics consistent without promoting active-PR behavior to protected truth. diff --git a/CHANGELOG.md b/CHANGELOG.md index c56a89df0..41b5cc52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Add a Noema-owned exact-claim evidence receipt contract whose execution and research producers serialize one canonical artifact that binds every receipt semantic field, including command/result/isolation/network or source revision/excerpt/retrieval policy. Admission accepts only a receipt ID from untrusted model output. The owner API first verifies the exact authenticated OpenCode-handoff manifest digest, canonical envelope bytes, reviewed producer-to-kind policy, and repository/head/workflow/run/attempt identity before it can construct an immutable typed index; admission then reconstructs each canonical artifact and verifies time/claim/artifact identity. The version-2 manifest now binds a separate producer-authenticated `ClaimEvidenceRequirement` containing the exact claim, independently required evidence kind, and `context` or `finding` publication authority. Raw current-head source lines are context only: they are withheld from finding-reference prompts and cannot publish a finding or `request_changes`; an explicitly producer-authorized source finding remains usable and retains exact path/line checks. Finding-free model `request_changes` and `blocked` verdicts cannot bypass receipt admission to publish a vacuous blocking review. Requirement/receipt kind mismatch, fixed-artifact semantic substitution, caller-supplied receipt dictionaries, model self-classification, stale identities, cross-kind receipts, marker-only sandbox output, noncanonical artifact bytes, and expired receipts fail closed before the GitHub publisher. This remains the owner prerequisite for ContextualWisdomLab/.github#1641 and issue #555. The reviewed `sandboxed_verify` adapter exists in owner source, but its actual central stdout/stderr/marker-to-manifest wiring and the trusted research producer are not yet integrated; exact-head hosted GREEN, immutable release, and the verified central consumer bump remain required. ## Unreleased +- Agent Runtime의 workflow-backed procedural guidance가 매 판단마다 기존 execution-scoped `NOEMA_WORKFLOW_STATE`의 current Workflow / Task Execution evidence를 다시 읽는다. re-admitted plan과 locally admitted procedural session의 canonical execution identity가 다르면 Durable Object를 선택하거나 읽기 전에 실패-폐쇄하고, 현재 cancellation·terminal·pre-start evidence는 guidance를 억제한다. 이 ACL은 두 번째 lifecycle DB, task/lifecycle mutation, retry, tool, Policy / Approval, provider routing 또는 activation authority를 만들지 않으며 non-workflow lifecycle freshness와 deployed Durable Object compatibility/p95/recovery는 별도 acceptance로 남긴다. issue #584, ADR 0017. - Agent Runtime에 tenant/task/execution-scoped immutable procedural graph와 bounded advisory context, paired held-out candidate screening을 추가한다. 모든 candidate decision은 `activationAuthorized: false`를 유지하고 tool·retry·Policy/Approval·provider routing·credential·foreign-domain authority를 부여하지 않는다. 그래프/평가 wire contract는 아직 Noema-local이며 cross-service publication은 context-graph-contracts의 immutable release를 기다린다. issue #584, ADR 0017. - Agent Runtime의 procedural guidance를 locally admitted session brand와 canonical execution lifecycle에 결합한다. 구조만 흉내 낸 session은 callback/property를 읽기 전에 거부하고, guidance는 동일 execution의 `running` 상태에서만 반환하며 accepted·cancellation-requested·terminal 상태에서는 context request를 읽지 않고 억제한다. 결과는 계속 `advisory_only`이고 tool·retry·Policy/Approval·transition authority를 만들지 않는다. issue #584. - External-extension lifecycle의 private Durable Object command surface에 `read_operability`를 추가해 exact stream-scoped SQLite `ctx.storage.sql.databaseSize`를 `{ database_size_bytes }`로만 노출한다. canonical object-name binding이 다르면 409로 거부하고, 음수·비정수 storage counter는 내부 오류로 실패-폐쇄해 #561의 실제 per-object storage-growth evidence producer가 synthetic fixture나 namespace aggregate 대신 deployed object-local byte counter를 소비할 수 있게 한다. 이 경로는 lifecycle event payload·foreign-owner truth·secret·provider routing을 노출하지 않으며 remote p95/contention/recovery, production activation authority, deployment 또는 immutable release acceptance를 대신하지 않는다. issue #561. diff --git a/docs/PRD.md b/docs/PRD.md index 37682779c..a613eac67 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -106,6 +106,8 @@ Protected `main` includes a bounded **procedural graph advisory runtime** inside Protected source also includes the #586 execution-lifecycle adapter. It projects procedural guidance only when the caller supplies a fresh authenticated `running` lifecycle snapshot for the same canonical execution identity and suppresses guidance for accepted, cancellation-requested, or terminal states. The adapter is not itself durable lifecycle/revocation authority and cannot prove that a canonical `running` snapshot has not become stale after the caller obtained it. +On this active branch, candidate #589 adds a workflow-backed current-state ACL without replacing that pure adapter. It re-admits the workflow plan, validates the locally admitted procedural session against the same canonical execution identity before selecting any execution-scoped Durable Object, then re-reads the existing Workflow / Task Execution owner before each guidance decision. Current cancellation, terminal task evidence, and pre-start evidence suppress guidance; only other current nonterminal workflow evidence can reach the existing running-only advisory gate. This candidate does not make Workflow / Task Execution the Agent Runtime lifecycle store and does not grant mutation, retry, Policy / Approval, tool, release, or activation authority. Non-workflow Agent Runtime callers still require their own fresh authenticated lifecycle source, and source/fake-Durable-Object tests do not establish deployed Durable Object compatibility, restart behavior, availability, or synchronous buyer-path p95. + Procedural graph ownership follows existing CWL boundaries. Any released cross-service schema belongs to `context-graph-contracts`; enterprise adoption/decision records belong to `enterprise-architecture-core`; model discovery/routing remains in `contextual-orchestrator`; credentials remain in Keyverse; graph content and outcome/evaluation truth remain with the consuming product. Noema owns only the execution-local advisory mechanics and deterministic candidate-screening port. Mutable sibling PR heads are not production dependencies. ## 5. Functional requirements @@ -135,7 +137,7 @@ Procedural graph ownership follows existing CWL boundaries. Any released cross-s | FR-021 | Model discovery, routing, test-time compute, provider failover, and provider credentials remain owned by `contextual-orchestrator`; Noema runtime code must not duplicate direct provider SDKs, credentials, fallback lists, or routing policy. | | FR-022 | Workflow/task, tool/capability, isolation, policy/approval, observability, recovery, Context Graph, and EA integration must cross explicit versioned ports/contracts; Context Graph integration must use immutable released versioned contracts, reject open or unreleased Draft contracts, and require conformance/admission evidence, canonical object/authority references, provenance, and valid/system time semantics. Arbitrary tool authority, ambient secret propagation, unbounded recursive work, silent side-effect retry, unreleased Context Graph source coupling, and cross-service SQL are forbidden. | | FR-023 | External-extension lifecycle authority must be append-only, exact-stream partitioned, restart-reconstructable, and CAS-serialized; current projection must be cryptographically bound to its audit tail, full audit must detect sequence/hash/stream truncation or substitution, exact duplicate replay must preserve committed history, and every genuinely new activation must bind a fresh Noema Policy / Approval and owner-evidence re-read without copying foreign-owner truth. | -| FR-024 | Procedural graph guidance must remain tenant/task/execution scoped, immutable and advisory-only; reject forged graph/session identities and unsafe input, return bounded localized context or explicit abstention, bind candidate screening to exact direct-child lineage and paired held-out context, prevent train/holdout leakage, reject safety or measured-score regression, keep activation unauthorized, and project execution guidance only against a caller-supplied fresh authenticated `running` lifecycle snapshot for the same execution. Durable lifecycle freshness/revocation, released contracts, authenticated evaluation evidence, Policy / Approval, persistence and rollout authority remain separate prerequisites. | +| FR-024 | Procedural graph guidance must remain tenant/task/execution scoped, immutable and advisory-only; reject forged graph/session identities and unsafe input, return bounded localized context or explicit abstention, bind candidate screening to exact direct-child lineage and paired held-out context, prevent train/holdout leakage, reject safety or measured-score regression, and keep activation unauthorized. The protected pure adapter requires a caller-supplied fresh authenticated same-execution `running` lifecycle snapshot. For workflow-backed guidance on this active branch, the plan/session execution identity must match before any execution-scoped durable lookup and the canonical Workflow / Task Execution owner must be re-read before every decision. Non-workflow lifecycle freshness, released contracts, authenticated evaluation evidence, Policy / Approval, durable graph/rejection history, deployment evidence and rollout authority remain separate prerequisites. | ## 6. Non-functional requirements @@ -232,4 +234,4 @@ An earlier stage never proves a later stage. - `docs/external-extension-lifecycle-recovery.md` — dedicated external-extension restart, audit-chain, corruption, CAS-contention, and rollback recovery contract. - `docs/DOCUMENTATION_GAP_AUDIT.md` — design sufficiency versus protected-main operational sufficiency. - runtime and automation threat models — distinct threat surfaces. -- `docs/LICENSING_AND_IP_TRANSFER.md` — owner/legal and exact-release rights boundary. +- `docs/LICENSING_AND_IP_TRANSFER.md` — owner/legal and exact-release rights boundary. \ No newline at end of file diff --git a/docs/TRD.md b/docs/TRD.md index a9485b691..fc5147b9e 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -60,7 +60,11 @@ Protected source includes four library-only Agent Runtime modules: `procedural-i `assessProceduralCandidate()` accepts only an admitted direct-child graph, exact evaluation-context digest, disjoint training/held-out case identities, complete paired baseline/candidate observations, finite normalized scores and explicit safety-violation counts. It rejects lineage/context mismatch, train/holdout leakage, missing/duplicate cases, any candidate safety violation, mean score regression, repeated rejection keys and unchanged structure. A passing result is only `eligibleForApproval`; `activationAuthorized` is always `false`. Receipt authentication, durable graph/rejection history, approval CAS, canary/rollback and production outcome measurement are deliberately later boundaries. -`guideProceduralExecution()` consumes only a locally admitted procedural session and a caller-supplied fresh authenticated lifecycle snapshot for the same canonical execution identity. It projects bounded advisory context only while that supplied lifecycle is `running`; accepted, cancellation-requested and terminal states suppress guidance. This pure adapter does not persist lifecycle state and cannot independently prove that a previously authenticated `running` snapshot has not become stale. Durable current-state/revocation remains a later State / Checkpoint + Policy / Approval authority. +`guideProceduralExecution()` consumes only a locally admitted procedural session and a caller-supplied fresh authenticated lifecycle snapshot for the same canonical execution identity. It projects bounded advisory context only while that supplied lifecycle is `running`; accepted, cancellation-requested and terminal states suppress guidance. This pure adapter does not persist lifecycle state and cannot independently prove that a previously authenticated `running` snapshot has not become stale. + +On this active branch, candidate `guideProceduralExecutionFromCurrentWorkflowState()` adds a workflow-backed freshness ACL without changing the pure adapter's ownership. It first re-admits the workflow plan and validates the locally admitted procedural session against that exact execution identity before any Durable Object is selected or read. It then issues only the existing private Workflow / Task Execution `read` command to the execution-scoped `NOEMA_WORKFLOW_STATE` owner, validates exact execution/plan identity, complete unique task identities, allowed task states, cancellation identity and transition sequence, and projects only the minimum Agent Runtime state needed for the existing running-only advisory gate. Cancellation and terminal work suppress guidance, initialized pre-start evidence remains unavailable, and other current nonterminal workflow evidence may be treated as running for advisory purposes. The ACL cannot claim/mutate tasks, create lifecycle transitions, retry effects, grant Policy / Approval or tool authority, or replace Agent Runtime lifecycle semantics. Cross-execution plan/session mismatch is rejected before another execution's durable owner can be read. + +This candidate closes only the cached-workflow-snapshot gap for workflow-backed executions at source level. Non-workflow Agent Runtime callers still require their own fresh authenticated lifecycle source. Fake/in-memory Durable Object tests do not establish deployed transaction compatibility, restart/failure behavior, availability, synchronous buyer-path p95, durable procedural graph/rejection history, approval CAS, canary/rollback, release, deployment, or activation evidence. Released cross-service procedural graph schemas belong to `context-graph-contracts`; enterprise adoption records belong to `enterprise-architecture-core`; model discovery/routing remains owned by `contextual-orchestrator`; credentials remain in Keyverse; graph content and product outcome truth remain with the consuming product. No mutable sibling PR-head dependency is accepted as production authority. ADR 0017 remains `Proposed`: protected source integration is not release, deployment, approval, canary or activation evidence. @@ -343,6 +347,8 @@ The protected procedural graph source is intentionally non-durable: graph/sessio Protected source implements procedural graph admission/session, offline direct-child candidate screening, and the #586 lifecycle-gated advisory projection with hostile tests for malformed descriptors, forged local authority, graph identity/scope, resource bounds, cycle-safe traversal, abstention, lineage/context mismatch, train/holdout leakage, paired evidence completeness, safety regression, measured-score regression, same-execution lifecycle binding, and non-running suppression. ADR 0017 remains `Proposed`; root architecture and traceability retain graph content as advisory-only and activation as unauthorized. This source is not a deployed route, durable graph store, model refiner, signed receipt verifier, automatic activation system, current-state revocation authority, or organization rollout. +On this active branch, the workflow-backed current-state ACL reuses the protected Workflow / Task Execution Durable Object only as current task/cancellation evidence for procedural guidance. It does not persist procedural graphs or create a second lifecycle database, and it validates local session/execution identity before any execution-scoped durable read. This candidate narrows stale workflow-backed guidance at source level but does not establish deployed Durable Object behavior, universal lifecycle freshness, Policy / Approval promotion, or rollout authority. + ## Implemented 다음은 current repository에 구현된 기술 계약이며 정확한 protected-main revision과 branch별 변경은 live GitHub source로 확인합니다. @@ -364,7 +370,7 @@ Protected source implements procedural graph admission/session, offline direct-c - issue #30의 organization-level runner-assignment root-cause evidence. - release/deployment provenance chain의 실제 production acceptance. - external-extension lifecycle actual Durable Object current-projection/contended-append p95 measurement, partition/lock/storage-growth capture, full audit rebuild, backup/restore or equivalent recovery rehearsal, and rollback/suspension verification before ADR 0015 can advance. -- released procedural wire-contract work, authenticated evaluation receipts, durable graph/rejection history and current-lifecycle/revocation authority, Policy / Approval CAS, canary/rollback evidence, and product-owner production outcome measurement before ADR 0017 can advance beyond its current Proposed/advisory-only state. +- released procedural wire-contract work, authenticated evaluation receipts, durable graph/rejection history and non-workflow current-lifecycle/revocation authority, deployed workflow-state ACL evidence, Policy / Approval CAS, canary/rollback evidence, and product-owner production outcome measurement before ADR 0017 can advance beyond its current Proposed/advisory-only state. ## External evidence @@ -376,8 +382,8 @@ repository source만으로 충족되지 않는 항목: - private vulnerability-reporting repository setting and benign exercise where required. - production environment protection and independent reviewer configuration. - production KPI/log provenance, deployment receipts/attestations. -- procedural graph evaluator identity/receipt authenticity, enterprise adoption approval, durable current-state/canary/rollback evidence, and product outcome truth from their owning systems. +- procedural graph evaluator identity/receipt authenticity, enterprise adoption approval, non-workflow durable current-state/canary/rollback evidence, deployed workflow-state ACL behavior, and product outcome truth from their owning systems. ## 17. References -설계의 표준·primary-source 근거와 APA 7th bibliography는 `docs/doctoring/architecture-trust-boundaries.md`를 canonical source로 사용합니다. 세부 API/운영 근거는 해당 doctoring/runbook의 source verification note를 따릅니다. External-extension lifecycle recovery procedure is `docs/external-extension-lifecycle-recovery.md`; lifecycle architecture remains governed by ADR 0015 and the canonical Context Map. Procedural graph method provenance and adoption evidence are documented in ADR 0017 and `docs/doctoring/procedural_graph_adoption.md`; method citations do not become CWL production evidence. +설계의 표준·primary-source 근거와 APA 7th bibliography는 `docs/doctoring/architecture-trust-boundaries.md`를 canonical source로 사용합니다. 세부 API/운영 근거는 해당 doctoring/runbook의 source verification note를 따릅니다. External-extension lifecycle recovery procedure is `docs/external-extension-lifecycle-recovery.md`; lifecycle architecture remains governed by ADR 0015 and the canonical Context Map. Procedural graph method provenance and adoption evidence are documented in ADR 0017 and `docs/doctoring/procedural_graph_adoption.md`; method citations do not become CWL production evidence. \ No newline at end of file diff --git a/docs/adr/0017-procedural-graph-guidance.md b/docs/adr/0017-procedural-graph-guidance.md index cd57997e0..1937363a6 100644 --- a/docs/adr/0017-procedural-graph-guidance.md +++ b/docs/adr/0017-procedural-graph-guidance.md @@ -1,7 +1,7 @@ # ADR-0017: Advisory procedural graphs with offline candidate screening -Status: Proposed. A source implementation on a feature branch is not protected-source, -release, or deployment acceptance. +Status: Proposed. Individual source slices are protected only when present on protected `main`; +this ADR status does not imply release, deployment, shadow, canary, or activation acceptance. Date: 2026-09-10 @@ -30,7 +30,8 @@ to an execution ID and exact graph digest. `procedural-evolution.ts` screens sup paired evaluation evidence. `procedural-input.ts` shares only strict data readers and content hashing inside this aggregate; it is not a generic organization utility. No HTTP route, `/exchange` change, OIDC trust expansion, persistence binding, -provider client, credential, package dependency, or workflow is introduced. +provider client, credential, package dependency, or workflow is introduced by the +core graph slice. The local schema is `noema.procedural-graph/v1`, not a released CWL wire contract. Nodes have canonical identifiers, including `Start`. Directed edges carry `from`, @@ -59,6 +60,28 @@ an exceeded edge budget produce an empty `abstain` result rather than the paper' full-graph fallback or a silently truncated prerequisite set. This intentional CWL adaptation needs comparison in the shadow pilot. +`procedural-execution.ts` is the pure Agent Runtime gate: a locally admitted session +receives advice only when a caller supplies fresh authenticated lifecycle state for +the same canonical execution. It intentionally owns no durable lifecycle storage. +For workflow-backed executions, `procedural-current-lifecycle.ts` adds a narrow +anti-corruption/current-state ACL over the existing `NOEMA_WORKFLOW_STATE` owner. +Every guidance decision re-admits the exact workflow plan and performs the existing +execution-scoped private `read` command instead of accepting a cached caller-owned +`running` snapshot. The response must match execution ID, plan ID, complete unique +task identities, canonical task states, cancellation identity, and monotonic +transition sequence or the ACL fails closed. + +That durable read does **not** move Agent Runtime lifecycle truth into Workflow / +Task Execution. It derives only a conservative guidance projection: current +cancellation evidence suppresses advice; a fully terminal task vector suppresses +advice; an initialized all-pending vector with no later transition evidence remains +pre-start; other nonterminal workflow evidence is eligible to pass through the +existing running-only procedural gate. The projection cannot create lifecycle +transitions, task claims, retries, tool capability, approval, publication, or +activation. Non-workflow Agent Runtime executions still require their own fresh +authenticated lifecycle source, and source tests do not prove deployed Durable +Object latency, availability, restart behavior, or caller composition. + Every context says `authority: advisory_only`. Text may still contain hostile instructions or sensitive content; this module is neither a prompt-injection classifier nor a secret/PII scrubber. Admission, minimization, taint handling, @@ -110,7 +133,7 @@ Existing executions must keep their pinned revision and separately honor revocat | Owner | Planned responsibility; not a claim of deployed integration | | --- | --- | -| Noema | Graph snapshot, guidance context, offline screening; later lifecycle/state adapters | +| Noema | Graph snapshot, guidance context, offline screening, and workflow-backed current-state guidance ACL; durable graph/rejection history and promotion/revocation remain separate work | | context-graph-contracts | Released language-neutral schemas, digest rules, conformance fixtures | | enterprise-architecture-core | Capability/owner map, versioned adoption matrix and evidence classes | | contextual-orchestrator | Existing gateway routing for later guide/solver/refiner calls; no client-side provider fallback | @@ -121,7 +144,7 @@ Existing executions must keep their pinned revision and separately honor revocat | psychometrics-commons / evaluation owner | Task-specific measures, rubric and standard-setting separation, held-out protocol and uncertainty | | .github and product owners | Central development profile and product-specific procedural graphs/adapters/tests | -1. Implement and review this deterministic core without enabling a production path. +1. Keep the deterministic core and workflow-backed current-state ACL advisory-only. 2. Have contract/EAC owners release interoperable schemas and ownership records. Do not consume mutable sibling PR heads or independently copy this runtime. 3. Integrate read-only shadow guidance through the existing orchestrator boundary @@ -137,15 +160,42 @@ Existing executions must keep their pinned revision and separately honor revocat Do not force this pattern into deterministic numerical kernels or create another central scheduler. Source adoption, shadow use, canary, active deployment, and rollback-tested operation must appear as separate states in the adoption matrix. -The active documentation lane may reconcile the ADR index and PRD/TRD/traceability -without this lane overwriting its root baseline or historical evidence. ## Acceptance and remaining limitations -The focused tests exercise the pure boundary; native repository typecheck, -repository-wide exact coverage, inherited security checks, independent review, -release artifacts, and deployed operational evidence remain distinct requirements. -See the [implementation plan and verification record](../superpowers/plans/2026-09-10-procedural-graphs.md). +The focused tests exercise the source boundary; native repository typecheck, +repository-wide exact coverage, inherited security checks, review, release artifacts, +and deployed operational evidence remain distinct requirements. See the +[implementation plan and verification record](../superpowers/plans/2026-09-10-procedural-graphs.md). No automatic LLM refiner, signed graph store, guidance prompt, MCP endpoint, -production caller integration, or organization-wide deployment is delivered by -this first source slice. +production caller integration, or organization-wide deployment is implied. + +The workflow-backed ACL closes only the caller-cache replay path when the current +Workflow / Task Execution Durable Object itself contains newer cancellation or +terminal task evidence. It is not a universal Agent Runtime lifecycle database and +must not be advertised as one. Real runtime acceptance requires exact deployed +composition plus failure/restart and buyer-path latency evidence; the p95 <=20 ms +target is measured against the deployed path rather than inferred from unit tests. + +There is still no production graph/trajectory store, signed receipt verifier, +automatic refiner, independently approved promotion API, or product invocation. +There is also no evidence yet that graph guidance improves CWL tasks. The owning +root product/technical baseline must retain these gaps without replacing historical +results. Do not mark ADR-0017 Accepted, publish a release, or advertise +organization-wide activation from source integration or tracking issues. + +## References + +Cloudflare. (2026). *Durable Object storage*. Cloudflare Developers. +https://developers.cloudflare.com/durable-objects/api/storage-api/ + +Cloudflare. (2026). *Invoke methods*. Cloudflare Developers. +https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/ + +Lu, Y., Chen, Y., Wu, S., & Arık, S. Ö. (2026). *Procedural graphs: Self-evolving +execution structures for LLM agents* [Preprint]. arXiv. +https://doi.org/10.48550/arXiv.2609.09153 + +코난쌤. (2026, September 10). *Procedural Graph: LLM 에이전트를 위한 자가진화 +절차 그래프 (arXiv 2609.09153) 논문 정리*. 코난쌤 블로그. +https://conanssam.com/posts/2026-09-10-procedural-graphs-self-evolving-llm-agents diff --git a/docs/doctoring/procedural_graph_adoption.md b/docs/doctoring/procedural_graph_adoption.md index 7c1f1876b..c04d3c6be 100644 --- a/docs/doctoring/procedural_graph_adoption.md +++ b/docs/doctoring/procedural_graph_adoption.md @@ -13,7 +13,9 @@ The canonical EA adoption matrix belongs to enterprise-architecture-core, not th Protected source integration: #585 and #586 are merged on protected `main`. This is source-integration evidence only. ADR 0017 remains `Proposed`, candidate screening remains `activationAuthorized: false`, and release, deployment, authenticated evaluation, durable promotion, -shadow/canary, rollback and product-outcome evidence remain separate authorities. +shadow/canary, rollback and product-outcome evidence remain separate authorities. The workflow-backed +current-state ACL is tracked in #589 until its own protected integration; this record does not promote +a Draft head into protected or deployed truth. ## What the sources support @@ -37,7 +39,7 @@ than the blog's interpretation or comparative scores. | Scope and identity | Graph tenant/task/digest are compared exactly. Execution IDs use Noema's existing `isCanonicalExecutionId`, not the narrower graph-node grammar. Graph identity and execution identity are different contracts. | | Local object admission | Only frozen graph sessions issued by the owning module may enter the lifecycle adapter. Copied objects, proxy wrappers and forged callbacks are rejected before session property access. This is local object integrity, not caller authentication. | | Unknown or oversized neighborhood | Return unavailable advice for `unknown_procedure` or `context_budget_exceeded`; do not turn abstention into success, return the entire graph, or silently drop prerequisite relationships. | -| Execution lifecycle | Given fresh authenticated lifecycle state, only a running execution receives advice. Accepted, cancelling and terminal executions do not evaluate the neighborhood request. The pure adapter is not a durable revocation store and cannot detect replay of an old running snapshot. | +| Execution lifecycle | The protected pure adapter accepts only a caller-supplied fresh authenticated lifecycle snapshot. For workflow-backed sessions, #589 adds a candidate ACL that re-reads the existing execution-scoped `NOEMA_WORKFLOW_STATE` owner before every guidance decision and suppresses advice when newer cancellation, terminal, or pre-start durable evidence exists. This is a conservative guidance projection, not a second Agent Runtime lifecycle store. | | Candidate comparison | Require exact base/candidate lineage, matching evaluation context, complete paired cases, disjoint train/holdout IDs and finite normalized scores. Reported candidate safety violations block eligibility regardless of mean gain. | | Independent acceptance | Arithmetic non-regression is not statistical significance, construct validity, standard setting or approval. Independent evaluation and final confirmation remain prerequisites. | | Data and secrets | No new credential, `.env` read, provider client, raw trajectory store or hidden-reasoning capture is introduced. Guidance text is still untrusted data; these modules do not detect prompt injection or scrub sensitive content. | @@ -80,19 +82,28 @@ Protected source subsequently integrated #585 as merge commit `ae525cdc4ecc28e6caf5e5a45809f568388b3f7f`. #586 exact head `5351723f4ce3c2d41d463555986ba49e6c8f0f20` had terminal-success application CI, reviewer CI, central Security Scan and patch-validator-image before normal merge. -These observations establish protected source integration; they do not establish an +Those observations establish protected source integration; they do not establish an immutable release, deployed runtime behavior, graph effectiveness or rollout authority. +#589 starts from the later protected documentation convergence and uses test-first +`8565dd32c2111b1f06a91f36bd2d7928834b97e2` to require a fresh durable read for +each workflow-backed guidance decision. The causal source reuses the private +execution-scoped Workflow / Task Execution `read` command rather than adding another +database or mutation endpoint. Its tests deliberately replay a first current response +that permits advice followed by a newer cancellation response and require the second +decision to suppress advice. Until #589 has unchanged exact-head hosted evidence and +normal protected integration, that source remains candidate truth. + ## Owner-led rollout and exit criteria | Stage | Responsible owner and concrete next delivery | Exit evidence | | --- | --- | --- | -| Source readiness | Noema: keep the integrated #585/#586 core and lifecycle adapter aligned with canonical Agent Runtime boundaries while repairing code-current documentation drift. | Protected merge ancestry plus unchanged exact-head typecheck, full tests/coverage, applicable security/image checks and review; source integration is complete but does not satisfy later rollout stages. | +| Source readiness | Noema: keep #585/#586 aligned and integrate the workflow-backed current-state ACL without crossing Workflow / Task or Agent Runtime ownership. | Protected ancestry plus unchanged exact-head typecheck, full tests/coverage, applicable security/image checks and review. | | Interchange release | context-graph-contracts #28: graph/context/evaluation/decision schema, digest semantics and hostile conformance fixtures. | Immutable released contract and compatible independent consumer fixtures. Local `noema.procedural-graph/v1` is not already that release. | | Ownership inventory | enterprise-architecture-core #50: task/profile owner, consumer port, contract pin, evaluation profile and rollback owner for each applicable product. | Evidence distinguishes proposed, source, released, shadow, canary, active and rollback-tested. Deterministic kernels may be not applicable with a recorded reason. | | First shadow connection | contextual-orchestrator #1116 plus .github and Naruon owners: connect guide/solver roles through the existing gateway without write-side activation. | Observed matched no-graph/fixed-graph/evolved-graph runs; task success, sequencing errors, duplicate effects, cost/tokens and latency reported separately. | | Independent evaluation | psychometrics-commons #447: task stimuli, item/rubric definitions, paired evidence protocol, validation-search and untouched final confirmation separation. | Authenticated producer and exact graph/model/tool/dataset/rubric/context binding; justified evidence size and uncertainty; independent acceptance. | -| Offline state integration | Noema State/Checkpoint and Policy/Approval: minimized observations, candidate storage, scoped rejection retention, approval, compare-and-swap promotion, rollback and revocation. | Crash/replay/stale-writer tests and authentic approval/evidence references; running sessions keep their pinned revision and obey current revocation. | +| Offline state integration | Noema State/Checkpoint and Policy/Approval: minimized observations, candidate storage, scoped rejection retention, approval, compare-and-swap promotion, rollback and revocation. Reuse existing execution/state authorities before adding persistence. | Crash/replay/stale-writer tests and authentic approval/evidence references; running sessions keep their pinned revision and obey current revocation. | | Product canary | Product owners: versioned adapter and domain-specific procedure/profile; no copied graph runtime. | Released contract conformance, observed invocation, domain regressions, independent side-effect controls and tested disable/rollback. | The first product scenarios are central review/finding verification and Naruon's @@ -113,6 +124,14 @@ is necessary for this source slice. ## Remaining gaps that block active adoption +The workflow-backed current-state ACL does not persist Agent Runtime lifecycle state +and does not make Workflow / Task Execution the lifecycle owner. It only prevents a +cached procedural `running` decision from surviving newer durable workflow evidence +that proves cancellation, terminal work, or pre-start state. Non-workflow executions +still need an authenticated current lifecycle source. The deployed Durable Object +read path also still needs real runtime compatibility/restart evidence and buyer-path +p95 measurement; source tests are not latency evidence. + There is no production graph/trajectory store, signed receipt verifier, automatic refiner, independently approved promotion API or product invocation in protected source. There is also no evidence yet that graph guidance improves CWL tasks or meets @@ -123,6 +142,12 @@ from source integration or the existence of tracking issues. ## References +Cloudflare. (2026). *Durable Object storage*. Cloudflare Developers. +https://developers.cloudflare.com/durable-objects/api/storage-api/ + +Cloudflare. (2026). *Invoke methods*. Cloudflare Developers. +https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/ + Lu, Y., Chen, Y., Wu, S., & Arık, S. Ö. (2026). *Procedural graphs: Self-evolving execution structures for LLM agents* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2609.09153 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 00553fa61..86bc93c1f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -4,11 +4,11 @@ 이 문서는 protected source, active candidate, transient workflow evidence와 foreign-owner authority를 분리한다. Open PR exact head, protected base, required workflow, review thread, release와 central dependency는 mutation·merge·release 직전에 다시 읽는다. predecessor GREEN, queued/pending/in-progress/skipped/cancelled run, 오래된 PR base snapshot과 scanner/model judgement는 다음 revision의 merge authority로 전용하지 않는다. queued는 GREEN이 아니다. -Current protected source는 mutation·merge·release 시점에 live protected `main`을 다시 조회해 결정한다. 이 문서 안의 exact source SHA는 dated observation 또는 protected history일 뿐 future merge 뒤 evergreen current authority로 사용하지 않는다. Dated protected observation for this repair는 `main@56550e5e6b0af81290800d7c5a0a4caa9040a39a`이며, 이 revision은 merged PR #582 exact `0f20a4dc78e423fd5df49e137a4eb286c7075ea4`의 exact stream-scoped SQLite storage observation primitive, merged #583의 live-authority/ADR-index repair, protected procedural graph source #585/#586, 그리고 merged #587의 source-vs-rollout doctoring repair를 포함한다. +Current protected source는 mutation·merge·release 시점에 live protected `main`을 다시 조회해 결정한다. 이 문서 안의 exact source SHA는 dated observation 또는 protected history일 뿐 future merge 뒤 evergreen current authority로 사용하지 않는다. Dated protected observation for this repair는 `main@77dd802f4aa79747054ced9ea75e2d8c54e61250`이며, 이 revision은 merged PR #582 exact `0f20a4dc78e423fd5df49e137a4eb286c7075ea4`의 exact stream-scoped SQLite storage observation primitive, merged #583의 live-authority/ADR-index repair, protected procedural graph source #585/#586, merged #587의 source-vs-rollout doctoring repair, 그리고 merged #588의 canonical documentation convergence를 포함한다. Moving central control-plane snapshot은 central `.github/main@7fd571dbcdbae6acf29d8f4ee704d7ba6297e4db`다. Noema runtime의 reviewed immutable central consumer pin은 `c9052e607e5f3cc76e73207e7786b21500721b79`이고 runtime authority 표현은 `ALLOWED_WORKFLOW_SHA = c9052e607e5f3cc76e73207e7786b21500721b79`다. Moving foreign head와 reviewed immutable pin을 같은 권위로 취급하지 않으며 central moving head가 전진했다고 consumer pin을 자동 승격하지 않는다. -Protected history에는 merged PR #535 exact `82b20b293f0a5f0ac0e69857c1b61dddfe478491`, merged PR #540 exact `05bc2d47c3899ebe17538070f9a30172f90307ac`, merged PR #542 exact `ca839298fcaeec409091dc909789b6f87eb67fdc`, merged PR #547 exact `30b7e7e5cdab8de65715834a16f994b2047eafa6`, merged PR #550 exact `f2ec2dc6709814070cc3e3d6932ce280aee966db`, merged PR #553 exact `3bd9f543e97ce856f78b1c608141436298ce9e74`, merged PR #558 exact `2f91bf8641212ecae435b5fbcc9084cc0acd6295`, merged PR #556 exact `860714cba46dba06260a5dce09d0e9152fcb0a8c`, merged PR #560 exact `5aab7c098f3478069127f34e398326415ec599a4`와 이후 external-extension lifecycle/operability source integrations #574, #577, #578, #579, #580, #581, #582, documentation-authority integration #583, procedural graph source integrations #585/#586, doctoring integration #587이 포함돼 있다. 이 식별자는 역사 증거이지 open-candidate authority가 아니다. +Protected history에는 merged PR #535 exact `82b20b293f0a5f0ac0e69857c1b61dddfe478491`, merged PR #540 exact `05bc2d47c3899ebe17538070f9a30172f90307ac`, merged PR #542 exact `ca839298fcaeec409091dc909789b6f87eb67fdc`, merged PR #547 exact `30b7e7e5cdab8de65715834a16f994b2047eafa6`, merged PR #550 exact `f2ec2dc6709814070cc3e3d6932ce280aee966db`, merged PR #553 exact `3bd9f543e97ce856f78b1c608141436298ce9e74`, merged PR #558 exact `2f91bf8641212ecae435b5fbcc9084cc0acd6295`, merged PR #556 exact `860714cba46dba06260a5dce09d0e9152fcb0a8c`, merged PR #560 exact `5aab7c098f3478069127f34e398326415ec599a4`와 이후 external-extension lifecycle/operability source integrations #574, #577, #578, #579, #580, #581, #582, documentation-authority integration #583, procedural graph source integrations #585/#586, doctoring integration #587, canonical documentation integration #588이 포함돼 있다. 이 식별자는 역사 증거이지 open-candidate authority가 아니다. 이 baseline과 executable documentation-authority test는 active documentation-authority lane 하나만 write한다. mutation 직전 open PR/Issue/branch를 fresh-read해 writer를 결정하며 merged/closed historical PR 번호를 active sole writer로 고정하지 않는다. 다른 feature lane의 과거 baseline blob은 ordinary/non-force semantic convergence 때 current authority로 승계하지 않는다. @@ -18,7 +18,7 @@ Noema Core Domain은 Agent Runtime과 Workflow / Task Execution이다. Tool / Ca `contextual-orchestrator`는 provider/model discovery, routing, retry/failover, test-time compute와 provider credential을 소유한다. Noema는 released gateway contract와 canonical `orchestrator/free` alias를 소비하며 direct provider SDK, provider key, provider/model/group fallback policy를 소유하지 않는다. `.github`는 organization reusable workflow/control-plane source다. Keyverse는 identity backend다. `quarantine-sandbox-runtime`, Wardnet, EgressWeave, AppGuardrail은 각자의 isolation/security/outbound/scanning truth를 소유한다. Noema는 그 owner evidence를 reference/pin으로 소비할 뿐 foreign implementation이나 domain table을 복제하지 않는다. Cross-service SQL과 mutable sibling PR dependency는 금지한다. -Baseline의 요구·설계·데이터·경계 authority는 `docs/PRD.md`, `docs/TRD.md`, `docs/UML.md`, `docs/ERD.md`, `docs/CONTEXT_MAP.md`다. 이 register는 그 문서와 ADR을 대체하지 않고 current Gap/Action/Status를 exact source·PR·workflow evidence에 결합한다. ADR 0015는 protected source에 포함됐지만 상태는 `Proposed`이며 production activation-authority integration, deployed lifecycle operability/recovery evidence, immutable shared-contract consumption, live pilot와 release evidence가 남아 있다. ADR 0017도 `Proposed`다. Protected #585/#586은 bounded Noema Agent Runtime advisory graph/session mechanics, deterministic direct-child screening, caller-supplied fresh authenticated lifecycle snapshot에 대한 running-only projection을 구현했지만 released wire contracts, authenticated evaluator receipts, durable history/current-lifecycle revocation, Policy / Approval CAS, canary/rollback과 product-outcome authority는 여전히 별도다. +Baseline의 요구·설계·데이터·경계 authority는 `docs/PRD.md`, `docs/TRD.md`, `docs/UML.md`, `docs/ERD.md`, `docs/CONTEXT_MAP.md`다. 이 register는 그 문서와 ADR을 대체하지 않고 current Gap/Action/Status를 exact source·PR·workflow evidence에 결합한다. ADR 0015는 protected source에 포함됐지만 상태는 `Proposed`이며 production activation-authority integration, deployed lifecycle operability/recovery evidence, immutable shared-contract consumption, live pilot와 release evidence가 남아 있다. ADR 0017도 `Proposed`다. Protected #585/#586은 bounded Noema Agent Runtime advisory graph/session mechanics, deterministic direct-child screening, caller-supplied fresh authenticated lifecycle snapshot에 대한 running-only projection을 구현했다. Active candidate #589는 workflow-backed execution에 한해 기존 execution-scoped Workflow / Task Execution Durable Object를 매 guidance decision 전에 다시 읽고, locally admitted session과 re-admitted plan의 execution identity를 durable lookup 전에 검증하는 current-state ACL을 추가한다. 이 candidate는 두 번째 lifecycle store나 mutation/approval authority가 아니며, non-workflow lifecycle freshness, released wire contracts, authenticated evaluator receipts, durable graph/rejection history, Policy / Approval CAS, deployed Durable Object p95/recovery, canary/rollback과 product-outcome authority는 여전히 별도다. ## Integrated exact-claim evidence — issue #555 / merged PR #556 @@ -42,7 +42,7 @@ Unbound core-receipt RED `5a50a9bcfe12f3938b30e4a3cb15af8d30134391`는 internal Invocation-authority substitution RED `abcd1fea4b28b826826fed6b23296b59ceda98ca`는 structurally compatible caller-supplied authority가 admission-bound live authority를 대체할 수 있음을 증명했다. Production `f8814b8fd7b66f40335df85c6aadab12aa760bc1`는 invocation에 admission 당시 결합한 동일 authority instance를 요구한다. `1acbf2f464cbfb0c5ad6e3991ecc9b04f17fb895`는 same-authority catalog/scan drift를 유지했고 `feed68db0ac0404607a292ed2686bf47e5e2be22`는 운영/rollback owner 경계를 갱신했다. Hosted application CI `34230994573`, job `102076920357`이 4,190 passed / 10 failed로 드러낸 stale fixture는 `532cfaadf655d3158434db8a1c3a985a33ad3a9f`, `ab2baeda9665df96753a03f1242455efe0662e41`, `273aa711d1c7611fadab9346944891548b30919a`에서 same-authority intent를 유지하면서 mutable test cache를 제거했다. -Exact-admission provenance RED `0a32ee0a88378931a07b7e3b61cc31e3b494a7ab`는 source admission A에서 발행한 authentic activation이 동일 trusted authority가 source B로 이동한 뒤에도 artifact/product/role/policy/time이 맞으면 B를 authorize할 수 있던 결함을 재현했다. Hosted application CI `34235691056`, job `102092675348`은 exact checkout/live-base/lockfile/install/typecheck 뒤 release tests에서 실패했다. Test-only `49115306b4abf7656f7fd136de8a3ff0c5a1968b`은 retained activation replay와 public invocation-receipt replay까지 확장했다. Canonical production `225a04ec2c833e9068e79ad7b70f3f5d8f6d934a`는 core의 activation/receipt provenance를 process-global set에서 exact `AdmittedExternalExtension`-bound map으로 옮겼고, `2e843825cc31a316a5834db3d355e8e4a18ca028`은 public wrapper에 잠시 중복됐던 binding kernel을 제거해 exact-admission activation/receipt authority를 core 한 곳에 남겼다. 같은 conformance invariant는 `context-graph-contracts#27`에 foreign-owner requirement로 넘겼고 mutable issue/branch를 Noema runtime dependency로 소비하지 않는다. +Exact-admission provenance RED `0a32ee0a88378931a07b7e3b61cc31e3b494a7ab`는 source admission A에서 발행한 authentic activation이 동일 trusted authority가 source B로 이동한 뒤에도 artifact/product/role/policy/time이 맞으면 B를 authorize할 수 있던 결함을 재현했다. Hosted application CI `34235691056`, job `102092675348`은 exact checkout/live-base/lockfile control, install/release typecheck 뒤 release tests에서 실패했다. Test-only `49115306b4abf7656f7fd136de8a3ff0c5a1968b`은 retained activation replay와 public invocation-receipt replay까지 확장했다. Canonical production `225a04ec2c833e9068e79ad7b70f3f5d8f6d934a`는 core의 activation/receipt provenance를 process-global set에서 exact `AdmittedExternalExtension`-bound map으로 옮겼고, `2e843825cc31a316a5834db3d355e8e4a18ca028`은 public wrapper에 잠시 중복됐던 binding kernel을 제거해 exact-admission activation/receipt authority를 core 한 곳에 남겼다. 같은 conformance invariant는 `context-graph-contracts#27`에 foreign-owner requirement로 넘겼고 mutable issue/branch를 Noema runtime dependency로 소비하지 않는다. Plaintext replay-retention RED `3f41d94af1454926b65b1801004fe245098742b0`는 public replay WeakMap이 reversible `JSON.stringify(normalizedRequest)`를 receipt lifetime 동안 보유해 instruction, observed content, rejected secret/product/hidden-reasoning inputs의 수명을 불필요하게 늘리던 결함을 고정했다. Hosted application CI `34237704683`, job `102099615873`은 exact checkout/live-base/lockfile control, install/release typecheck 뒤 release tests에서 실패했다. Production `79182c7be196c42fb94450cae9a7857ae67b5434`는 retained replay identity를 versioned/domain-separated SHA-256 digest로 바꾸고 every semantic field, key-order independence, fixed-width/no-plaintext regression을 추가했다. ADR `ac6b6c088f034a8778bdc8a859f7157223883b8d`는 process-local WeakMap lifetime, restart fail-closed, explicit digest-version migration과 FIPS 180-4 authority를 기록하며 `Proposed`를 유지한다. @@ -70,6 +70,12 @@ Noema does not absorb downstream owners to complete this lane. Released procedur Source integration is complete for #585/#586. ADR 0017 remains `Proposed` until immutable released contracts, authenticated evaluation receipts, durable graph/rejection history plus current-lifecycle/revocation authority, Policy / Approval CAS, canary/rollback evidence and product-owner production outcome evidence exist. Canonical documentation must describe that separation without treating historical PR numbers as moving current authority. +## Active workflow-backed procedural current-state candidate — PR #589 + +Draft #589 adds a read-only Agent Runtime ACL over the existing execution-scoped Workflow / Task Execution Durable Object. Every workflow-backed guidance decision re-admits the plan, verifies that the locally admitted procedural session has the same canonical execution identity before any Durable Object lookup, then reads current workflow state through the existing private `read` command. Exact execution/plan identity, complete unique task identities, allowed task states, cancellation identity and transition sequence are validated before a conservative lifecycle projection reaches the protected running-only procedural gate. Current cancellation, terminal work and pre-start evidence suppress guidance. Cross-execution mismatch is rejected without reading the foreign execution object. + +This candidate does not create a second lifecycle database, mutate Workflow / Task Execution state, grant retry/tool/Policy / Approval authority, or turn graph/evaluation data into activation truth. The source-level ACL also does not prove deployed Durable Object compatibility, restart/failure behavior, synchronous p95, non-workflow lifecycle freshness, immutable release, graph persistence/promotion, canary/rollback or product outcome. Those remain later acceptance classes under issue #584. + ## Evidence and merge rules Review resolution, CI, reviewer-ci, required Security, image/SBOM/provenance, branch ancestry, release는 separate evidence classes다. Every source mutation/restack invalidates predecessor workflow evidence. `queued`, `pending`, `in_progress`, `skipped`, `cancelled`, stale 또는 absent-required evidence는 passing이 아니다. @@ -86,7 +92,7 @@ PR 0은 useful work를 닫아 제조하지 않는다. Open lane은 normal merge | --- | --- | --- | --- | --- | --- | --- | | P0 | Exact-claim evidence supply chain | Tool/research claim이 producer evidence 없이 reviewer authority가 될 위험 | protected #556 + release/consumer lanes | Source integrated; producer/release/consumer open | protected source + execution/research producer + immutable Noema release + released central consumer corpus RED→GREEN | release/producer evidence를 별도 lane에서 완성 | | P0 | External extension lifecycle evidence | third-party extension의 stale/revoked authority, process-local lifecycle, forged operability 또는 unbound storage/recovery evidence가 runtime truth로 오인될 위험 | protected #574/#577/#578/#579/#580/#581/#582 + issue #561 | Lifecycle/SQLite DO source integrated; production activation adapter + deployed operability/recovery/release evidence open | immutable source + Noema Policy/Approval + immutable foreign-owner refs + append-only lifecycle/CAS/restart + exact deployed p95/contention/storage/recovery + deployment/release provenance | fail-closed `active`를 유지하고 immutable owner-issued refs가 준비된 뒤 production adapter를 연결한 후 actual deployed #561 acceptance를 실행 | -| P0 | Procedural graph advisory activation boundary | deterministic local graph/session/screening 결과나 caller-cached lifecycle snapshot이 signed contract, durable current-state approval 또는 product authority처럼 소비될 위험 | protected #585/#586 + issue #584 | Source integrated; canonical docs convergence in review; activation intentionally unavailable | protected exact source + released `context-graph-contracts` contract + authenticated paired evaluation receipts + explicit Policy/Approval + durable history/current-state revocation CAS + canary/rollback + product-owner outcome evidence | canonical docs를 protected source와 수렴한 뒤 authenticated evaluation + durable lifecycle/revocation + Policy/Approval CAS를 owner-approved successor로 진행 | +| P0 | Procedural graph advisory activation boundary | deterministic local graph/session/screening 결과나 caller-cached lifecycle snapshot이 signed contract, durable current-state approval 또는 product authority처럼 소비될 위험 | protected #585/#586 + Draft #589 + issue #584 | Core/pure adapter protected; workflow-backed current-state ACL in review; activation intentionally unavailable | protected exact source + released `context-graph-contracts` contract + authenticated paired evaluation receipts + explicit Policy/Approval + durable graph/rejection history/promotion CAS + deployed current-state evidence + canary/rollback + product-owner outcome evidence | #589 exact-head repair/docs/checks를 완료해 정상 통합한 뒤 authenticated evaluation + durable graph/rejection history + Policy/Approval CAS를 별도 owner-approved successor로 진행 | | P0 | Protected-main governance closure | required Security workflow만으로 PR/review/history/deletion/bypass 통제를 증명했다고 오인할 위험 | issue #27 | Open; external control evidence absent | fresh live ruleset + required PR/review/conversation/history/deletion controls + independent bypass/break-glass evidence + protected-source governance receipt | source가 만들 수 없는 organization/admin control은 issue #27에서 독립 검증 | | P0 | Strict orchestrator/free consumer release | source 통합만으로 immutable consumer activation을 증명할 수 없음 | merged #535 + release lane | Source integrated; release/consumer open | version/tag/package/SBOM/provenance/reproducibility/rollback + released consumer | release-ready protected head에서만 publication | | P0 | Patch-validator operational publication | source/image CI success만으로 reusable immutable runtime을 증명할 수 없음 | issue #66 | Open; publication evidence absent | protected execution + immutable image/signature/SBOM/provenance/reproducibility/rollback | protected-main operational image receipt 뒤 immutable publication/signing/attestation/activation을 별도 증거로 완성 | diff --git a/src/agent-runtime/procedural-current-lifecycle.ts b/src/agent-runtime/procedural-current-lifecycle.ts new file mode 100644 index 000000000..3390a577e --- /dev/null +++ b/src/agent-runtime/procedural-current-lifecycle.ts @@ -0,0 +1,205 @@ +import { guideProceduralExecution, type ProceduralExecutionGuidance } from "./procedural-execution"; +import type { ExecutionLifecycle, ExecutionState } from "./execution-lifecycle"; +import type { ProceduralSession } from "./procedural-graph"; +import { + routeWorkflowStateCommand, + type WorkflowStateDurableObjectEnv, +} from "../workflow-task-execution/workflow-state-durable-object"; +import { + admitWorkflowTaskPlan, + type AdmittedWorkflowTaskPlan, + type WorkflowTaskPlan, +} from "../workflow-task-execution/task-plan"; + +const CURRENT_WORKFLOW_TASK_STATES: ReadonlySet = new Set([ + "pending", + "running", + "succeeded", + "failed", + "cancelled", + "blocked", +]); +const TERMINAL_WORKFLOW_TASK_STATES: ReadonlySet = new Set([ + "succeeded", + "failed", + "cancelled", + "blocked", +]); +const AUTHORITY_ID_PATTERN = /^[\x21-\x7e]{1,128}$/u; + +type CurrentWorkflowTaskState = "pending" | "running" | "succeeded" | "failed" | "cancelled" | "blocked"; + +type CurrentWorkflowEvidence = { + readonly executionId: string; + readonly cancellationRequested: boolean; + readonly taskStates: readonly CurrentWorkflowTaskState[]; + readonly transitionSequence: number; +}; + +/** + * Stable diagnostic codes for failures while obtaining current Workflow / Task Execution evidence; + * none of these codes grants retry, lifecycle transition, Policy / Approval, or tool authority. + */ +export type ProceduralCurrentLifecycleErrorCode = + | "workflow_state_conflict" + | "workflow_state_unavailable" + | "invalid_workflow_state_response"; + +/** + * Fail-closed error raised when the canonical durable Workflow / Task Execution owner cannot provide + * trustworthy current evidence for the procedural-guidance ACL. This diagnostic is not authority. + */ +export class ProceduralCurrentLifecycleError extends Error { + readonly code: ProceduralCurrentLifecycleErrorCode; + + constructor(code: ProceduralCurrentLifecycleErrorCode) { + super(code); + this.name = "ProceduralCurrentLifecycleError"; + this.code = code; + } +} + +function rejectCurrentLifecycle(code: ProceduralCurrentLifecycleErrorCode): never { + throw new ProceduralCurrentLifecycleError(code); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function currentTaskState(value: unknown): CurrentWorkflowTaskState { + if (typeof value !== "string" || !CURRENT_WORKFLOW_TASK_STATES.has(value as CurrentWorkflowTaskState)) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + return value as CurrentWorkflowTaskState; +} + +function currentWorkflowEvidence( + value: unknown, + plan: AdmittedWorkflowTaskPlan, +): CurrentWorkflowEvidence { + if (!isRecord(value) || value.executionId !== plan.executionId || value.planId !== plan.planId) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + if (!Number.isSafeInteger(value.transitionSequence) || (value.transitionSequence as number) < 0) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + if (!isRecord(value.cancellation) || typeof value.cancellation.requested !== "boolean") { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + const cancellationId = value.cancellation.cancellationId; + if ( + (value.cancellation.requested && (typeof cancellationId !== "string" || !AUTHORITY_ID_PATTERN.test(cancellationId))) + || (!value.cancellation.requested && cancellationId !== null) + ) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + if (!Array.isArray(value.tasks) || value.tasks.length !== plan.tasks.length) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + + const expectedTaskIds = new Set(plan.tasks.map(({ taskId }) => taskId)); + const observedTaskIds = new Set(); + const taskStates: CurrentWorkflowTaskState[] = []; + for (const task of value.tasks) { + if (!isRecord(task) || typeof task.taskId !== "string") { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + if (!expectedTaskIds.has(task.taskId) || observedTaskIds.has(task.taskId)) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + observedTaskIds.add(task.taskId); + taskStates.push(currentTaskState(task.state)); + } + + return Object.freeze({ + executionId: plan.executionId, + cancellationRequested: value.cancellation.requested, + taskStates: Object.freeze(taskStates), + transitionSequence: value.transitionSequence as number, + }); +} + +async function readCurrentWorkflowEvidence( + env: WorkflowStateDurableObjectEnv, + plan: AdmittedWorkflowTaskPlan, +): Promise { + let response: Response; + try { + response = await routeWorkflowStateCommand(env, { operation: "read", plan }); + } catch { + return rejectCurrentLifecycle("workflow_state_unavailable"); + } + + if (response.status === 409) return rejectCurrentLifecycle("workflow_state_conflict"); + if (response.status === 503) return rejectCurrentLifecycle("workflow_state_unavailable"); + if (response.status !== 200) return rejectCurrentLifecycle("invalid_workflow_state_response"); + + let body: unknown; + try { + body = await response.json(); + } catch { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + if (!isRecord(body) || body.ok !== true) { + return rejectCurrentLifecycle("invalid_workflow_state_response"); + } + return currentWorkflowEvidence(body.data, plan); +} + +function proceduralGateState(evidence: CurrentWorkflowEvidence): ExecutionState { + if (evidence.cancellationRequested) return "cancellation_requested"; + + if (evidence.taskStates.every((state) => TERMINAL_WORKFLOW_TASK_STATES.has(state))) { + if (evidence.taskStates.some((state) => state === "failed" || state === "blocked")) return "failed"; + if (evidence.taskStates.some((state) => state === "cancelled")) return "cancelled"; + return "succeeded"; + } + + if (evidence.transitionSequence <= 1 && evidence.taskStates.every((state) => state === "pending")) { + return "accepted"; + } + return "running"; +} + +/** + * Re-reads Noema's canonical execution-scoped Workflow / Task Execution Durable Object before each + * procedural-guidance decision, then projects only the minimum conservative lifecycle state needed + * to suppress stale advice. The locally admitted procedural session is first checked against the + * re-admitted plan execution identity, before any Durable Object lookup, so a cross-execution caller + * cannot use this ACL to read another execution's workflow-state authority. The projection is an + * Agent Runtime ACL input, not a second lifecycle store: cancellation and terminal durable task + * evidence can remove guidance authority, while this function cannot create task claims, lifecycle + * transitions, retries, approvals, tools, or product truth. A successful source-level read is still + * not production latency evidence; any synchronous buyer/runtime use must separately measure the + * deployed Durable Object path against its p95 target. + * + * @param env Existing Noema workflow-state Durable Object binding that owns current task authority. + * @param plan Untrusted workflow plan re-admitted and bound to the execution-scoped durable owner. + * @param session Locally admitted execution-pinned procedural graph session used only for advisory context. + * @param request Bounded procedural neighborhood request, read only when fresh evidence projects running work. + * @returns Frozen procedural guidance produced by the existing Agent Runtime gate from fresh durable evidence. + */ +export async function guideProceduralExecutionFromCurrentWorkflowState( + env: WorkflowStateDurableObjectEnv, + plan: WorkflowTaskPlan, + session: ProceduralSession, + request: unknown, +): Promise { + const admittedPlan = admitWorkflowTaskPlan(plan); + + // Validate the local Agent Runtime session/execution binding before selecting or reading a + // Workflow / Task Execution Durable Object. The accepted state guarantees `request` is not read. + guideProceduralExecution( + Object.freeze({ executionId: admittedPlan.executionId, state: "accepted" }), + session, + null, + ); + + const evidence = await readCurrentWorkflowEvidence(env, admittedPlan); + const lifecycle: ExecutionLifecycle = Object.freeze({ + executionId: evidence.executionId, + state: proceduralGateState(evidence), + }); + return guideProceduralExecution(lifecycle, session, request); +} \ No newline at end of file diff --git a/test/procedural-current-lifecycle-fail-closed.test.ts b/test/procedural-current-lifecycle-fail-closed.test.ts new file mode 100644 index 000000000..219395b66 --- /dev/null +++ b/test/procedural-current-lifecycle-fail-closed.test.ts @@ -0,0 +1,223 @@ +import { describe, expect, it } from "vitest"; + +import { createProceduralGraph, startProceduralSession } from "../src/agent-runtime/procedural-graph"; +import { + guideProceduralExecutionFromCurrentWorkflowState, + type ProceduralCurrentLifecycleErrorCode, +} from "../src/agent-runtime/procedural-current-lifecycle"; +import type { WorkflowStateDurableObjectEnv } from "../src/workflow-task-execution/workflow-state-durable-object"; +import type { WorkflowTaskPlan } from "../src/workflow-task-execution/task-plan"; + +type TaskState = "pending" | "running" | "succeeded" | "failed" | "cancelled" | "blocked"; + +type ResponseStep = Response | Error; + +const executionId = "run-current-lifecycle-fail-closed"; + +function plan(): WorkflowTaskPlan { + return { + executionId, + planId: "plan-current-lifecycle-fail-closed", + maxConcurrency: 1, + tasks: [ + { taskId: "review", dependsOn: [], effect: "pure" }, + { taskId: "verify", dependsOn: ["review"], effect: "pure" }, + ], + }; +} + +function snapshot(states: readonly TaskState[], transitionSequence = 2): Record { + return { + executionId, + planId: plan().planId, + policy: { + policyVersion: "workflow-execution-policy.v1", + schedulingPolicy: "admission_order", + maxAutomaticRecoveryAttempts: 3, + }, + cancellation: { requested: false, cancellationId: null }, + checkpoint: { executionId, sequence: 0, stateDigest: "a".repeat(64) }, + tasks: states.map((state, index) => ({ + taskId: plan().tasks[index]!.taskId, + state, + attempt: state === "pending" ? 0 : 1, + activeClaimId: state === "running" ? `claim-${index}` : null, + effectStarted: null, + })), + transitionSequence, + transitionReceipts: [], + }; +} + +function ok(data: unknown): Response { + return new Response(JSON.stringify({ ok: true, data }), { status: 200 }); +} + +class WorkflowNamespace { + private readonly steps: ResponseStep[]; + + constructor(steps: readonly ResponseStep[]) { + this.steps = [...steps]; + } + + idFromName(name: string): DurableObjectId { + return { name, toString: () => name } as unknown as DurableObjectId; + } + + get(_id: DurableObjectId): DurableObjectStub { + return { + fetch: async () => { + const step = this.steps.shift(); + if (step === undefined) throw new Error("no response configured"); + if (step instanceof Error) throw step; + return step.clone(); + }, + } as unknown as DurableObjectStub; + } +} + +function env(...steps: ResponseStep[]): WorkflowStateDurableObjectEnv { + return { + NOEMA_WORKFLOW_STATE: new WorkflowNamespace(steps) as unknown as DurableObjectNamespace, + }; +} + +async function proceduralSession() { + const graph = await createProceduralGraph({ + schemaVersion: "noema.procedural-graph/v1", + tenantId: "tenant-a", + taskType: "pr-repair", + graphId: "current-lifecycle-fail-closed", + revision: 1, + parentDigest: null, + nodes: ["Start", "review"], + edges: [{ + from: "Start", + relation: "leads_to", + to: "review", + condition: "", + guidance: "Use only current durable evidence", + pitfalls: "Do not trust cached lifecycle state", + }], + }); + return startProceduralSession(graph, { + tenantId: "tenant-a", + taskType: "pr-repair", + executionId, + graphDigest: graph.digest, + }); +} + +async function expectFailure(step: ResponseStep, code: ProceduralCurrentLifecycleErrorCode): Promise { + const session = await proceduralSession(); + await expect(guideProceduralExecutionFromCurrentWorkflowState( + env(step), + plan(), + session, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + )).rejects.toMatchObject({ name: "ProceduralCurrentLifecycleError", code }); +} + +describe("procedural current lifecycle fail-closed evidence validation", () => { + it("classifies durable owner conflict, outage, transport failure, and unexpected status separately", async () => { + await expectFailure( + new Response(JSON.stringify({ ok: false, error: "conflict" }), { status: 409 }), + "workflow_state_conflict", + ); + await expectFailure( + new Response(JSON.stringify({ ok: false, error: "storage_unavailable" }), { status: 503 }), + "workflow_state_unavailable", + ); + await expectFailure(new Error("binding unavailable"), "workflow_state_unavailable"); + await expectFailure( + new Response(JSON.stringify({ ok: false, error: "invalid_request" }), { status: 400 }), + "invalid_workflow_state_response", + ); + }); + + it("rejects malformed success envelopes before they can become lifecycle evidence", async () => { + await expectFailure(new Response("{", { status: 200 }), "invalid_workflow_state_response"); + await expectFailure(ok(null), "invalid_workflow_state_response"); + await expectFailure( + new Response(JSON.stringify({ ok: false, data: snapshot(["running", "pending"]) }), { status: 200 }), + "invalid_workflow_state_response", + ); + }); + + it.each([ + ["cross execution", { ...snapshot(["running", "pending"]), executionId: "other-run" }], + ["cross plan", { ...snapshot(["running", "pending"]), planId: "other-plan" }], + ["noninteger transition sequence", { ...snapshot(["running", "pending"]), transitionSequence: "2" }], + ["negative transition sequence", { ...snapshot(["running", "pending"]), transitionSequence: -1 }], + ["missing cancellation", { ...snapshot(["running", "pending"]), cancellation: null }], + ["nonboolean cancellation", { ...snapshot(["running", "pending"]), cancellation: { requested: "no", cancellationId: null } }], + ["missing cancellation identity", { ...snapshot(["running", "pending"]), cancellation: { requested: true, cancellationId: null } }], + ["noncanonical cancellation identity", { ...snapshot(["running", "pending"]), cancellation: { requested: true, cancellationId: "bad id" } }], + ["unexpected cancellation identity", { ...snapshot(["running", "pending"]), cancellation: { requested: false, cancellationId: "cancel-unused" } }], + ["nonarray tasks", { ...snapshot(["running", "pending"]), tasks: null }], + ["wrong task count", { ...snapshot(["running", "pending"]), tasks: [snapshot(["running", "pending"]).tasks as unknown] }], + ])("rejects malformed top-level current evidence: %s", async (_label, data) => { + await expectFailure(ok(data), "invalid_workflow_state_response"); + }); + + it.each([ + ["nonrecord task", [null, { taskId: "verify", state: "pending" }]], + ["nonstring task identity", [{ taskId: 7, state: "running" }, { taskId: "verify", state: "pending" }]], + ["foreign task", [{ taskId: "foreign", state: "running" }, { taskId: "verify", state: "pending" }]], + ["duplicate task", [{ taskId: "review", state: "running" }, { taskId: "review", state: "pending" }]], + ["nonstring state", [{ taskId: "review", state: 7 }, { taskId: "verify", state: "pending" }]], + ["unknown state", [{ taskId: "review", state: "invented" }, { taskId: "verify", state: "pending" }]], + ])("rejects malformed task-state evidence: %s", async (_label, tasks) => { + await expectFailure(ok({ ...snapshot(["running", "pending"]), tasks }), "invalid_workflow_state_response"); + }); + + it.each([ + [["failed", "blocked"] as const, "failed"], + [["blocked", "cancelled"] as const, "failed"], + [["succeeded", "cancelled"] as const, "cancelled"], + [["succeeded", "succeeded"] as const, "succeeded"], + ])("maps terminal workflow evidence %j only to a suppressing Agent Runtime ACL projection", async (states, expectedState) => { + const session = await proceduralSession(); + const result = await guideProceduralExecutionFromCurrentWorkflowState( + env(ok(snapshot(states, 8))), + plan(), + session, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(result.available).toBe(false); + expect(result.reason).toBe("terminal_execution"); + expect(result.lifecycleState).toBe(expectedState); + }); + + it("treats all-pending state as pre-start only before durable execution evidence advances", async () => { + const session = await proceduralSession(); + const preStart = await guideProceduralExecutionFromCurrentWorkflowState( + env(ok(snapshot(["pending", "pending"], 0))), + plan(), + session, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(preStart.reason).toBe("execution_not_started"); + + const recovered = await guideProceduralExecutionFromCurrentWorkflowState( + env(ok(snapshot(["pending", "pending"], 2))), + plan(), + session, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(recovered.reason).toBe("running_execution"); + expect(recovered.available).toBe(true); + }); + + it("keeps mixed current work running even when the first durable transition count is one", async () => { + const session = await proceduralSession(); + const result = await guideProceduralExecutionFromCurrentWorkflowState( + env(ok(snapshot(["running", "pending"], 1))), + plan(), + session, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(result.available).toBe(true); + expect(result.lifecycleState).toBe("running"); + }); +}); diff --git a/test/procedural-current-lifecycle.test.ts b/test/procedural-current-lifecycle.test.ts new file mode 100644 index 000000000..56ed33e60 --- /dev/null +++ b/test/procedural-current-lifecycle.test.ts @@ -0,0 +1,228 @@ +import { describe, expect, it } from "vitest"; + +import { createProceduralGraph, startProceduralSession } from "../src/agent-runtime/procedural-graph"; +import { + ProceduralCurrentLifecycleError, + guideProceduralExecutionFromCurrentWorkflowState, +} from "../src/agent-runtime/procedural-current-lifecycle"; +import type { WorkflowStateDurableObjectEnv } from "../src/workflow-task-execution/workflow-state-durable-object"; +import type { WorkflowTaskPlan } from "../src/workflow-task-execution/task-plan"; + +const digest = (character: string): string => character.repeat(64); + +type TaskState = "pending" | "running" | "succeeded" | "failed" | "cancelled" | "blocked"; + +function plan(executionId = "run-current-lifecycle-001"): WorkflowTaskPlan { + return { + executionId, + planId: "plan-current-lifecycle-001", + maxConcurrency: 1, + tasks: [ + { taskId: "review", dependsOn: [], effect: "pure" }, + { taskId: "verify", dependsOn: ["review"], effect: "pure" }, + ], + }; +} + +function snapshot( + states: readonly TaskState[], + options: { cancellation?: boolean; transitionSequence?: number } = {}, +): Record { + const candidatePlan = plan(); + return { + executionId: candidatePlan.executionId, + planId: candidatePlan.planId, + policy: { + policyVersion: "workflow-execution-policy.v1", + schedulingPolicy: "admission_order", + maxAutomaticRecoveryAttempts: 3, + }, + cancellation: options.cancellation + ? { requested: true, cancellationId: "cancel-current-lifecycle-001" } + : { requested: false, cancellationId: null }, + checkpoint: { + executionId: candidatePlan.executionId, + sequence: 0, + stateDigest: digest("a"), + }, + tasks: states.map((state, index) => ({ + taskId: candidatePlan.tasks[index]!.taskId, + state, + attempt: state === "pending" ? 0 : 1, + activeClaimId: state === "running" ? `claim-${index + 1}` : null, + effectStarted: state === "running" ? false : null, + })), + transitionSequence: options.transitionSequence ?? 1, + transitionReceipts: [], + }; +} + +function ok(data: Record): Response { + return new Response(JSON.stringify({ ok: true, data }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +class SequencedWorkflowNamespace { + readonly requests: Record[] = []; + private readonly responses: (() => Response)[]; + + constructor(responses: readonly Response[]) { + this.responses = responses.map((response) => () => response.clone()); + } + + idFromName(name: string): DurableObjectId { + return { name, toString: () => name } as unknown as DurableObjectId; + } + + get(_id: DurableObjectId): DurableObjectStub { + return { + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init); + this.requests.push(await request.clone().json() as Record); + const next = this.responses.shift(); + if (next === undefined) throw new Error("unexpected workflow-state read"); + return next(); + }, + } as unknown as DurableObjectStub; + } +} + +async function session(executionId = plan().executionId) { + const graph = await createProceduralGraph({ + schemaVersion: "noema.procedural-graph/v1", + tenantId: "tenant-a", + taskType: "pr-repair", + graphId: "current-lifecycle", + revision: 1, + parentDigest: null, + nodes: ["Start", "review"], + edges: [{ + from: "Start", + relation: "leads_to", + to: "review", + condition: "", + guidance: "Review fresh execution evidence", + pitfalls: "Do not replay stale running state", + }], + }); + return startProceduralSession(graph, { + tenantId: "tenant-a", + taskType: "pr-repair", + executionId, + graphDigest: graph.digest, + }); +} + +function runtimeEnv(namespace: SequencedWorkflowNamespace): WorkflowStateDurableObjectEnv { + return { NOEMA_WORKFLOW_STATE: namespace as unknown as DurableObjectNamespace }; +} + +describe("procedural guidance current durable lifecycle ACL", () => { + it("re-reads durable workflow authority so a stale running decision cannot survive cancellation", async () => { + const namespace = new SequencedWorkflowNamespace([ + ok(snapshot(["succeeded", "pending"], { transitionSequence: 4 })), + ok(snapshot(["succeeded", "cancelled"], { cancellation: true, transitionSequence: 6 })), + ]); + const proceduralSession = await session(); + const request = { lastProcedure: null, hops: 1, maxEdges: 4 }; + + const running = await guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), + plan(), + proceduralSession, + request, + ); + expect(running.available).toBe(true); + expect(running.reason).toBe("running_execution"); + + const cancelled = await guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), + plan(), + proceduralSession, + request, + ); + expect(cancelled.available).toBe(false); + expect(cancelled.reason).toBe("cancellation_requested"); + expect(namespace.requests).toHaveLength(2); + expect(namespace.requests.every((command) => command.operation === "read")).toBe(true); + }); + + it("suppresses guidance before any durable workflow execution transition and after terminal work", async () => { + const namespace = new SequencedWorkflowNamespace([ + ok(snapshot(["pending", "pending"], { transitionSequence: 1 })), + ok(snapshot(["succeeded", "succeeded"], { transitionSequence: 7 })), + ]); + const proceduralSession = await session(); + + const accepted = await guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), plan(), proceduralSession, { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(accepted.available).toBe(false); + expect(accepted.reason).toBe("execution_not_started"); + expect(accepted.lifecycleState).toBe("accepted"); + + const terminal = await guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), plan(), proceduralSession, { lastProcedure: null, hops: 1, maxEdges: 4 }, + ); + expect(terminal.available).toBe(false); + expect(terminal.reason).toBe("terminal_execution"); + expect(terminal.lifecycleState).toBe("succeeded"); + }); + + it("does not inspect a hostile guidance request when fresh durable authority is non-running", async () => { + const namespace = new SequencedWorkflowNamespace([ + ok(snapshot(["succeeded", "cancelled"], { cancellation: true, transitionSequence: 6 })), + ]); + const proceduralSession = await session(); + const hostile = new Proxy({}, { + ownKeys() { throw new Error("guidance request must not be read"); }, + getOwnPropertyDescriptor() { throw new Error("guidance request must not be read"); }, + }); + + await expect(guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), plan(), proceduralSession, hostile, + )).resolves.toMatchObject({ available: false, reason: "cancellation_requested" }); + }); + + it("rejects a cross-execution procedural session before contacting another workflow-state owner", async () => { + const namespace = new SequencedWorkflowNamespace([]); + const proceduralSession = await session(); + + await expect(guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(namespace), + plan("run-current-lifecycle-foreign"), + proceduralSession, + { lastProcedure: null, hops: 1, maxEdges: 4 }, + )).rejects.toMatchObject({ + name: "ProceduralExecutionError", + message: "execution_identity_mismatch", + }); + expect(namespace.requests).toHaveLength(0); + }); + + it("fails closed when the durable owner cannot provide a trustworthy current snapshot", async () => { + const conflict = new SequencedWorkflowNamespace([ + new Response(JSON.stringify({ ok: false, error: "conflict" }), { status: 409 }), + ]); + const malformed = new SequencedWorkflowNamespace([ + ok({ ...snapshot(["succeeded", "pending"], { transitionSequence: 4 }), planId: "foreign-plan" }), + ]); + const proceduralSession = await session(); + + await expect(guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(conflict), plan(), proceduralSession, { lastProcedure: null, hops: 1, maxEdges: 4 }, + )).rejects.toMatchObject({ + name: "ProceduralCurrentLifecycleError", + code: "workflow_state_conflict", + } satisfies Partial); + + await expect(guideProceduralExecutionFromCurrentWorkflowState( + runtimeEnv(malformed), plan(), proceduralSession, { lastProcedure: null, hops: 1, maxEdges: 4 }, + )).rejects.toMatchObject({ + name: "ProceduralCurrentLifecycleError", + code: "invalid_workflow_state_response", + } satisfies Partial); + }); +}); \ No newline at end of file