From f9070138a43f8c318b0936ba2cdffe9c92a2a58d Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Fri, 31 Jul 2026 12:00:37 +0200 Subject: [PATCH 1/2] docs(vrs): define typed resource reference edges --- .../vrs/04-resource-reference/requirements.md | 73 ++++++ docs/vrs/04-resource-reference/spec.md | 218 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 docs/vrs/04-resource-reference/requirements.md create mode 100644 docs/vrs/04-resource-reference/spec.md diff --git a/docs/vrs/04-resource-reference/requirements.md b/docs/vrs/04-resource-reference/requirements.md new file mode 100644 index 00000000..edd0e980 --- /dev/null +++ b/docs/vrs/04-resource-reference/requirements.md @@ -0,0 +1,73 @@ +# Resource reference requirements + +## Context + +st2 has two durable edge types that point to a Resource: + +- an Agent Spec binding gives a declared Resource an agent-local name; and +- a linked-reference record gives an agent-owned Resource a relation and + optional descriptive metadata. + +Both edge types need the same typed Resource identity. They do not have the +same authority, storage, mutation, projection, or lifetime. + +This document refines root requirements +[`R20` and `R21`](../requirements.md). It resolves +the contract questions in [st2 issue 122](https://github.com/compoundingtech/st2/issues/122) +without changing the linked-reference boundary in +[st2 issue 62](https://github.com/compoundingtech/st2/issues/62). Where this +file and the root requirements disagree, the root requirements win. + +## Requirements + +- **RESOURCE-R01 One portable reference value:** Every declared binding and + linked-reference record carries a `ResourceRef` with exactly two required + fields: a non-empty, open `_tag` and an exact RFC 3986 absolute `uri`. st2 + preserves both strings and does not resolve the URI or attach authority, + readiness, access, or lifecycle policy to it. +- **RESOURCE-R02 Distinct edge identities:** A declared binding is identified + by agent identity plus binding name. A linked-reference record is identified + by agent identity plus its durable record ID. A Resource URI is not an edge + identity. Several edges may refer to the same exact URI. +- **RESOURCE-R03 Separate authority and lifetime:** Agent Spec authors and + catalog publishers control declared bindings. The owning agent controls its + linked-reference records. A mutation on one edge type never creates, + rewrites, or removes the other edge type. +- **RESOURCE-R04 Separate projections:** Agent Spec readers and + `st2 agents --json` expose declared bindings. `st2 resource` remains a + linked-reference-only surface. No command presents a combined mutable + Resource inventory. +- **RESOURCE-R05 Open link relations:** A link relation is absent or a + non-empty, open string. st2 does not define a closed relation registry and + does not infer mutation authority from a relation. +- **RESOURCE-R06 Lossless exact-URI grouping:** A read-only consumer may group + edges only when their `uri` strings are byte-for-byte equal. Grouping retains + every edge, `_tag`, and item of edge metadata. It does not normalize a URI, + select a winning `_tag`, coalesce records, or change storage. +- **RESOURCE-R07 Explicit legacy-link transition:** Readers accept both the + canonical `_tag` plus `uri` link form and the former `url` form. A legacy + `url` receives the same documented scheme-derived default `_tag` as a new + link whose author omits an explicit type. New writes use only the canonical + form. Reads never rewrite a legacy file. +- **RESOURCE-R08 Nondisruptive edge changes:** Adding, changing, or removing + only Resource bindings or linked-reference records does not change a task's + effective launch definition and does not stop, replace, or relaunch healthy + work. + +## Acceptable tradeoffs + +- **RESOURCE-T01 Compatibility default is less precise:** A URI scheme is a + deterministic fallback type for an untyped legacy link, but an explicit + `_tag` can describe the Resource more precisely. Compatibility is preferred + to inventing type knowledge during migration. +- **RESOURCE-T02 Duplicate views over destructive deduplication:** Consumers + may see several edges for one exact URI. Keeping their distinct authorities + and metadata is preferred to a simpler view that loses durable intent. + +## Non-goals + +- Resource resolution, a Resource registry, or mutation of an external object. +- Treating possession of a URI as authority. +- Moving linked outputs into Agent Spec. +- Making an external content store, publisher, or controller necessary for + plain-folder Agent Spec bindings or linked-reference records. diff --git a/docs/vrs/04-resource-reference/spec.md b/docs/vrs/04-resource-reference/spec.md new file mode 100644 index 00000000..4b1eab43 --- /dev/null +++ b/docs/vrs/04-resource-reference/spec.md @@ -0,0 +1,218 @@ +# Resource reference specification + +This document specifies the shared value and the two distinct Resource edge +types required by [requirements.md](./requirements.md). + +## Status + +Draft. This is an API and migration contract for a later implementation. + +## Data model + +The shared value is: + +```text +ResourceRef { + _tag: NonEmptyString, + uri: AbsoluteUri +} +``` + +`_tag` is an open discriminator. st2 preserves it exactly and does not require +registration. `uri` uses the existing Agent Spec absolute-URI validation and +is preserved byte-for-byte. A valid value does not imply that the Resource +exists, is reachable, or grants access. + +The two edges embed that value: + +```text +Binding { + name: NonEmptyString, + resource: ResourceRef +} + +Link { + id: RecordId, + relation: NonEmptyString?, + resource: ResourceRef, + title: String?, + tags: [String], + body: String? +} +``` + +The embedding may remain flat on an existing wire. In particular, the +canonical Agent Spec form stays: + +```kdl +resource "work" _tag="github-issue" uri="github-issue://example/project/123" +``` + +and its JSON projection stays: + +```json +{"name":"work","_tag":"github-issue","uri":"github-issue://example/project/123"} +``` + +The shared value is an API boundary, not a requirement to add a nested +`resource` object to those established forms. + +## Identity, authority, and mutation + +| Edge | Durable identity | Source of truth | Writer | Mutation | +| --- | --- | --- | --- | --- | +| Binding | agent identity + `name` | Agent Spec declaration | declaration author or catalog publisher | replace the declaration | +| Link | agent identity + `id` | `resources/links/.md` | owning agent | append or remove one link record | + +The URI identifies the referenced Resource. It does not identify either edge. +Two binding names, two link IDs, or one edge of each kind may carry the same +URI without conflict. + +A declaration edit does not write under `resources/links/`. A link add or +remove does not write an Agent Spec. Neither operation mutates the object named +by the URI. + +## Projections + +Declared bindings remain on the Agent Spec read model and in +`st2 agents --json`. Linked-reference records remain on +`st2 resource add|ls|read|remove`. + +`st2 resource ls` does not add declared bindings, agent inbox state, context, +or other durable material. A consumer that needs both edge types reads both +projections and joins them in memory. The joined view has no write operation. + +The linked-reference read model exposes `id`, `_tag`, `uri`, `relation`, +`title`, `tags`, and `body`. Human `ls` output includes the record ID, `_tag`, +URI, and optional title. Human `read` output labels `_tag` as `type` and +replaces the former `url` label with `uri`. + +## Link relation + +`relation` remains optional, open text. If present, it must be non-empty. Values +such as `output`, `reference`, and `blocked-by` are conventions, not a closed +enumeration. Unknown values survive reads and writes unchanged. + +The `Link` edge tag supplies the edge kind. `relation` refines why that link +exists; it does not replace `ResourceRef._tag` and does not grant authority. + +## Exact-URI view grouping + +The only generic grouping key is the exact `ResourceRef.uri` string. + +For example, a binding and two links with the exact URI +`https://example.test/work/1` may appear as one read-only group containing all +three edges. If one spelling changes case, escaping, a trailing slash, query, +or fragment, it forms a different group. st2 performs no normalization or +network lookup. + +A group retains every edge in stable source order and retains every `_tag`. +Different `_tag` values for one exact URI are visible disagreement. A consumer +must not choose one silently. Grouping never deletes or rewrites an edge. + +## Canonical linked-reference wire + +New link files use `_tag` and `uri` in YAML frontmatter: + +```markdown +--- +_tag: github-pull-request +uri: https://github.com/example/project/pull/123 +title: Resource reference contract +tags: docs, vrs +relation: output +--- +Optional notes. +``` + +The filename remains the durable link ID. `title`, `tags`, `relation`, and the +body remain link metadata rather than fields on `ResourceRef`. + +The authoring interface is: + +```text +st2 resource add [--type <_tag>] [--title ] + [--tag ...] [--relation ] [--body-stdin] +``` + +`--type` is the Resource discriminator. Existing `--tag` continues to collect +link-search metadata. When `--type` is absent, the writer derives `_tag` from +the URI scheme, converted to lowercase, and writes that value explicitly. +An explicit `--type` is preferred when the scheme is less precise than the +Resource type. + +All new writes use canonical `_tag` plus `uri` frontmatter. The writer never +emits `url`. + +## Legacy `url` transition + +The link reader recognizes exactly two forms: + +| Form | Required fields | Result | +| --- | --- | --- | +| Canonical | `_tag`, `uri` | validate and construct that exact `ResourceRef` | +| Legacy | `url` | validate the URL as an absolute URI; derive `_tag` from its lowercase scheme; preserve the URL bytes as `uri` | + +A file that mixes `url` with `_tag` or `uri`, or supplies only one canonical +field, is ambiguous and fails to decode. An invalid absolute URI or empty +explicit `_tag` also fails to decode. A list operation reports the record ID +and error instead of omitting the bad record. + +The dual reader has no expiry in this contract. Reading or listing a legacy +record does not rewrite it, change its ID, or change its metadata. Removing a +legacy record removes only that record. Every later add writes the canonical +form, so new state converges without a fleet-wide rewrite. + +The scheme-derived tag is a compatibility value, not inferred domain +knowledge. For example, legacy +`https://github.com/example/project/pull/123` becomes: + +```json +{"_tag":"https","uri":"https://github.com/example/project/pull/123"} +``` + +An author who knows that this is a GitHub pull request can instead add a new +link with `_tag` `github-pull-request`. + +## Plain-folder behavior and task liveness + +Both edge types remain fully usable from their current plain files: direct +Agent Spec KDL and `resources/links/*.md`. No registry, resolver, remote +service, or alternate content store is required to parse, project, add, or +remove them. + +Resource data is excluded from effective task launch identity. A reconciler +may adopt changed Resource metadata, but a Resource-only change cannot request +a stop, replacement, or relaunch. + +## Implementation map + +The implementation must introduce public `agent_spec::spec::ResourceRef` with +one reusable constructor and validator rather than duplicate the envelope. +The existing `agent_spec::spec::Resource` remains the binding wrapper and +delegates its `_tag` and `uri` fields to that value while preserving its +current accessors and flat serialization. The linked-record model also carries +that same public value. + +The current ownership map is: + +- [`agent-spec::spec::Resource`](../../../crates/agent-spec/src/spec.rs) owns + the current declared binding fields and absolute-URI validation. +- [`agent-spec` KDL lowering](../../../crates/agent-spec/src/kdl_format.rs) + owns the flat canonical binding syntax. +- [`st2::resource`](../../../src/resource.rs) owns linked-record storage, + parsing, and rendering. +- [`ResourceCmd`](../../../src/main.rs) owns the linked-reference CLI. +- [`resource_only_changes_do_not_replace_or_relaunch_a_live_task`](../../../tests/reconcile.rs) + is the existing nondisruption proof. + +Implementation evidence must cover: + +1. the same constructor validating both edge types; +2. unchanged Agent Spec KDL and JSON projection; +3. canonical link write and read; +4. legacy read with a scheme-derived tag and no rewrite; +5. mixed or incomplete wire refusal with a visible record ID; +6. separate declared and linked projections; +7. exact-URI grouping that retains every edge and tag; and +8. no healthy task replacement for either Resource-only edge change. From 8ad6ddbdad3d706f2e5f80407d7ad49602500a43 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Fri, 31 Jul 2026 15:34:37 +0200 Subject: [PATCH 2/2] docs(vrs): simplify ResourceRef contract --- .../vrs/04-resource-reference/requirements.md | 95 ++++---- docs/vrs/04-resource-reference/spec.md | 210 +++++++++--------- 2 files changed, 143 insertions(+), 162 deletions(-) diff --git a/docs/vrs/04-resource-reference/requirements.md b/docs/vrs/04-resource-reference/requirements.md index edd0e980..fa931be6 100644 --- a/docs/vrs/04-resource-reference/requirements.md +++ b/docs/vrs/04-resource-reference/requirements.md @@ -4,70 +4,61 @@ st2 has two durable edge types that point to a Resource: -- an Agent Spec binding gives a declared Resource an agent-local name; and -- a linked-reference record gives an agent-owned Resource a relation and - optional descriptive metadata. +- An Agent Spec binding gives a Resource an agent-local name. +- A link record gives a Resource a relation and optional metadata. -Both edge types need the same typed Resource identity. They do not have the -same authority, storage, mutation, projection, or lifetime. +Both edge types use the same typed Resource identity. They have separate +authority, storage, mutation, projection, and lifetime rules. This document refines root requirements [`R20` and `R21`](../requirements.md). It resolves -the contract questions in [st2 issue 122](https://github.com/compoundingtech/st2/issues/122) -without changing the linked-reference boundary in -[st2 issue 62](https://github.com/compoundingtech/st2/issues/62). Where this -file and the root requirements disagree, the root requirements win. +[st2 issue 122](https://github.com/compoundingtech/st2/issues/122) and keeps the +link boundary in [st2 issue 62](https://github.com/compoundingtech/st2/issues/62). +The root requirements have authority if the documents disagree. ## Requirements -- **RESOURCE-R01 One portable reference value:** Every declared binding and - linked-reference record carries a `ResourceRef` with exactly two required - fields: a non-empty, open `_tag` and an exact RFC 3986 absolute `uri`. st2 - preserves both strings and does not resolve the URI or attach authority, - readiness, access, or lifecycle policy to it. -- **RESOURCE-R02 Distinct edge identities:** A declared binding is identified - by agent identity plus binding name. A linked-reference record is identified - by agent identity plus its durable record ID. A Resource URI is not an edge - identity. Several edges may refer to the same exact URI. +- **RESOURCE-R01 One portable reference value:** Each binding and link record + contains one `ResourceRef`. It has exactly two required fields: a non-empty, + open `_tag` and an exact RFC 3986 absolute `uri`. st2 preserves both strings. + It does not resolve the URI or add authority, access, readiness, or lifecycle + policy. +- **RESOURCE-R02 Distinct edge identities:** A binding identity is the agent + identity plus binding name. A link identity is the agent identity plus durable + record ID. A URI is not an edge identity. Many edges can use the same URI. - **RESOURCE-R03 Separate authority and lifetime:** Agent Spec authors and - catalog publishers control declared bindings. The owning agent controls its - linked-reference records. A mutation on one edge type never creates, - rewrites, or removes the other edge type. + catalog publishers control bindings. The owning agent controls its link + records. A change to one edge type does not create, change, or remove the + other edge type. - **RESOURCE-R04 Separate projections:** Agent Spec readers and - `st2 agents --json` expose declared bindings. `st2 resource` remains a - linked-reference-only surface. No command presents a combined mutable - Resource inventory. -- **RESOURCE-R05 Open link relations:** A link relation is absent or a - non-empty, open string. st2 does not define a closed relation registry and - does not infer mutation authority from a relation. -- **RESOURCE-R06 Lossless exact-URI grouping:** A read-only consumer may group - edges only when their `uri` strings are byte-for-byte equal. Grouping retains - every edge, `_tag`, and item of edge metadata. It does not normalize a URI, - select a winning `_tag`, coalesce records, or change storage. -- **RESOURCE-R07 Explicit legacy-link transition:** Readers accept both the - canonical `_tag` plus `uri` link form and the former `url` form. A legacy - `url` receives the same documented scheme-derived default `_tag` as a new - link whose author omits an explicit type. New writes use only the canonical - form. Reads never rewrite a legacy file. -- **RESOURCE-R08 Nondisruptive edge changes:** Adding, changing, or removing - only Resource bindings or linked-reference records does not change a task's - effective launch definition and does not stop, replace, or relaunch healthy - work. + `st2 agents --json` show bindings. `st2 resource` shows only link records. No + command provides one combined mutable Resource inventory. +- **RESOURCE-R05 Open link relations:** A relation is absent or is a non-empty, + open string. st2 has no closed relation registry. A relation does not give + mutation authority. +- **RESOURCE-R06 Lossless exact-URI grouping:** A read-only consumer can group + edges only when the `uri` strings are byte-for-byte equal. A group keeps each + edge, `_tag`, and metadata item. Grouping does not normalize a URI, select one + `_tag`, combine records, or change storage. +- **RESOURCE-R07 Explicit legacy-link transition:** Readers accept the + canonical `_tag` and `uri` form and the former `url` form. For a legacy `url`, + the reader uses the same scheme-derived default `_tag` that a new untyped link + uses. New writes use only the canonical form. A read does not rewrite a file. +- **RESOURCE-R08 Nondisruptive edge changes:** A change to only bindings or link + records does not change the effective task launch definition. It does not + stop, replace, or relaunch healthy work. ## Acceptable tradeoffs -- **RESOURCE-T01 Compatibility default is less precise:** A URI scheme is a - deterministic fallback type for an untyped legacy link, but an explicit - `_tag` can describe the Resource more precisely. Compatibility is preferred - to inventing type knowledge during migration. -- **RESOURCE-T02 Duplicate views over destructive deduplication:** Consumers - may see several edges for one exact URI. Keeping their distinct authorities - and metadata is preferred to a simpler view that loses durable intent. +- **RESOURCE-T01 Less precise compatibility default:** A URI scheme gives a + deterministic fallback type for an untyped legacy link. An explicit `_tag` can give + a more precise type. Compatibility is better than invented type knowledge. +- **RESOURCE-T02 Duplicate views preserve intent:** A consumer can see many + edges for one exact URI. Separate edges preserve their authority and metadata. ## Non-goals -- Resource resolution, a Resource registry, or mutation of an external object. -- Treating possession of a URI as authority. -- Moving linked outputs into Agent Spec. -- Making an external content store, publisher, or controller necessary for - plain-folder Agent Spec bindings or linked-reference records. +- Resolve a Resource, add a Resource registry, or change an external object. +- Treat a URI as authority. +- Move linked outputs into Agent Spec. +- Require an external store, publisher, or controller for plain-folder data. diff --git a/docs/vrs/04-resource-reference/spec.md b/docs/vrs/04-resource-reference/spec.md index 4b1eab43..dfae29e5 100644 --- a/docs/vrs/04-resource-reference/spec.md +++ b/docs/vrs/04-resource-reference/spec.md @@ -1,11 +1,11 @@ # Resource reference specification -This document specifies the shared value and the two distinct Resource edge -types required by [requirements.md](./requirements.md). +This document defines the shared value and the two Resource edge types in +[requirements.md](./requirements.md). ## Status -Draft. This is an API and migration contract for a later implementation. +Draft. This document defines an API and migration contract for later work. ## Data model @@ -18,12 +18,12 @@ ResourceRef { } ``` -`_tag` is an open discriminator. st2 preserves it exactly and does not require -registration. `uri` uses the existing Agent Spec absolute-URI validation and -is preserved byte-for-byte. A valid value does not imply that the Resource -exists, is reachable, or grants access. +`_tag` is an open discriminator. st2 preserves it and does not require +registration. `uri` uses the Agent Spec absolute-URI validation. st2 preserves +the URI byte-for-byte. A valid value does not prove that the Resource exists, +is reachable, or gives access. -The two edges embed that value: +Each edge contains the shared value: ```text Binding { @@ -41,76 +41,68 @@ Link { } ``` -The embedding may remain flat on an existing wire. In particular, the -canonical Agent Spec form stays: +An existing wire can keep the fields flat. The Agent Spec form stays: ```kdl resource "work" _tag="github-issue" uri="github-issue://example/project/123" ``` -and its JSON projection stays: +Its JSON projection stays: ```json {"name":"work","_tag":"github-issue","uri":"github-issue://example/project/123"} ``` -The shared value is an API boundary, not a requirement to add a nested -`resource` object to those established forms. +The API uses one shared value. It does not require a nested `resource` object on +an established wire. ## Identity, authority, and mutation -| Edge | Durable identity | Source of truth | Writer | Mutation | -| --- | --- | --- | --- | --- | -| Binding | agent identity + `name` | Agent Spec declaration | declaration author or catalog publisher | replace the declaration | -| Link | agent identity + `id` | `resources/links/.md` | owning agent | append or remove one link record | +| Edge | Identity | Source and writer | Mutation | +| --- | --- | --- | --- | +| Binding | agent + `name` | Agent Spec; author or catalog publisher | replace declaration | +| Link | agent + `id` | `resources/links/.md`; owning agent | add or remove one record | -The URI identifies the referenced Resource. It does not identify either edge. -Two binding names, two link IDs, or one edge of each kind may carry the same -URI without conflict. +The URI identifies the referenced Resource. It does not identify an edge. Many +bindings and links can use the same URI without a conflict. -A declaration edit does not write under `resources/links/`. A link add or -remove does not write an Agent Spec. Neither operation mutates the object named -by the URI. +A declaration change does not write to `resources/links/`. A link change does +not write an Agent Spec. Neither operation changes the object named by the URI. ## Projections -Declared bindings remain on the Agent Spec read model and in -`st2 agents --json`. Linked-reference records remain on -`st2 resource add|ls|read|remove`. +Agent Spec readers and `st2 agents --json` show bindings. The commands +`st2 resource add|ls|read|remove` show only link records. -`st2 resource ls` does not add declared bindings, agent inbox state, context, -or other durable material. A consumer that needs both edge types reads both -projections and joins them in memory. The joined view has no write operation. +`st2 resource ls` does not show bindings, inbox state, context, or other durable +data. A consumer can read both projections and join them in memory. The joined +view is read-only. -The linked-reference read model exposes `id`, `_tag`, `uri`, `relation`, -`title`, `tags`, and `body`. Human `ls` output includes the record ID, `_tag`, -URI, and optional title. Human `read` output labels `_tag` as `type` and -replaces the former `url` label with `uri`. +The link read model shows `id`, `_tag`, `uri`, `relation`, `title`, `tags`, and +`body`. Human `ls` output shows the ID, `_tag`, URI, and optional title. Human +`read` output labels `_tag` as `type` and labels the URI as `uri`, not `url`. ## Link relation -`relation` remains optional, open text. If present, it must be non-empty. Values -such as `output`, `reference`, and `blocked-by` are conventions, not a closed -enumeration. Unknown values survive reads and writes unchanged. +`relation` is optional open text. A present value is not empty. Values such as +`output`, `reference`, and `blocked-by` are conventions. Unknown values survive +reads and writes. -The `Link` edge tag supplies the edge kind. `relation` refines why that link -exists; it does not replace `ResourceRef._tag` and does not grant authority. +The link tag gives the edge kind. `relation` gives the reason for the link. It +does not replace `ResourceRef._tag` or give authority. -## Exact-URI view grouping +## Exact-URI grouping -The only generic grouping key is the exact `ResourceRef.uri` string. +The only generic grouping key is the exact `ResourceRef.uri` string. A binding +and two links with `https://example.test/work/1` can form one read-only group. +If case, escaping, a slash, a query, or a fragment differs, the URI forms a +different group. st2 does not normalize the value or use the network. -For example, a binding and two links with the exact URI -`https://example.test/work/1` may appear as one read-only group containing all -three edges. If one spelling changes case, escaping, a trailing slash, query, -or fragment, it forms a different group. st2 performs no normalization or -network lookup. +A group keeps every edge in stable source order. It also keeps every `_tag`. +Different tags for one URI remain visible. A consumer does not select one tag +without an explicit policy. Grouping does not delete or rewrite an edge. -A group retains every edge in stable source order and retains every `_tag`. -Different `_tag` values for one exact URI are visible disagreement. A consumer -must not choose one silently. Grouping never deletes or rewrites an edge. - -## Canonical linked-reference wire +## Canonical link wire New link files use `_tag` and `uri` in YAML frontmatter: @@ -125,94 +117,92 @@ relation: output Optional notes. ``` -The filename remains the durable link ID. `title`, `tags`, `relation`, and the -body remain link metadata rather than fields on `ResourceRef`. +The filename is the durable link ID. `title`, `tags`, `relation`, and the body +are link metadata. They are not fields on `ResourceRef`. -The authoring interface is: +The authoring command is: ```text st2 resource add [--type <_tag>] [--title ] [--tag ...] [--relation ] [--body-stdin] ``` -`--type` is the Resource discriminator. Existing `--tag` continues to collect -link-search metadata. When `--type` is absent, the writer derives `_tag` from -the URI scheme, converted to lowercase, and writes that value explicitly. -An explicit `--type` is preferred when the scheme is less precise than the -Resource type. +`--type` sets the Resource discriminator. `--tag` continues to add link-search +metadata. If `--type` is absent, the writer converts the URI scheme to lowercase +and writes it as `_tag`. Use an explicit type when the scheme is not precise. -All new writes use canonical `_tag` plus `uri` frontmatter. The writer never -emits `url`. +New writes use only `_tag` and `uri`. They do not write `url`. ## Legacy `url` transition -The link reader recognizes exactly two forms: +The link reader accepts exactly two forms: + +- The canonical form has `_tag` and `uri`. The reader validates both fields and + constructs the exact `ResourceRef`. +- The legacy form has `url`. The reader validates an absolute URI, derives the + lowercase scheme tag, and preserves the URL bytes as `uri`. + +A file fails to decode if it mixes `url` with `_tag` or `uri`, or if it has only +one canonical field. An invalid URI or an empty explicit `_tag` also fails. A +list operation reports the record ID and the error. It does not omit the record. -| Form | Required fields | Result | -| --- | --- | --- | -| Canonical | `_tag`, `uri` | validate and construct that exact `ResourceRef` | -| Legacy | `url` | validate the URL as an absolute URI; derive `_tag` from its lowercase scheme; preserve the URL bytes as `uri` | +The dual reader has no expiry in this contract. A read does not rewrite a legacy +record or change its ID or metadata. A remove operation removes only that record. +Each later add uses the canonical form. New state can converge without a fleet +rewrite. -A file that mixes `url` with `_tag` or `uri`, or supplies only one canonical -field, is ambiguous and fails to decode. An invalid absolute URI or empty -explicit `_tag` also fails to decode. A list operation reports the record ID -and error instead of omitting the bad record. +The scheme tag is a compatibility value. It is not domain knowledge. For +example, this legacy value: -The dual reader has no expiry in this contract. Reading or listing a legacy -record does not rewrite it, change its ID, or change its metadata. Removing a -legacy record removes only that record. Every later add writes the canonical -form, so new state converges without a fleet-wide rewrite. +```text +https://github.com/example/project/pull/123 +``` -The scheme-derived tag is a compatibility value, not inferred domain -knowledge. For example, legacy -`https://github.com/example/project/pull/123` becomes: +becomes: ```json {"_tag":"https","uri":"https://github.com/example/project/pull/123"} ``` -An author who knows that this is a GitHub pull request can instead add a new -link with `_tag` `github-pull-request`. +An author can instead add a new link with `_tag` `github-pull-request`. ## Plain-folder behavior and task liveness -Both edge types remain fully usable from their current plain files: direct -Agent Spec KDL and `resources/links/*.md`. No registry, resolver, remote -service, or alternate content store is required to parse, project, add, or -remove them. +Both edge types work from their current plain files: Agent Spec KDL and +`resources/links/*.md`. Parse, project, add, and remove operations need no +registry, resolver, remote service, or alternate content store. -Resource data is excluded from effective task launch identity. A reconciler -may adopt changed Resource metadata, but a Resource-only change cannot request -a stop, replacement, or relaunch. +Resource data is not part of the effective task launch identity. A reconciler +can adopt changed Resource metadata. A Resource-only change cannot request a +stop, replacement, or relaunch. ## Implementation map -The implementation must introduce public `agent_spec::spec::ResourceRef` with -one reusable constructor and validator rather than duplicate the envelope. -The existing `agent_spec::spec::Resource` remains the binding wrapper and -delegates its `_tag` and `uri` fields to that value while preserving its -current accessors and flat serialization. The linked-record model also carries -that same public value. +The implementation must add public `agent_spec::spec::ResourceRef`. It must use +one constructor and validator for both edge types. The existing +`agent_spec::spec::Resource` remains the binding wrapper. It delegates `_tag` +and `uri` to the shared value and keeps its accessors and flat serialization. +The link model also contains the same public value. -The current ownership map is: +The current owners are: - [`agent-spec::spec::Resource`](../../../crates/agent-spec/src/spec.rs) owns - the current declared binding fields and absolute-URI validation. -- [`agent-spec` KDL lowering](../../../crates/agent-spec/src/kdl_format.rs) - owns the flat canonical binding syntax. -- [`st2::resource`](../../../src/resource.rs) owns linked-record storage, - parsing, and rendering. -- [`ResourceCmd`](../../../src/main.rs) owns the linked-reference CLI. + binding fields and absolute-URI validation. +- [`agent-spec` KDL lowering](../../../crates/agent-spec/src/kdl_format.rs) owns + the flat binding syntax. +- [`st2::resource`](../../../src/resource.rs) owns link storage, parsing, and + rendering. +- [`ResourceCmd`](../../../src/main.rs) owns the link CLI. - [`resource_only_changes_do_not_replace_or_relaunch_a_live_task`](../../../tests/reconcile.rs) - is the existing nondisruption proof. - -Implementation evidence must cover: - -1. the same constructor validating both edge types; -2. unchanged Agent Spec KDL and JSON projection; -3. canonical link write and read; -4. legacy read with a scheme-derived tag and no rewrite; -5. mixed or incomplete wire refusal with a visible record ID; -6. separate declared and linked projections; -7. exact-URI grouping that retains every edge and tag; and -8. no healthy task replacement for either Resource-only edge change. + proves current nondisruption. + +Implementation evidence must prove: + +1. One constructor validates both edge types. +2. Agent Spec KDL and JSON projections do not change. +3. A canonical link write and read succeed. +4. A legacy read derives the scheme tag and does not rewrite the record. +5. A mixed or incomplete wire fails and reports the record ID. +6. Binding and link projections stay separate. +7. Exact-URI grouping keeps every edge and tag. +8. A Resource-only edge change does not replace healthy work.