From 819d5395153dd4ad28688b4b3b73776db79555a9 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sun, 27 Sep 2026 19:49:04 +0500 Subject: [PATCH] feat: a release of this repository Published at 0.0.77. Propose changes through this repository's issues and pull requests. --- crates/harness-runtime/src/facts.rs | 8 +- crates/harness-runtime/src/wire.rs | 89 ++++++++++++++++++++- crates/provider-v3/src/vocabulary.rs | 21 +++-- provider-kit/v3/KIT-IDENTITY.json | 4 +- provider-kit/v3/SHA256SUMS | 6 +- provider-kit/v3/manifest.json | 13 ++- provider-kit/v3/provider-info.schema.json | 3 +- provider-kit/v3/status-response.schema.json | 39 +++++++++ 8 files changed, 162 insertions(+), 21 deletions(-) diff --git a/crates/harness-runtime/src/facts.rs b/crates/harness-runtime/src/facts.rs index 135e5b0..35984fa 100644 --- a/crates/harness-runtime/src/facts.rs +++ b/crates/harness-runtime/src/facts.rs @@ -1047,9 +1047,8 @@ mod tests { Operation::SoftwareUpdate, Operation::SoftwareRemove, Operation::PatchInstructionRegion, - // Performed by the kernel but withheld everywhere until a released - // consumer accepts the name — declaring it earlier would make - // every older reader refuse the whole provider-info answer. + // A surface this build does not carry: SAMPLE declares no + // instruction region, so neither region operation is declared. Operation::DetachInstructionRegion, ] { assert!( @@ -1060,13 +1059,14 @@ mod tests { } #[test] - fn an_instruction_surface_without_software_still_declares_the_patch() { + fn an_instruction_surface_without_software_still_declares_the_region_lifecycle() { let named = Harness { instruction_region: Some("AGENTS.md"), ..SAMPLE }; let info = named.provider_info().unwrap(); assert!(info.declares(Operation::PatchInstructionRegion)); + assert!(info.declares(Operation::DetachInstructionRegion)); assert!(!info.declares(Operation::SoftwareInstall)); assert!(!info.declares(Operation::Launch)); } diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index 7bc487f..9bc72db 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -537,12 +537,40 @@ fn status_of( }, ), ("backups", backup_status(pool, resolved, harness, scope)?), + ( + "instruction_region", + instruction_attachment_status(harness, resolved), + ), ] { answer.insert(key.to_owned(), value); } Ok(serde_json::Value::Object(answer)) } +/// The managed instruction attachment as `status` reports it, or `null` for a +/// harness that declares no user-global instruction surface. Reads nothing +/// else: an unreadable file, and one whose markers are ambiguous, both report +/// `section_present: false` — there is no well-formed owned section either +/// way, and `present` is what keeps those two apart for the caller. +fn instruction_attachment_status(harness: &Harness, resolved: &Target) -> serde_json::Value { + let Some(relative) = harness.instruction_region else { + return serde_json::Value::Null; + }; + let surface = resolved.root().join(relative); + let existing = crate::instruction_region::read_utf8(&surface).unwrap_or_else(|_| String::new()); + let section = if crate::instruction_region::markers_well_formed(&existing) { + crate::instruction_region::extract(&existing) + } else { + None + }; + serde_json::json!({ + "path": relative, + "present": surface.exists(), + "section_present": section.is_some(), + "section_sha256": section.map(|held| setup_core::digest::of_bytes(held.as_bytes())), + }) +} + /// Everything a clean managed target's own state says about how it got here. /// /// Serialized from the record rather than listed again here. The contract's @@ -4706,6 +4734,62 @@ mod tests { ); } + #[test] + fn status_reports_the_instruction_attachment_or_its_absence() { + const BARE: Harness = Harness { + instruction_region: None, + ..TEST + }; + let target = seeded("status-attachment"); + let bare = dispatch(&BARE, argv::parse(args("status", &target, &[])).unwrap()).unwrap(); + assert!( + bare["instruction_region"].is_null(), + "a harness without the surface must answer null, not a state: {bare}" + ); + + // File absent: nothing to report beyond that. + fs::remove_file(target.join("AGENTS.md")).unwrap(); + let missing = run(args("status", &target, &[])); + assert_eq!(missing["instruction_region"]["path"], "AGENTS.md"); + assert_eq!(missing["instruction_region"]["present"], false); + assert_eq!(missing["instruction_region"]["section_present"], false); + assert!(missing["instruction_region"]["section_sha256"].is_null()); + + // File present, section absent. + let target = seeded("status-attachment-file"); + let plain = run(args("status", &target, &[])); + assert_eq!(plain["instruction_region"]["present"], true); + assert_eq!(plain["instruction_region"]["section_present"], false); + assert!(plain["instruction_region"]["section_sha256"].is_null()); + + // Section present: the digest is the region's own, never target_digest. + plan_then_apply( + &target, + "patch_instruction_region", + &["--instruction-section", INSTRUCTION_SECTION], + ); + let attached = run(args("status", &target, &[])); + assert_eq!(attached["instruction_region"]["section_present"], true); + let digest = attached["instruction_region"]["section_sha256"] + .as_str() + .unwrap(); + assert_eq!( + digest, + setup_core::digest::of_bytes(INSTRUCTION_SECTION.as_bytes()), + "section_sha256 must be the region digest: {attached}" + ); + + // Ambiguous markers read as "no well-formed section", not a crash. + fs::write( + target.join("AGENTS.md"), + b":::end-ai-stp\n:::begin-ai-stp\n", + ) + .unwrap(); + let ambiguous = run(args("status", &target, &[])); + assert_eq!(ambiguous["instruction_region"]["present"], true); + assert_eq!(ambiguous["instruction_region"]["section_present"], false); + } + #[test] fn replacement_spares_a_never_touch_path_inside_a_namespace_it_empties() { const SPARES: Harness = Harness { @@ -8529,10 +8613,9 @@ mod tests { assert!(info.declares(Operation::SoftwareUpdate)); assert!(info.declares(Operation::SoftwareRemove)); assert!(info.declares(Operation::PatchInstructionRegion)); - // Implemented, but withheld until a released consumer accepts the name. assert!( - !info.declares(Operation::DetachInstructionRegion), - "declaring before a reader ships makes older consumers refuse the whole answer" + info.declares(Operation::DetachInstructionRegion), + "the detach went out with the consumer release that accepts the name" ); } diff --git a/crates/provider-v3/src/vocabulary.rs b/crates/provider-v3/src/vocabulary.rs index d0e4da1..1c1b0fc 100644 --- a/crates/provider-v3/src/vocabulary.rs +++ b/crates/provider-v3/src/vocabulary.rs @@ -142,10 +142,9 @@ pub enum Operation { /// payload it is free to empty. PatchInstructionRegion, /// Remove only the marked instruction section, keeping every byte outside - /// the markers. Optional; takes no `--instruction-section`. Implemented in - /// the kernel but withheld from `provider-info` until a released consumer - /// accepts the name — declaring it first would refuse every older reader - /// the whole `provider-info` answer. + /// the markers. Optional; takes no `--instruction-section`. Declared only + /// once a released consumer accepts the name — an older reader refuses the + /// whole `provider-info` answer on an unknown operation. DetachInstructionRegion, } @@ -163,6 +162,7 @@ impl Operation { Self::SoftwareRemove, Self::Launch, Self::PatchInstructionRegion, + Self::DetachInstructionRegion, ]; /// The operations every provider must support. @@ -187,7 +187,8 @@ impl Operation { Self::SoftwareRemove, ]; - /// [`CORE`] plus the instruction-region patch, without software or launch. + /// [`CORE`] plus the instruction-region lifecycle, without software or + /// launch. pub const CORE_AND_INSTRUCTION: &'static [Self] = &[ Self::Backup, Self::Install, @@ -195,9 +196,11 @@ impl Operation { Self::Replace, Self::Restore, Self::PatchInstructionRegion, + Self::DetachInstructionRegion, ]; - /// [`CORE_AND_SOFTWARE`] plus the instruction-region patch, without launch. + /// [`CORE_AND_SOFTWARE`] plus the instruction-region lifecycle, without + /// launch. pub const CORE_AND_SOFTWARE_AND_INSTRUCTION: &'static [Self] = &[ Self::Backup, Self::Install, @@ -209,6 +212,7 @@ impl Operation { Self::SoftwareUpdate, Self::SoftwareRemove, Self::PatchInstructionRegion, + Self::DetachInstructionRegion, ]; /// [`ALL`] without the instruction-region patch. @@ -256,7 +260,10 @@ impl Operation { /// Operations this kernel can perform, whether or not they may be declared /// to the consumer yet. [`ALL`] is the declarable set; the difference is - /// exactly the members awaiting a released consumer that accepts the name. + /// exactly the members awaiting a released consumer that accepts the name, + /// and today that difference is empty. A new operation whose name an older + /// reader cannot parse joins this list first and [`ALL`] only when the + /// consumer shipping the name is released. pub const IMPLEMENTED: &'static [Self] = &[ Self::Install, Self::Replace, diff --git a/provider-kit/v3/KIT-IDENTITY.json b/provider-kit/v3/KIT-IDENTITY.json index 1ed9e58..8c81e8a 100644 --- a/provider-kit/v3/KIT-IDENTITY.json +++ b/provider-kit/v3/KIT-IDENTITY.json @@ -1,12 +1,12 @@ { - "aggregate_digest": "sha256:e2a35eaf2e0f834913962d9a6337948eeb78290b4ad8c3af7674620215610b0e", + "aggregate_digest": "sha256:b8941c16214c49b2e527f75f71df97cdcb18a1923861391d4b0db03db7cbe4e2", "files": [ "conformance-cases.json", "manifest.json", "provider-info.schema.json", "status-response.schema.json" ], - "kit_version": "0.2.13", + "kit_version": "0.2.14", "protocol_version": 3, "schema": "ai-stp-provider-kit-identity/1" } diff --git a/provider-kit/v3/SHA256SUMS b/provider-kit/v3/SHA256SUMS index e4d165d..6a55a13 100644 --- a/provider-kit/v3/SHA256SUMS +++ b/provider-kit/v3/SHA256SUMS @@ -1,4 +1,4 @@ fe04d03b15cfe8d5b61835eafd3ea788074684e2be69d419f9c22a37ea0461b7 conformance-cases.json -cfc55bb6f301589ded449c12ba0309e12238db7bbce3f5e63fc45e9731d4bee5 manifest.json -2f28fef790dc2685e9790f4ae3123b1c6d581c94f87d15b75823ea8d575036aa provider-info.schema.json -7e67760f24bbc42a1242dad7ad3b378eef7ae15023c6813a33d582dfd2232293 status-response.schema.json +4c32e4320d6838cbea0f852d2ac3dd7095559120acdb94c5a51b2e2a4c2bea1f manifest.json +aeac0a4226796bbc5825eaa61c8b6c90e3a7e5ba9dec7f8f7bcc05f075d87d61 provider-info.schema.json +6af570bbecce664b6270b401904f97e632fdbae941fbb8925a0379beba5394bc status-response.schema.json diff --git a/provider-kit/v3/manifest.json b/provider-kit/v3/manifest.json index 7516384..accb4e5 100644 --- a/provider-kit/v3/manifest.json +++ b/provider-kit/v3/manifest.json @@ -39,7 +39,7 @@ ], "decision": "docs/adr/ADR-0061-capability-negotiated-provider-protocol-v3.md", "generated_from": "apps/cli/src/ai_stp_cli/provider/protocol_v3.py", - "kit_version": "0.2.13", + "kit_version": "0.2.14", "operation_network": { "backup": [ { @@ -51,6 +51,16 @@ "phase": "apply" } ], + "detach_instruction_region": [ + { + "network_requirement": "none", + "phase": "plan" + }, + { + "network_requirement": "none", + "phase": "apply" + } + ], "install": [ { "network_requirement": "none", @@ -160,6 +170,7 @@ "launch" ], "optional_operations": [ + "detach_instruction_region", "launch", "patch_instruction_region", "reset", diff --git a/provider-kit/v3/provider-info.schema.json b/provider-kit/v3/provider-info.schema.json index cc88b58..9a1b70e 100644 --- a/provider-kit/v3/provider-info.schema.json +++ b/provider-kit/v3/provider-info.schema.json @@ -269,7 +269,8 @@ "software_update", "software_remove", "launch", - "patch_instruction_region" + "patch_instruction_region", + "detach_instruction_region" ], "type": "string" }, diff --git a/provider-kit/v3/status-response.schema.json b/provider-kit/v3/status-response.schema.json index 4f38c9c..33dc3ea 100644 --- a/provider-kit/v3/status-response.schema.json +++ b/provider-kit/v3/status-response.schema.json @@ -269,6 +269,45 @@ "minLength": 1, "type": "string" }, + "instruction_region": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "path": { + "minLength": 1, + "type": "string" + }, + "present": { + "type": "boolean" + }, + "section_present": { + "type": "boolean" + }, + "section_sha256": { + "oneOf": [ + { + "$ref": "#/$defs/digest" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "path", + "present", + "section_present", + "section_sha256" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, "journal": { "oneOf": [ {