diff --git a/dag/extdeps/cloud/gcp/gcp.dag b/dag/extdeps/cloud/gcp/gcp.dag index 1001a600369..c19ab010d80 100644 --- a/dag/extdeps/cloud/gcp/gcp.dag +++ b/dag/extdeps/cloud/gcp/gcp.dag @@ -5,7 +5,8 @@ import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import std.types { Bool, FilePath, Int, List, Map, NonEmptyStr, Secret, Unit } import std.string_type { String } -import v2.std.network { POST } +import extdeps.ietf.http_semantics { POST } + data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https diff --git a/dag/extdeps/cloud/gcp/iam.dag b/dag/extdeps/cloud/gcp/iam.dag index 19269b81c66..6791581a62d 100644 --- a/dag/extdeps/cloud/gcp/iam.dag +++ b/dag/extdeps/cloud/gcp/iam.dag @@ -7,8 +7,9 @@ import extdeps.cloud.gcp.gcp { GcpProjectId, ServiceAccountEmail, GcpOperation } import extdeps.transports.rest { RestOutcome } import std.types { Bool, Int, Json, List, NonEmptyStr, Secret, String } import v2.std.algebra { filter, fold_list } -import v2.std.network { GET, POST } import v2.std.optional { Absent, Present } +import extdeps.ietf.http_semantics { GET, POST } + data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https diff --git a/dag/extdeps/cloud/gcp/iam_admin.dag b/dag/extdeps/cloud/gcp/iam_admin.dag index 1feb8755ee6..829907e1f33 100644 --- a/dag/extdeps/cloud/gcp/iam_admin.dag +++ b/dag/extdeps/cloud/gcp/iam_admin.dag @@ -7,8 +7,9 @@ import extdeps.cloud.gcp.gcp { GcpProjectId, ServiceAccountEmail } import extdeps.cloud.gcp.iam { GcpPolicy, GcpBinding } import std.types { Int, List, Map, NonEmptyStr, Secret, String } import extdeps.transports.rest { RestOutcome } -import v2.std.network { POST } import extdeps.transports.rest { RestOutcome } +import extdeps.ietf.http_semantics { POST } + data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https diff --git a/dag/extdeps/cloud/gcp/secret_manager.dag b/dag/extdeps/cloud/gcp/secret_manager.dag index 44daf0d0928..238042cb8f2 100644 --- a/dag/extdeps/cloud/gcp/secret_manager.dag +++ b/dag/extdeps/cloud/gcp/secret_manager.dag @@ -8,7 +8,6 @@ import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import std.types { Bool, Bytes, Int, List, Map, NonEmptyStr, Secret, String } import v2.std.algebra { fold_list, length, skip } -import v2.std.network { GET, POST } import v2.std.optional { Present } // GcpProjectId and GcpErrorShape were reached as BARE references before this // import existed, which resolves only when some unrelated module in the assembled @@ -18,6 +17,7 @@ import extdeps.cloud.gcp.gcp { GcpProjectId } import extdeps.cloud.gcp.errors { GcpErrorShape } import extdeps.transports.rest { RestOutcome } import v2.std.operation_argv { OperationRef } +import extdeps.ietf.http_semantics { GET, POST } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/cloud/gcp/serviceusage.dag b/dag/extdeps/cloud/gcp/serviceusage.dag index 1482f27a400..efbba9c4d58 100644 --- a/dag/extdeps/cloud/gcp/serviceusage.dag +++ b/dag/extdeps/cloud/gcp/serviceusage.dag @@ -5,8 +5,9 @@ import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.cloud.gcp.gcp { GcpProjectId } import std.types { List, Secret, String } -import v2.std.network { POST, GET } import extdeps.transports.rest { RestOutcome } +import extdeps.ietf.http_semantics { POST, GET } + data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https diff --git a/dag/extdeps/cloud/gcp/sts.dag b/dag/extdeps/cloud/gcp/sts.dag index 38041530777..9366a733c7f 100644 --- a/dag/extdeps/cloud/gcp/sts.dag +++ b/dag/extdeps/cloud/gcp/sts.dag @@ -4,7 +4,8 @@ module extdeps.cloud.gcp.sts import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import std.types { Int, NonEmptyStr, Secret } -import v2.std.network { GET, POST } +import extdeps.ietf.http_semantics { GET, POST } + data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https diff --git a/dag/extdeps/ebay/inventory.dag b/dag/extdeps/ebay/inventory.dag index 215d958c73a..505259dec16 100644 --- a/dag/extdeps/ebay/inventory.dag +++ b/dag/extdeps/ebay/inventory.dag @@ -1,6 +1,6 @@ module extdeps.ebay.inventory -import std.types { NonEmptyStr, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, AuthScheme } import extdeps.currency.currency { CurrencyCode } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -8,6 +8,7 @@ import extdeps.coverage { CoverageClosureEntry, CoverageClosureFact } import extdeps.ebay.ebay { production_api_base, MarketplaceEnum } import extdeps.ebay.errors { EbayErrorShape } import extdeps.ebay.ebay_contracts { condition_wire_contract, format_type_wire_contract } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/ebay/oauth.dag b/dag/extdeps/ebay/oauth.dag index b213f3e2331..f444a3e1cbc 100644 --- a/dag/extdeps/ebay/oauth.dag +++ b/dag/extdeps/ebay/oauth.dag @@ -1,9 +1,10 @@ module extdeps.ebay.oauth -import std.types { NonEmptyStr, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, AuthScheme } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.ebay.ebay { production_api_base } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/github/gists.dag b/dag/extdeps/github/gists.dag index 8392392fbb4..a4e2f9e28e4 100644 --- a/dag/extdeps/github/gists.dag +++ b/dag/extdeps/github/gists.dag @@ -1,11 +1,12 @@ module extdeps.github.gists import extdeps.github.github { GitHubUser, default_api_base } -import std.types { Timestamp, GistId, NonEmptyStr, HttpMethod, AuthScheme } +import std.types { Timestamp, GistId, NonEmptyStr, AuthScheme } import extdeps.github.errors { GitHubErrorShape } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/github/pulls.dag b/dag/extdeps/github/pulls.dag index 0e98bef6e9f..d18fa0062be 100644 --- a/dag/extdeps/github/pulls.dag +++ b/dag/extdeps/github/pulls.dag @@ -1,7 +1,7 @@ module extdeps.github.pulls import extdeps.github.github { GitHubUser, default_api_base, default_per_page } -import std.types { CommitSha, EpochMs, FilePath, GitRef, NonEmptyStr, Timestamp, HttpMethod, AuthScheme } +import std.types { CommitSha, EpochMs, FilePath, GitRef, NonEmptyStr, Timestamp, AuthScheme } import std.string_type { String } import extdeps.github.errors { GitHubErrorShape } import std.credentials { CredentialSource, EnvVar } @@ -15,6 +15,7 @@ import extdeps.github.pulls_contracts { review_state_wire_contract, review_event_wire_contract } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/github/rulesets.dag b/dag/extdeps/github/rulesets.dag index 0b4f3965014..fb5fa2d0431 100644 --- a/dag/extdeps/github/rulesets.dag +++ b/dag/extdeps/github/rulesets.dag @@ -405,15 +405,19 @@ fn required_status_check_standing(rules: List) -> RequiredStatu MultipleRequiredStatusChecksRules { count: length(matched) } } else { fold(matched, init: NoRequiredStatusChecksRule, f: fn(_, rule) { - let missing = required_status_check_missing_fields(p: rule.parameters) - if length(missing) > 0 { - RequiredStatusChecksParametersIncomplete { missing: missing } - } else { - RequiredStatusChecks { - contexts: map(rsc_checks(v: rule.parameters.required_status_checks), c => c.context), - strict: rsc_bool(v: rule.parameters.strict_required_status_checks_policy), - do_not_enforce_on_create: rsc_bool(v: rule.parameters.do_not_enforce_on_create), - } + match rule.parameters { + Absent => RequiredStatusChecksParametersIncomplete { missing: ["parameters"] } + Present { value: params } => + let missing = required_status_check_missing_fields(p: params) + if length(missing) > 0 { + RequiredStatusChecksParametersIncomplete { missing: missing } + } else { + RequiredStatusChecks { + contexts: map(rsc_checks(v: params.required_status_checks), c => c.context), + strict: rsc_bool(v: params.strict_required_status_checks_policy), + do_not_enforce_on_create: rsc_bool(v: params.do_not_enforce_on_create), + } + } } }) } @@ -588,26 +592,30 @@ fn merge_queue_standing(rules: List) -> MergeQueueStanding { MultipleMergeQueueRules { count: length(matched) } } else { fold(matched, init: NoMergeQueueRule, f: fn(_, rule) { - let missing = merge_queue_missing_fields(p: rule.parameters) - if length(missing) > 0 { - MergeQueueParametersIncomplete { missing: missing } - } else { - MergeQueueConfigured { - grouping_strategy: parse_merge_queue_grouping_strategy( - raw: merge_queue_str(v: rule.parameters.grouping_strategy)), - merge_method: parse_merge_queue_merge_method( - raw: merge_queue_str(v: rule.parameters.merge_method)), - check_response_timeout: minute( - count: merge_queue_int(v: rule.parameters.check_response_timeout_minutes)), - max_entries_to_build: merge_queue_entry_count( - count: merge_queue_int(v: rule.parameters.max_entries_to_build)), - max_entries_to_merge: merge_queue_entry_count( - count: merge_queue_int(v: rule.parameters.max_entries_to_merge)), - min_entries_to_merge: merge_queue_entry_count( - count: merge_queue_int(v: rule.parameters.min_entries_to_merge)), - min_entries_to_merge_wait: minute( - count: merge_queue_int(v: rule.parameters.min_entries_to_merge_wait_minutes)), - } + match rule.parameters { + Absent => MergeQueueParametersIncomplete { missing: ["parameters"] } + Present { value: params } => + let missing = merge_queue_missing_fields(p: params) + if length(missing) > 0 { + MergeQueueParametersIncomplete { missing: missing } + } else { + MergeQueueConfigured { + grouping_strategy: parse_merge_queue_grouping_strategy( + raw: merge_queue_str(v: params.grouping_strategy)), + merge_method: parse_merge_queue_merge_method( + raw: merge_queue_str(v: params.merge_method)), + check_response_timeout: minute( + count: merge_queue_int(v: params.check_response_timeout_minutes)), + max_entries_to_build: merge_queue_entry_count( + count: merge_queue_int(v: params.max_entries_to_build)), + max_entries_to_merge: merge_queue_entry_count( + count: merge_queue_int(v: params.max_entries_to_merge)), + min_entries_to_merge: merge_queue_entry_count( + count: merge_queue_int(v: params.min_entries_to_merge)), + min_entries_to_merge_wait: minute( + count: merge_queue_int(v: params.min_entries_to_merge_wait_minutes)), + } + } } }) } diff --git a/dag/extdeps/google/sheets.dag b/dag/extdeps/google/sheets.dag index f691dc6f03a..0f83add0557 100644 --- a/dag/extdeps/google/sheets.dag +++ b/dag/extdeps/google/sheets.dag @@ -1,6 +1,6 @@ module extdeps.google.sheets -import std.types { NonEmptyStr, String, Int, List, Secret, Bool, Float, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, String, Int, List, Secret, Bool, Float, AuthScheme } import extdeps.transports.rest { RestOutcome, RestOk } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.credentials { EnvVar } @@ -15,6 +15,7 @@ import extdeps.external_authority { import extdeps.uri { Uri, Https } import extdeps.api_rate_limit { UpstreamRateLimit, RateLimitPeriod, PerMinute } import extdeps.coverage { CoverageClosureEntry, CoverageClosureFact } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/http/server.dag b/dag/extdeps/http/server.dag index 3b5f94a644e..f91e43a8301 100644 --- a/dag/extdeps/http/server.dag +++ b/dag/extdeps/http/server.dag @@ -2,7 +2,7 @@ module extdeps.http.server import extdeps.tailscale.identity { TailnetIdentity } import extdeps.uri_path { PathTemplate, PathParamBinding } -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, HttpStatus, Port } +import std.types { HttpStatus, Port } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.uri_path { @@ -10,6 +10,7 @@ import extdeps.uri_path { match_path_template, ParsedPathTemplate, MalformedPathTemplate, parse_path_template, } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -143,6 +144,8 @@ fn http_method_label(method: HttpMethod) -> String { DELETE => "DELETE" HEAD => "HEAD" OPTIONS => "OPTIONS" + CONNECT => "CONNECT" + TRACE => "TRACE" } } @@ -161,6 +164,8 @@ fn parse_http_method_label(label: String) -> HttpMethodParse { else if label == "DELETE" { KnownMethod { method: DELETE } } else if label == "HEAD" { KnownMethod { method: HEAD } } else if label == "OPTIONS" { KnownMethod { method: OPTIONS } } + else if label == "CONNECT" { KnownMethod { method: CONNECT } } + else if label == "TRACE" { KnownMethod { method: TRACE } } else { UnknownMethod { label: label } } } diff --git a/dag/extdeps/ietf/http_semantics.dag b/dag/extdeps/ietf/http_semantics.dag new file mode 100644 index 00000000000..f86a0a1f832 --- /dev/null +++ b/dag/extdeps/ietf/http_semantics.dag @@ -0,0 +1,55 @@ +module extdeps.ietf.http_semantics + +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } + +// RFC 9110 (HTTP Semantics, June 2022; obsoletes RFC 7231) section 9 METHODS, plus PATCH from +// RFC 5789 -- the one home of the request-method set. It was declared twice before this module: std.types HttpMethod with +// seven arms and v2.std.network HttpMethod with nine. Neither was the upstream: the method set is +// an IETF fact, so it lives beside the other extdeps.ietf modules and both std declarations are +// gone rather than one re-exporting the other (DESIGN section 3, external upstream decomposition). +// The seven-arm copy was not a narrower concept but an incomplete transcription: CONNECT (9.3.6) +// and TRACE (9.3.8) are registered methods, and a consumer that could not name them had no way to +// refuse them by name. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.rfc-editor.org/rfc/rfc9110#section-9" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.ietf.http_semantics", + decl_name: "HttpMethod", + field: WholeDeclaration + } + }, + first_citation: extdeps.ietf.http_semantics.extdeps_external_authority_anchor, + further_citations: [rfc5789_patch_authority] +} + +// PATCH is not an RFC 9110 method: it is defined by RFC 5789, which is why it is cited separately. +data rfc5789_patch_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.rfc-editor.org/rfc/rfc5789" + } +} + +// The eight RFC 9110 section 9.3 method definitions in the RFC's order, then RFC 5789 PATCH. The +// IANA HTTP Method Registry also lists WebDAV and other extension methods; they are not modeled +// here, and a consumer reading a wire label outside this set refuses it by name +// (extdeps.http.server parse_http_method_label UnknownMethod). +type HttpMethod + = GET + | HEAD + | POST + | PUT + | DELETE + | CONNECT + | OPTIONS + | TRACE + | PATCH diff --git a/dag/extdeps/ntfy/ntfy.dag b/dag/extdeps/ntfy/ntfy.dag index c7b1da90a09..cde6cd852cc 100644 --- a/dag/extdeps/ntfy/ntfy.dag +++ b/dag/extdeps/ntfy/ntfy.dag @@ -3,8 +3,8 @@ module extdeps.ntfy.ntfy import std.types { NonEmptyStr, String, Int, Secret, List, Bool } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } -import v2.std.network { POST } import extdeps.transports.rest { RestOutcome, RestOk, RestStatusRefused, RestTransportRefused, RestBodyUndecodable } +import extdeps.ietf.http_semantics { POST } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/sec/edgar_rest.dag b/dag/extdeps/sec/edgar_rest.dag index c70e2a7e42b..e01799cb5db 100644 --- a/dag/extdeps/sec/edgar_rest.dag +++ b/dag/extdeps/sec/edgar_rest.dag @@ -1,6 +1,6 @@ module extdeps.sec.edgar_rest -import std.types { NonEmptyStr, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, AuthScheme } import extdeps.sec.edgar { CompanyConceptResponse, CompanyFactsResponse, @@ -9,6 +9,7 @@ import extdeps.sec.edgar { } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/tailscale/acl_api.dag b/dag/extdeps/tailscale/acl_api.dag index 65ebc3f2a56..2a2497b8c7e 100644 --- a/dag/extdeps/tailscale/acl_api.dag +++ b/dag/extdeps/tailscale/acl_api.dag @@ -1,8 +1,9 @@ module extdeps.tailscale.acl_api -import std.types { String, Secret, NonEmptyStr, HttpMethod, AuthScheme, HttpStatus } +import std.types { String, Secret, NonEmptyStr, AuthScheme, HttpStatus } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/tcgplayer/catalog.dag b/dag/extdeps/tcgplayer/catalog.dag index 46991f8b144..504960b82a1 100644 --- a/dag/extdeps/tcgplayer/catalog.dag +++ b/dag/extdeps/tcgplayer/catalog.dag @@ -1,10 +1,11 @@ module extdeps.tcgplayer.catalog -import std.types { NonEmptyStr, Timestamp, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, Timestamp, AuthScheme } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.coverage { CoverageClosureEntry, CoverageClosureFact } import extdeps.tcgplayer.tcgplayer { production_api_base, TcgplayerErrorEnvelope } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/tcgplayer/pricing.dag b/dag/extdeps/tcgplayer/pricing.dag index ff056c88a34..5b1933a11fb 100644 --- a/dag/extdeps/tcgplayer/pricing.dag +++ b/dag/extdeps/tcgplayer/pricing.dag @@ -1,11 +1,12 @@ module extdeps.tcgplayer.pricing -import std.types { NonEmptyStr, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, AuthScheme } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.tcgplayer.tcgplayer { production_api_base, TcgplayerErrorEnvelope } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/tcgplayer/store.dag b/dag/extdeps/tcgplayer/store.dag index 948cb743930..769fe76b3d4 100644 --- a/dag/extdeps/tcgplayer/store.dag +++ b/dag/extdeps/tcgplayer/store.dag @@ -1,11 +1,12 @@ module extdeps.tcgplayer.store -import std.types { NonEmptyStr, HttpMethod, AuthScheme } +import std.types { NonEmptyStr, AuthScheme } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, NamedField } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.tcgplayer.tcgplayer { production_api_base, TcgplayerErrorEnvelope } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/tcgplayer/tcgplayer.dag b/dag/extdeps/tcgplayer/tcgplayer.dag index 73fd0ec7346..5bea0bde3d6 100644 --- a/dag/extdeps/tcgplayer/tcgplayer.dag +++ b/dag/extdeps/tcgplayer/tcgplayer.dag @@ -1,9 +1,10 @@ module extdeps.tcgplayer.tcgplayer -import std.types { NonEmptyStr, HttpMethod, AuthScheme, Secret } +import std.types { NonEmptyStr, AuthScheme, Secret } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.coverage { CoverageClosureEntry, CoverageClosureFact } +import extdeps.ietf.http_semantics { HttpMethod } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { diff --git a/dag/extdeps/transports/rest.dag b/dag/extdeps/transports/rest.dag index a100f50be8f..a0e00cfe043 100644 --- a/dag/extdeps/transports/rest.dag +++ b/dag/extdeps/transports/rest.dag @@ -2,7 +2,7 @@ module extdeps.transports.rest import std.dissolution { DissolutionCondition, unbound_dissolution } -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, FermiDepth, NonEmptyStr, HttpStatus, Bool, String, Int, List } +import std.types { FermiDepth, NonEmptyStr, HttpStatus, Bool, String, Int, List } import std.content_hash { Fnv1a64Structural } import std.serialization { WireFormat } import std.fidelity { TransportFidelity } @@ -11,6 +11,7 @@ import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, UriScheme, Http, Https, Tftp, File, Ftp, Javascript, Data, Vbscript, Mailto } import v2.std.operation_argv { OperationRef } import v2.std.optional { Absent, Present } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -343,15 +344,20 @@ fn rest_exchange_fixture_lookup( } } +// TRACE is a safe method (RFC 9110 section 9.3.8), so it reads. CONNECT (9.3.6) is neither safe +// nor idempotent and opens a tunnel whose traffic the grant cannot see, so it demands the stronger +// verb rather than being admitted as a read. fn rest_method_verb(method: HttpMethod) -> std.effect_grant.Verb { match method { GET => Read HEAD => Read OPTIONS => Read + TRACE => Read POST => Write PUT => Write PATCH => Write DELETE => Write + CONNECT => Write } } diff --git a/dag/gunbc/auth/approval_broker.dag b/dag/gunbc/auth/approval_broker.dag index cb5fb0ea824..46f74c454a2 100644 --- a/dag/gunbc/auth/approval_broker.dag +++ b/dag/gunbc/auth/approval_broker.dag @@ -2,7 +2,6 @@ module gunbc.auth.approval_broker import std.types { NonEmptyStr, String, Timestamp } import std.logic { Bool } -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } import std.scoped_authorization { AttemptIdentity } import extdeps.tailscale.identity { TailnetIdentity, IdentityMatch, IdentityMatches, IdentityMismatch, IdentityMissing, @@ -15,6 +14,7 @@ import gunbc.auth.approval_capability { verify_capability, capability_refusal_reason, proposed_decision_name, } import extdeps.crypto.mac { MacVerification } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE } // ── What this module owns, and what it deliberately does not ───────────────────────────────── // THE LIFECYCLE IS SHARED; THE MEANING OF SUCCESS IS NOT. @@ -54,13 +54,15 @@ fn decision_method_admission(m: HttpMethod) -> DecisionMethodAdmission { GET => DecisionMethodRetrievesOnly { method: GET } HEAD => DecisionMethodRetrievesOnly { method: HEAD } OPTIONS => DecisionMethodRetrievesOnly { method: OPTIONS } + TRACE => DecisionMethodRetrievesOnly { method: TRACE } PUT => DecisionMethodUnsupported { method: PUT } PATCH => DecisionMethodUnsupported { method: PATCH } DELETE => DecisionMethodUnsupported { method: DELETE } + CONNECT => DecisionMethodUnsupported { method: CONNECT } } } -data safe_methods_must_not_decide_note: NonEmptyStr = "GET, HEAD and OPTIONS render the confirmation page and never consume the decision right. Link previews, mail scanners, chat unfurlers and browser prefetch all issue GETs at URLs nobody has looked at; a GET that decided would let any of them spend an approval and attribute it to the operator. The page's button POSTs." +data safe_methods_must_not_decide_note: NonEmptyStr = "GET, HEAD, OPTIONS and TRACE render the confirmation page and never consume the decision right. Link previews, mail scanners, chat unfurlers and browser prefetch all issue GETs at URLs nobody has looked at; a GET that decided would let any of them spend an approval and attribute it to the operator. The page's button POSTs." // ── Redeeming a capability ─────────────────────────────────────────────────────────────────── // The order is fixed and each stage narrows what the next one may assume: diff --git a/dag/gunbc/auth/approval_broker_serve.dag b/dag/gunbc/auth/approval_broker_serve.dag index 32202633871..b374fa5886c 100644 --- a/dag/gunbc/auth/approval_broker_serve.dag +++ b/dag/gunbc/auth/approval_broker_serve.dag @@ -35,7 +35,7 @@ import gunbc.auth.approval_broker_endpoint { approval_broker_status_path_template, approval_broker_confirm_path_prefix, } -import std.types { String, Int, Bool, List, NonEmptyStr, CommitSha, HttpMethod, GET, POST, HttpStatus } +import std.types { String, Int, Bool, List, NonEmptyStr, CommitSha, HttpStatus } import extdeps.uri_path { PathParamBinding, path_param_value } import std.markup { Fragment, attr } import gunbc.site.markup { txt, el, el_attrs, page_render_refusal_body } @@ -55,6 +55,7 @@ import extdeps.http.server { http_method_label_join, } import extdeps.languages.html { EmitOk, EmitRejected, try_serialize_html_source } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST } // ── THE APPROVAL BROKER IS ITS OWN SERVED PROCESS ──────────────────────────────────────────── // WHY THIS MODULE EXISTS AT ALL, stated as the receipt rather than as a preference (side-chat diff --git a/dag/gunbc/auth/approval_decision_store.dag b/dag/gunbc/auth/approval_decision_store.dag index aa7d21e1a65..b8603ff172c 100644 --- a/dag/gunbc/auth/approval_decision_store.dag +++ b/dag/gunbc/auth/approval_decision_store.dag @@ -1,7 +1,7 @@ module gunbc.auth.approval_decision_store import extdeps.numeric.base16 { base16_is_lower_digit_code_point } -import std.types { Bool, Int, List, NonEmptyStr, String, Timestamp, HttpMethod, Secret } +import std.types { Bool, Int, List, NonEmptyStr, String, Timestamp, Secret } import std.content_hash { ContentHash, serialize_content_hash } import std.scoped_authorization { AttemptIdentity, @@ -62,6 +62,7 @@ import gunbc.auth.approval_broker { redeem_capability, redemption_refusal_reason, } +import extdeps.ietf.http_semantics { HttpMethod } // THE DURABLE HALF OF THE APPROVAL LOOP: WHERE A REQUEST WAITS, WHERE A DECISION LANDS, AND THE // KEY THAT MAKES A TAPPED LINK MEAN SOMETHING. gunbc.auth.approval_broker owns the lifecycle and diff --git a/dag/gunbc/auth/approval_ntfy_deployment.dag b/dag/gunbc/auth/approval_ntfy_deployment.dag index 6db2b2284de..1daef21b64d 100644 --- a/dag/gunbc/auth/approval_ntfy_deployment.dag +++ b/dag/gunbc/auth/approval_ntfy_deployment.dag @@ -12,7 +12,6 @@ import extdeps.ntfy.ntfy { NtfyTopic, NtfyPublishReceipt, NtfyErrorShape } import extdeps.transports.rest { RestOutcome, RestOk } import extdeps.uri { Uri, Https } import std.types { Secret } -import v2.std.network { POST } import gunbc.executor_privileged_operation { ExecutorPrivilegedOperation, EnsureSystemAccount, executor_privileged_operation_converge_command, } @@ -21,6 +20,7 @@ import gunbc.managed_directory { DeclaredDirectoryMode, DirectoryOwnerAllGroupTr import gunbc.fleet_intent_network { fleet_intent_network, operator_host_srv1 } import extdeps.cloud.gcp.secret_ref { SecretRef, secret_ref_version_resource } import gunbc.secret_provision { fleet_secret_ref, secret_ref_pin_version } +import extdeps.ietf.http_semantics { POST } // ── The self-hosted notification server on srv1 ────────────────────────────────────────────── // It follows gunbc-publication exactly: its own system account, its own state directory owned by diff --git a/dag/gunbc/gunbhub_serve.dag b/dag/gunbc/gunbhub_serve.dag index dbd56625f27..5feb68193a8 100644 --- a/dag/gunbc/gunbhub_serve.dag +++ b/dag/gunbc/gunbhub_serve.dag @@ -1,6 +1,6 @@ module gunbc.gunbhub_serve -import std.types { HttpMethod, HttpStatus, GET } +import std.types { HttpStatus } import extdeps.uri_path { PathTemplate, UrlPathToken } import extdeps.uri_path { PathTemplateParseResult, @@ -32,6 +32,7 @@ import extdeps.http.server { ServedStaticRoute, ServedStaticRouteTable, } +import extdeps.ietf.http_semantics { HttpMethod, GET } type GunbhubServeRequest { method: HttpMethod diff --git a/dag/gunbc/instruments/approval_store_live_probe.dag b/dag/gunbc/instruments/approval_store_live_probe.dag index 170647032ac..a2c57d369f4 100644 --- a/dag/gunbc/instruments/approval_store_live_probe.dag +++ b/dag/gunbc/instruments/approval_store_live_probe.dag @@ -1,6 +1,6 @@ module tools.approval_store_live_probe -import std.types { Bool, NonEmptyStr, String, Timestamp, POST, Secret } +import std.types { Bool, NonEmptyStr, String, Timestamp, Secret } import std.process { ProcessExit, ExitSuccess, exit_failure } import std.content_hash { ContentHash, Sha256Hash, Sha256Digest } import std.scoped_authorization { AttemptIdentity, AuthorizationRequest, AuthorizationGranted, AuthorizationDenied, AuthorizationPending } @@ -20,6 +20,7 @@ import gunbc.auth.approval_decision_store { store_redemption_reason, grant_from_store, GrantDerived, GrantStillPending, GrantEscalationNotFiled, GrantForAnotherIntent, GrantStoreUnreadable, } +import extdeps.ietf.http_semantics { POST } // THE LIVE RECEIPT FOR THE EFFECTFUL HALF OF THE APPROVAL DECISION STORE, which the hermetic // witness (test.claim.approval_decision_store_witness_test) cannot reach: a request filed into a diff --git a/dag/gunbc/node_http_server_emit.dag b/dag/gunbc/node_http_server_emit.dag index c39f177901d..47817c8ae45 100644 --- a/dag/gunbc/node_http_server_emit.dag +++ b/dag/gunbc/node_http_server_emit.dag @@ -1,6 +1,5 @@ module gunbc.node_http_server_emit -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } import extdeps.uri_path { PathTemplate, LiteralToken, ParamToken } import extdeps.http.server { http_method_label, @@ -40,6 +39,7 @@ import extdeps.languages.typescript.program { ts_prefix, ts_spread, } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } // DISPOSITION (P5, UI-model migration): the belt-A ROADMAP dashboard emit is DELETED. Belt B (gunbc // serve / gunbc.roadmap_serve) retired it from deploy membership (live_deploy_spec_belt_b_note: the diff --git a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag index 6573c020a86..38943f5dd26 100644 --- a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag +++ b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag @@ -25,6 +25,8 @@ data surface_shorthand_preempts_resolved_identity: RecurringFailureMode = Recurr "THIRD SPECIMEN (2026-09-19, gunbc#11603, review 68098/68166). A bare `Https` or `Http`, imported by name from extdeps.uri, evaluates to NoSuchVariable in any closure that also reaches gunbc.cloudflare.r2_origin, while the same literal evaluates in a minimal module; v2.extdeps.coordination CoordinationEffectKind also declares an `Http` variant, and the resolved import is discarded for a bare-spelling lookup that the colliding family defeats. The module-qualified `extdeps.uri.Https` evaluates correctly in the same closure, which is the discriminator and the route #11603 now uses (the interim uri_scheme_is_https predicate was deleted). Still open: the bare imported form. Reproduce with `gunbc run --entry dag/test/claim/cloudflare_boot_origin_witness_test.dag --function readback_refuses_plain_http` after replacing the qualified reference in boot_origin.dag with the bare imported one.", + "FOURTH SPECIMEN (2026-09-24, gunbc#12185, found by side-chat review). The service-receiver stamp joins on the bare NAME, not the owning declaration: v1.compiler.infer infer_var_binding_kind stamps ServiceValueBinding when the service registry holds the local's name and the local's type is the nominal reference the scope fold inserted (local_is_service_root_binding), without comparing that reference's owner module to the registry row's. gunbc.runner_guest_image calls `Filesystem.Write(...)` and reads `.success`/`.error`, which is extdeps.filesystem.filesystem_io's service Filesystem. When #12185 first named a source for that bare read, it imported std.resources { Filesystem } (a resource whose capability is lowercase `write` with output `written`), and the module still type-checked and emitted, because the registry row matched by name. The import was corrected to extdeps.filesystem.filesystem_io { Filesystem }, checked by reading the call: `.success` and `.error` exist only on that service's Write, not on the resource's `write`. The join itself is unrepaired. THE DISCRIMINATING RED IS UNAUTHORABLE ON EVERY ROUTE TODAY: on the seed, the name-only join is the defect, so a mutant import back to std.resources type-checks and emits green; on v2, v2.extdeps.languages.dag has no resource production (the resource shape refuses at parse) and v2.compiler.namespace_graft places only fn/data/type declarations in the containment tree, so a service receiver has no declaring path to assert. Next rung, two capabilities, either of which makes the red authorable: (a) the seed's service stamp requires the local's resolved declaration and the registry row to be the SAME declaration (owner module plus name), so a two-declaration fixture refuses the mutant import at the call; (b) v2 grafts service declarations into the containment tree and gains a resource production, so the receiver's declaring path is assertable.", + "Next-rung trigger: host literals and intentional primitive representations are typed parser/resolver constructions, and evaluator value construction is total over every resolved binding with no raw-spelling arm able to override it.)", ], diff --git a/dag/gunbc/roadmap/roadmap_serve.dag b/dag/gunbc/roadmap/roadmap_serve.dag index 2df91a98157..e20232dc3ce 100644 --- a/dag/gunbc/roadmap/roadmap_serve.dag +++ b/dag/gunbc/roadmap/roadmap_serve.dag @@ -1,7 +1,7 @@ module gunbc.roadmap_serve import extdeps.tailscale.identity { tailnet_identity_of } -import std.types { String, Int, Bool, List, CommitSha, HttpMethod, GET, POST, HttpStatus } +import std.types { String, Int, Bool, List, CommitSha, HttpStatus } import extdeps.uri_path { PathParamBinding, path_param_value } import std.markup { Fragment } import std.disposition { Disposition, Scaffold, RealizationDispatch } @@ -108,6 +108,7 @@ import gunbc.roadmap_dashboard_instance { dashboard_instance_dispatch_worktree_root, dashboard_instance_dispatch_attempt_state_root, } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST } // The gunbc-served dashboard instantiation (docs/plans/gunbc-served-dashboard-design.md; belt B, // parent-approved 2026-07-20). H = RoadmapServeHandler: every row is a DynamicRoute whose handler diff --git a/dag/gunbc/roadmap/roadmap_static_site.dag b/dag/gunbc/roadmap/roadmap_static_site.dag index ac2bbf42c8b..169ebb039c4 100644 --- a/dag/gunbc/roadmap/roadmap_static_site.dag +++ b/dag/gunbc/roadmap/roadmap_static_site.dag @@ -1,6 +1,6 @@ module gunbc.roadmap_static_site -import std.types { String, Bool, List, CommitSha, NonEmptyStr, HttpMethod, GET, Port } +import std.types { String, Bool, List, CommitSha, NonEmptyStr, Port } import std.content_hash { Fnv1a64Structural, content_hash_atom } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } @@ -46,6 +46,7 @@ import extdeps.languages.json.emit { JsonKeyValue, } import extdeps.tools.curl { curl_localhost_bounded_get_argv_prefix } +import extdeps.ietf.http_semantics { HttpMethod, GET } type SiteArtifactKind = RoadmapMarkdown diff --git a/dag/gunbc/runner/runner_guest_image.dag b/dag/gunbc/runner/runner_guest_image.dag index 13ca6049664..e311bb46529 100644 --- a/dag/gunbc/runner/runner_guest_image.dag +++ b/dag/gunbc/runner/runner_guest_image.dag @@ -6,6 +6,7 @@ import std.measure { Mebibyte, mebibyte } import std.process { ProcessExit, ExitSuccess, exit_failure } import std.disposition { Disposition, SingleAuthority } import std.dissolution { DissolutionCondition, unbound_dissolution } +import extdeps.filesystem.filesystem_io { Filesystem } import extdeps.shell import extdeps.exec.command { ArgvCommand } import extdeps.crypto.hash { Digest, sha256_digest } diff --git a/dag/gunbc/stage0/stage0_crate_partition_generated.dag b/dag/gunbc/stage0/stage0_crate_partition_generated.dag index 1d5308abe2a..49acb153848 100644 --- a/dag/gunbc/stage0/stage0_crate_partition_generated.dag +++ b/dag/gunbc/stage0/stage0_crate_partition_generated.dag @@ -62,6 +62,7 @@ data generated_partition_crate_rows: List = [ "std_graph", "extdeps_uri", "extdeps_external_authority", + "extdeps_ietf_http_semantics", "std_process_termination", "std_primitive_projection", "extdeps_container_oci_digest", diff --git a/dag/std/effects.dag b/dag/std/effects.dag index 157d95d2b51..699fd53b755 100644 --- a/dag/std/effects.dag +++ b/dag/std/effects.dag @@ -1,11 +1,12 @@ module std.effects -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, String, List } +import std.types { String, List } import std.realization_schedule { string_list_eq } import extdeps.uri_path { PathTemplate, has_path_params, last_path_param } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE } type EffectShape = ReadEffect @@ -172,6 +173,7 @@ type DerivedOpEffect { type DeriveOpEffectResult = DerivedEffect { effect: DerivedOpEffect } + | MethodHasNoOperationEffect { operation_name: String, method: HttpMethod } | MalformedPathInput { operation_name: String, method: HttpMethod, @@ -180,29 +182,39 @@ type DeriveOpEffectResult reason: String } +// CONNECT (RFC 9110 section 9.3.6) asks the recipient to open a tunnel; it names no resource +// operation, so no EffectShape describes it. It is a typed MethodHasNoEffectShape rather than any +// shape arm -- a CreateEffect or ExecuteEffect here would be a fabricated classification that the +// idempotency obligations downstream would then trust. TRACE (9.3.8) is safe, so it reads. +type MethodEffectShape + = MethodShaped { shape: EffectShape } + | MethodHasNoEffectShape + fn derive_effect_shape( method: HttpMethod, path: PathTemplate -) -> EffectShape { +) -> MethodEffectShape { match method { - GET => ReadEffect - HEAD => ReadEffect - OPTIONS => ReadEffect - POST => CreateEffect { cause: PostAlways } + GET => MethodShaped { shape: ReadEffect } + HEAD => MethodShaped { shape: ReadEffect } + OPTIONS => MethodShaped { shape: ReadEffect } + TRACE => MethodShaped { shape: ReadEffect } + CONNECT => MethodHasNoEffectShape + POST => MethodShaped { shape: CreateEffect { cause: PostAlways } } DELETE => match last_path_param(template: path) { - Present { value: p } => DeleteEffect { key_source: PathParam { param: p } } - Absent => CreateEffect { cause: KeylessFallback { method: DELETE } } + Present { value: p } => MethodShaped { shape: DeleteEffect { key_source: PathParam { param: p } } } + Absent => MethodShaped { shape: CreateEffect { cause: KeylessFallback { method: DELETE } } } } PUT => match last_path_param(template: path) { - Present { value: p } => UpsertEffect { key_source: PathParam { param: p } } - Absent => CreateEffect { cause: KeylessFallback { method: PUT } } + Present { value: p } => MethodShaped { shape: UpsertEffect { key_source: PathParam { param: p } } } + Absent => MethodShaped { shape: CreateEffect { cause: KeylessFallback { method: PUT } } } } PATCH => match last_path_param(template: path) { - Present { value: p } => UpsertEffect { key_source: PathParam { param: p } } - Absent => CreateEffect { cause: KeylessFallback { method: PATCH } } + Present { value: p } => MethodShaped { shape: UpsertEffect { key_source: PathParam { param: p } } } + Absent => MethodShaped { shape: CreateEffect { cause: KeylessFallback { method: PATCH } } } } } } @@ -212,13 +224,17 @@ fn derive_op_effect( method: HttpMethod, path: PathTemplate ) -> DeriveOpEffectResult { - let shape = derive_effect_shape(method: method, path: path) - DerivedEffect { effect: DerivedOpEffect { - operation_name: operation_name, - method: method, - path_template: path, - shape: shape - } } + match derive_effect_shape(method: method, path: path) { + MethodShaped { shape: shape } => + DerivedEffect { effect: DerivedOpEffect { + operation_name: operation_name, + method: method, + path_template: path, + shape: shape + } } + MethodHasNoEffectShape => + MethodHasNoOperationEffect { operation_name: operation_name, method: method } + } } type ModifierAgreement @@ -284,7 +300,8 @@ fn check_modifier_vs_derivation( GET => Agrees HEAD => Agrees OPTIONS => Agrees - _ => Disagrees { reason: "readonly declared but method is not GET/HEAD/OPTIONS" } + TRACE => Agrees + _ => Disagrees { reason: "readonly declared but method is not GET/HEAD/OPTIONS/TRACE" } } } else { Agrees diff --git a/dag/std/types.dag b/dag/std/types.dag index ae2e32c2cf8..0891fa0a639 100644 --- a/dag/std/types.dag +++ b/dag/std/types.dag @@ -316,8 +316,6 @@ type BinaryFilePath = FilePath type MimeType = String -type HttpMethod = GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS - type AuthScheme = Bearer | Header { name: String } @@ -343,19 +341,6 @@ type FilesystemHandle = FilePath where brand("FilesystemHandle") type NetworkHandle = Unit where brand("NetworkHandle") type ToolHandle = String where brand("ToolHandle") -type TransportRequest { - method: HttpMethod - url: String - headers: Json - body: String -} - -type TransportResponse { - status: Int - headers: Json - body: String -} - type FileResponse { path: String success: Bool diff --git a/dag/test/claim/approval_broker_serve_witness_test.dag b/dag/test/claim/approval_broker_serve_witness_test.dag index ac5576de5e8..fca76f5b84b 100644 --- a/dag/test/claim/approval_broker_serve_witness_test.dag +++ b/dag/test/claim/approval_broker_serve_witness_test.dag @@ -1,7 +1,7 @@ module test.claim.approval_broker_serve_witness_test import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import std.types { Bool, String, NonEmptyStr, HttpMethod, GET, POST, PUT } +import std.types { Bool, String, NonEmptyStr } import extdeps.http.server { ServedRouteTable, ServedRouteTableBuilt, ServedRouteTableBuildRefused, @@ -40,6 +40,7 @@ import gunbc.auth.approval_broker_endpoint { approval_broker_submit_url, approval_broker_status_url, } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/approval_broker_witness_test.dag b/dag/test/claim/approval_broker_witness_test.dag index 6f632b1fb40..9e4b1734aa4 100644 --- a/dag/test/claim/approval_broker_witness_test.dag +++ b/dag/test/claim/approval_broker_witness_test.dag @@ -23,8 +23,9 @@ import extdeps.crypto.mac { } import extdeps.tailscale.identity { TailnetIdentity, TailnetIdentityAbsent, tailnet_identity_of } import std.scoped_authorization { AttemptIdentity } -import std.types { NonEmptyStr, Timestamp, HttpMethod, GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE } +import std.types { NonEmptyStr, Timestamp } import std.logic { Bool } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE, CONNECT } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -123,6 +124,32 @@ test fn head_and_options_do_not_decide_either() -> Bool { && !is_admitted(r: redeem_with(method: OPTIONS, identity: operator_identity())) } +// TRACE is safe (RFC 9110 section 9.3.8), so it is refused exactly as GET is: it retrieves and +// never decides. CONNECT (9.3.6) is neither safe nor a decision, so it is refused as UNSUPPORTED. +// Each is asserted at its exact refusal arm: the method gate is the wall under test, so a mutation +// that let either method commit must turn one of these red. +test fn trace_is_refused_as_a_safe_method() -> Bool { + match redeem_with(method: TRACE, identity: operator_identity()) { + RedemptionRefused { cause: c } => + match c { + RedemptionMethodNotCommitting { method: _ } => true + _ => false + } + RedemptionAdmitted { claims: _, decided_by: _ } => false + } +} + +test fn connect_is_refused_as_unsupported() -> Bool { + match redeem_with(method: CONNECT, identity: operator_identity()) { + RedemptionRefused { cause: c } => + match c { + RedemptionMethodUnsupported { method: _ } => true + _ => false + } + RedemptionAdmitted { claims: _, decided_by: _ } => false + } +} + // The safe-method refusal must READ as "not here", because the operator will see it when their // phone opens the link -- it is the confirmation page's normal path, not an error. test fn the_safe_method_refusal_points_at_the_button() -> Bool { diff --git a/dag/test/claim/approval_decision_store_witness_test.dag b/dag/test/claim/approval_decision_store_witness_test.dag index 9987eafdbfa..3a40330014a 100644 --- a/dag/test/claim/approval_decision_store_witness_test.dag +++ b/dag/test/claim/approval_decision_store_witness_test.dag @@ -1,7 +1,7 @@ module test.claim.approval_decision_store_witness_test import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import std.types { Bool, NonEmptyStr, String, Timestamp, POST, GET, Secret } +import std.types { Bool, NonEmptyStr, String, Timestamp, Secret } import std.scoped_authorization { AttemptIdentity } import extdeps.crypto.mac { MacKey, HmacSha256, mac_verify, MacVerified, MacTagMismatch, mac_key_material_hex_length, mac_suite_key_size } import std.measure { byte_size_count } @@ -18,6 +18,7 @@ import gunbc.auth.approval_decision_store { IdentityHeaderTrustEstablished, IdentityHeaderUnattributed, IdentityHeaderTrustNotEstablished, approval_identity_trust, approval_identity_trust_for, redeem_over_store, RedemptionIdentityUntrusted, } +import extdeps.ietf.http_semantics { POST, GET } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/effects_witness_test.dag b/dag/test/claim/effects_witness_test.dag index 8ee02192529..6774654fcb4 100644 --- a/dag/test/claim/effects_witness_test.dag +++ b/dag/test/claim/effects_witness_test.dag @@ -1,6 +1,6 @@ module test.claim.effects_witness_test -import std.types { HttpMethod, GET, POST, PUT, DELETE, String, List, Bool } +import std.types { String, List, Bool } import extdeps.uri_path { PathTemplate, has_path_params, last_path_param } import extdeps.uri_path { parse_path_template, @@ -11,7 +11,7 @@ import std.effects { EffectShape, ReadEffect, UpsertEffect, DeleteEffect, CreateEffect, AppendEffect, CreateCause, PostAlways, CreateIfAbsent, KeylessFallback, KeySource, PathParam, InputField, - DerivedOpEffect, DeriveOpEffectResult, DerivedEffect, MalformedPathInput, + DerivedOpEffect, DeriveOpEffectResult, DerivedEffect, MalformedPathInput, MethodHasNoOperationEffect, derive_op_effect, check_modifier_vs_derivation, compose_effects, OperationEffect, CompositionVerdict, IdempotentComposition, BrokenBy, @@ -20,6 +20,7 @@ import std.effects { is_idempotent_effect, create_effect_is_dedupable, create_double_init_collapsible, } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, DELETE } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -37,6 +38,12 @@ fn derived_op(name: String, method: HttpMethod, path: PathTemplate) -> DerivedOp path_template: path, shape: AppendEffect } + MethodHasNoOperationEffect => DerivedOpEffect { + operation_name: name, + method: method, + path_template: path, + shape: AppendEffect + } } } @@ -150,6 +157,7 @@ fn derived_raw_cause_is_keyless_with_method(name: String, method: HttpMethod, ra match derive_op_effect(operation_name: name, method: method, path: path) { DerivedEffect { effect: effect } => derived_cause_is_keyless_with_method(shape: effect.shape, expected: expected) MalformedPathInput => false + MethodHasNoOperationEffect => false } MalformedPathTemplate => false } @@ -161,6 +169,7 @@ fn derived_raw_cause_is_post_always(name: String, method: HttpMethod, raw: Strin match derive_op_effect(operation_name: name, method: method, path: path) { DerivedEffect { effect: effect } => derived_cause_is_post_always(shape: effect.shape) MalformedPathInput => false + MethodHasNoOperationEffect => false } MalformedPathTemplate => false } @@ -274,6 +283,7 @@ test fn effects_derivation_witnesses() -> Bool { match derive_op_effect(operation_name: "TypedBoundary", method: PUT, path: path) { DerivedEffect { effect: effect } => true MalformedPathInput => false + MethodHasNoOperationEffect => false } MalformedPathTemplate => false } diff --git a/dag/test/claim/gunbhub_serve_witness_test.dag b/dag/test/claim/gunbhub_serve_witness_test.dag index 3913f757803..c2868b9f9c3 100644 --- a/dag/test/claim/gunbhub_serve_witness_test.dag +++ b/dag/test/claim/gunbhub_serve_witness_test.dag @@ -27,8 +27,8 @@ import gunbc.gunbhub_serve { } import gunbc.gunbhub_serve_html { gunbhub_html_render_keystone_holds } import std.markup { Fragment } -import std.types { GET, POST } import v2.std.live_tree { LiveTreeDisposition } +import extdeps.ietf.http_semantics { GET, POST } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/http_method_set_witness_test.dag b/dag/test/claim/http_method_set_witness_test.dag new file mode 100644 index 00000000000..b7b0f94265b --- /dev/null +++ b/dag/test/claim/http_method_set_witness_test.dag @@ -0,0 +1,76 @@ +module test.claim.http_method_set_witness + +import std.types { String, Bool } +import extdeps.ietf.http_semantics { HttpMethod, GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH } +import extdeps.http.server { HttpMethodParse, KnownMethod, UnknownMethod, parse_http_method_label, http_method_label } +import extdeps.transports.rest { rest_method_verb } +import std.effect_grant { Read, Write, verb_eq } +import extdeps.uri_path { ParsedPathTemplate, MalformedPathTemplate, parse_path_template } +import std.effects { + DeriveOpEffectResult, DerivedEffect, MalformedPathInput, MethodHasNoOperationEffect, + ReadEffect, derive_op_effect +} + +// THE ONE METHOD SET, read through its real consumers. Before extdeps.ietf.http_semantics, the +// wire parser, the label renderer, the grant verb and the effect derivation were all written over +// std.types HttpMethod's seven arms, so a request labelled "CONNECT" or "TRACE" parsed as +// UnknownMethod: the witness below that those two labels now parse as KnownMethod is red against +// that tree and green on this one. + +fn label_round_trips(m: HttpMethod) -> Bool { + match parse_http_method_label(label: http_method_label(method: m)) { + KnownMethod { method: back } => http_method_label(method: back) == http_method_label(method: m) + UnknownMethod { label: _ } => false + } +} + +fn label_is_known(label: String) -> Bool { + match parse_http_method_label(label: label) { + KnownMethod { method: _ } => true + UnknownMethod { label: _ } => false + } +} + +test fn connect_and_trace_parse_as_known_methods() -> Bool { + label_is_known(label: "CONNECT") + && label_is_known(label: "TRACE") + && !label_is_known(label: "PROPFIND") +} + +test fn every_method_label_round_trips() -> Bool { + label_round_trips(m: GET) + && label_round_trips(m: HEAD) + && label_round_trips(m: POST) + && label_round_trips(m: PUT) + && label_round_trips(m: DELETE) + && label_round_trips(m: CONNECT) + && label_round_trips(m: OPTIONS) + && label_round_trips(m: TRACE) + && label_round_trips(m: PATCH) +} + +test fn trace_reads_and_connect_needs_write() -> Bool { + verb_eq(a: rest_method_verb(method: TRACE), b: Read) + && verb_eq(a: rest_method_verb(method: CONNECT), b: Write) +} + +test fn connect_derives_no_operation_effect_and_trace_reads() -> Bool { + match parse_path_template(raw: "/tunnel") { + ParsedPathTemplate { template: path } => + (match derive_op_effect(operation_name: "Tunnel", method: CONNECT, path: path) { + MethodHasNoOperationEffect { operation_name: _, method: _ } => true + DerivedEffect { effect: _ } => false + MalformedPathInput => false + }) + && (match derive_op_effect(operation_name: "Trace", method: TRACE, path: path) { + DerivedEffect { effect: e } => + match e.shape { + ReadEffect => true + _ => false + } + MethodHasNoOperationEffect { operation_name: _, method: _ } => false + MalformedPathInput => false + }) + MalformedPathTemplate => false + } +} diff --git a/dag/test/claim/http_serve_route_witness_test.dag b/dag/test/claim/http_serve_route_witness_test.dag index 539ec7be7be..1215367dc80 100644 --- a/dag/test/claim/http_serve_route_witness_test.dag +++ b/dag/test/claim/http_serve_route_witness_test.dag @@ -15,7 +15,8 @@ import extdeps.uri_path { } import extdeps.uri_path { PathTemplate, PathParamBinding } import gunbc.node_http_server_emit { emit_path_template_regex_source } -import std.types { String, Bool, List, Int, HttpMethod, GET, POST } +import std.types { String, Bool, List, Int } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST } type ProbeHandler = ProbePage diff --git a/dag/test/claim/instrument_motion_page_witness_test.dag b/dag/test/claim/instrument_motion_page_witness_test.dag index 1aa76f16f10..a6eb401a028 100644 --- a/dag/test/claim/instrument_motion_page_witness_test.dag +++ b/dag/test/claim/instrument_motion_page_witness_test.dag @@ -27,7 +27,8 @@ import gunbc.design.instrument_bob { } import extdeps.http.server { ServeHttpRequest, ServeHttpResponse } import test.claim.live_deploy.deploy_release_fixture { deploy_witness_release_revision } -import std.types { String, Int, Bool, List, HttpMethod, GET } +import std.types { String, Int, Bool, List } +import extdeps.ietf.http_semantics { HttpMethod, GET } data witness_subject_note: String = "THE MOTION LAB'S VERTICAL SLICE, exercised through the route rather than through the page function, because the thing that can break in production is the composition — a handler arm that renders the wrong page, an asset route serving the wrong body, a page whose attributes and whose client program have drifted apart. Calling instrument_motion_page() directly would prove the fragment and leave every seam above it untested. diff --git a/dag/test/claim/rest_exchange_replay_test.dag b/dag/test/claim/rest_exchange_replay_test.dag index 9a5f23cc948..3cd652dddfe 100644 --- a/dag/test/claim/rest_exchange_replay_test.dag +++ b/dag/test/claim/rest_exchange_replay_test.dag @@ -1,6 +1,6 @@ module test.claim.rest_exchange_replay_test -import std.types { Bool, String, List, GET, Int } +import std.types { Bool, String, List, Int } import std.content_hash { Fnv1a64Structural } import std.materialization_ladder { Frame, SharedStateFrame } import std.effect_grant { @@ -24,6 +24,7 @@ import v2.std.witness_evaluation { import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import extdeps.github.workflow_runs { WorkflowRunList, Completed, Success } import test.fixture.rest_exchange_replay_probe +import extdeps.ietf.http_semantics { GET } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/roadmap/roadmap_serve_witness_test.dag b/dag/test/claim/roadmap/roadmap_serve_witness_test.dag index 1b0b174a1d8..4393ef9423e 100644 --- a/dag/test/claim/roadmap/roadmap_serve_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_serve_witness_test.dag @@ -56,7 +56,7 @@ import extdeps.http.server { } import extdeps.uri_path { PathParamBinding, path_param_value } import gunbc.auth.approval_broker_serve { approval_broker_route_specs } -import std.types { String, Int, Bool, List, HttpMethod, GET, POST, PUT } +import std.types { String, Int, Bool, List } import gunbc.roadmap_authority { RoadmapAuthorityProjection, RoadmapAuthorityProjected, RoadmapAuthorityProjectionRefused, } @@ -69,6 +69,7 @@ import gunbc.v1_deletion_plan { v1_exit_finish_lines, v1_finish_lines_accepted_c import gunbc.roadmap_model { RoadmapNodeId } import gunbc.auth.approval_status_wire { filed_approval_status_text, StatusNotFiled } import extdeps.languages.html { EmitOk, EmitRejected, try_serialize_html_source } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT } // Test-local extraction: a Refused build yields the EMPTY table, so every selection witness below // goes RED (nothing selects) — the discriminating direction; the production path never does this diff --git a/src/v1/effect_derivation.dag b/src/v1/effect_derivation.dag index 8ae2e9611e4..3efcfa95892 100644 --- a/src/v1/effect_derivation.dag +++ b/src/v1/effect_derivation.dag @@ -1,11 +1,12 @@ module v1.compiler.effect_derivation -import std.types { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } +import extdeps.ietf.http_semantics { HttpMethod, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } import std.effects { EffectShape, ReadEffect, UpsertEffect, DeleteEffect, CreateEffect, AppendEffect, ExecuteEffect, KeySource, PathParam, InputField, CompositeKey, - DerivedOpEffect, DeriveOpEffectResult, DerivedEffect, MalformedPathInput, + DerivedOpEffect, DeriveOpEffectResult, DerivedEffect, MalformedPathInput, MethodHasNoOperationEffect, + MethodEffectShape, MethodShaped, MethodHasNoEffectShape, ModifierAgreement, Agrees, Disagrees, DerivationUnknown, ModifierCheck, IdempotencyTestObligation, diff --git a/src/v1/stage0/src/emitted_population.rs b/src/v1/stage0/src/emitted_population.rs index 3a587ab768e..6e1c13dea52 100644 --- a/src/v1/stage0/src/emitted_population.rs +++ b/src/v1/stage0/src/emitted_population.rs @@ -7,6 +7,7 @@ // src/extdeps_currency_currency.rs // src/extdeps_external_authority.rs // src/extdeps_filesystem_rust_realization.rs +// src/extdeps_ietf_http_semantics.rs // src/extdeps_languages_dag_emit.rs // src/extdeps_languages_dag_syntax.rs // src/extdeps_languages_dag_types.rs diff --git a/src/v1/stage0/src/extdeps_ietf_http_semantics.rs b/src/v1/stage0/src/extdeps_ietf_http_semantics.rs new file mode 100644 index 00000000000..361f5620ecd --- /dev/null +++ b/src/v1/stage0/src/extdeps_ietf_http_semantics.rs @@ -0,0 +1,99 @@ +// Generated by v1 compiler -- do not edit. +// Source module: extdeps.ietf.http_semantics + +use self::HttpMethod::*; +pub use crate::extdeps_external_authority::{ + ExternalAuthority, ExternalModelScope, ExternalSubjectRef, +}; +use crate::extdeps_uri::UriScheme::Https; +pub use crate::extdeps_uri::{Uri, UriScheme}; +use crate::std_decl_ref::DeclField::WholeDeclaration; +pub use crate::std_decl_ref::{DeclField, DeclarationRef}; +use crate::v1_rt; +use crate::v1_rt::{VecCompat, VecJoin}; +use crate::NonEmptyBTreeSet; +use crate::NonEmptyVec; +use im::{vector as vec, HashMap, OrdSet as BTreeSet, Vector as Vec}; +use std::rc::Rc; + +pub fn extdeps_external_authority_anchor() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(ExternalAuthority { + uri: Rc::new(Uri { + scheme: UriScheme::Https, + locator: "www.rfc-editor.org/rfc/rfc9110#section-9".to_string(), + }), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + +pub fn extdeps_model_scope() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(ExternalModelScope { + subject: Rc::new(ExternalSubjectRef { + declaration: Rc::new(DeclarationRef { + module_path: "extdeps.ietf.http_semantics".to_string(), + decl_name: "HttpMethod".to_string(), + field: Rc::new(DeclField::WholeDeclaration), + }), + }), + first_citation: crate::extdeps_ietf_http_semantics::extdeps_external_authority_anchor(), + further_citations: Rc::new(vec![rfc5789_patch_authority()]), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + +pub fn rfc5789_patch_authority() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(ExternalAuthority { + uri: Rc::new(Uri { + scheme: UriScheme::Https, + locator: "www.rfc-editor.org/rfc/rfc5789".to_string(), + }), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] +#[serde(tag = "_variant")] +pub enum HttpMethod { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct GET; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct HEAD; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct POST; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct PUT; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct DELETE; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CONNECT; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct OPTIONS; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct TRACE; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct PATCH; diff --git a/src/v1/stage0/src/gunbc_stage0_crate_partition_generated.rs b/src/v1/stage0/src/gunbc_stage0_crate_partition_generated.rs index 0e76177cceb..8c4426b1c82 100644 --- a/src/v1/stage0/src/gunbc_stage0_crate_partition_generated.rs +++ b/src/v1/stage0/src/gunbc_stage0_crate_partition_generated.rs @@ -54,7 +54,7 @@ pub fn generated_partition_crate_rows() -> Rc package_name: "v1-stage0-std-core".to_string(), crate_dir: "src/v1/stage0_std_core".to_string(), kind: GeneratedPartitionCrateKind::GeneratedLayeredCoreCrate, - modules: Rc::new(vec!["std_content_hash".to_string(), "std_coercion".to_string(), "extdeps_currency_currency".to_string(), "std_decl_ref".to_string(), "std_keyed_row".to_string(), "std_keyed_roster".to_string(), "std_roster_frontier".to_string(), "std_dissolution".to_string(), "std_disposition".to_string(), "std_error_primitives".to_string(), "std_emit_model".to_string(), "std_magnitude".to_string(), "std_measure".to_string(), "std_types".to_string(), "std_unicode_types".to_string(), "std_algebra".to_string(), "std_nat".to_string(), "std_node".to_string(), "std_syntax".to_string(), "std_computation".to_string(), "std_termination".to_string(), "std_checked_arithmetic".to_string(), "std_induction".to_string(), "std_graph".to_string(), "extdeps_uri".to_string(), "extdeps_external_authority".to_string(), "std_process_termination".to_string(), "std_primitive_projection".to_string(), "extdeps_container_oci_digest".to_string(), "extdeps_units_dimensionless".to_string(), "extdeps_units_iec_80000_13".to_string(), "extdeps_units_iso8601".to_string(), "extdeps_units_iso_80000_3".to_string(), "std_occurrence_identity".to_string(), "std_source_annotation".to_string(), "std_target_representation".to_string(), "std_literal_elaboration".to_string(), "std_operator_realization".to_string(), "std_import".to_string(), "v1_std_core".to_string(), "gunbc_rust_emitted_edge".to_string()]), + modules: Rc::new(vec!["std_content_hash".to_string(), "std_coercion".to_string(), "extdeps_currency_currency".to_string(), "std_decl_ref".to_string(), "std_keyed_row".to_string(), "std_keyed_roster".to_string(), "std_roster_frontier".to_string(), "std_dissolution".to_string(), "std_disposition".to_string(), "std_error_primitives".to_string(), "std_emit_model".to_string(), "std_magnitude".to_string(), "std_measure".to_string(), "std_types".to_string(), "std_unicode_types".to_string(), "std_algebra".to_string(), "std_nat".to_string(), "std_node".to_string(), "std_syntax".to_string(), "std_computation".to_string(), "std_termination".to_string(), "std_checked_arithmetic".to_string(), "std_induction".to_string(), "std_graph".to_string(), "extdeps_uri".to_string(), "extdeps_external_authority".to_string(), "extdeps_ietf_http_semantics".to_string(), "std_process_termination".to_string(), "std_primitive_projection".to_string(), "extdeps_container_oci_digest".to_string(), "extdeps_units_dimensionless".to_string(), "extdeps_units_iec_80000_13".to_string(), "extdeps_units_iso8601".to_string(), "extdeps_units_iso_80000_3".to_string(), "std_occurrence_identity".to_string(), "std_source_annotation".to_string(), "std_target_representation".to_string(), "std_literal_elaboration".to_string(), "std_operator_realization".to_string(), "std_import".to_string(), "v1_std_core".to_string(), "gunbc_rust_emitted_edge".to_string()]), reexport_packages: Rc::new(vec!["v1-stage0-runtime".to_string()]), carries_non_empty_wrappers: false, }), Rc::new(GeneratedPartitionCrateRow { diff --git a/src/v1/stage0/src/std_effects.rs b/src/v1/stage0/src/std_effects.rs index 61903817bf3..bdf660b3030 100644 --- a/src/v1/stage0/src/std_effects.rs +++ b/src/v1/stage0/src/std_effects.rs @@ -7,12 +7,16 @@ use self::DeriveOpEffectResult::*; use self::EffectShape::*; use self::IdempotencyEvidence::*; use self::KeySource::*; +use self::MethodEffectShape::*; use self::ModifierAgreement::*; +pub use crate::extdeps_ietf_http_semantics::HttpMethod; +use crate::extdeps_ietf_http_semantics::HttpMethod::{ + CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, +}; pub use crate::extdeps_uri_path::PathTemplate; pub use crate::extdeps_uri_path::{has_path_params, last_path_param}; pub use crate::std_realization_schedule::string_list_eq; -use crate::std_types::HttpMethod::{DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT}; -pub use crate::std_types::{HttpMethod, List}; +pub use crate::std_types::List; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; use crate::NonEmptyBTreeSet; @@ -239,6 +243,10 @@ pub enum DeriveOpEffectResult { DerivedEffect { effect: Rc, }, + MethodHasNoOperationEffect { + operation_name: String, + method: HttpMethod, + }, MalformedPathInput { operation_name: String, method: HttpMethod, @@ -248,44 +256,82 @@ pub enum DeriveOpEffectResult { }, } -pub fn derive_effect_shape( - method: HttpMethod, - path: Rc, -) -> Rc>> { +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum MethodEffectShape { + MethodShaped { + shape: Rc>>, + }, + MethodHasNoEffectShape, +} +impl MethodEffectShape { + pub fn shape(&self) -> Rc>> { + match self { + MethodEffectShape::MethodShaped { shape: __val, .. } => __val.clone(), + MethodEffectShape::MethodHasNoEffectShape => panic!("no shape on unit variant"), + } + } +} + +pub fn derive_effect_shape(method: HttpMethod, path: Rc) -> Rc { match method.clone() { - HttpMethod::GET => Rc::new(EffectShape::ReadEffect), - HttpMethod::HEAD => Rc::new(EffectShape::ReadEffect), - HttpMethod::OPTIONS => Rc::new(EffectShape::ReadEffect), - HttpMethod::POST => Rc::new(EffectShape::CreateEffect { - cause: Rc::new(CreateCause::PostAlways), + HttpMethod::GET => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::ReadEffect), + }), + HttpMethod::HEAD => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::ReadEffect), + }), + HttpMethod::OPTIONS => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::ReadEffect), + }), + HttpMethod::TRACE => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::ReadEffect), + }), + HttpMethod::CONNECT => Rc::new(MethodEffectShape::MethodHasNoEffectShape), + HttpMethod::POST => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::CreateEffect { + cause: Rc::new(CreateCause::PostAlways), + }), }), HttpMethod::DELETE => match crate::extdeps_uri_path::last_path_param(path.clone()) { - Some(p) => Rc::new(EffectShape::DeleteEffect { - key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + Some(p) => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::DeleteEffect { + key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + }), }), - std::option::Option::None => Rc::new(EffectShape::CreateEffect { - cause: Rc::new(CreateCause::KeylessFallback { - method: HttpMethod::DELETE, + std::option::Option::None => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::CreateEffect { + cause: Rc::new(CreateCause::KeylessFallback { + method: HttpMethod::DELETE, + }), }), }), }, HttpMethod::PUT => match crate::extdeps_uri_path::last_path_param(path.clone()) { - Some(p) => Rc::new(EffectShape::UpsertEffect { - key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + Some(p) => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::UpsertEffect { + key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + }), }), - std::option::Option::None => Rc::new(EffectShape::CreateEffect { - cause: Rc::new(CreateCause::KeylessFallback { - method: HttpMethod::PUT, + std::option::Option::None => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::CreateEffect { + cause: Rc::new(CreateCause::KeylessFallback { + method: HttpMethod::PUT, + }), }), }), }, HttpMethod::PATCH => match crate::extdeps_uri_path::last_path_param(path.clone()) { - Some(p) => Rc::new(EffectShape::UpsertEffect { - key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + Some(p) => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::UpsertEffect { + key_source: Rc::new(KeySource::PathParam { param: p.clone() }), + }), }), - std::option::Option::None => Rc::new(EffectShape::CreateEffect { - cause: Rc::new(CreateCause::KeylessFallback { - method: HttpMethod::PATCH, + std::option::Option::None => Rc::new(MethodEffectShape::MethodShaped { + shape: Rc::new(EffectShape::CreateEffect { + cause: Rc::new(CreateCause::KeylessFallback { + method: HttpMethod::PATCH, + }), }), }), }, @@ -297,16 +343,23 @@ pub fn derive_op_effect( method: HttpMethod, path: Rc, ) -> Rc { - { - let shape = derive_effect_shape(method.clone(), path.clone()); - Rc::new(DeriveOpEffectResult::DerivedEffect { - effect: Rc::new(DerivedOpEffect { + match (*derive_effect_shape(method.clone(), path.clone())).clone() { + MethodEffectShape::MethodShaped { shape: shape, .. } => { + Rc::new(DeriveOpEffectResult::DerivedEffect { + effect: Rc::new(DerivedOpEffect { + operation_name: operation_name.clone(), + method: method.clone(), + path_template: path.clone(), + shape: shape.clone(), + }), + }) + } + MethodEffectShape::MethodHasNoEffectShape => { + Rc::new(DeriveOpEffectResult::MethodHasNoOperationEffect { operation_name: operation_name.clone(), method: method.clone(), - path_template: path.clone(), - shape: shape.clone(), - }), - }) + }) + } } } @@ -397,9 +450,11 @@ pub fn check_modifier_vs_derivation( HttpMethod::GET => Rc::new(ModifierAgreement::Agrees), HttpMethod::HEAD => Rc::new(ModifierAgreement::Agrees), HttpMethod::OPTIONS => Rc::new(ModifierAgreement::Agrees), + HttpMethod::TRACE => Rc::new(ModifierAgreement::Agrees), _ => Rc::new(ModifierAgreement::Disagrees { - reason: "readonly declared but method is not GET/HEAD/OPTIONS" - .to_string(), + reason: + "readonly declared but method is not GET/HEAD/OPTIONS/TRACE" + .to_string(), }), } } else { diff --git a/src/v1/stage0/src/std_types.rs b/src/v1/stage0/src/std_types.rs index 992a486035b..181cdad4b55 100644 --- a/src/v1/stage0/src/std_types.rs +++ b/src/v1/stage0/src/std_types.rs @@ -5,7 +5,6 @@ use self::AuthScheme::*; use self::Bool::*; use self::DocSourceKind::*; use self::FermiDepth::*; -use self::HttpMethod::*; use self::TopologyNodeKind::*; pub use crate::std_algebra::{ algebra_type_param_names, carrier_container_algebra_rows, carrier_container_alias_rows, @@ -442,20 +441,6 @@ pub type BinaryFilePath = String; pub type MimeType = String; -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] -#[serde(tag = "_variant")] -pub enum HttpMethod { - GET, - POST, - PUT, - PATCH, - DELETE, - HEAD, - OPTIONS, -} - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "_variant")] pub enum AuthScheme { @@ -488,21 +473,6 @@ pub type NetworkHandle = (); pub type ToolHandle = String; -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct TransportRequest { - pub method: HttpMethod, - pub url: String, - pub headers: serde_json::Value, - pub body: String, -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct TransportResponse { - pub status: i64, - pub headers: serde_json::Value, - pub body: String, -} - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FileResponse { pub path: String, @@ -640,17 +610,3 @@ pub struct M; pub struct L; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Xl; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct GET; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct POST; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct PUT; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct PATCH; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct DELETE; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct HEAD; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct OPTIONS; diff --git a/src/v1/stage0/src/v1_compiler_effect_derivation.rs b/src/v1/stage0/src/v1_compiler_effect_derivation.rs index 987919eefa4..1709fe016be 100644 --- a/src/v1/stage0/src/v1_compiler_effect_derivation.rs +++ b/src/v1/stage0/src/v1_compiler_effect_derivation.rs @@ -1,17 +1,24 @@ // Generated by v1 compiler -- do not edit. // Source module: v1.compiler.effect_derivation +pub use crate::extdeps_ietf_http_semantics::HttpMethod; +use crate::extdeps_ietf_http_semantics::HttpMethod::{ + DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, +}; use crate::extdeps_uri_path::PathTemplateParseResult::{MalformedPathTemplate, ParsedPathTemplate}; use crate::extdeps_uri_path::UrlPathToken::{LiteralToken, ParamToken}; pub use crate::extdeps_uri_path::{has_path_params, last_path_param, parse_path_template}; pub use crate::extdeps_uri_path::{PathTemplate, PathTemplateParseResult, UrlPathToken}; use crate::std_effects::CompositionVerdict::{BrokenBy, IdempotentComposition}; -use crate::std_effects::DeriveOpEffectResult::{DerivedEffect, MalformedPathInput}; +use crate::std_effects::DeriveOpEffectResult::{ + DerivedEffect, MalformedPathInput, MethodHasNoOperationEffect, +}; use crate::std_effects::EffectShape::{ AppendEffect, CreateEffect, DeleteEffect, ExecuteEffect, ReadEffect, UpsertEffect, }; use crate::std_effects::IdempotencyEvidence::{IdentityEffect, LatticeEffect, NonIdempotent}; use crate::std_effects::KeySource::{CompositeKey, InputField, PathParam}; +use crate::std_effects::MethodEffectShape::{MethodHasNoEffectShape, MethodShaped}; use crate::std_effects::ModifierAgreement::{Agrees, DerivationUnknown, Disagrees}; pub use crate::std_effects::{ check_modifier_vs_derivation, compose_effects, derive_effect_shape, derive_op_effect, @@ -19,11 +26,9 @@ pub use crate::std_effects::{ }; pub use crate::std_effects::{ CompositionVerdict, DeriveOpEffectResult, DerivedOpEffect, EffectShape, IdempotencyEvidence, - IdempotencyTestObligation, KeySource, ModifierAgreement, ModifierCheck, OperationEffect, - WorkflowEffectConcern, + IdempotencyTestObligation, KeySource, MethodEffectShape, ModifierAgreement, ModifierCheck, + OperationEffect, WorkflowEffectConcern, }; -pub use crate::std_types::HttpMethod; -use crate::std_types::HttpMethod::{DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT}; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; use crate::NonEmptyBTreeSet; diff --git a/src/v1/stage0_std_core/src/lib.rs b/src/v1/stage0_std_core/src/lib.rs index 851f07809be..39aaed21e0c 100644 --- a/src/v1/stage0_std_core/src/lib.rs +++ b/src/v1/stage0_std_core/src/lib.rs @@ -99,6 +99,9 @@ pub mod extdeps_uri; #[path = "../../stage0/src/extdeps_external_authority.rs"] pub mod extdeps_external_authority; #[rustfmt::skip] +#[path = "../../stage0/src/extdeps_ietf_http_semantics.rs"] +pub mod extdeps_ietf_http_semantics; +#[rustfmt::skip] #[path = "../../stage0/src/std_process_termination.rs"] pub mod std_process_termination; #[rustfmt::skip] diff --git a/src/v2/std/network.dag b/src/v2/std/network.dag index 0690ec284f3..ba4d8cd38a8 100644 --- a/src/v2/std/network.dag +++ b/src/v2/std/network.dag @@ -6,17 +6,6 @@ import v2.std.optional { import v2.std.text { String } import v2.std.nat { Nat } -type HttpMethod - = GET - | POST - | PUT - | PATCH - | DELETE - | HEAD - | OPTIONS - | CONNECT - | TRACE - type Rfc3986AbnfProduction { name: String, expression: String, diff --git a/src/v2/workflow/rust_crate_partition.dag b/src/v2/workflow/rust_crate_partition.dag index a4a9b340ffd..689130b78bd 100644 --- a/src/v2/workflow/rust_crate_partition.dag +++ b/src/v2/workflow/rust_crate_partition.dag @@ -330,6 +330,7 @@ fn stage0_std_core_modules() -> List { "std_graph", "extdeps_uri", "extdeps_external_authority", + "extdeps_ietf_http_semantics", "std_process_termination", "std_primitive_projection", "extdeps_container_oci_digest", @@ -552,6 +553,7 @@ fn stage0_cross_unit_import_edges() -> List { stage0_module_dag_edge(from: "std_measure", to: "extdeps_units_iso8601"), stage0_module_dag_edge(from: "std_measure", to: "extdeps_units_iso_80000_3"), stage0_module_dag_edge(from: "std_effects", to: "std_types"), + stage0_module_dag_edge(from: "std_effects", to: "extdeps_ietf_http_semantics"), stage0_module_dag_edge(from: "std_execution_mode", to: "std_types"), stage0_module_dag_edge(from: "std_interface_summary", to: "std_decl_ref"), stage0_module_dag_edge(from: "std_interface_summary", to: "std_disposition"), @@ -562,6 +564,7 @@ fn stage0_cross_unit_import_edges() -> List { stage0_module_dag_edge(from: "extdeps_uri", to: "std_unicode_types"), stage0_module_dag_edge(from: "std_unicode_types", to: "std_types"), stage0_module_dag_edge(from: "extdeps_external_authority", to: "extdeps_uri"), + stage0_module_dag_edge(from: "extdeps_ietf_http_semantics", to: "extdeps_external_authority"), stage0_module_dag_edge(from: "extdeps_external_authority", to: "std_roster_frontier"), stage0_module_dag_edge(from: "std_keyed_row", to: "std_types"), stage0_module_dag_edge(from: "std_keyed_roster", to: "std_keyed_row"),