diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d31ecaac3..9288bb9f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -91,6 +91,8 @@ jobs: - 'examples/cloudnativepg/**' - 'hack/e2e-workspace-lifecycle*.sh' - '.github/workflows/ci.yaml' + # Coder SDK bumps change how every aggregated-API call reaches the backend. + - 'vendor/github.com/coder/coder/v2/**' installer: - 'api/**/*.go' - 'config/crd/bases/**' diff --git a/docs/how-to/mcp-server.md b/docs/how-to/mcp-server.md index 34a02095d..5996a0f95 100644 --- a/docs/how-to/mcp-server.md +++ b/docs/how-to/mcp-server.md @@ -73,16 +73,17 @@ curl -fsS http://127.0.0.1:8090/readyz ## Request rules -The bearer token check runs first. After it, the MCP Go SDK (1.4.1) enforces these transport checks. They are protections, not authentication, and they can cause surprising errors: +The bearer token check runs first. After it, the server and the MCP Go SDK (1.7.0) enforce these transport checks. They are protections, not authentication, and they can cause surprising errors: | Rule | Error when broken | | --- | --- | +| Cross-site POST requests (detected via `Origin` or `Sec-Fetch-Site`) are rejected. Clients that send neither header are allowed. | `403 Forbidden` | | Requests arriving on a loopback address must use a loopback `Host` (`localhost:8090`, `127.0.0.1:8090`). A reverse proxy that connects over loopback but keeps a Service or external hostname fails. | `403 Forbidden` | -| POST requests must have exactly `Content-Type: application/json`. A missing header, form types, and `application/json; charset=utf-8` all fail. | `415 Unsupported Media Type` | -| Cross-site POST requests (detected via `Origin` or `Sec-Fetch-Site`) are rejected. Clients that send neither header are allowed. | Rejected | +| POST requests must use the `application/json` media type. Parameters such as `charset=utf-8` are allowed; a missing header and form types fail. | `415 Unsupported Media Type` | +| Request bodies are limited to 4 MiB. | `413 Request Entity Too Large` | Also note: - JSON field names are case-sensitive. A key with an appended null character does not alias the original key. - Service names, session IDs, and other client-supplied headers are not credentials. Only the bearer token is. -- Do not disable the SDK's localhost or cross-origin protections to work around routing problems. +- Do not disable the localhost or cross-origin protections to work around routing problems. diff --git a/go.mod b/go.mod index 679c61cb8..466191345 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/coder/coder-k8s go 1.26.9 require ( - github.com/coder/coder/v2 v2.35.8 + github.com/coder/coder/v2 v2.37.3 github.com/coder/websocket v1.8.14 github.com/google/cel-go v0.30.0 github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 - github.com/modelcontextprotocol/go-sdk v1.4.1 - github.com/stretchr/testify v1.11.1 + github.com/modelcontextprotocol/go-sdk v1.7.0 + github.com/stretchr/testify v1.12.0 github.com/yuin/gopher-lua v1.1.2 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da k8s.io/api v0.35.0 @@ -95,9 +95,10 @@ require ( github.com/alingse/nilnesserr v0.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.43.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -130,8 +131,8 @@ require ( github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect github.com/coder/serpent v0.15.0 // indirect - github.com/coder/terraform-provider-coder/v2 v2.18.0 // indirect - github.com/coreos/go-oidc/v3 v3.19.0 // indirect + github.com/coder/terraform-provider-coder/v2 v2.19.0 // indirect + github.com/coreos/go-oidc/v3 v3.20.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect @@ -141,7 +142,7 @@ require ( github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.10.1 // indirect + github.com/ebitengine/purego v0.10.2 // indirect github.com/elastic/crd-ref-docs v0.3.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/ettle/strcase v0.2.0 // indirect @@ -196,7 +197,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -218,7 +219,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.31.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.10.1-0.20260721164921-2f73dc013b2c // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect @@ -234,7 +235,7 @@ require ( github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/compress v1.18.7 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect @@ -292,10 +293,10 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_golang v1.24.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.69.0 // indirect - github.com/prometheus/procfs v0.20.1 // indirect + github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/procfs v0.21.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect @@ -334,7 +335,7 @@ require ( github.com/spf13/viper v1.16.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect @@ -349,7 +350,7 @@ require ( github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.4.1 // indirect - github.com/valyala/fasthttp v1.72.0 // indirect + github.com/valyala/fasthttp v1.73.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -363,7 +364,7 @@ require ( github.com/ykadowak/zerologlint v0.1.5 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zclconf/go-cty v1.18.1 // indirect + github.com/zclconf/go-cty v1.19.0 // indirect github.com/zeebo/errs v1.4.0 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect @@ -380,7 +381,7 @@ require ( go.opentelemetry.io/collector/pdata v1.51.1-0.20260205185216-81bc641f26c0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.145.1-0.20260205185216-81bc641f26c0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect go.opentelemetry.io/otel v1.45.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect @@ -412,7 +413,7 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea // indirect google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.74.0 // indirect diff --git a/go.sum b/go.sum index 55ab07c2e..29beaf3f1 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= -cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= -cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= -cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= +cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q= +cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA= +cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= @@ -127,19 +127,21 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= -github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= +github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-textseg/v17 v17.0.1 h1:bpMXRgQ5cEoRNuQke1a80/Nl6w3G5eoIbWo9f3gXkAs= +github.com/apparentlymart/go-textseg/v17 v17.0.1/go.mod h1:fa8X4jgGeevslICIY6LcdjkSecWnXmYd9Lk34z/VxZs= github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= -github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= -github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2 v1.43.4 h1:b9FTvbRwy+JCsfp2Wp6wV/KbOx3Aj7nkoFb2cRX0IhE= +github.com/aws/aws-sdk-go-v2 v1.43.4/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -205,20 +207,22 @@ github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoK github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coder/coder/v2 v2.35.8 h1:DjfylK0ongzujV5VypJM8NTXWu5UXg6gJZU6oKy02iE= -github.com/coder/coder/v2 v2.35.8/go.mod h1:0F9lmKQWD7ypqVJpTagavZHmzm3c6TjOCzGka6Un1pM= +github.com/coder/coder/v2 v2.37.3 h1:vi6FUGth5aQxsizGwpm6MbtWrbS/TwcTrprZHN1l2Nk= +github.com/coder/coder/v2 v2.37.3/go.mod h1:pZvSpxDWkuVFXq6X/JvnW+BHPuEr1hc5VkwrJ8aJJng= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= +github.com/coder/quartz v0.3.0 h1:bUoSEJ77NBfKtUqv6CPSC0AS8dsjqAqqAv7bN02m1mg= +github.com/coder/quartz v0.3.0/go.mod h1:BgE7DOj/8NfvRgvKw0jPLDQH/2Lya2kxcTaNJ8X0rZk= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= github.com/coder/retry v1.5.1/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY= github.com/coder/serpent v0.15.0 h1:jobR7DnPsxzEMD0cRiailwlY+4v6HAPS/8emIgBpaIU= github.com/coder/serpent v0.15.0/go.mod h1:7OIvFBYMd+OqarMy5einBl8AtRr8LliopVU7pyrwucY= -github.com/coder/terraform-provider-coder/v2 v2.18.0 h1:b60ixwf7pVPuiL0GkHZf+1mVj94/HZhCNpsfjAK34mI= -github.com/coder/terraform-provider-coder/v2 v2.18.0/go.mod h1:Yowo7rLIWw3OOhWSY7LjB57kld3xiFEcUbSI04cnRpU= +github.com/coder/terraform-provider-coder/v2 v2.19.0 h1:ShiwZiUZ3wyUt4+ccrdWlUKu+24aDgzMLD6Yf6xKtAw= +github.com/coder/terraform-provider-coder/v2 v2.19.0/go.mod h1:MWTw/k+oQxySR4/wZIunBxTlFva8hH6t2hLikTpyomU= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I= -github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= +github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -246,8 +250,8 @@ github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= -github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= +github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elastic/crd-ref-docs v0.3.0 h1:9bGSUkBR56Z7TuDGQAu3KGbBkagwwZ6RkZmS+qvDuDM= github.com/elastic/crd-ref-docs v0.3.0/go.mod h1:8td3UC8CaO5M+G115O3FRKLmplmX+p0EqLMLGM6uNdk= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= @@ -398,8 +402,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= -github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= @@ -469,12 +473,12 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.25.1 h1:PRutYRGM8pixV3B8812NYoBK5O+yuf3qcB/70KFKGiU= github.com/hashicorp/terraform-exec v0.25.1/go.mod h1:+izOYrs9sKMQK4OYvGDnrSSJHY/pm4e4eXFqSL2Q5mA= -github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= -github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/terraform-json v0.28.0 h1:dOkJT55rWfU6T1/VklHde51ym4LfNP+9xYR3ZizAJe4= +github.com/hashicorp/terraform-json v0.28.0/go.mod h1:PJIRf+Yzu5iLb52c/xYp1tUOL4jzMzfIAB5gvWWKIWE= github.com/hashicorp/terraform-plugin-go v0.31.0 h1:0Fz2r9DQ+kNNl6bx8HRxFd1TfMKUvnrOtvJPmp3Z0q8= github.com/hashicorp/terraform-plugin-go v0.31.0/go.mod h1:A88bDhd/cW7FnwqxQRz3slT+QY6yzbHKc6AOTtmdeS8= -github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= -github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= +github.com/hashicorp/terraform-plugin-log v0.10.1-0.20260721164921-2f73dc013b2c h1:gv/A478cgseHeET58uEP0aEH9kx+YVEJ7WBikI/6S3g= +github.com/hashicorp/terraform-plugin-log v0.10.1-0.20260721164921-2f73dc013b2c/go.mod h1:XygBz8+m5kgwTb73MMyrnUjeNQeVWECEfg+h2opMsj0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 h1:2yPUd7esMOpuTaG3y1iEla1iw+tla+3ZEkkBnmOAre4= github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1/go.mod h1:sq8qsxh+PwdvTQFcd17kfCoBgQo46ADNMvCpKE7t/gY= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= @@ -517,8 +521,8 @@ github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -606,8 +610,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -685,14 +689,14 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= -github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= @@ -780,8 +784,9 @@ github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUe github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -792,8 +797,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= @@ -832,8 +838,8 @@ github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M= -github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk= +github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s= +github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -881,8 +887,8 @@ github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= -github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= +github.com/zclconf/go-cty v1.19.0 h1:IV8WdqYZc2c5rLX9bEoLNXKojBAp0MZPBHMIrCoa/s4= +github.com/zclconf/go-cty v1.19.0/go.mod h1:12W89jGn3JCOIQi7infWr9m80rOkb5RNYJqXMZcN4c8= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -955,8 +961,8 @@ go.opentelemetry.io/collector/processor/xprocessor v0.145.0 h1:DaIE7MxRlg0OL1o2P go.opentelemetry.io/collector/processor/xprocessor v0.145.0/go.mod h1:kUwRyKBU/kjCmXodd+0z7CpvcP0A9G9/QL+MaJt4U2o= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= @@ -1151,12 +1157,12 @@ gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad h1:cYL1DPJAQr4JMvhfGao0PDXoaf03ifMljAuDyrbMBd0= -google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:cVHIikDNAdx8ISZeW+2rYkEMf3xn0GSaBYmVnWXQBUo= +google.golang.org/genproto v0.0.0-20260729162451-8efbd57d26e0 h1:xJf8e9ReUqiexuIT3OWJkvA4RdgR8K4Hfheq8gaKxIM= +google.golang.org/genproto v0.0.0-20260729162451-8efbd57d26e0/go.mod h1:0MNk3ibJAyOwDZVlp18knQe3jRyFxpcU06xjxhgjx0M= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea h1:kVhQEPTpKQahD5+JSBTfBB19wcgQTTjAIn45MBqnyHk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/internal/aggregated/storage/workspace_transition.go b/internal/aggregated/storage/workspace_transition.go index b5123e8ea..1d178a389 100644 --- a/internal/aggregated/storage/workspace_transition.go +++ b/internal/aggregated/storage/workspace_transition.go @@ -187,9 +187,12 @@ func confirmsPost(ctx context.Context, sdk *codersdk.Client, previousID uuid.UUI } // isUncertainStatus reports whether a Coder or proxy status leaves it open whether the build -// was queued. +// was queued. A 2xx status means the POST got through, but the answer names no build: Coder +// answers 201, so the SDK returns a *codersdk.Error for any other 2xx, and since codersdk v2.37 +// also for a 201 whose body is not the build's JSON (for example a proxy page). func isUncertainStatus(status int) bool { - return status == http.StatusBadGateway || status == http.StatusServiceUnavailable || status == http.StatusGatewayTimeout + return (status >= http.StatusOK && status < http.StatusMultipleChoices) || + status == http.StatusBadGateway || status == http.StatusServiceUnavailable || status == http.StatusGatewayTimeout } // afterConflict handles a Coder 409 on the build POST: another build became active after the diff --git a/internal/aggregated/storage/workspace_transition_test.go b/internal/aggregated/storage/workspace_transition_test.go index d02dbf019..224b70110 100644 --- a/internal/aggregated/storage/workspace_transition_test.go +++ b/internal/aggregated/storage/workspace_transition_test.go @@ -491,6 +491,10 @@ func TestWorkspaceTransitionUncertainPost(t *testing.T) { {"a proxy answered 502", stop, false, nil, http.StatusBadGateway, false}, {"a proxy answered 503 after the build was queued", stop, false, &found{transition: stop}, http.StatusServiceUnavailable, true}, {"Coder answered 504", stop, false, nil, http.StatusGatewayTimeout, false}, + // Coder answers the POST with 201. Since codersdk v2.37, a 201 without the build's JSON is a + // *codersdk.Error with status 201. Another 2xx always was one. + {"a proxy answered 201 with an HTML page after the build was queued", stop, false, &found{transition: stop}, http.StatusCreated, true}, + {"a proxy answered 200 with an HTML page", stop, false, nil, http.StatusOK, false}, {"another user queued the same transition", stop, false, &found{transition: stop, byOther: true}, 0, false}, {"the new build has the other transition", stop, false, &found{transition: start}, 0, false}, {"a start with the requested version", start, true, &found{transition: start}, 0, true}, @@ -521,11 +525,16 @@ func TestWorkspaceTransitionUncertainPost(t *testing.T) { if tc.found != nil { f.update(func(ws *codersdk.Workspace) { ws.LatestBuild = queued }) } - if tc.status != 0 { + switch { + case tc.status >= 200 && tc.status < 300: + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(tc.status) + _, _ = w.Write([]byte("proxy page")) + case tc.status != 0: writeLogFakeError(w, tc.status) - return + default: + stalledPost(w, r) } - stalledPost(w, r) } }) s := NewWorkspaceTransitionStorage(newTestWorkspaces(t, f, 300*time.Millisecond), tc.transition) diff --git a/internal/app/mcpapp/http.go b/internal/app/mcpapp/http.go index a9428dbb5..76fe94b5c 100644 --- a/internal/app/mcpapp/http.go +++ b/internal/app/mcpapp/http.go @@ -31,15 +31,18 @@ const ( var setupLog = ctrl.Log.WithName("setup") // newMCPHTTPHandler keeps the production transport options shared with security tests. -func newMCPHTTPHandler(server *mcp.Server) *mcp.StreamableHTTPHandler { +func newMCPHTTPHandler(server *mcp.Server) http.Handler { if server == nil { panic("assertion failed: MCP server must not be nil") } - return mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { + handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, &mcp.StreamableHTTPOptions{ SessionTimeout: streamableHTTPSessionTimeout, }) + // go-sdk v1.6.0 stopped applying cross-origin protection by default; keep rejecting + // cross-origin browser requests (Origin / Sec-Fetch-Site) with 403. + return http.NewCrossOriginProtection().Handler(handler) } // newMCPHTTPMux builds the production HTTP routing. Only the exact health paths are unauthenticated; diff --git a/internal/app/mcpapp/http_test.go b/internal/app/mcpapp/http_test.go index 5a28f2665..a4cbcedb7 100644 --- a/internal/app/mcpapp/http_test.go +++ b/internal/app/mcpapp/http_test.go @@ -54,7 +54,10 @@ func TestHTTPTransportSecurity(t *testing.T) { {name: "plain text", contentType: "text/plain", wantStatus: http.StatusUnsupportedMediaType}, {name: "form encoded", contentType: "application/x-www-form-urlencoded", wantStatus: http.StatusUnsupportedMediaType}, {name: "missing content type", contentType: "omit", wantStatus: http.StatusUnsupportedMediaType}, - {name: "JSON charset parameter", contentType: "application/json; charset=utf-8", wantStatus: http.StatusUnsupportedMediaType}, + // go-sdk v1.7.0 matches the base media type. A charset parameter keeps the request + // non-simple for CORS, so browsers still preflight it. + {name: "JSON charset parameter", contentType: "application/json; charset=utf-8", wantCall: true}, + {name: "oversized body", body: workspaceCall + strings.Repeat(" ", mcp.DefaultMaxRequestBodyBytes), wantStatus: http.StatusRequestEntityTooLarge}, // A noncanonical key must not override the exact "method" key (GHSA-wvj2-96wp-fq3f). {name: "case mismatch", body: strings.Replace(workspaceCall, `"method":"tools/call"`, `"method":"ping","Method":"tools/call"`, 1)}, // GHSA-q382-vc8q-7jhj concerns null characters in keys, not a JSON null value. diff --git a/internal/controller/codertemplatetest_create_test.go b/internal/controller/codertemplatetest_create_test.go index 68a70bd42..653109634 100644 --- a/internal/controller/codertemplatetest_create_test.go +++ b/internal/controller/codertemplatetest_create_test.go @@ -42,6 +42,11 @@ func TestTemplateTestCreateResults(t *testing.T) { {name: "502 before commit", fault: &fakeFault{Status: 502}, phase: coderv1alpha1.CoderTemplateTestPhaseRunning, reason: "ConfirmingCreate", message: "Coder answered 502", confirmed: "ConfirmingCreate"}, {name: "504 after commit", fault: &fakeFault{Status: 504, AfterCommit: true}, phase: coderv1alpha1.CoderTemplateTestPhaseRunning, reason: "ConfirmingCreate", confirmed: "WaitingForBuild"}, {name: "connection reset", fault: &fakeFault{Reset: true}, phase: coderv1alpha1.CoderTemplateTestPhaseRunning, reason: "ConfirmingCreate", confirmed: "ConfirmingCreate"}, + // Coder answers create with 201. Since codersdk v2.37, a 201 without + // the workspace's JSON is a *codersdk.Error with status 201. Another + // 2xx always was one. Both stay uncertain, never rejected. + {name: "200 proxy page", fault: &fakeFault{Status: 200, HTML: "proxy"}, phase: coderv1alpha1.CoderTemplateTestPhaseRunning, reason: "ConfirmingCreate", message: "Coder answered 200", confirmed: "ConfirmingCreate"}, + {name: "201 proxy page after commit", fault: &fakeFault{Status: 201, AfterCommit: true, HTML: "proxy"}, phase: coderv1alpha1.CoderTemplateTestPhaseRunning, reason: "ConfirmingCreate", message: "Coder answered 201: Received an HTML response", confirmed: "WaitingForBuild"}, {name: "wrong workspace in the answer", fault: &fakeFault{Rewrite: func(a any) any { ws := a.(codersdk.Workspace) ws.Name = "other" diff --git a/internal/controller/codertemplatetest_readiness_test.go b/internal/controller/codertemplatetest_readiness_test.go index 761f98a31..e375b1e78 100644 --- a/internal/controller/codertemplatetest_readiness_test.go +++ b/internal/controller/codertemplatetest_readiness_test.go @@ -159,6 +159,20 @@ func TestTemplateTestReadiness404(t *testing.T) { requireTemplateTestRunning(t, e.reconcile(t, key, 1), "WaitingForBuild", "is pending") } +// TestTemplateTestReadinessProxyPage checks that a 2xx answer without the +// workspace's JSON, a *codersdk.Error with that status since codersdk v2.37, +// keeps the test waiting and never proves deletion. +func TestTemplateTestReadinessProxyPage(t *testing.T) { + t.Parallel() + e := newTemplateTestEnv(t) + key, _, _ := e.startedTest(t) + e.fake.failNext(routeWorkspace, fakeFault{Status: 200, HTML: "proxy"}) + tt := e.reconcile(t, key, 1) + requireTemplateTestRunning(t, tt, "CoderUnavailable", "Coder answered 200: Received an HTML response") + require.Nil(t, meta.FindStatusCondition(tt.Status.Conditions, coderv1alpha1.CoderTemplateTestConditionWorkspaceDeleted)) + requireTemplateTestRunning(t, e.reconcile(t, key, 1), "WaitingForBuild", "is pending") +} + func TestTemplateTestReadinessWrongAnswers(t *testing.T) { t.Parallel() e := newTemplateTestEnv(t) diff --git a/internal/controller/fakecoder_test.go b/internal/controller/fakecoder_test.go index b1ab0130b..17f6d4f21 100644 --- a/internal/controller/fakecoder_test.go +++ b/internal/controller/fakecoder_test.go @@ -51,6 +51,10 @@ type fakeFault struct { Rewrite func(answer any) any // Detail goes into the error answer's detail and a validation error. Detail string + // HTML answers Status with this text/html body instead of a JSON error, + // like a proxy or SSO page. With a 2xx Status, codersdk v2.37 returns a + // *codersdk.Error with that status. + HTML string } type fakeWorkspace struct { @@ -182,6 +186,10 @@ func (f *fakeCoder) route(mux *http.ServeMux, pattern, route string, h func(*htt _ = tcp.SetLinger(0) // Send RST instead of FIN. } _ = conn.Close() + case fault.HTML != "": + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(fault.Status) + _, _ = w.Write([]byte(fault.HTML)) default: resp := codersdk.Response{Message: fmt.Sprintf("fake fault %d", fault.Status), Detail: fault.Detail} if fault.Detail != "" { diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/LICENSE b/vendor/github.com/apparentlymart/go-textseg/v17/LICENSE new file mode 100644 index 000000000..702ccb8a2 --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Martin Atkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/textseg/grapheme_clusters.go b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/grapheme_clusters.go new file mode 100644 index 000000000..046ead089 --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/grapheme_clusters.go @@ -0,0 +1,79 @@ +package textseg + +import ( + "github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops" + "github.com/apparentlymart/go-textseg/v17/textseg/internal/machine" +) + +// ScanGraphemeClusters is a split function for [bufio.Scanner] that splits +// on grapheme cluster boundaries. +// +// Note that while this function does some minimal UTF-8 validation as part +// of its work, it never actually returns an error and instead just reports +// invalid bytes as individual grapheme clusters. Also in particular it does +// not currently check for overlong encodings or out-of-range Unicode scalar +// values, and so callers should perform their own validation if needed. The +// handling of overlong and out-of-range input may change in future releases. +func ScanGraphemeClusters(data []byte, atEOF bool) (int, []byte, error) { + if len(data) == 0 { + return 0, nil, nil + } + + properties, count := charprops.LookupFirstChar(data) + if properties == charprops.Error { + return 1, data[:1], nil + } + if count == 0 { + if len(data) > count && atEOF { + // If we're already at EOF then this is invalid input, which + // we treat as each invalid byte being a separate grapheme cluster. + return 1, data[:1], nil + } + return 0, nil, nil + } + remain := data[count:] + + state := machine.Begin(properties) + prev := properties + for { + if len(remain) == 0 { + if atEOF { + // If we're at the end of the file then whatever we've + // accumulated so far is a grapheme cluster. + return count, data[:count], nil + } + // If we're not at the end of the file then we'll need more + // bytes before we can decide if we've reached the end of a + // grapheme cluster. + return 0, nil, nil + } + + next, moreCount := charprops.LookupFirstChar(remain) + if next == charprops.Error { + // If the next sequence is invalid then we'll just return here + // and let the next call deal with that. + return count, data[:count], nil + } + if moreCount == 0 { + // More bytes required to complete the next UTF-8 sequence. + if len(remain) != 0 && atEOF { + // If we're already at EOF then the next UTF-8 sequence is + // invalid, so we'll report what we already accumulated and + // then let a subsequent call deal with the invalid byte. + return count, data[:count], nil + } + return 0, nil, nil + } + remain = remain[moreCount:] + + split, nextState := state.Transition(prev, next) + if split { + // We've found the next split point, so we'll just return what + // we have and then let the next call pick up from here. + return count, data[:count], nil + } + count += moreCount + state = nextState + prev = next + } +} diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/properties.go b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/properties.go new file mode 100644 index 000000000..9cd98a18f --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/properties.go @@ -0,0 +1,238 @@ +package charprops + +import ( + "fmt" +) + +// CharProperties is effectively a tuple of both a [GCBProperty] and an +// [InCBProperty] value, stored compactly as a bitwise-OR of valid values +// of those two types. +type CharProperties uint8 + +const Error = CharProperties(uint8(GCBError) | uint8(InCBError)) + +func MakeCharProperties(gcpProp GCBProperty, emojiProp GCBProperty, inCBProp InCBProperty) CharProperties { + return CharProperties(uint8(gcpProp) | uint8(emojiProp) | uint8(inCBProp)) +} + +func (cp CharProperties) GCBProperty() GCBProperty { + return GCBProperty(cp & 0x0f) +} + +func (cp CharProperties) InCBProperty() InCBProperty { + return InCBProperty(cp & 0xf0) +} + +func (cp CharProperties) String() string { + if cp == 0 { + return "None" + } + if cp == Error { + return "Error" + } + gcbP := cp.GCBProperty() + inCBP := cp.InCBProperty() + switch { + case inCBP == InCBNone: + return gcbP.String() + case gcbP == GCBNone: + return "InCB=" + inCBP.String() + default: + return fmt.Sprintf("%s|InCB=%s", gcbP, inCBP) + } +} + +// GCBProperty is an enumeration of Grapheme_Cluster_Break property values, +// from [UAX#29 Section 3.1]. +// +// The ExtendedPictographic property is actually derived from the Emoji +// standard's character tables, but is treated by UAX#29 as mutually-exclusive +// with the grapheme cluster break property value and so included in this +// enumeration for simplicity's sake. +// +// Note that the values of this type only set bits in the low nybble because +// they are intended to be bitwise-ORed with GCBProperty to produce +// [CharProperties] values. +// +// [UAX#29 Section 3.1]: https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Break_Property_Values +type GCBProperty uint8 + +const ( + GCBNone GCBProperty = 0x00 + GCBCR GCBProperty = 0x01 + GCBControl GCBProperty = 0x02 + GCBExtend GCBProperty = 0x03 + GCBExtendedPictographic GCBProperty = 0x04 + GCBL GCBProperty = 0x05 + GCBLF GCBProperty = 0x06 + GCBLV GCBProperty = 0x07 + GCBLVT GCBProperty = 0x08 + GCBPrepend GCBProperty = 0x09 + GCBRegionalIndicator GCBProperty = 0x0a + GCBSpacingMark GCBProperty = 0x0b + GCBT GCBProperty = 0x0c + GCBV GCBProperty = 0x0d + GCBZWJ GCBProperty = 0x0e + + // GCBError is the GCB portion of [Error], which is not a real property + // value but instead represents that the grapheme cluster segmentation + // function encountered an invalid UTF-8 sequence. + GCBError GCBProperty = 0x0f +) + +// LookupGCBProperty returns the [GCPProperty] value corresponding to the +// given property name, or [GCBNone] if the name is not recognized. +func LookupGCBProperty(name string) GCBProperty { + switch name { + case "CR": + return GCBCR + case "Control": + return GCBControl + case "Extend": + return GCBExtend + case "L": + return GCBL + case "LF": + return GCBLF + case "LV": + return GCBLV + case "LVT": + return GCBLVT + case "Prepend": + return GCBPrepend + case "Regional_Indicator": + return GCBRegionalIndicator + case "SpacingMark": + return GCBSpacingMark + case "T": + return GCBT + case "V": + return GCBV + case "ZWJ": + return GCBZWJ + default: + return GCBNone + } +} + +// LookupEmojiProperty returns the [GCPProperty] value corresponding to the +// given emoji property name, or [GCBNone] if the name is not recognized. +// +// Note that because the segmentation rules treat the relevant emoji properties +// as mutually exclusive with the main grapheme clustering properties, we +// use [GCPProperty] to represent both of them and assume that no character +// will have both a nonzero result from [LookupGCPProperty] and from +// [LookupEmojiProperty], and so it's okay to collapse them together into the +// same enumeration. If they ever stop being mutually-exclusive then we'll +// need a different strategy. +func LookupEmojiProperty(name string) GCBProperty { + switch name { + case "Extended_Pictographic": + return GCBExtendedPictographic + default: + return GCBNone // we don't need any other properties for our purposes here + } +} + +func (p GCBProperty) IsAnyControl() bool { + switch p { + case GCBLF, GCBCR, GCBControl: + return true + default: + return false + } +} + +func (p GCBProperty) String() string { + switch p { + case GCBCR: + return "CR" + case GCBControl: + return "Control" + case GCBExtend: + return "Extend" + case GCBL: + return "L" + case GCBLF: + return "LF" + case GCBLV: + return "LV" + case GCBLVT: + return "LVT" + case GCBPrepend: + return "Prepend" + case GCBRegionalIndicator: + return "Regional_Indicator" + case GCBSpacingMark: + return "SpacingMark" + case GCBT: + return "T" + case GCBV: + return "V" + case GCBZWJ: + return "ZWJ" + case GCBExtendedPictographic: + return "Extended_Pictographic" + case GCBNone: + return "None" + case GCBError: + return "Error" + default: + return fmt.Sprintf("0x%02x", uint8(p)) + } +} + +// InCBProperty is an enumeration of Indic_Conjunct_Break property values, +// as defined in DerivedCoreProperties.txt based on +// [the rules in UAX#44]. +// +// These are used in the rule that avoids splitting orthographic syllables in +// inappropriated ways, [GB9c]. +// +// Note that the values of this type only set bits in the high nybble because +// they are intended to be bitwise-ORed with GCBProperty to produce +// [CharProperties] values. +// +// [the rules in UAX#44]: https://www.unicode.org/reports/tr44/#Indic_Conjunct_Break +// [GB9c]: https://www.unicode.org/reports/tr29/#GB9c +type InCBProperty uint8 + +const ( + InCBNone InCBProperty = 0x00 + InCBConsonant InCBProperty = 0x10 + InCBExtend InCBProperty = 0x20 + InCBLinker InCBProperty = 0x30 + + // GCBError is the GCB portion of [Error], which is not a real property + // value but instead represents that the grapheme cluster segmentation + // function encountered an invalid UTF-8 sequence. + InCBError InCBProperty = 0xf0 +) + +// LookupGCBProperty returns the [InCBProperty] value corresponding to the +// given property name, or [InCBNone] if the name is not recognized. +func LookupInCBProperty(name string) InCBProperty { + switch name { + case "Consonant": + return InCBConsonant + case "Extend": + return InCBExtend + case "Linker": + return InCBLinker + default: + return InCBNone + } +} + +func (p InCBProperty) String() string { + switch p { + case InCBConsonant: + return "Consonant" + case InCBExtend: + return "Extend" + case InCBLinker: + return "Linker" + default: + return fmt.Sprintf("0x%02x", uint8(p)) + } +} diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table.go b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table.go new file mode 100644 index 000000000..0aa85eef6 --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table.go @@ -0,0 +1,66 @@ +package charprops + +//go:generate go run ./generate.go + +var zeroProperties CharProperties + +// LookupFirstChar identifies the grapheme cluster segmentation properties and +// the byte length of the first UTF-8 sequence in the given buffer. +// +// If there are not enough bytes in the buffer to recognize a complete UTF-8 +// sequence then this returns a length of zero and a meaningless +// [CharProperties] value. +// +// If the buffer begins with something that cannot possibly be extended to a +// valid UTF-8 sequence then the returned properties are [Error] and length +// as 1 to represent treating just the next byte as an error. +func LookupFirstChar(p []byte) (props CharProperties, length int) { + if len(p) == 0 { + return zeroProperties, 0 + } + + first := p[0] + if first < 128 { + length = 1 + } else if (first & 0b11100000) == 0b11000000 { + length = 2 + } else if (first & 0b11110000) == 0b11100000 { + length = 3 + } else if (first & 0b11111000) == 0b11110000 { + length = 4 + } else { + // Invalid initial byte. + return Error, 1 + } + + if len(p) < length { + // Buffer begins with incomplete UTF-8 sequence. + return zeroProperties, 0 + } + // All of the subsequent bytes of the sequence we're decoding (if any) + // must be UTF-8 continuation bytes. + for i := 1; i < length; i++ { + if (p[i] & 0b11000000) != 0b10000000 { + return Error, 1 + } + } + + switch length { + case 1: + return lookupProps[first], length + case 2: + blockIdx := int(lookupIndices[first&0b111111]) + return lookupProps[(blockIdx<<6)+int(p[1]&0b111111)], length + case 3: + blockIdx := int(lookupIndices[first&0b111111]) + blockIdx = int(lookupIndices[(blockIdx<<6)+int(p[1]&0b111111)]) + return lookupProps[(blockIdx<<6)+int(p[2]&0b111111)], length + case 4: + blockIdx := int(lookupIndices[first&0b111111]) + blockIdx = int(lookupIndices[(blockIdx<<6)+int(p[1]&0b111111)]) + blockIdx = int(lookupIndices[(blockIdx<<6)+int(p[2]&0b111111)]) + return lookupProps[(blockIdx<<6)+int(p[3]&0b111111)], length + default: + panic("unreachable") // (because we should've caught this case above) + } +} diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table_raw.go b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table_raw.go new file mode 100644 index 000000000..10de9f82f --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops/property_table_raw.go @@ -0,0 +1,2357 @@ +// Generated by generate.go. DO NOT EDIT! + +package charprops + +var lookupIndices = [...]uint8{ + // block 0x0000 + 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x04, 0x05, 0x02, 0x02, + 0x02, 0x02, 0x06, 0x02, 0x02, 0x02, 0x07, 0x08, + 0x09, 0x0a, 0x02, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x05, 0x0a, + 0x15, 0x16, 0x16, 0x18, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + // block 0x0001 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + // block 0x0002 + 0x31, 0x32, 0x33, 0x10, 0x34, 0x35, 0x36, 0x37, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x39, 0x3a, 0x3b, 0x3c, + 0x3d, 0x10, 0x3e, 0x10, 0x3f, 0x10, 0x10, 0x10, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x10, 0x10, 0x49, 0x10, 0x10, 0x10, 0x4a, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0003 + 0x4b, 0x4c, 0x10, 0x4d, 0x4e, 0x10, 0x4f, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x50, 0x10, 0x10, 0x51, + 0x10, 0x10, 0x10, 0x52, 0x10, 0x10, 0x53, 0x54, + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x5c, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x5d, 0x5e, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x5f, 0x10, 0x60, 0x10, 0x61, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0004 + 0x62, 0x10, 0x63, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x64, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0005 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0006 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x65, 0x66, 0x67, 0x10, 0x10, 0x10, 0x10, + 0x68, 0x10, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, + 0x6f, 0x70, 0x71, 0x72, 0x10, 0x10, 0x10, 0x73, + 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, + 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, + // block 0x0007 + 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, + 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, + // block 0x0008 + 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, + 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, + // block 0x0009 + 0x78, 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7b, 0x7c, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x000a + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x7d, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x7e, 0x10, 0x10, 0x01, 0x10, 0x10, 0x66, 0x7f, + // block 0x000b + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x000c + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x80, + 0x02, 0x02, 0x02, 0x81, 0x02, 0x82, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x83, 0x02, 0x02, 0x84, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x85, 0x86, 0x02, 0x02, + 0x02, 0x02, 0x87, 0x88, 0x02, 0x89, 0x8a, 0x02, + // block 0x000d + 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, + 0x93, 0x94, 0x02, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0x9b, 0x9c, 0x9d, 0x02, 0x02, 0x9e, 0x9f, + 0xa0, 0xa1, 0xa2, 0x02, 0xa3, 0x02, 0x02, 0x02, + 0xa4, 0x02, 0x02, 0x02, 0xa5, 0xa6, 0x02, 0xa7, + 0xa8, 0xa9, 0xaa, 0x02, 0x02, 0xab, 0x02, 0x02, + 0xac, 0x02, 0xad, 0x02, 0xae, 0xaf, 0xb0, 0x02, + 0x02, 0x02, 0x02, 0xb1, 0xb2, 0xb3, 0x02, 0x02, + // block 0x000e + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0xb4, 0xb5, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x000f + 0x02, 0x02, 0x02, 0x02, 0xb6, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0xb7, 0xb8, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0xb9, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0xba, 0xbb, 0xbc, + // block 0x0010 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0xbd, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x0011 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0xbe, 0xbf, 0x02, 0x02, + // block 0x0012 + 0x02, 0x02, 0x02, 0x02, 0x02, 0xc0, 0xc1, 0x02, + 0x02, 0xc2, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0xc3, 0xc4, 0xc5, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x0013 + 0xc6, 0x02, 0xc7, 0x02, 0xb8, 0x02, 0x02, 0x02, + 0x02, 0x02, 0xc8, 0xc9, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0xc9, 0x02, 0x02, 0x02, 0xca, + 0x02, 0x02, 0x02, 0xcb, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0xcc, 0x02, 0xcd, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x0014 + 0xce, 0x02, 0xcf, 0xd0, 0x02, 0xd1, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd6, 0xd7, 0xd6, 0xd8, 0xd9, + 0xd6, 0xd6, 0xd6, 0xda, 0xdb, 0xdc, 0xdd, 0xde, + 0xd6, 0xdf, 0xd6, 0xe0, 0x02, 0x02, 0x02, 0xe1, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xd6, 0xd6, + 0x02, 0xe8, 0xd6, 0xd6, 0x02, 0x02, 0x02, 0x02, + 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, + 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, 0xdb, + // block 0x0015 + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0c, 0x0d, 0x0b, 0x0e, 0x0b, 0x0b, 0x0f, 0x0b, + 0x0b, 0x0b, 0x0b, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + // block 0x0016 + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + // block 0x0017 + 0xe9, 0x04, 0xea, 0xea, 0x04, 0x04, 0x04, 0xeb, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, + // block 0x0018 + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x17, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, +} +var lookupProps = [...]CharProperties{ + // block 0x0000 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x06, 0x02, 0x02, 0x01, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0001 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + // block 0x0002 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0003 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0004 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0005 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0006 + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0007 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x23, + // block 0x0008 + 0x00, 0x23, 0x23, 0x00, 0x23, 0x23, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0009 + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x000a + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x000b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x09, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x23, + 0x23, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x000c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x000d + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x000e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x000f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + // block 0x0010 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0011 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x23, 0x23, 0x23, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0012 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0013 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x09, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0014 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x09, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0015 + 0x23, 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x23, 0x0b, 0x23, 0x00, 0x0b, 0x0b, + // block 0x0016 + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x33, 0x0b, 0x0b, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0017 + 0x00, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, + 0x10, 0x10, 0x00, 0x00, 0x23, 0x00, 0x23, 0x0b, + // block 0x0018 + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x0b, + 0x0b, 0x00, 0x00, 0x0b, 0x0b, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + // block 0x0019 + 0x00, 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x0b, 0x0b, + // block 0x001a + 0x0b, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x00, 0x00, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x001b + 0x00, 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x00, 0x00, 0x23, 0x00, 0x0b, 0x0b, + // block 0x001c + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x23, + 0x23, 0x0b, 0x00, 0x0b, 0x0b, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x001d + 0x00, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x00, 0x00, 0x23, 0x00, 0x23, 0x23, + // block 0x001e + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x0b, + 0x0b, 0x00, 0x00, 0x0b, 0x0b, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x001f + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, + // block 0x0020 + 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x0b, 0x0b, + 0x0b, 0x00, 0x0b, 0x0b, 0x0b, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0021 + 0x23, 0x0b, 0x0b, 0x0b, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x00, 0x00, 0x23, 0x00, 0x23, 0x23, + // block 0x0022 + 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x00, 0x23, 0x23, + 0x23, 0x00, 0x23, 0x23, 0x23, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, + 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0023 + 0x00, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x0b, 0x23, + // block 0x0024 + 0x23, 0x0b, 0x23, 0x0b, 0x0b, 0x00, 0x23, 0x23, + 0x23, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0025 + 0x23, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x23, 0x23, 0x00, 0x23, 0x0b, + // block 0x0026 + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x00, 0x0b, 0x0b, + 0x0b, 0x00, 0x0b, 0x0b, 0x0b, 0x33, 0x09, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0027 + 0x00, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0028 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x00, 0x23, 0x00, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0029 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x00, 0x0b, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x002a + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x002b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x00, 0x0b, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + // block 0x002c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x002d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x23, + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, + // block 0x002e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, + // block 0x002f + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + // block 0x0030 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0031 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x33, 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x10, + // block 0x0032 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0b, 0x0b, + 0x23, 0x23, 0x10, 0x10, 0x10, 0x10, 0x23, 0x23, + 0x23, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, + 0x10, 0x23, 0x23, 0x23, 0x23, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0033 + 0x10, 0x10, 0x23, 0x00, 0x0b, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0034 + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + // block 0x0035 + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + // block 0x0036 + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + // block 0x0037 + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + // block 0x0038 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0039 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x003a + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x003b + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x23, 0x23, 0x0b, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x0b, + // block 0x003c + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x23, 0x0b, + 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x33, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x003d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x02, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x003e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x003f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x23, + 0x23, 0x0b, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x0b, 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + // block 0x0040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x0b, 0x0b, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x0041 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x0b, 0x23, 0x0b, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x33, 0x00, 0x23, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x23, + // block 0x0042 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0043 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0044 + 0x23, 0x23, 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x0b, + // block 0x0045 + 0x0b, 0x0b, 0x23, 0x23, 0x33, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0046 + 0x23, 0x23, 0x0b, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x0b, + 0x23, 0x23, 0x23, 0x33, 0x23, 0x23, 0x10, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, + // block 0x0047 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, + 0x23, 0x23, 0x0b, 0x0b, 0x0b, 0x23, 0x0b, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0048 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x0b, 0x0b, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0049 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x0b, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x004a + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x004b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x03, 0x2e, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + // block 0x004c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x004d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x004e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x004f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0050 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0051 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0052 + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0053 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0054 + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x00, + // block 0x0055 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x04, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0056 + 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x04, 0x00, 0x00, 0x04, 0x00, 0x04, 0x04, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, + // block 0x0057 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x04, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, + // block 0x0058 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x00, 0x04, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + // block 0x0059 + 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x005a + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x005b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + // block 0x005c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x005d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x005e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x005f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0060 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + // block 0x0061 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0062 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + // block 0x0063 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0064 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0065 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + // block 0x0066 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0067 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0068 + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x23, 0x23, 0x0b, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0069 + 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + // block 0x006a + 0x0b, 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + // block 0x006b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x006c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x0b, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x00, 0x00, 0x00, + // block 0x006d + 0x23, 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x23, 0x0b, 0x0b, 0x23, 0x23, + 0x23, 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x0b, 0x0b, + // block 0x006e + 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x23, 0x00, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, + // block 0x006f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, + 0x0b, 0x23, 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0070 + 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x23, 0x00, 0x10, 0x10, + // block 0x0071 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x23, 0x23, 0x23, 0x00, 0x00, 0x23, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + // block 0x0072 + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x0b, 0x23, 0x23, 0x0b, 0x0b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x33, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0073 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x23, 0x0b, 0x0b, + 0x23, 0x0b, 0x0b, 0x00, 0x0b, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0074 + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x0075 + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x0076 + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x0077 + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + // block 0x0078 + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x0079 + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x007a + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + // block 0x007b + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + // block 0x007c + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, + // block 0x007d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x007e + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x007f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, + // block 0x0080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + // block 0x0081 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0082 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0083 + 0x10, 0x23, 0x23, 0x23, 0x00, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, + 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x33, + // block 0x0084 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0085 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0086 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0087 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0088 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0089 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x008a + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x008b + 0x0b, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x008c + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + // block 0x008d + 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x0b, + 0x0b, 0x23, 0x23, 0x00, 0x00, 0x09, 0x00, 0x00, + // block 0x008e + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x008f + 0x23, 0x23, 0x23, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x0b, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x33, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0090 + 0x00, 0x00, 0x00, 0x00, 0x10, 0x0b, 0x0b, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0091 + 0x23, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, + // block 0x0092 + 0x23, 0x00, 0x09, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x0b, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0093 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x23, + 0x23, 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + // block 0x0094 + 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0095 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0096 + 0x23, 0x23, 0x0b, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, 0x23, 0x0b, + // block 0x0097 + 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x00, 0x00, 0x0b, + 0x0b, 0x00, 0x00, 0x0b, 0x0b, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0b, 0x0b, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x0098 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x00, 0x10, 0x00, 0x00, 0x10, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, + 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x0099 + 0x23, 0x00, 0x23, 0x00, 0x00, 0x23, 0x00, 0x23, + 0x23, 0x23, 0x0b, 0x00, 0x0b, 0x0b, 0x23, 0x23, + 0x33, 0x09, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x009a + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x009b + 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x0b, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x009c + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x23, 0x0b, 0x0b, 0x23, 0x0b, 0x23, + // block 0x009d + 0x23, 0x0b, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x009e + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x0b, 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x0b, 0x23, + // block 0x009f + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x0b, 0x0b, 0x23, 0x0b, 0x23, + // block 0x00a1 + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a2 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x0b, 0x23, 0x0b, 0x0b, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, 0x23, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x0b, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a5 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x23, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x00, 0x0b, + 0x0b, 0x00, 0x00, 0x23, 0x23, 0x23, 0x33, 0x09, + // block 0x00a6 + 0x0b, 0x09, 0x0b, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x23, 0x23, 0x0b, 0x0b, 0x0b, 0x0b, + 0x23, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00a8 + 0x10, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, + // block 0x00a9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, + 0x0b, 0x23, 0x23, 0x23, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + // block 0x00aa + 0x10, 0x10, 0x10, 0x10, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, + 0x23, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ab + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x0b, 0x23, 0x23, 0x23, 0x0b, 0x23, 0x0b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ac + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0b, 0x23, + // block 0x00ad + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x0b, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x0b, 0x23, 0x23, 0x0b, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ae + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x23, 0x23, 0x00, 0x23, + // block 0x00af + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x09, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x00, + 0x23, 0x23, 0x00, 0x0b, 0x0b, 0x23, 0x0b, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x0b, 0x0b, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b2 + 0x23, 0x23, 0x09, 0x0b, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x0b, 0x0b, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x0b, 0x0b, + // block 0x00b3 + 0x23, 0x23, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x00b5 + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x0b, 0x0b, 0x0b, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00b9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0d, + 0x0d, 0x0d, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ba + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + // block 0x00bb + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00bc + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00bd + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00be + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x00bf + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x00c1 + 0x23, 0x23, 0x23, 0x00, 0x00, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c2 + 0x00, 0x00, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c3 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x00c4 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c5 + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c6 + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x00, 0x23, 0x23, 0x00, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00c9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ca + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00cb + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00cc + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00cd + 0x00, 0x00, 0x00, 0x00, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00ce + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00cf + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00d0 + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + // block 0x00d2 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d3 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + // block 0x00d4 + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, + // block 0x00d5 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d6 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d7 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d8 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, + 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00d9 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x00, 0x00, 0x04, 0x04, 0x04, 0x00, 0x04, + 0x04, 0x04, 0x04, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x00da + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, + // block 0x00db + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, + // block 0x00dc + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x04, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00dd + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + // block 0x00de + 0x00, 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00df + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00e0 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00e1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00e2 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00e3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // block 0x00e4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + // block 0x00e5 + 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00e6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, + // block 0x00e7 + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00e8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + // block 0x00e9 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + // block 0x00ea + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + // block 0x00eb + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, +} diff --git a/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/machine/state.go b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/machine/state.go new file mode 100644 index 000000000..d671b4e2e --- /dev/null +++ b/vendor/github.com/apparentlymart/go-textseg/v17/textseg/internal/machine/state.go @@ -0,0 +1,211 @@ +package machine + +import ( + "github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops" +) + +// The state machine implemented here is a Go port of a Rust implementation +// in the "grapheme_machine" package on crates.io. It's more general than +// the main textseg package currently needs but maybe a future version will +// expose more of this in the public API if a compelling use-case emerges for +// that. + +// State represents different states we can transition through while detecting +// grapheme cluster boundaries. +// +// A State value essentially summarizes a set of category transitions that +// happened before the current one, so that we can detect arbitrary-long +// grapheme clusters using only finite storage. +// +// (In order to actually _use_ a detected grapheme cluster after its bounds +// have been found would require the caller to have buffered everything that +// appeared since the last boundary, but that's the caller's problem.) +type State int + +const ( + // StateBase is the initial state at the beginning of the text or when the + // following should be treated as if it were at the beginning of the text. + StateBase State = iota + + // When the previous character was [charprops.GCBRegionalIndicator] but + // its predecessor was not, and therefore if the next character + // is also [charprops.GCBRegionalIndicator] the two together represent + // an emoji flag under rules GB12 and GB13. + StateAwaitEmojiFlag + + // When consecutive scalar values before the previous matched + // [charprops.GCBExtendedPictographic] followed by any number of + // [charprops.GCBExtend], and therefore if the next character is + // [charprops.GCBZWJ] we should transition to [StateGB11AfterZWJ]. + StateGB11BeforeZWJ + + // The previous scalar value of category [charprops.GCBZWJ] arrived while + // in [StateGB11BeforeZWJ], and therefore rule GB11 is active. + StateGB11AfterZWJ + + // We encountered [charprops.InCBConsonant] followed by zero or more + // [charprops.InCBExtend] with no [charprops.InCBLinker] in between. + StateGB9cConsonant + + // We encountered [charprops.InCBConsonant] followed by at least one + // [charprops.InCBLinker] and zero or more [charprops.InCBExtend] in any + // order. + StateGB9cLinker +) + +// Begin initializes the state machine by providing the properties for the +// first character. Call [State.Transition] on the resulting state once +// the second character is known, passing the same properties as "first" in +// its "prev" argument. +func Begin(first charprops.CharProperties) State { + return StateBase.nextState(first) +} + +// Given the previous category and the next category, Transition returns whether +// there is a grapheme cluster boundary between two characters of those +// categories in the receiving state, and which state should be used for the +// next transition. +// +// Correct use requires that the "prev" of one call equal the "next" of the +// call that selected the recieving state. If that is not upheld then the +// results are unspecified. +func (s State) Transition(prev, next charprops.CharProperties) (bool, State) { + nextState := s.nextState(next) + + // The following was slightly more readable with Rust pattern matching + // syntax, but this is a direct translation of the same logic into Go if + // statements. + + prevC := prev.GCBProperty() + nextC := next.GCBProperty() + + // GB1 and GB2 aren't covered here because the caller is expected to + // use this function only at the boundary between two scalar values, and + // not at the beginning or end of the input. There is always a grapheme + // cluster boundary at the beginning and end of the input. + + // GB3: Do not break between CR and LF... + if prevC == charprops.GCBCR && nextC == charprops.GCBLF { + return false, nextState + } + // GB4 and GB5: ...but otherwise, break before and after any control characters + if prevC.IsAnyControl() || nextC.IsAnyControl() { + return true, nextState + } + // GB6: Do not break Hangul syllable or other conjoining sequences. + if prevC == charprops.GCBL && (nextC == charprops.GCBL || nextC == charprops.GCBV || nextC == charprops.GCBLV || nextC == charprops.GCBLVT) { + return false, nextState + } + // GB7: Do not break Hangul syllable or other conjoining sequences. + if (prevC == charprops.GCBLV || prevC == charprops.GCBV) && (nextC == charprops.GCBV || nextC == charprops.GCBT) { + return false, nextState + } + // GB8: Do not break Hangul syllable or other conjoining sequences. + if (prevC == charprops.GCBLVT || prevC == charprops.GCBT) && nextC == charprops.GCBT { + return false, nextState + } + // GB9: Do not break before extending characters or ZWJ + if nextC == charprops.GCBExtend || nextC == charprops.GCBZWJ { + return false, nextState + } + // GB9a: Do not break before spacing marks... + if nextC == charprops.GCBSpacingMark { + return false, nextState + } + // GB9a: ...or after Prepend characters + if prevC == charprops.GCBPrepend { + return false, nextState + } + // GB9c: Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker + if s.gb9cActive() { + prevI := prev.InCBProperty() + nextI := next.InCBProperty() + if (prevI == charprops.InCBLinker || prevI == charprops.InCBExtend) && nextI == charprops.InCBConsonant { + return false, nextState + } + } + // (GB10 was from an earlier version of the specification but is no longer used) + // GB11: Do not break within emoji modifier sequences or emoji zwj sequences. + if s.gb11Active() { + if prevC == charprops.GCBZWJ && nextC == charprops.GCBExtendedPictographic { + return false, nextState + } + } + // GB12 and GB13: Do not break within emoji flag sequences. + if s.gb13Active() { + if prevC == charprops.GCBRegionalIndicator && nextC == charprops.GCBRegionalIndicator { + return false, nextState + } + } + // GB999: Break in all other locations + return true, nextState +} + +func (s State) nextState(next charprops.CharProperties) State { + // Two of the multi-character prefixes can begin regardless of what + // preceeds them. These don't need to be covered by the state-specific + // branches that follow. + if next.GCBProperty() == charprops.GCBExtendedPictographic { + return StateGB11BeforeZWJ + } + if next.InCBProperty() == charprops.InCBConsonant { + return StateGB9cConsonant + } + gcbProp := next.GCBProperty() + incbProp := next.InCBProperty() + switch s { + case StateBase: + switch gcbProp { + case charprops.GCBRegionalIndicator: + return StateAwaitEmojiFlag + default: + return StateBase + } + case StateAwaitEmojiFlag: + return StateBase + case StateGB11BeforeZWJ: + switch gcbProp { + case charprops.GCBZWJ: + return StateGB11AfterZWJ + case charprops.GCBExtend: + return StateGB11BeforeZWJ + default: + return StateBase + } + case StateGB11AfterZWJ: + return StateBase + case StateGB9cConsonant: + switch incbProp { + case charprops.InCBLinker: + return StateGB9cLinker + case charprops.InCBExtend: + return StateGB9cConsonant + default: + return StateBase + } + case StateGB9cLinker: + switch incbProp { + case charprops.InCBLinker, charprops.InCBExtend: + return StateGB9cLinker + default: + return StateBase + } + default: + panic("invalid state") + } +} + +func (s State) gb9cActive() bool { + // GB9c is active only in [StateGB9cLinker]. + return s == StateGB9cLinker +} + +func (s State) gb11Active() bool { + // GB11 is active only in [StateGB11AfterZWJ]. + return s == StateGB11AfterZWJ +} + +func (s State) gb13Active() bool { + // GB12/GB13 is active only in [StateAwaitEmojiFlag]. + return s == StateAwaitEmojiFlag +} diff --git a/vendor/github.com/coder/coder/v2/coderd/rbac/policy/policy.go b/vendor/github.com/coder/coder/v2/coderd/rbac/policy/policy.go index 035a0a347..067c42ee0 100644 --- a/vendor/github.com/coder/coder/v2/coderd/rbac/policy/policy.go +++ b/vendor/github.com/coder/coder/v2/coderd/rbac/policy/policy.go @@ -85,6 +85,22 @@ var chatActions = map[Action]ActionDefinition{ ActionShare: "share a chat with other users or groups", } +var mcpServerConfigActions = map[Action]ActionDefinition{ + ActionCreate: "create a new MCP server config", + ActionRead: "read MCP server config", + ActionUpdate: "update MCP server config", + ActionShare: "share MCP server config with other users or groups", + ActionDelete: "delete MCP server config", +} + +var chatModelConfigActions = map[Action]ActionDefinition{ + ActionCreate: "create a new chat model config", + ActionRead: "read chat model configs", + ActionUpdate: "update a chat model config", + ActionDelete: "delete a chat model config", + ActionShare: "share a chat model config with other users or groups", +} + // RBACPermissions is indexed by the type var RBACPermissions = map[string]PermissionDefinition{ // Wildcard is every object, and the action "*" provides all actions. @@ -114,6 +130,9 @@ var RBACPermissions = map[string]PermissionDefinition{ "chat": { Actions: chatActions, }, + "chat_model_config": { + Actions: chatModelConfigActions, + }, // Dormant workspaces have the same perms as workspaces. "workspace_dormant": { Actions: workspaceActions, @@ -140,6 +159,14 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionRead: "read and use a workspace proxy", }, }, + "workspace_build_orchestration": { + Actions: map[Action]ActionDefinition{ + ActionCreate: "create a workspace build orchestration", + ActionRead: "read a workspace build orchestration", + ActionUpdate: "update a workspace build orchestration", + ActionDelete: "delete a workspace build orchestration", + }, + }, "license": { Actions: map[Action]ActionDefinition{ ActionCreate: "create a license", @@ -445,4 +472,8 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionDelete: "delete boundary usage statistics", }, }, + "mcp_server_config": { + Name: "MCPServerConfig", + Actions: mcpServerConfigActions, + }, } diff --git a/vendor/github.com/coder/coder/v2/coderd/tracing/httpmw.go b/vendor/github.com/coder/coder/v2/coderd/tracing/httpmw.go index 26b57a1d2..b0a174e0b 100644 --- a/vendor/github.com/coder/coder/v2/coderd/tracing/httpmw.go +++ b/vendor/github.com/coder/coder/v2/coderd/tracing/httpmw.go @@ -2,20 +2,31 @@ package tracing import ( "context" + "encoding/hex" "fmt" "net/http" + "net/url" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" "go.opentelemetry.io/otel/semconv/v1.14.0/httpconv" "go.opentelemetry.io/otel/semconv/v1.14.0/netconv" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/coderd/httpmw/patternmatcher" ) +// SessionIDBaggageKey is the W3C baggage key clients use to propagate the +// per-session correlation ID described in the connection-log RFC. The value is +// a 16-byte identifier encoded as a 32-character hexadecimal string. +const SessionIDBaggageKey = "client_session_id" + // Middleware adds tracing to http routes. func Middleware(tracerProvider trace.TracerProvider) func(http.Handler) http.Handler { // We only want to create spans on the following route patterns, however @@ -29,37 +40,36 @@ func Middleware(tracerProvider trace.TracerProvider) func(http.Handler) http.Han "/external-auth/*/callback", }.MustCompile() - var tracer trace.Tracer - if tracerProvider != nil { - tracer = tracerProvider.Tracer(TracerName) + if tracerProvider == nil { + tracerProvider = noop.NewTracerProvider() } + tracer := tracerProvider.Tracer(TracerName) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if tracer == nil || !re.MatchString(r.URL.Path) { + if !re.MatchString(r.URL.Path) { next.ServeHTTP(rw, r) return } - // Extract the trace context from the request headers. - tmp := otel.GetTextMapPropagator() - hc := propagation.HeaderCarrier(r.Header) - ctx := tmp.Extract(r.Context(), hc) + // Read the client_session_id from the request and add it to the log + // context. This is done even when tracing is disabled so that logs can + // always be correlated by client_session_id. + sessionID := sessionIDFromRequest(r) + if sessionID != "" { + r = r.WithContext(slog.With(r.Context(), slog.F("client_session_id", sessionID))) + } - // start span with default span name. Span name will be updated to "method route" format once request finishes. - ctx, span := tracer.Start(ctx, fmt.Sprintf("%s %s", r.Method, r.RequestURI)) + // Start span with default span name. Span name will be updated to + // "method route" format once request finishes. The initial name + // excludes the query string because span names are exported to + // tracing backends at span start and some endpoints accept + // bearer credentials as query parameters. + r, span := StartHTTPSpan(tracer, rw, r, fmt.Sprintf("%s %s", r.Method, r.URL.Path)) defer span.End() - r = r.WithContext(ctx) - - if span.SpanContext().HasTraceID() && span.SpanContext().HasSpanID() { - // Technically these values are included in the Traceparent - // header, but they are easier to read for humans this way. - rw.Header().Set("X-Trace-ID", span.SpanContext().TraceID().String()) - rw.Header().Set("X-Span-ID", span.SpanContext().SpanID().String()) - // Inject the trace context into the response headers. - hc := propagation.HeaderCarrier(rw.Header()) - tmp.Inject(ctx, hc) + if sessionID != "" { + span.SetAttributes(attribute.String("client_session_id", sessionID)) } sw, ok := rw.(*StatusWriter) @@ -75,6 +85,80 @@ func Middleware(tracerProvider trace.TracerProvider) func(http.Handler) http.Han } } +// sessionIDFromRequest extracts and validates the client_session_id from the +// request. It prefers the W3C baggage member and falls back to the +// client_session_id query parameter. The query parameter fallback exists +// because browser WebSocket clients (such as the web terminal PTY) cannot set +// arbitrary baggage headers. It returns an empty string when neither source +// provides a valid value. +func sessionIDFromRequest(r *http.Request) string { + if id := sessionIDFromHeaders(r.Header); id != "" { + return id + } + return sessionIDFromQueryString(r.URL.Query()) +} + +// sessionIDFromHeaders extracts and validates the client_session_id baggage member +// from the request headers. It returns an empty string when the member is +// absent or malformed. Extraction uses an explicit baggage propagator so it +// does not depend on the globally configured text map propagator. +func sessionIDFromHeaders(h http.Header) string { + ctx := propagation.Baggage{}.Extract(context.Background(), propagation.HeaderCarrier(h)) + id := baggage.FromContext(ctx).Member(SessionIDBaggageKey).Value() + if !validSessionID(id) { + return "" + } + return id +} + +// sessionIDFromQueryString extracts and validates the client_session_id query +// parameter. It returns an empty string when the parameter is absent or +// malformed. It mirrors sessionIDFromHeaders for the query-parameter fallback +// used by browser WebSocket clients (such as the web terminal PTY) that cannot +// set arbitrary baggage headers. +func sessionIDFromQueryString(q url.Values) string { + id := q.Get(SessionIDBaggageKey) + if !validSessionID(id) { + return "" + } + return id +} + +// validSessionID reports whether s is a 32-character lowercase hexadecimal +// string (a 16-byte value), the encoding the RFC mandates for the session ID. +// Only lowercase is accepted so that case-sensitive searches correlate +// reliably. Validating also guards against logging arbitrary client-controlled +// baggage values. +func validSessionID(s string) bool { + b, err := hex.DecodeString(s) + if err != nil || len(b) != 16 { + return false + } + // hex.DecodeString also accepts upper-case, so require the canonical + // lowercase encoding. + return hex.EncodeToString(b) == s +} + +// StartHTTPSpan starts a span, propagating inbound trace context and writing +// X-Trace-ID/X-Span-ID response headers. The caller must end the span. +func StartHTTPSpan(tracer trace.Tracer, rw http.ResponseWriter, r *http.Request, name string) (*http.Request, trace.Span) { + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + + ctx, span := tracer.Start(ctx, name) + r = r.WithContext(ctx) + + if span.SpanContext().HasTraceID() && span.SpanContext().HasSpanID() { + // Technically these values are included in the Traceparent header, but + // they are easier to read for humans this way. + rw.Header().Set("X-Trace-ID", span.SpanContext().TraceID().String()) + rw.Header().Set("X-Span-ID", span.SpanContext().SpanID().String()) + propagator.Inject(ctx, propagation.HeaderCarrier(rw.Header())) + } + + return r, span +} + // EndHTTPSpan captures request and response data after the handler is done. func EndHTTPSpan(r *http.Request, status int, span trace.Span) { // set the resource name as we get it only once the handler is executed diff --git a/vendor/github.com/coder/coder/v2/coderd/util/ptr/ptr.go b/vendor/github.com/coder/coder/v2/coderd/util/ptr/ptr.go index 3500805c6..2f6fb9d6f 100644 --- a/vendor/github.com/coder/coder/v2/coderd/util/ptr/ptr.go +++ b/vendor/github.com/coder/coder/v2/coderd/util/ptr/ptr.go @@ -1,7 +1,11 @@ // Package ptr contains some utility methods related to pointers. package ptr -import "golang.org/x/exp/constraints" +import ( + "database/sql" + + "golang.org/x/exp/constraints" +) type number interface { constraints.Integer | constraints.Float @@ -38,3 +42,13 @@ func NilToDefault[T any](s *T, def T) T { func NilOrZero[T number](v *T) bool { return v == nil || *v == 0 } + +// FromNullString returns a pointer to the string when it is valid and nil +// otherwise. +func FromNullString(v sql.NullString) *string { + if !v.Valid { + return nil + } + value := v.String + return &value +} diff --git a/vendor/github.com/coder/coder/v2/coderd/util/strings/strings.go b/vendor/github.com/coder/coder/v2/coderd/util/strings/strings.go index d2594b80a..11a05cb82 100644 --- a/vendor/github.com/coder/coder/v2/coderd/util/strings/strings.go +++ b/vendor/github.com/coder/coder/v2/coderd/util/strings/strings.go @@ -64,8 +64,19 @@ func Truncate(s string, n int, opts ...TruncateOption) string { if n < 1 { return "" } - runes := []rune(s) - if len(runes) <= n { + + // Find the byte offset of the (n+1)th rune, if any; early exit + // avoids decoding s past what's needed. + runeCount := 0 + cutoff := -1 + for i := range s { + runeCount++ + if runeCount > n { + cutoff = i + break + } + } + if cutoff < 0 { return s } @@ -73,6 +84,7 @@ func Truncate(s string, n int, opts ...TruncateOption) string { if options&TruncateWithEllipsis != 0 { maxLen-- } + runes := []rune(s[:cutoff]) var sb strings.Builder if options&TruncateWithFullWords != 0 { // Convert the rune-safe prefix to a string, then find diff --git a/vendor/github.com/coder/coder/v2/codersdk/agentfirewall.go b/vendor/github.com/coder/coder/v2/codersdk/agentfirewall.go index b8ef4a75a..e76af490d 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/agentfirewall.go +++ b/vendor/github.com/coder/coder/v2/codersdk/agentfirewall.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "strconv" @@ -31,7 +30,7 @@ func (c *Client) AgentFirewallSessionByID(ctx context.Context, id uuid.UUID) (Ag return AgentFirewallSession{}, ReadBodyAsError(res) } var session AgentFirewallSession - return session, json.NewDecoder(res.Body).Decode(&session) + return session, ReadBodyAsJSON(res, &session) } // AgentFirewallLog represents a single audit event from an agent firewall proxy. @@ -101,5 +100,5 @@ func (c *Client) AgentFirewallSessionLogs(ctx context.Context, sessionID uuid.UU } var resp AgentFirewallSessionLogsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/aibridge.go b/vendor/github.com/coder/coder/v2/codersdk/aibridge.go index fd6adb185..7b717e5af 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/aibridge.go +++ b/vendor/github.com/coder/coder/v2/codersdk/aibridge.go @@ -2,16 +2,133 @@ package codersdk import ( "context" - "encoding/json" "fmt" + "io" "net/http" "strings" "time" "github.com/google/uuid" "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/util/slice" +) + +// MaxAISpendLimitMicros is the highest AI spend limit that can be configured, +// $1,000,000 per member per budget period. +const MaxAISpendLimitMicros int64 = 1_000_000_000_000 + +// AIBudgetLimitSource identifies which tier produced the user's +// effective budget limit. +type AIBudgetLimitSource string + +const ( + // AIBudgetLimitSourceUserOverride indicates the limit came from a + // per-user override. + AIBudgetLimitSourceUserOverride AIBudgetLimitSource = "user_override" + // AIBudgetLimitSourceGroup indicates the limit came from a group + // budget selected by the deployment budget policy. + AIBudgetLimitSourceGroup AIBudgetLimitSource = "group" ) +// AIBudgetLimit is an AI spend limit and the tier that produced it. Both +// fields are always populated together. +type AIBudgetLimit struct { + SpendLimitMicros int64 `json:"spend_limit_micros"` + LimitSource AIBudgetLimitSource `json:"limit_source"` +} + +// UserAIBudgetSummary is the effective AI budget for a user. When no budget +// applies, the effective group falls back to the Everyone group with a null +// budget. +type UserAIBudgetSummary struct { + UserID uuid.UUID `json:"user_id" format:"uuid"` + // EffectiveGroupID is the group the spend is attributed to, falling back to + // the Everyone group when no budget applies. Null only when the user has no + // organization membership. + EffectiveGroupID *uuid.UUID `json:"effective_group_id" format:"uuid"` + // EffectiveBudget is the spend limit that applies to the user, whether it + // came from a group budget or a user override. Null when no budget + // applies, leaving the user's spend unlimited. + EffectiveBudget *AIBudgetLimit `json:"effective_budget"` +} + +// AISpendPeriodWindow is the [Start, End) window over which AI spend is +// aggregated. +type AISpendPeriodWindow struct { + // PeriodStart is the inclusive lower bound of the current budget + // period. + PeriodStart time.Time `json:"period_start" format:"date-time"` + // PeriodEnd is the exclusive upper bound of the current budget + // period. + PeriodEnd time.Time `json:"period_end" format:"date-time"` +} + +// UserAISpendStatus is the current AI spend snapshot for a user within +// the active budget period. +type UserAISpendStatus struct { + UserAIBudgetSummary + AISpendPeriodWindow + // CurrentSpendMicros is the user's spend on their effective group over + // the current budget period. + CurrentSpendMicros int64 `json:"current_spend_micros"` +} + +// OrganizationGroupsAISpend reports AI spend for a set of groups in the +// active budget period. +type OrganizationGroupsAISpend struct { + AISpendPeriodWindow + Groups []OrganizationGroupAISpend `json:"groups"` +} + +// OrganizationGroupAISpend is the current AI spend snapshot for a group +// within the active budget period. +type OrganizationGroupAISpend struct { + GroupID uuid.UUID `json:"group_id" format:"uuid"` + // SpendLimitMicros is the group's configured AI spend budget per member. + // Null when the group has no configured budget. + SpendLimitMicros *int64 `json:"spend_limit_micros"` + // TotalSpendLimitMicros is the currently configured combined budget of the + // members attributed to this group, with each member's override replacing + // their share. Null when the group has no budget, and zero when no members + // are attributed to it. + TotalSpendLimitMicros *int64 `json:"total_spend_limit_micros"` + // CurrentSpendMicros is the group's spend over the current budget period. + CurrentSpendMicros int64 `json:"current_spend_micros"` +} + +// GroupAISpend is the current AI spend snapshot for a single group within +// the active budget period. +type GroupAISpend struct { + AISpendPeriodWindow + OrganizationGroupAISpend +} + +// GroupMembersAISpend reports per-member AI spend attributed to a specific +// group in the active budget period. +type GroupMembersAISpend struct { + AISpendPeriodWindow + Members []GroupMemberAISpend `json:"members"` +} + +// GroupMemberAISpend is a single member's AI spend attributed to the queried +// group in the current budget period. +type GroupMemberAISpend struct { + UserID uuid.UUID `json:"user_id" format:"uuid"` + // EffectiveGroupID is the user's effective budget group within the queried + // group's organization, falling back to the Everyone group when no budget + // applies. Null when the effective group belongs to a different organization + // than the queried group. + EffectiveGroupID *uuid.UUID `json:"effective_group_id" format:"uuid"` + // GroupBudget is the budget when the queried group is this user's + // effective budget source. Null when the user's budget resolves to another + // group or no budget applies to the user. + GroupBudget *AIBudgetLimit `json:"group_budget"` + // GroupSpendMicros is the user's spend attributed to the queried group + // over the current budget period. + GroupSpendMicros int64 `json:"group_spend_micros"` +} + type AIBridgeSession struct { ID string `json:"id"` Initiator MinimalUser `json:"initiator"` @@ -23,8 +140,13 @@ type AIBridgeSession struct { EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` Threads int64 `json:"threads"` TokenUsageSummary AIBridgeSessionTokenUsageSummary `json:"token_usage_summary"` - LastPrompt *string `json:"last_prompt,omitempty"` - LastActiveAt time.Time `json:"last_active_at" format:"date-time"` + // NetworkCalls summarizes the Agent Firewall network requests made during the + // session. A nil value means the session did not pass through Agent + // Firewall, so network call monitoring was not active, which the UI + // surfaces as "Disabled". + NetworkCalls *AIBridgeSessionNetworkCallSummary `json:"network_calls,omitempty"` + LastPrompt *string `json:"last_prompt,omitempty"` + LastActiveAt time.Time `json:"last_active_at" format:"date-time"` } type AIBridgeSessionTokenUsageSummary struct { @@ -34,6 +156,21 @@ type AIBridgeSessionTokenUsageSummary struct { CacheWriteInputTokens int64 `json:"cache_write_input_tokens"` } +// AIBridgeSessionNetworkCallSummary aggregates the Agent Firewall network +// calls made during a session. Blocked counts calls denied by the firewall +// allow-list. +type AIBridgeSessionNetworkCallSummary struct { + Total int64 `json:"total"` + Blocked int64 `json:"blocked"` +} + +// AIBridgeSessionNetworkDomain is one destination host contacted during a +// session, with the number of network calls made to it. +type AIBridgeSessionNetworkDomain struct { + Domain string `json:"domain"` + Count int64 `json:"count"` +} + type AIBridgeListSessionsResponse struct { Count int64 `json:"count"` Sessions []AIBridgeSession `json:"sessions"` @@ -54,7 +191,23 @@ type AIBridgeSessionThreadsResponse struct { StartedAt time.Time `json:"started_at" format:"date-time"` EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` TokenUsageSummary AIBridgeSessionThreadsTokenUsage `json:"token_usage_summary"` - Threads []AIBridgeThread `json:"threads"` + // NetworkCalls summarizes the Agent Firewall network calls made during the + // session. A nil value means the session did not pass through Agent + // Firewall, so network call monitoring was not active, which the UI + // surfaces as "Disabled". + NetworkCalls *AIBridgeSessionNetworkCallSummary `json:"network_calls,omitempty"` + // NetworkTopDomains lists the most contacted destination hosts, ordered by + // call count descending. NetworkDomainCount is the total number of distinct + // domains, used to render a "+N more" overflow beyond the listed domains. + NetworkTopDomains []AIBridgeSessionNetworkDomain `json:"network_top_domains,omitempty"` + NetworkDomainCount int64 `json:"network_domain_count,omitempty"` + // NetworkCallLogs is the chronological list of individual network calls made + // during the session, holding the earliest calls up to a server-side cap. + // NetworkCalls remains authoritative for whole-session totals, so a shorter + // list than NetworkCalls.Total means the list was truncated. Empty when the + // session did not pass through Agent Firewall. + NetworkCallLogs []AgentFirewallLog `json:"network_call_logs,omitempty"` + Threads []AIBridgeThread `json:"threads"` } // AIBridgeSessionThreadsTokenUsage represents aggregated token usage @@ -80,6 +233,13 @@ type AIBridgeThread struct { EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` TokenUsage AIBridgeSessionThreadsTokenUsage `json:"token_usage"` AgenticActions []AIBridgeAgenticAction `json:"agentic_actions"` + // ErrorType is the categorized terminal upstream error from the root + // interception, or nil when the interception succeeded. See the + // aibridge_interception_error_type enum for possible values. + ErrorType *string `json:"error_type,omitempty"` + // ErrorMessage is the raw terminal upstream error message from the root + // interception. Nil when the interception succeeded. + ErrorMessage *string `json:"error_message,omitempty"` // AgentFirewallSessionID links this thread to an agent firewall // confinement session. Nil when the request did not pass through // the agent firewall. @@ -200,7 +360,7 @@ func (c *Client) AIBridgeListSessions(ctx context.Context, filter AIBridgeListSe return AIBridgeListSessionsResponse{}, ReadBodyAsError(res) } var resp AIBridgeListSessionsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // AIBridgeGetSessionThreads returns a single session with expanded @@ -227,7 +387,7 @@ func (c *Client) AIBridgeGetSessionThreads(ctx context.Context, sessionID string return AIBridgeSessionThreadsResponse{}, ReadBodyAsError(res) } var resp AIBridgeSessionThreadsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // AIBridgeListClients returns the distinct AI clients visible to the caller. @@ -241,7 +401,37 @@ func (c *Client) AIBridgeListClients(ctx context.Context) ([]string, error) { return nil, ReadBodyAsError(res) } var clients []string - return clients, json.NewDecoder(res.Body).Decode(&clients) + return clients, ReadBodyAsJSON(res, &clients) +} + +// ExportOrganizationAISpend returns a CSV of per-user, per-group, per-model, +// per-provider AI spend for the organization over the requested period. Both +// bounds are optional and interpreted as UTC, and zero values fall back to the +// current budget period on the server. The caller is responsible for closing +// the returned ReadCloser. +func (c *Client) ExportOrganizationAISpend(ctx context.Context, organization uuid.UUID, opts AISpendPeriodWindow) (io.ReadCloser, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/ai/spend/export", organization.String()), + nil, + func(r *http.Request) { + q := r.URL.Query() + if !opts.PeriodStart.IsZero() { + q.Set("period_start", opts.PeriodStart.UTC().Format(time.RFC3339Nano)) + } + if !opts.PeriodEnd.IsZero() { + q.Set("period_end", opts.PeriodEnd.UTC().Format(time.RFC3339Nano)) + } + r.URL.RawQuery = q.Encode() + }, + ) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + if res.StatusCode != http.StatusOK { + defer res.Body.Close() + return nil, ReadBodyAsError(res) + } + return res.Body, nil } type GroupAIBudget struct { @@ -252,6 +442,7 @@ type GroupAIBudget struct { } type UpsertGroupAIBudgetRequest struct { + // SpendLimitMicros must not exceed MaxAISpendLimitMicros. SpendLimitMicros int64 `json:"spend_limit_micros" validate:"gte=0"` } @@ -270,7 +461,7 @@ func (c *Client) GroupAIBudget(ctx context.Context, group uuid.UUID) (GroupAIBud return GroupAIBudget{}, ReadBodyAsError(res) } var resp GroupAIBudget - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpsertGroupAIBudget creates or updates the AI spend budget for the given group. @@ -288,7 +479,7 @@ func (c *Client) UpsertGroupAIBudget(ctx context.Context, group uuid.UUID, req U return GroupAIBudget{}, ReadBodyAsError(res) } var resp GroupAIBudget - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteGroupAIBudget removes the AI spend budget for the given group. @@ -319,14 +510,15 @@ type UserAIBudgetOverride struct { type UpsertUserAIBudgetOverrideRequest struct { // GroupID is the group the user's spend is attributed to. The user must // be a member of this group. - GroupID uuid.UUID `json:"group_id" format:"uuid" validate:"required"` - SpendLimitMicros int64 `json:"spend_limit_micros" validate:"gte=0"` + GroupID uuid.UUID `json:"group_id" format:"uuid" validate:"required"` + // SpendLimitMicros must not exceed MaxAISpendLimitMicros. + SpendLimitMicros int64 `json:"spend_limit_micros" validate:"gte=0"` } // UserAIBudgetOverride returns the AI spend budget override configured for the given user. func (c *Client) UserAIBudgetOverride(ctx context.Context, user uuid.UUID) (UserAIBudgetOverride, error) { res, err := c.Request(ctx, http.MethodGet, - fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()), + fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()), nil, ) if err != nil { @@ -338,13 +530,13 @@ func (c *Client) UserAIBudgetOverride(ctx context.Context, user uuid.UUID) (User return UserAIBudgetOverride{}, ReadBodyAsError(res) } var resp UserAIBudgetOverride - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpsertUserAIBudgetOverride creates or updates the AI spend budget override for the given user. func (c *Client) UpsertUserAIBudgetOverride(ctx context.Context, user uuid.UUID, req UpsertUserAIBudgetOverrideRequest) (UserAIBudgetOverride, error) { res, err := c.Request(ctx, http.MethodPut, - fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()), + fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()), req, ) if err != nil { @@ -356,13 +548,13 @@ func (c *Client) UpsertUserAIBudgetOverride(ctx context.Context, user uuid.UUID, return UserAIBudgetOverride{}, ReadBodyAsError(res) } var resp UserAIBudgetOverride - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteUserAIBudgetOverride removes the AI spend budget override for the given user. func (c *Client) DeleteUserAIBudgetOverride(ctx context.Context, user uuid.UUID) error { res, err := c.Request(ctx, http.MethodDelete, - fmt.Sprintf("/api/v2/users/%s/ai/budget", user.String()), + fmt.Sprintf("/api/v2/users/%s/ai/budget/override", user.String()), nil, ) if err != nil { @@ -375,3 +567,95 @@ func (c *Client) DeleteUserAIBudgetOverride(ctx context.Context, user uuid.UUID) } return nil } + +// UserAISpendStatus returns the current AI spend snapshot for the given user +// within the active budget period. +func (c *Client) UserAISpendStatus(ctx context.Context, user uuid.UUID) (UserAISpendStatus, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/users/%s/ai/spend", user.String()), + nil, + ) + if err != nil { + return UserAISpendStatus{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return UserAISpendStatus{}, ReadBodyAsError(res) + } + var resp UserAISpendStatus + return resp, ReadBodyAsJSON(res, &resp) +} + +// OrganizationGroupsAISpend returns AI spend for the given groups within the +// organization for the active budget period. At most 100 group IDs may be +// requested per call, and callers with more groups are expected to batch +// across multiple requests. +func (c *Client) OrganizationGroupsAISpend(ctx context.Context, organization uuid.UUID, groupIDs []uuid.UUID) (OrganizationGroupsAISpend, error) { + ids := slice.List(groupIDs, func(id uuid.UUID) string { return id.String() }) + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/groups/ai/spend", organization.String()), + nil, + func(r *http.Request) { + q := r.URL.Query() + q.Set("group_ids", strings.Join(ids, ",")) + r.URL.RawQuery = q.Encode() + }, + ) + if err != nil { + return OrganizationGroupsAISpend{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return OrganizationGroupsAISpend{}, ReadBodyAsError(res) + } + var resp OrganizationGroupsAISpend + return resp, ReadBodyAsJSON(res, &resp) +} + +// GroupAISpend returns AI spend for the given group within the active budget +// period. +func (c *Client) GroupAISpend(ctx context.Context, group uuid.UUID) (GroupAISpend, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/groups/%s/ai/spend", group.String()), + nil, + ) + if err != nil { + return GroupAISpend{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupAISpend{}, ReadBodyAsError(res) + } + var resp GroupAISpend + return resp, ReadBodyAsJSON(res, &resp) +} + +// GroupMembersAISpend returns AI spend attributed to the given group for the +// specified users within the active budget period. At most 100 user IDs may be +// requested per call, and callers with more members are expected to batch +// across multiple requests. +func (c *Client) GroupMembersAISpend(ctx context.Context, group uuid.UUID, userIDs []uuid.UUID) (GroupMembersAISpend, error) { + ids := slice.List(userIDs, func(id uuid.UUID) string { return id.String() }) + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/groups/%s/members/ai/spend", group.String()), + nil, + func(r *http.Request) { + q := r.URL.Query() + q.Set("user_ids", strings.Join(ids, ",")) + r.URL.RawQuery = q.Encode() + }, + ) + if err != nil { + return GroupMembersAISpend{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupMembersAISpend{}, ReadBodyAsError(res) + } + var resp GroupMembersAISpend + return resp, ReadBodyAsJSON(res, &resp) +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/aigatewaykeys.go b/vendor/github.com/coder/coder/v2/codersdk/aigatewaykeys.go index 3bd0d4644..e3aa6e145 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/aigatewaykeys.go +++ b/vendor/github.com/coder/coder/v2/codersdk/aigatewaykeys.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -48,7 +47,7 @@ func (c *Client) CreateAIGatewayKey(ctx context.Context, req CreateAIGatewayKeyR return CreateAIGatewayKeyResponse{}, ReadBodyAsError(res) } var resp CreateAIGatewayKeyResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // ListAIGatewayKeys lists all AI Gateway keys. @@ -63,7 +62,7 @@ func (c *Client) ListAIGatewayKeys(ctx context.Context) ([]AIGatewayKey, error) return nil, ReadBodyAsError(res) } var resp []AIGatewayKey - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteAIGatewayKey deletes an AI Gateway key by ID. diff --git a/vendor/github.com/coder/coder/v2/codersdk/aimodelprices.go b/vendor/github.com/coder/coder/v2/codersdk/aimodelprices.go new file mode 100644 index 000000000..f584bf7a1 --- /dev/null +++ b/vendor/github.com/coder/coder/v2/codersdk/aimodelprices.go @@ -0,0 +1,127 @@ +package codersdk + +import ( + "context" + "net/http" + "time" +) + +// AIModelPrice is a per-model token price used by AI Gateway to compute the +// cost of an interception. +// +// Prices are integer micro-units per million tokens, so 1000000 is $1.00 per +// million tokens. A nil price means the price is not known, which the cost +// calculation treats the same as zero. Distinguish that from an explicit 0, +// which declares the model free of charge. +type AIModelPrice struct { + Provider string `json:"provider"` + Model string `json:"model"` + InputPrice *int64 `json:"input_price"` + OutputPrice *int64 `json:"output_price"` + CacheReadPrice *int64 `json:"cache_read_price"` + CacheWritePrice *int64 `json:"cache_write_price"` + Source AIModelPriceSource `json:"source"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` +} + +// AIModelPriceSource is where a model price came from. +type AIModelPriceSource string + +const ( + // AIModelPriceSourceDefault is a price from the embedded price book. + AIModelPriceSourceDefault AIModelPriceSource = "default" + // AIModelPriceSourceCustom is a price set through the API. + AIModelPriceSourceCustom AIModelPriceSource = "custom" +) + +// AIModelPriceSourceFilter selects which prices a listing reports. It is +// distinct from AIModelPriceSource because no stored price is "all". +// +// @typescript-ignore AIModelPriceSourceFilter +type AIModelPriceSourceFilter string + +const ( + AIModelPriceSourceFilterDefault = AIModelPriceSourceFilter(AIModelPriceSourceDefault) + AIModelPriceSourceFilterCustom = AIModelPriceSourceFilter(AIModelPriceSourceCustom) + // AIModelPriceSourceFilterAll reports every price a model holds, so a model + // carrying both appears twice. + AIModelPriceSourceFilterAll AIModelPriceSourceFilter = "all" +) + +// MaxAIModelPricesBytes bounds an upsert request body. +const MaxAIModelPricesBytes = 1 << 20 // 1 MiB + +// UpsertAIModelPricesRequest sets prices for the listed models. Models absent +// from the request are left untouched. +type UpsertAIModelPricesRequest struct { + Prices []AIModelPriceUpsert `json:"prices"` +} + +// AIModelPriceUpsert is one model's prices in an upsert request. It carries +// only the writable fields of AIModelPrice. +type AIModelPriceUpsert struct { + Provider string `json:"provider"` + Model string `json:"model"` + InputPrice *int64 `json:"input_price"` + OutputPrice *int64 `json:"output_price"` + CacheReadPrice *int64 `json:"cache_read_price"` + CacheWritePrice *int64 `json:"cache_write_price"` +} + +// AIModelPricesFilter narrows the listed model prices. An empty field does not +// filter on that attribute. +// +// @typescript-ignore AIModelPricesFilter +type AIModelPricesFilter struct { + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` + // Source narrows to prices from one source. A model with both reports only + // its custom price unless this is set. + Source AIModelPriceSourceFilter `json:"source,omitempty"` +} + +func (f AIModelPricesFilter) asRequestOption() RequestOption { + return func(r *http.Request) { + query := r.URL.Query() + if f.Provider != "" { + query.Set("provider", f.Provider) + } + if f.Model != "" { + query.Set("model", f.Model) + } + if f.Source != "" { + query.Set("source", string(f.Source)) + } + r.URL.RawQuery = query.Encode() + } +} + +// ListAIModelPrices returns the AI model prices matching the filter. +func (c *ExperimentalClient) ListAIModelPrices(ctx context.Context, filter AIModelPricesFilter) ([]AIModelPrice, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/experimental/ai/model-prices", nil, filter.asRequestOption()) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var prices []AIModelPrice + return prices, ReadBodyAsJSON(res, &prices) +} + +// UpsertAIModelPrices sets prices for the models in req. The request is +// rejected in full if any model fails validation. +func (c *ExperimentalClient) UpsertAIModelPrices(ctx context.Context, req UpsertAIModelPricesRequest) error { + res, err := c.Request(ctx, http.MethodPost, "/api/experimental/ai/model-prices", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/aiproviders.go b/vendor/github.com/coder/coder/v2/codersdk/aiproviders.go index 5632ff568..6e81e3616 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/aiproviders.go +++ b/vendor/github.com/coder/coder/v2/codersdk/aiproviders.go @@ -195,12 +195,22 @@ type AIProvider struct { Type AIProviderType `json:"type"` Name string `json:"name"` DisplayName string `json:"display_name"` + Icon string `json:"icon"` Enabled bool `json:"enabled"` BaseURL string `json:"base_url"` APIKeys []AIProviderKey `json:"api_keys"` Settings AIProviderSettings `json:"settings"` CreatedAt time.Time `json:"created_at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" format:"date-time"` + + // Status carries runtime routing status; nil when empty. + Status *AIProviderStatus `json:"status,omitempty"` +} + +// AIProviderStatus carries non-fatal routing warnings. Direct +// routing remains available for the provider. +type AIProviderStatus struct { + Warnings []string `json:"warnings,omitempty"` } // AIProviderKey is a single API key registered on a provider. The @@ -222,6 +232,7 @@ type CreateAIProviderRequest struct { Type AIProviderType `json:"type"` Name string `json:"name"` DisplayName string `json:"display_name,omitempty"` + Icon string `json:"icon,omitempty"` Enabled bool `json:"enabled"` BaseURL string `json:"base_url"` APIKeys []string `json:"api_keys,omitempty"` @@ -275,6 +286,15 @@ func (req CreateAIProviderRequest) Validate() []ValidationError { } if req.Settings.Bedrock != nil { validations = append(validations, validateAIProviderRoleARN(req.Settings.Bedrock.RoleARN)...) + validations = append(validations, validateAIProviderBedrockProtocol(req.Settings.Bedrock.Protocol)...) + if req.Settings.Bedrock.ExternalID != "" { + validations = append(validations, ValidationError{ + Field: "settings.external_id", + Detail: "external_id is server-generated and cannot be set", + }) + } + validations = append(validations, validateAIProviderBedrockMantleRegion(*req.Settings.Bedrock)...) + validations = append(validations, validateAIProviderBedrockModels(*req.Settings.Bedrock)...) } if req.Type == AIProviderTypeCopilot && len(req.APIKeys) > 0 { validations = append(validations, ValidationError{ @@ -294,6 +314,7 @@ func (req CreateAIProviderRequest) Validate() []ValidationError { // clears all keys. type UpdateAIProviderRequest struct { DisplayName *string `json:"display_name,omitempty"` + Icon *string `json:"icon,omitempty"` Enabled *bool `json:"enabled,omitempty"` BaseURL *string `json:"base_url,omitempty"` APIKeys *[]AIProviderKeyMutation `json:"api_keys,omitempty"` @@ -324,15 +345,24 @@ func (req UpdateAIProviderRequest) Validate() []ValidationError { if req.APIKeys != nil { validations = append(validations, validateAIProviderKeyMutations(*req.APIKeys)...) } + // Despite arriving on a PATCH, a bedrock settings blob is a full + // replacement rather than a per-field patch: the caller must set every + // field, except AccessKey, AccessKeySecret, and ExternalID, which + // mergeAIProviderSettings carries forward from the stored row when + // omitted. Omitting any other field clears it, so the checks below apply + // to the patch exactly as they would to what gets stored. if req.Settings != nil && req.Settings.Bedrock != nil { validations = append(validations, validateAIProviderRoleARN(req.Settings.Bedrock.RoleARN)...) + validations = append(validations, validateAIProviderBedrockProtocol(req.Settings.Bedrock.Protocol)...) + validations = append(validations, validateAIProviderBedrockMantleRegion(*req.Settings.Bedrock)...) + validations = append(validations, validateAIProviderBedrockModels(*req.Settings.Bedrock)...) } return validations } // IsEmpty reports whether the patch carries no fields. func (req UpdateAIProviderRequest) IsEmpty() bool { - return req.DisplayName == nil && req.Enabled == nil && req.BaseURL == nil && req.APIKeys == nil && req.Settings == nil + return req.DisplayName == nil && req.Icon == nil && req.Enabled == nil && req.BaseURL == nil && req.APIKeys == nil && req.Settings == nil } func validateAIProviderName(name string) []ValidationError { @@ -349,6 +379,55 @@ func validateAIProviderName(name string) []ValidationError { return validations } +func validateAIProviderBedrockProtocol(protocol AIProviderBedrockProtocol) []ValidationError { + switch protocol { + case "", AIProviderBedrockProtocolInvokeModel, AIProviderBedrockProtocolMantle: + return nil + default: + return []ValidationError{{ + Field: "settings.protocol", + Detail: fmt.Sprintf("unsupported bedrock protocol %q, must be one of %q or %q", protocol, AIProviderBedrockProtocolInvokeModel, AIProviderBedrockProtocolMantle), + }} + } +} + +func validateAIProviderBedrockMantleRegion(b AIProviderBedrockSettings) []ValidationError { + // The Mantle protocol signs requests with SigV4, which requires a region. + if b.ResolvedProtocol() == AIProviderBedrockProtocolMantle && b.Region == "" { + return []ValidationError{{ + Field: "settings.region", + Detail: "region is required for the mantle protocol", + }} + } + return nil +} + +// validateAIProviderBedrockModels requires the model identifiers that the +// invoke-model protocol substitutes into every upstream request. Without them +// the provider cannot be constructed at runtime (see +// config.AWSBedrock.Validate), so it would be skipped at gateway startup and +// every request to it would 404. The mantle protocol forwards the client's +// model unchanged and needs neither field. +func validateAIProviderBedrockModels(b AIProviderBedrockSettings) []ValidationError { + if b.ResolvedProtocol() != AIProviderBedrockProtocolInvokeModel { + return nil + } + var validations []ValidationError + if b.Model == "" { + validations = append(validations, ValidationError{ + Field: "settings.model", + Detail: "model is required for the invoke-model protocol", + }) + } + if b.SmallFastModel == "" { + validations = append(validations, ValidationError{ + Field: "settings.small_fast_model", + Detail: "small_fast_model is required for the invoke-model protocol", + }) + } + return validations +} + func validateAIProviderRoleARN(roleARN string) []ValidationError { if roleARN == "" { return nil @@ -473,7 +552,7 @@ func (c *Client) AIProviders(ctx context.Context) ([]AIProvider, error) { return nil, ReadBodyAsError(res) } var providers []AIProvider - return providers, json.NewDecoder(res.Body).Decode(&providers) + return providers, ReadBodyAsJSON(res, &providers) } // AIProvider fetches a single AI provider by ID or name. @@ -487,7 +566,7 @@ func (c *Client) AIProvider(ctx context.Context, idOrName string) (AIProvider, e return AIProvider{}, ReadBodyAsError(res) } var provider AIProvider - return provider, json.NewDecoder(res.Body).Decode(&provider) + return provider, ReadBodyAsJSON(res, &provider) } // CreateAIProvider creates a new AI provider. @@ -501,7 +580,7 @@ func (c *Client) CreateAIProvider(ctx context.Context, req CreateAIProviderReque return AIProvider{}, ReadBodyAsError(res) } var provider AIProvider - return provider, json.NewDecoder(res.Body).Decode(&provider) + return provider, ReadBodyAsJSON(res, &provider) } // UpdateAIProvider partially updates an AI provider identified by @@ -516,7 +595,7 @@ func (c *Client) UpdateAIProvider(ctx context.Context, idOrName string, req Upda return AIProvider{}, ReadBodyAsError(res) } var provider AIProvider - return provider, json.NewDecoder(res.Body).Decode(&provider) + return provider, ReadBodyAsJSON(res, &provider) } // DeleteAIProvider soft-deletes an AI provider identified by ID or diff --git a/vendor/github.com/coder/coder/v2/codersdk/aiproviders_bedrock.go b/vendor/github.com/coder/coder/v2/codersdk/aiproviders_bedrock.go index 360b76333..b3bc94e9e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/aiproviders_bedrock.go +++ b/vendor/github.com/coder/coder/v2/codersdk/aiproviders_bedrock.go @@ -8,6 +8,23 @@ const AIProviderSettingsTypeBedrock = "bedrock" // AIProviderBedrockSettings. const AIProviderBedrockSettingsVersion = 1 +// AIProviderBedrockProtocol selects which AWS Bedrock wire protocol a provider +// targets. +type AIProviderBedrockProtocol string + +const ( + // AIProviderBedrockProtocolInvokeModel is the legacy InvokeModel protocol + // (bedrock-runtime.{region}.amazonaws.com), which translates the native + // Messages request into Bedrock's InvokeModel format. It is the default + // for the zero value. + AIProviderBedrockProtocolInvokeModel AIProviderBedrockProtocol = "invoke-model" + // AIProviderBedrockProtocolMantle is the mantle protocol + // (bedrock-mantle.{region}.api.aws/anthropic/v1/messages). It is a + // passthrough: the gateway forwards the native Messages request body + // unchanged and only applies AWS SigV4 signing (service bedrock-mantle). + AIProviderBedrockProtocolMantle AIProviderBedrockProtocol = "mantle" +) + // AIProviderBedrockSettings configures providers that authenticate // against AWS Bedrock. AccessKey and AccessKeySecret are write-only: // servers strip them from GET and list responses. Both secret fields @@ -35,19 +52,33 @@ type AIProviderBedrockSettings struct { // IRSA / EKS Pod Identity / EC2 Instance Profile) signs the AssumeRole // call, and the resulting temporary credentials sign Bedrock requests. RoleARN string `json:"role_arn,omitempty"` + // ExternalID is the STS external ID sent on the AssumeRole call when + // RoleARN is set. The server generates and owns it: create and update + // reject any client-supplied value that differs from the stored one (an + // update may echo the stored value back). + ExternalID string `json:"external_id,omitempty"` + // Protocol selects the Bedrock wire protocol. An empty value resolves to + // AIProviderBedrockProtocolInvokeModel, so existing rows keep the legacy + // behavior. + Protocol AIProviderBedrockProtocol `json:"protocol,omitempty"` +} + +// ResolvedProtocol returns the configured protocol, mapping the empty value to +// the legacy InvokeModel protocol. +func (b AIProviderBedrockSettings) ResolvedProtocol() AIProviderBedrockProtocol { + if b.Protocol == "" { + return AIProviderBedrockProtocolInvokeModel + } + return b.Protocol } // IsConfigured reports whether any load-bearing Bedrock field is set, // indicating that the operator wants the provider to authenticate via // AWS Bedrock rather than as a bearer-token Anthropic provider. // -// Model and SmallFastModel are intentionally excluded: they have -// deployment-level defaults declared in codersdk/deployment.go, so -// they're always non-empty in a real deployment and cannot serve as -// a detection signal. Region and credentials have no defaults and -// therefore reliably indicate operator intent. Credentials alone are -// not required because Bedrock can also authenticate via the AWS -// environment (instance profile, AWS_PROFILE, IRSA, etc.). +// Region and credentials have no defaults and therefore reliably indicate +// operator intent. Credentials alone are not required because Bedrock can +// also authenticate via the AWS environment (instance profile, AWS_PROFILE, IRSA, etc.). func (b AIProviderBedrockSettings) IsConfigured() bool { if b.Region != "" { return true diff --git a/vendor/github.com/coder/coder/v2/codersdk/aitasks.go b/vendor/github.com/coder/coder/v2/codersdk/aitasks.go index 43d318956..158c6c569 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/aitasks.go +++ b/vendor/github.com/coder/coder/v2/codersdk/aitasks.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "strings" @@ -34,7 +33,7 @@ func (c *Client) CreateTask(ctx context.Context, user string, request CreateTask } var task Task - if err := json.NewDecoder(res.Body).Decode(&task); err != nil { + if err := ReadBodyAsJSON(res, &task); err != nil { return Task{}, err } @@ -192,7 +191,7 @@ func (c *Client) Tasks(ctx context.Context, filter *TasksFilter) ([]Task, error) } var tres TasksListResponse - if err := json.NewDecoder(res.Body).Decode(&tres); err != nil { + if err := ReadBodyAsJSON(res, &tres); err != nil { return nil, err } @@ -212,7 +211,7 @@ func (c *Client) TaskByID(ctx context.Context, id uuid.UUID) (Task, error) { } var task Task - if err := json.NewDecoder(res.Body).Decode(&task); err != nil { + if err := ReadBodyAsJSON(res, &task); err != nil { return Task{}, err } @@ -234,7 +233,7 @@ func (c *Client) TaskByOwnerAndName(ctx context.Context, owner, ident string) (T } var task Task - if err := json.NewDecoder(res.Body).Decode(&task); err != nil { + if err := ReadBodyAsJSON(res, &task); err != nil { return Task{}, err } @@ -346,7 +345,7 @@ func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (Paus } var resp PauseTaskResponse - if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { + if err := ReadBodyAsJSON(res, &resp); err != nil { return PauseTaskResponse{}, err } @@ -369,7 +368,7 @@ func (c *Client) ResumeTask(ctx context.Context, user string, id uuid.UUID) (Res } var resp ResumeTaskResponse - if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { + if err := ReadBodyAsJSON(res, &resp); err != nil { return ResumeTaskResponse{}, err } @@ -415,7 +414,7 @@ func (c *Client) TaskLogs(ctx context.Context, user string, id uuid.UUID) (TaskL } var logs TaskLogsResponse - if err := json.NewDecoder(res.Body).Decode(&logs); err != nil { + if err := ReadBodyAsJSON(res, &logs); err != nil { return TaskLogsResponse{}, xerrors.Errorf("decoding task logs response: %w", err) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/apikey.go b/vendor/github.com/coder/coder/v2/codersdk/apikey.go index 6bb514920..6d3a7da9c 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/apikey.go +++ b/vendor/github.com/coder/coder/v2/codersdk/apikey.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -74,7 +73,7 @@ func (c *Client) CreateToken(ctx context.Context, userID string, req CreateToken } var apiKey GenerateAPIKeyResponse - return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) + return apiKey, ReadBodyAsJSON(res, &apiKey) } // CreateAPIKey generates an API key for the user ID provided. @@ -93,7 +92,7 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyR } var apiKey GenerateAPIKeyResponse - return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) + return apiKey, ReadBodyAsJSON(res, &apiKey) } type TokensFilter struct { @@ -132,7 +131,7 @@ func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) return nil, ReadBodyAsError(res) } apiKey := []APIKeyWithOwner{} - return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) + return apiKey, ReadBodyAsJSON(res, &apiKey) } // APIKeyByID returns the api key by id. @@ -146,7 +145,7 @@ func (c *Client) APIKeyByID(ctx context.Context, userID string, id string) (*API return nil, ReadBodyAsError(res) } apiKey := &APIKey{} - return apiKey, json.NewDecoder(res.Body).Decode(apiKey) + return apiKey, ReadBodyAsJSON(res, apiKey) } // APIKeyByName returns the api key by name. @@ -160,7 +159,7 @@ func (c *Client) APIKeyByName(ctx context.Context, userID string, name string) ( return nil, ReadBodyAsError(res) } apiKey := &APIKey{} - return apiKey, json.NewDecoder(res.Body).Decode(apiKey) + return apiKey, ReadBodyAsJSON(res, apiKey) } // DeleteAPIKey deletes API key by id. @@ -201,5 +200,5 @@ func (c *Client) GetTokenConfig(ctx context.Context, userID string) (TokenConfig return TokenConfig{}, ReadBodyAsError(res) } tokenConfig := TokenConfig{} - return tokenConfig, json.NewDecoder(res.Body).Decode(&tokenConfig) + return tokenConfig, ReadBodyAsJSON(res, &tokenConfig) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/apikey_scopes_gen.go b/vendor/github.com/coder/coder/v2/codersdk/apikey_scopes_gen.go index 5471823e7..13cbd3841 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/apikey_scopes_gen.go +++ b/vendor/github.com/coder/coder/v2/codersdk/apikey_scopes_gen.go @@ -59,6 +59,12 @@ const ( APIKeyScopeChatRead APIKeyScope = "chat:read" APIKeyScopeChatShare APIKeyScope = "chat:share" APIKeyScopeChatUpdate APIKeyScope = "chat:update" + APIKeyScopeChatModelConfigAll APIKeyScope = "chat_model_config:*" + APIKeyScopeChatModelConfigCreate APIKeyScope = "chat_model_config:create" + APIKeyScopeChatModelConfigDelete APIKeyScope = "chat_model_config:delete" + APIKeyScopeChatModelConfigRead APIKeyScope = "chat_model_config:read" + APIKeyScopeChatModelConfigShare APIKeyScope = "chat_model_config:share" + APIKeyScopeChatModelConfigUpdate APIKeyScope = "chat_model_config:update" APIKeyScopeCoderAll APIKeyScope = "coder:all" APIKeyScopeCoderApikeysManageSelf APIKeyScope = "coder:apikeys.manage_self" APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect" @@ -104,6 +110,12 @@ const ( APIKeyScopeLicenseCreate APIKeyScope = "license:create" APIKeyScopeLicenseDelete APIKeyScope = "license:delete" APIKeyScopeLicenseRead APIKeyScope = "license:read" + APIKeyScopeMcpServerConfigAll APIKeyScope = "mcp_server_config:*" + APIKeyScopeMcpServerConfigCreate APIKeyScope = "mcp_server_config:create" + APIKeyScopeMcpServerConfigDelete APIKeyScope = "mcp_server_config:delete" + APIKeyScopeMcpServerConfigRead APIKeyScope = "mcp_server_config:read" + APIKeyScopeMcpServerConfigShare APIKeyScope = "mcp_server_config:share" + APIKeyScopeMcpServerConfigUpdate APIKeyScope = "mcp_server_config:update" APIKeyScopeNotificationMessageAll APIKeyScope = "notification_message:*" APIKeyScopeNotificationMessageCreate APIKeyScope = "notification_message:create" APIKeyScopeNotificationMessageDelete APIKeyScope = "notification_message:delete" @@ -219,6 +231,11 @@ const ( APIKeyScopeWorkspaceAgentResourceMonitorCreate APIKeyScope = "workspace_agent_resource_monitor:create" APIKeyScopeWorkspaceAgentResourceMonitorRead APIKeyScope = "workspace_agent_resource_monitor:read" APIKeyScopeWorkspaceAgentResourceMonitorUpdate APIKeyScope = "workspace_agent_resource_monitor:update" + APIKeyScopeWorkspaceBuildOrchestrationAll APIKeyScope = "workspace_build_orchestration:*" + APIKeyScopeWorkspaceBuildOrchestrationCreate APIKeyScope = "workspace_build_orchestration:create" + APIKeyScopeWorkspaceBuildOrchestrationDelete APIKeyScope = "workspace_build_orchestration:delete" + APIKeyScopeWorkspaceBuildOrchestrationRead APIKeyScope = "workspace_build_orchestration:read" + APIKeyScopeWorkspaceBuildOrchestrationUpdate APIKeyScope = "workspace_build_orchestration:update" APIKeyScopeWorkspaceDormantAll APIKeyScope = "workspace_dormant:*" APIKeyScopeWorkspaceDormantApplicationConnect APIKeyScope = "workspace_dormant:application_connect" APIKeyScopeWorkspaceDormantCreate APIKeyScope = "workspace_dormant:create" @@ -246,6 +263,8 @@ var PublicAPIKeyScopes = []APIKeyScope{ APIKeyScopeApiKeyDelete, APIKeyScopeApiKeyRead, APIKeyScopeApiKeyUpdate, + APIKeyScopeChatModelConfigRead, + APIKeyScopeChatModelConfigShare, APIKeyScopeCoderAll, APIKeyScopeCoderApikeysManageSelf, APIKeyScopeCoderApplicationConnect, diff --git a/vendor/github.com/coder/coder/v2/codersdk/appname.go b/vendor/github.com/coder/coder/v2/codersdk/appname.go new file mode 100644 index 000000000..dc0249bda --- /dev/null +++ b/vendor/github.com/coder/coder/v2/codersdk/appname.go @@ -0,0 +1,75 @@ +package codersdk + +import ( + "strings" + + utilstrings "github.com/coder/coder/v2/coderd/util/strings" +) + +// maxAppNameLength caps an app name in runes. +const maxAppNameLength = 64 + +// AppFamilyName is the bounded set of names that arbitrary app names group +// into, for callers that need a fixed value such as a metric label or the +// Connection_Type enum. Server-side only, not part of the HTTP API. +// +// @typescript-ignore AppFamilyName +type AppFamilyName string + +const ( + AppFamilyVSCode AppFamilyName = "vscode" + AppFamilyJetBrains AppFamilyName = "jetbrains" + AppFamilySSH AppFamilyName = "ssh" + AppFamilyReconnectingPTY AppFamilyName = "reconnecting_pty" + AppFamilyUnknown AppFamilyName = "unknown" +) + +// appNameFamilies never gates storage, so a missing alias only costs an +// AppFamilyUnknown label. Keys are the IDs Coder's registry modules use. +var appNameFamilies = map[string]AppFamilyName{ + "vscode": AppFamilyVSCode, + "vscode_insiders": AppFamilyVSCode, + "vscode_web": AppFamilyVSCode, + "code_server": AppFamilyVSCode, + "cursor": AppFamilyVSCode, + "windsurf": AppFamilyVSCode, + "positron": AppFamilyVSCode, + "vscodium": AppFamilyVSCode, + "codium": AppFamilyVSCode, + "antigravity": AppFamilyVSCode, + "trae": AppFamilyVSCode, + "kiro": AppFamilyVSCode, + "devin": AppFamilyVSCode, + + "jetbrains": AppFamilyJetBrains, + // Zed has no Connection_Type or session count field of its own, so it + // rolls up under SSH. The raw name still reaches storage. + "zed": AppFamilySSH, + "ssh": AppFamilySSH, + "reconnecting_pty": AppFamilyReconnectingPTY, +} + +// AppNameFamily normalizes an app name and returns its family, or +// AppFamilyUnknown. +func AppNameFamily(appName string) AppFamilyName { + if family, ok := appNameFamilies[NormalizeAppName(appName)]; ok { + return family + } + return AppFamilyUnknown +} + +// NormalizeAppName prepares a client-supplied app name for storage and +// lookup: it strips the null bytes Postgres TEXT rejects, trims, truncates, +// lowercases, and folds hyphens to underscores. Empty becomes +// AppFamilyUnknown. +func NormalizeAppName(appName string) string { + appName = strings.ReplaceAll(appName, "\x00", "") + // Trim before truncating so padding does not spend the budget, and after + // in case the cut lands in whitespace. + appName = strings.TrimSpace(appName) + appName = strings.TrimSpace(utilstrings.Truncate(appName, maxAppNameLength)) + if appName == "" { + return string(AppFamilyUnknown) + } + return strings.ReplaceAll(strings.ToLower(appName), "-", "_") +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/audit.go b/vendor/github.com/coder/coder/v2/codersdk/audit.go index 6193b0681..83ac629bc 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/audit.go +++ b/vendor/github.com/coder/coder/v2/codersdk/audit.go @@ -14,22 +14,23 @@ import ( type ResourceType string const ( - ResourceTypeTemplate ResourceType = "template" - ResourceTypeTemplateVersion ResourceType = "template_version" - ResourceTypeUser ResourceType = "user" - ResourceTypeWorkspace ResourceType = "workspace" - ResourceTypeWorkspaceBuild ResourceType = "workspace_build" - ResourceTypeGitSSHKey ResourceType = "git_ssh_key" - ResourceTypeAPIKey ResourceType = "api_key" - ResourceTypeGroup ResourceType = "group" - ResourceTypeLicense ResourceType = "license" - ResourceTypeConvertLogin ResourceType = "convert_login" - ResourceTypeHealthSettings ResourceType = "health_settings" - ResourceTypeNotificationsSettings ResourceType = "notifications_settings" - ResourceTypePrebuildsSettings ResourceType = "prebuilds_settings" - ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" - ResourceTypeOrganization ResourceType = "organization" - ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app" + ResourceTypeTemplate ResourceType = "template" + ResourceTypeTemplateVersion ResourceType = "template_version" + ResourceTypeUser ResourceType = "user" + ResourceTypeWorkspace ResourceType = "workspace" + ResourceTypeWorkspaceBuild ResourceType = "workspace_build" + ResourceTypeGitSSHKey ResourceType = "git_ssh_key" + ResourceTypeAPIKey ResourceType = "api_key" + ResourceTypeGroup ResourceType = "group" + ResourceTypeLicense ResourceType = "license" + ResourceTypeConvertLogin ResourceType = "convert_login" + ResourceTypeHealthSettings ResourceType = "health_settings" + ResourceTypeNotificationsSettings ResourceType = "notifications_settings" + ResourceTypePrebuildsSettings ResourceType = "prebuilds_settings" + ResourceTypeOAuth2ProviderSettings ResourceType = "oauth2_provider_settings" + ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" + ResourceTypeOrganization ResourceType = "organization" + ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app" // nolint:gosec // This is not a secret. ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" ResourceTypeCustomRole ResourceType = "custom_role" @@ -43,17 +44,21 @@ const ( ResourceTypeWorkspaceAgent ResourceType = "workspace_agent" // Deprecated: Workspace App connections are now included in the // connection log. - ResourceTypeWorkspaceApp ResourceType = "workspace_app" - ResourceTypeTask ResourceType = "task" - ResourceTypeAISeat ResourceType = "ai_seat" - ResourceTypeAIProvider ResourceType = "ai_provider" - ResourceTypeAIProviderKey ResourceType = "ai_provider_key" - ResourceTypeAIGatewayKey ResourceType = "ai_gateway_key" - ResourceTypeGroupAIBudget ResourceType = "group_ai_budget" - ResourceTypeUserAIBudgetOverride ResourceType = "user_ai_budget_override" - ResourceTypeChat ResourceType = "chat" - ResourceTypeUserSecret ResourceType = "user_secret" - ResourceTypeUserSkill ResourceType = "user_skill" + ResourceTypeWorkspaceApp ResourceType = "workspace_app" + ResourceTypeTask ResourceType = "task" + ResourceTypeAISeat ResourceType = "ai_seat" + ResourceTypeAIProvider ResourceType = "ai_provider" + ResourceTypeAIProviderKey ResourceType = "ai_provider_key" + ResourceTypeAIGatewayKey ResourceType = "ai_gateway_key" + ResourceTypeGroupAIBudget ResourceType = "group_ai_budget" + ResourceTypeUserAIBudgetOverride ResourceType = "user_ai_budget_override" + ResourceTypeChat ResourceType = "chat" + ResourceTypeMCPServerConfig ResourceType = "mcp_server_config" + ResourceTypeChatModelConfig ResourceType = "chat_model_config" + ResourceTypeUserSecret ResourceType = "user_secret" + ResourceTypeUserSkill ResourceType = "user_skill" + ResourceTypeChatInstructionSettings ResourceType = "chat_instruction_settings" + ResourceTypeChatOperationalSettings ResourceType = "chat_operational_settings" ) func (r ResourceType) FriendlyString() string { @@ -90,6 +95,8 @@ func (r ResourceType) FriendlyString() string { return "notifications_settings" case ResourceTypePrebuildsSettings: return "prebuilds_settings" + case ResourceTypeOAuth2ProviderSettings: + return "oauth2 provider settings" case ResourceTypeOAuth2ProviderApp: return "oauth2 app" case ResourceTypeOAuth2ProviderAppSecret: @@ -126,10 +133,18 @@ func (r ResourceType) FriendlyString() string { return "user ai budget override" case ResourceTypeChat: return "chat" + case ResourceTypeMCPServerConfig: + return "mcp server config" + case ResourceTypeChatModelConfig: + return "chat model config" case ResourceTypeUserSecret: return "user secret" case ResourceTypeUserSkill: return "user skill" + case ResourceTypeChatInstructionSettings: + return "chat instruction settings" + case ResourceTypeChatOperationalSettings: + return "chat operational settings" default: return "unknown" } @@ -252,7 +267,7 @@ type CreateTestAuditLogRequest struct { // AuditLogs retrieves audit logs from the given page. func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit", nil, req.Pagination.asRequestOption(), func(r *http.Request) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit", nil, req.asRequestOption(), func(r *http.Request) { q := r.URL.Query() var params []string if req.SearchQuery != "" { @@ -271,7 +286,7 @@ func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogR } var logRes AuditLogResponse - err = json.NewDecoder(res.Body).Decode(&logRes) + err = ReadBodyAsJSON(res, &logRes) if err != nil { return AuditLogResponse{}, err } diff --git a/vendor/github.com/coder/coder/v2/codersdk/authorization.go b/vendor/github.com/coder/coder/v2/codersdk/authorization.go index 49c963473..99cc3b77e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/authorization.go +++ b/vendor/github.com/coder/coder/v2/codersdk/authorization.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "net/http" ) @@ -71,5 +70,5 @@ func (c *Client) AuthCheck(ctx context.Context, req AuthorizationRequest) (Autho return AuthorizationResponse{}, ReadBodyAsError(res) } var resp AuthorizationResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/chats.go b/vendor/github.com/coder/coder/v2/codersdk/chats.go index b374c5f24..7f0cd1c61 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/chats.go +++ b/vendor/github.com/coder/coder/v2/codersdk/chats.go @@ -10,13 +10,13 @@ import ( "mime" "net/http" "net/url" + "slices" "strconv" "strings" "time" "github.com/google/uuid" "github.com/invopop/jsonschema" - "github.com/shopspring/decimal" "golang.org/x/xerrors" "github.com/coder/websocket" @@ -78,8 +78,8 @@ var AllChatAttachmentMediaTypes = []ChatAttachmentMediaType{ // CompactionThresholdKey returns the user-config key for a specific // model configuration's compaction threshold. -func CompactionThresholdKey(modelConfigID uuid.UUID) string { - return ChatCompactionThresholdKeyPrefix + modelConfigID.String() +func CompactionThresholdKey(modelID uuid.UUID) string { + return ChatCompactionThresholdKeyPrefix + modelID.String() } // ChatStatus represents the status of a chat. @@ -87,10 +87,7 @@ type ChatStatus string const ( ChatStatusWaiting ChatStatus = "waiting" - ChatStatusPending ChatStatus = "pending" ChatStatusRunning ChatStatus = "running" - ChatStatusPaused ChatStatus = "paused" - ChatStatusCompleted ChatStatus = "completed" ChatStatusError ChatStatus = "error" ChatStatusRequiresAction ChatStatus = "requires_action" ChatStatusInterrupting ChatStatus = "interrupting" @@ -107,26 +104,30 @@ const ( // Chat represents a chat session with an AI agent. type Chat struct { - ID uuid.UUID `json:"id" format:"uuid"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - OwnerID uuid.UUID `json:"owner_id" format:"uuid"` - OwnerUsername string `json:"owner_username,omitempty"` - OwnerName string `json:"owner_name,omitempty"` - WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"` - BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"` - AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"` - ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"` - RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"` - LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"` - Title string `json:"title"` - Status ChatStatus `json:"status"` - PlanMode ChatPlanMode `json:"plan_mode,omitempty"` - LastError *ChatError `json:"last_error,omitempty"` - LastTurnSummary *string `json:"last_turn_summary"` - DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` - Archived bool `json:"archived"` + ID uuid.UUID `json:"id" format:"uuid"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + OwnerID uuid.UUID `json:"owner_id" format:"uuid"` + OwnerUsername string `json:"owner_username,omitempty"` + OwnerName string `json:"owner_name,omitempty"` + WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"` + BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"` + AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"` + ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"` + RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"` + LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"` + LastReasoningEffort *string `json:"last_reasoning_effort,omitempty"` + Title string `json:"title"` + Status ChatStatus `json:"status"` + PlanMode ChatPlanMode `json:"plan_mode,omitempty"` + LastError *ChatError `json:"last_error,omitempty"` + LastTurnSummary *string `json:"last_turn_summary"` + // Summary is the persisted whole-chat summary, generated in the background. + // It is nil until the first summary has been produced. + Summary *string `json:"summary"` + DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` + Archived bool `json:"archived"` // Shared is true when this chat's root chat has explicit user or group ACL entries. Shared bool `json:"shared"` PinOrder int32 `json:"pin_order"` @@ -140,9 +141,12 @@ type Chat struct { // Context reports the chat's pinned workspace-context state and // whether it has drifted from the agent's latest pushed snapshot. // Nil when the chat has no pinned context yet. - Context *ChatContext `json:"context,omitempty"` - Warnings []string `json:"warnings,omitempty"` - ClientType ChatClientType `json:"client_type"` + Context *ChatContext `json:"context,omitempty"` + // QueuedForCapacity reports that the chat is waiting for a concurrent + // agent slot. Single-chat reads derive it; list responses leave it false. + QueuedForCapacity bool `json:"queued_for_capacity,omitempty"` + Warnings []string `json:"warnings,omitempty"` + ClientType ChatClientType `json:"client_type"` // Children holds child (subagent) chats nested under this root // chat. Always initialized to an empty slice so the JSON field // is present as []. Child chats cannot create their own @@ -223,7 +227,7 @@ const ( // ChatContextTool is one tool exposed by a pinned MCP server, reported on the // single-chat GET response. Metadata only; the input schema is omitted. type ChatContextTool struct { - // Name is the tool name with the "__" prefix the agent adds + // Name is the tool name with the `__` prefix the agent adds // stripped, so it reads as the server exposes it. Name string `json:"name"` // Description is the tool's human-readable summary; may be empty. @@ -238,6 +242,7 @@ type ChatFileMetadata struct { OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` Name string `json:"name"` MimeType string `json:"mime_type"` + SizeBytes int64 `json:"size_bytes"` CreatedAt time.Time `json:"created_at" format:"date-time"` } @@ -288,6 +293,15 @@ const ( ChatMessagePartTypeFileReference ChatMessagePartType = "file-reference" ChatMessagePartTypeContextFile ChatMessagePartType = "context-file" ChatMessagePartTypeSkill ChatMessagePartType = "skill" + // ChatMessagePartTypeHookContext is model context injected into a user + // prompt by a lifecycle hook. It is included in model prompt assembly + // and stripped from every client-facing conversion; the server rejects + // it in client-submitted content. + ChatMessagePartTypeHookContext ChatMessagePartType = "hook-context" + // ChatMessagePartTypeHookNotice is a user-facing lifecycle hook notice, + // either attached to a prompt or in its own row. It is excluded from model + // prompts and rejected in client-submitted content. + ChatMessagePartTypeHookNotice ChatMessagePartType = "hook-notice" ) // AllChatMessagePartTypes returns all known ChatMessagePartType values. @@ -302,6 +316,8 @@ func AllChatMessagePartTypes() []ChatMessagePartType { ChatMessagePartTypeFileReference, ChatMessagePartTypeContextFile, ChatMessagePartTypeSkill, + ChatMessagePartTypeHookContext, + ChatMessagePartTypeHookNotice, } } @@ -330,8 +346,7 @@ func AllChatMessagePartTypes() []ChatMessagePartType { // and wastes space in persisted chat_messages rows. type ChatMessagePart struct { Type ChatMessagePartType `json:"type"` - Text string `json:"text" variants:"text,reasoning"` - Signature string `json:"signature,omitempty"` + Text string `json:"text" variants:"text,reasoning,hook-notice"` ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call?,tool-result?"` ToolName string `json:"tool_name,omitempty" variants:"tool-call?,tool-result?"` MCPServerConfigID uuid.NullUUID `json:"mcp_server_config_id,omitempty" format:"uuid" variants:"tool-call?,tool-result?"` @@ -368,6 +383,8 @@ type ChatMessagePart struct { // ProviderExecuted indicates the tool call was executed by // the provider (e.g. Anthropic computer use). ProviderExecuted bool `json:"provider_executed,omitempty" variants:"tool-call?,tool-result?"` + // HookRewritten indicates that a lifecycle hook replaced model-proposed tool input. + HookRewritten bool `json:"hook_rewritten,omitempty" variants:"tool-call?"` // CreatedAt is the timestamp this part carries. The semantics // depend on the part type: for tool-call and tool-result parts // it is the time the call was emitted or the result was @@ -548,13 +565,18 @@ type ToolResult struct { // CreateChatRequest is the request to create a new chat. type CreateChatRequest struct { - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - Content []ChatInputPart `json:"content"` - SystemPrompt string `json:"system_prompt,omitempty"` - WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"` - ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` - MCPServerIDs []uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"` - Labels map[string]string `json:"labels,omitempty"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + // OwnerID makes another user the chat owner. It defaults to the + // caller. The chat runs with the owner's credentials, so setting it + // requires site-wide authority over that user. + OwnerID *uuid.UUID `json:"owner_id,omitempty" format:"uuid"` + Content []ChatInputPart `json:"content"` + SystemPrompt string `json:"system_prompt,omitempty"` + WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"` + ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` + MCPServerIDs []uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"` + Labels map[string]string `json:"labels,omitempty"` // UnsafeDynamicTools declares client-executed tools that the // LLM can invoke. This API is highly experimental and highly // subject to change. @@ -615,7 +637,8 @@ type CreateChatMessageRequest struct { BusyBehavior ChatBusyBehavior `json:"busy_behavior,omitempty" enums:"queue,interrupt"` // PlanMode switches the chat's persistent plan mode. // nil: no change, ptr to "plan": enable, ptr to "": clear. - PlanMode *ChatPlanMode `json:"plan_mode,omitempty"` + PlanMode *ChatPlanMode `json:"plan_mode,omitempty"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` } // EditChatMessageRequest is the request to edit a user message in a chat. @@ -624,23 +647,38 @@ type EditChatMessageRequest struct { // ModelConfigID, when set, overrides the model used for the // replacement user message and the assistant turn that follows. // When nil the original message's model is preserved. - ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` + ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` + // MCPServerIDs, when set, replaces the chat's MCP server selection + // before the replacement turn runs. When nil the current selection + // is preserved. + MCPServerIDs *[]uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"` } // CreateChatMessageResponse is the response from adding a message to a chat. type CreateChatMessageResponse struct { - Message *ChatMessage `json:"message,omitempty"` + Message *ChatMessage `json:"message,omitempty"` + // Messages contains all user-visible messages inserted by the send, in + // insertion order. A queued send on an errored chat may promote the + // previous queue head, so clients must upsert the full batch. + Messages []ChatMessage `json:"messages,omitempty"` QueuedMessage *ChatQueuedMessage `json:"queued_message,omitempty"` Queued bool `json:"queued"` Warnings []string `json:"warnings,omitempty"` } // EditChatMessageResponse is the response from editing a message in a chat. -// Edits are always synchronous (no queueing), so the message is returned -// directly. type EditChatMessageResponse struct { - Message ChatMessage `json:"message"` - Warnings []string `json:"warnings,omitempty"` + Message ChatMessage `json:"message"` + // Messages holds every user-visible message inserted by the edit, in + // insertion order. Hook-generated suffix messages may follow Message, + // so clients must upsert the full batch. + Messages []ChatMessage `json:"messages,omitempty"` + // DeletedMessageIDs holds the IDs of previously visible messages the + // edit removed, including stale hook notices from the edited turn. + // Clients should drop them from local caches. + DeletedMessageIDs []int64 `json:"deleted_message_ids,omitempty"` + Warnings []string `json:"warnings,omitempty"` } // UploadChatFileResponse is the response from uploading a chat file. @@ -648,6 +686,16 @@ type UploadChatFileResponse struct { ID uuid.UUID `json:"id" format:"uuid"` } +// ChatFileDownloadURLResponse contains a short-lived URL for downloading a chat file. +type ChatFileDownloadURLResponse struct { + URL string `json:"url" format:"uri"` + ExpiresAt time.Time `json:"expires_at" format:"date-time"` + SHA256 string `json:"sha256"` + SizeBytes int64 `json:"size_bytes"` + Name string `json:"name"` + MimeType string `json:"mime_type"` +} + // ChatMessagesResponse contains the messages and queued messages for a chat. type ChatMessagesResponse struct { Messages []ChatMessage `json:"messages"` @@ -656,7 +704,7 @@ type ChatMessagesResponse struct { } // ChatPrompt is a single user-authored prompt in a chat, returned by -// GET /api/experimental/chats/{chat}/prompts. The text field contains +// GET /api/v2/chats/{chat}/prompts. The text field contains // the concatenated text payload of the underlying chat message; non-text // parts (tool calls, files, attachments) are omitted by the server. type ChatPrompt struct { @@ -665,7 +713,7 @@ type ChatPrompt struct { } // ChatPromptsResponse is the payload of -// GET /api/experimental/chats/{chat}/prompts. Prompts are returned +// GET /api/v2/chats/{chat}/prompts. Prompts are returned // newest first so the client can index directly into the slice for // up/down arrow history cycling. type ChatPromptsResponse struct { @@ -682,30 +730,6 @@ const ( ChatModelProviderUnavailableReasonUserAPIKeyRequired ChatModelProviderUnavailableReason = "user_api_key_required" ) -// ChatModel represents a model in the chat model catalog. -type ChatModel struct { - ID string `json:"id"` - Provider string `json:"provider"` - Model string `json:"model"` - DisplayName string `json:"display_name"` -} - -// ChatModelProvider represents provider availability and model results. -type ChatModelProvider struct { - Provider string `json:"provider"` - Available bool `json:"available"` - UnavailableReason ChatModelProviderUnavailableReason `json:"unavailable_reason,omitempty"` - Models []ChatModel `json:"models"` -} - -// ChatModelsResponse is the catalog returned from chat model discovery. -type ChatModelsResponse struct { - Providers []ChatModelProvider `json:"providers"` - // UnsupportedProviders lists configured providers the Agents harness - // cannot use, so the UI can explain the empty state. - UnsupportedProviders []ChatUnsupportedProvider `json:"unsupported_providers"` -} - // ChatUnsupportedProvider is a configured provider the Agents harness cannot // use. type ChatUnsupportedProvider struct { @@ -741,14 +765,16 @@ type UpdateChatPlanModeInstructionsRequest struct { PlanModeInstructions string `json:"plan_mode_instructions"` } -// ChatModelOverrideContext identifies which chat model override context a -// deployment override applies to. +// ChatModelOverrideContext identifies which chat model override context an +// organization override applies to. type ChatModelOverrideContext string const ( ChatModelOverrideContextGeneral ChatModelOverrideContext = "general" ChatModelOverrideContextExplore ChatModelOverrideContext = "explore" ChatModelOverrideContextTitleGeneration ChatModelOverrideContext = "title_generation" + ChatModelOverrideContextCompaction ChatModelOverrideContext = "compaction" + ChatModelOverrideContextAdvisor ChatModelOverrideContext = "advisor" ) // Valid reports whether the override context is one of the supported values. @@ -756,7 +782,9 @@ func (c ChatModelOverrideContext) Valid() bool { switch c { case ChatModelOverrideContextGeneral, ChatModelOverrideContextExplore, - ChatModelOverrideContextTitleGeneration: + ChatModelOverrideContextTitleGeneration, + ChatModelOverrideContextCompaction, + ChatModelOverrideContextAdvisor: return true default: return false @@ -769,21 +797,28 @@ func AllChatModelOverrideContexts() []ChatModelOverrideContext { ChatModelOverrideContextGeneral, ChatModelOverrideContextExplore, ChatModelOverrideContextTitleGeneration, + ChatModelOverrideContextCompaction, + ChatModelOverrideContextAdvisor, } } -// ChatModelOverrideResponse is the response body for the chat model override -// configuration endpoint. +// ChatModelOverrideResponse is the response body for one chat model override. type ChatModelOverrideResponse struct { - Context ChatModelOverrideContext `json:"context"` - ModelConfigID string `json:"model_config_id"` - IsMalformed bool `json:"is_malformed"` + Context ChatModelOverrideContext `json:"context"` + ModelConfigID string `json:"model_config_id"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` +} + +// ChatModelOverridesResponse is the response body for organization chat model overrides. +type ChatModelOverridesResponse struct { + Overrides []ChatModelOverrideResponse `json:"overrides"` } // UpdateChatModelOverrideRequest is the request body for updating the chat // model override configuration endpoint. type UpdateChatModelOverrideRequest struct { - ModelConfigID string `json:"model_config_id"` + ModelConfigID string `json:"model_config_id"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` } // ChatPersonalModelOverrideContext identifies which chat context the user @@ -808,11 +843,11 @@ const ( // ChatPersonalModelOverride is a resolved user personal model override. type ChatPersonalModelOverride struct { - Context ChatPersonalModelOverrideContext `json:"context"` - Mode ChatPersonalModelOverrideMode `json:"mode"` - ModelConfigID string `json:"model_config_id"` - IsSet bool `json:"is_set"` - IsMalformed bool `json:"is_malformed"` + Context ChatPersonalModelOverrideContext `json:"context"` + Mode ChatPersonalModelOverrideMode `json:"mode"` + ModelConfigID string `json:"model_config_id"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` + IsSet bool `json:"is_set"` } // ChatPersonalModelOverrideDeploymentDefaults describes the deployment-level @@ -835,8 +870,9 @@ type UserChatPersonalModelOverridesResponse struct { // UpdateUserChatPersonalModelOverrideRequest is the request body for updating // a user personal model override. type UpdateUserChatPersonalModelOverrideRequest struct { - Mode ChatPersonalModelOverrideMode `json:"mode"` - ModelConfigID string `json:"model_config_id"` + Mode ChatPersonalModelOverrideMode `json:"mode"` + ModelConfigID string `json:"model_config_id"` + ReasoningEffort *string `json:"reasoning_effort,omitempty"` } // ChatPersonalModelOverridesAdminSettings describes whether users may manage @@ -891,29 +927,48 @@ type AdvisorConfig struct { // MaxOutputTokens caps the advisor model response tokens. 0 means // use the runtime default. MaxOutputTokens int64 `json:"max_output_tokens"` - // ModelConfigID selects a specific chat model config to power the - // advisor. uuid.Nil means reuse the outer chat model. The runtime - // must fall back to the outer chat model when this ID cannot be - // resolved (e.g. the referenced model config was soft-deleted or - // its provider was disabled after the admin saved this config). - ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"` } -// UpdateAdvisorConfigRequest is the request body for updating advisor -// runtime configuration. It is a type alias for AdvisorConfig because -// the request and response shapes are currently identical. -type UpdateAdvisorConfigRequest = AdvisorConfig +// UpdateAdvisorConfigRequest is the request body for updating advisor runtime configuration. +type UpdateAdvisorConfigRequest struct { + MaxUsesPerRun int `json:"max_uses_per_run"` + MaxOutputTokens int64 `json:"max_output_tokens"` + // Deprecated: moved to the organization model override endpoint. + DeprecatedModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` + // Deprecated: moved to the organization model override endpoint. + DeprecatedReasoningEffort *string `json:"reasoning_effort,omitempty"` +} + +// ChatComputerUseProvider identifies the provider that backs computer use for +// the virtual desktop. +type ChatComputerUseProvider string + +const ( + ChatComputerUseProviderAnthropic ChatComputerUseProvider = "anthropic" + ChatComputerUseProviderOpenAI ChatComputerUseProvider = "openai" +) + +// AllChatComputerUseProviders contains every ChatComputerUseProvider value. +var AllChatComputerUseProviders = []ChatComputerUseProvider{ + ChatComputerUseProviderAnthropic, + ChatComputerUseProviderOpenAI, +} + +// Valid reports whether p is a supported computer use provider. +func (p ChatComputerUseProvider) Valid() bool { + return slices.Contains(AllChatComputerUseProviders, p) +} // ChatComputerUseProviderResponse is the response for getting the computer use // provider setting. type ChatComputerUseProviderResponse struct { - Provider string `json:"provider"` + Provider ChatComputerUseProvider `json:"provider"` } // UpdateChatComputerUseProviderRequest is the request to update the computer use // provider setting. type UpdateChatComputerUseProviderRequest struct { - Provider string `json:"provider"` + Provider ChatComputerUseProvider `json:"provider"` } // ChatDebugLoggingAdminSettings describes the runtime admin setting @@ -1070,7 +1125,7 @@ type ChatDebugStep struct { } // DefaultChatWorkspaceTTL is the default TTL for chat workspaces. -// Zero means disabled — the template's own autostop setting applies. +// Zero means disabled; the template's own autostop setting applies. const DefaultChatWorkspaceTTL = 0 // DefaultChatAutoArchiveDays is the default auto-archive window, in @@ -1087,7 +1142,7 @@ const DefaultChatDebugRetentionDays int32 = 30 // workspace TTL setting. type ChatWorkspaceTTLResponse struct { // WorkspaceTTLMillis is the workspace TTL in milliseconds. - // Zero means disabled — the template's own autostop setting applies. + // Zero means disabled; the template's own autostop setting applies. WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"` } @@ -1095,7 +1150,7 @@ type ChatWorkspaceTTLResponse struct { // workspace TTL setting. type UpdateChatWorkspaceTTLRequest struct { // WorkspaceTTLMillis is the workspace TTL in milliseconds. - // Zero means disabled — the template's own autostop setting applies. + // Zero means disabled; the template's own autostop setting applies. WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"` } @@ -1149,13 +1204,6 @@ func ParseChatWorkspaceTTL(s string) (time.Duration, error) { return d, nil } -// ChatTemplateAllowlist is the request and response body for the -// chat template allowlist configuration endpoint. An empty list -// means all templates are allowed. -type ChatTemplateAllowlist struct { - TemplateIDs []string `json:"template_ids"` -} - // ChatProviderConfigSource describes how a provider entry is sourced. type ChatProviderConfigSource string @@ -1170,6 +1218,7 @@ type ChatProviderConfig struct { ID uuid.UUID `json:"id" format:"uuid"` Provider string `json:"provider"` DisplayName string `json:"display_name"` + Icon string `json:"icon"` Enabled bool `json:"enabled"` HasAPIKey bool `json:"has_api_key"` CentralAPIKeyEnabled bool `json:"central_api_key_enabled"` @@ -1185,6 +1234,7 @@ type ChatProviderConfig struct { type CreateChatProviderConfigRequest struct { Provider string `json:"provider"` DisplayName string `json:"display_name,omitempty"` + Icon string `json:"icon,omitempty"` APIKey string `json:"api_key,omitempty"` BaseURL string `json:"base_url,omitempty"` Enabled *bool `json:"enabled,omitempty"` @@ -1196,6 +1246,7 @@ type CreateChatProviderConfigRequest struct { // UpdateChatProviderConfigRequest updates a chat provider config. type UpdateChatProviderConfigRequest struct { DisplayName string `json:"display_name,omitempty"` + Icon string `json:"icon,omitempty"` APIKey *string `json:"api_key,omitempty"` BaseURL *string `json:"base_url,omitempty"` Enabled *bool `json:"enabled,omitempty"` @@ -1210,6 +1261,7 @@ type AIProviderSummary struct { Type AIProviderType `json:"type"` Name string `json:"name"` DisplayName string `json:"display_name"` + Icon string `json:"icon"` Enabled bool `json:"enabled"` Deleted bool `json:"deleted"` } @@ -1235,6 +1287,8 @@ type UserChatProviderConfig struct { ProviderID uuid.UUID `json:"provider_id" format:"uuid"` Provider string `json:"provider"` DisplayName string `json:"display_name"` + Icon string `json:"icon"` + Enabled bool `json:"enabled"` HasUserAPIKey bool `json:"has_user_api_key"` HasCentralAPIKeyFallback bool `json:"has_central_api_key_fallback"` BYOKEnabled bool `json:"byok_enabled"` @@ -1246,9 +1300,10 @@ type CreateUserChatProviderKeyRequest struct { APIKey string `json:"api_key"` } -// ChatModelConfig is an admin-managed model configuration. -type ChatModelConfig struct { +// ChatModel is an org-scoped model configuration. +type ChatModel struct { ID uuid.UUID `json:"id" format:"uuid"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` AIProviderID uuid.UUID `json:"ai_provider_id" format:"uuid"` Model string `json:"model"` DisplayName string `json:"display_name"` @@ -1257,8 +1312,27 @@ type ChatModelConfig struct { ContextLimit int64 `json:"context_limit"` CompressionThreshold int32 `json:"compression_threshold"` ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` + // ReasoningEfforts lists selectable reasoning effort values through + // the model's configured maximum. + ReasoningEfforts []string `json:"reasoning_efforts,omitempty"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` +} + +// ChatModelACL is the access control list for an organization-scoped chat +// model. Each principal includes the identity details needed to display and +// manage the ACL without separate directory lookups. +type ChatModelACL struct { + Users []ChatUser `json:"users"` + Groups []ChatGroup `json:"groups"` +} + +// UpdateChatModelACLRequest is a sparse update of a chat model ACL. Only the +// listed principals change. ChatRoleDeleted removes an entry, while an omitted +// map or principal is unchanged. +type UpdateChatModelACLRequest struct { + UserRoles map[string]ChatRole `json:"user_roles,omitempty"` + GroupRoles map[string]ChatRole `json:"group_roles,omitempty"` } // ChatModelProviderOptions contains typed provider-specific options. @@ -1284,7 +1358,6 @@ type ChatModelOpenAIProviderOptions struct { MaxToolCalls *int64 `json:"max_tool_calls,omitempty" description:"Maximum number of tool calls per response"` ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` - ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` ReasoningSummary *string `json:"reasoning_summary,omitempty" description:"Controls whether reasoning tokens are summarized in the response" enum:"auto,concise,detailed"` MaxCompletionTokens *int64 `json:"max_completion_tokens,omitempty" description:"Upper bound on tokens the model may generate"` TextVerbosity *string `json:"text_verbosity,omitempty" description:"Controls the verbosity of the text response" enum:"low,medium,high"` @@ -1310,18 +1383,19 @@ type ChatModelAnthropicThinkingOptions struct { type ChatModelAnthropicProviderOptions struct { SendReasoning *bool `json:"send_reasoning,omitempty" description:"Whether to include reasoning content in the response"` Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty" description:"Configuration for extended thinking"` - Effort *string `json:"effort,omitempty" label:"Reasoning Effort" description:"Controls the level of reasoning effort" enum:"low,medium,high,xhigh,max"` ThinkingDisplay *string `json:"thinking_display,omitempty" label:"Thinking Display" description:"Controls how Anthropic returns thinking content" enum:"summarized,omitted"` DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty" description:"Whether to disable parallel tool execution"` WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Anthropic web search tool for grounding responses with real-time information"` AllowedDomains []string `json:"allowed_domains,omitempty" label:"Web Search: Allowed Domains" description:"Restrict web search to these domains (cannot be used with blocked_domains)" visible_when:"web_search_enabled" conflicts_with:"blocked_domains"` BlockedDomains []string `json:"blocked_domains,omitempty" label:"Web Search: Blocked Domains" description:"Block web search on these domains (cannot be used with allowed_domains)" visible_when:"web_search_enabled" conflicts_with:"allowed_domains"` + Context1MEnabled *bool `json:"context_1m_enabled,omitempty" label:"1M Context Window" description:"Send the anthropic-beta context-1m-2025-08-07 header to unlock the 1M token context window on supported Claude models. Pair with a matching Context Limit. Long-context pricing and higher latency may apply above 200K tokens."` } // ChatModelGoogleThinkingConfig configures Google thinking behavior. type ChatModelGoogleThinkingConfig struct { - ThinkingBudget *int64 `json:"thinking_budget,omitempty" description:"Maximum number of tokens the model may use for thinking"` - IncludeThoughts *bool `json:"include_thoughts,omitempty" description:"Whether to include thinking content in the response"` + ThinkingBudget *int64 `json:"thinking_budget,omitempty" description:"Maximum number of tokens the model may use for thinking (cannot be used with thinking_level)" conflicts_with:"thinking_config.thinking_level"` + ThinkingLevel *string `json:"thinking_level,omitempty" description:"Thinking level for Gemini 3+ models, used when the user has not selected a reasoning effort (cannot be used with thinking_budget)" enum:"minimal,low,medium,high" conflicts_with:"thinking_config.thinking_budget"` + IncludeThoughts *bool `json:"include_thoughts,omitempty" description:"Whether to include thinking content in the response"` } // ChatModelGoogleSafetySetting configures Google safety filtering. @@ -1341,17 +1415,15 @@ type ChatModelGoogleProviderOptions struct { // ChatModelOpenAICompatProviderOptions configures OpenAI-compatible behavior. type ChatModelOpenAICompatProviderOptions struct { - User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` - ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` } // ChatModelReasoningOptions configures reasoning behavior for model // providers that support it. type ChatModelReasoningOptions struct { - Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` - Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` - MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` - Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` + Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` + Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` + MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` } // ChatModelOpenRouterProvider configures OpenRouter routing preferences. @@ -1396,70 +1468,94 @@ type ChatModelVercelProviderOptions struct { ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"` } -// ModelCostConfig stores pricing metadata for a chat model. -type ModelCostConfig struct { - InputPricePerMillionTokens *decimal.Decimal `json:"input_price_per_million_tokens,omitempty" description:"Input token price in USD per 1M tokens"` - OutputPricePerMillionTokens *decimal.Decimal `json:"output_price_per_million_tokens,omitempty" description:"Output token price in USD per 1M tokens"` - CacheReadPricePerMillionTokens *decimal.Decimal `json:"cache_read_price_per_million_tokens,omitempty" description:"Cache read token price in USD per 1M tokens"` - CacheWritePricePerMillionTokens *decimal.Decimal `json:"cache_write_price_per_million_tokens,omitempty" description:"Cache write or cache creation token price in USD per 1M tokens"` +// Reasoning effort levels, ordered low to high for clamping and comparison. +const ( + ChatModelReasoningEffortNone = "none" + ChatModelReasoningEffortMinimal = "minimal" + ChatModelReasoningEffortLow = "low" + ChatModelReasoningEffortMedium = "medium" + ChatModelReasoningEffortHigh = "high" + ChatModelReasoningEffortXHigh = "xhigh" + ChatModelReasoningEffortMax = "max" +) + +var chatModelReasoningEffortValues = []string{ + ChatModelReasoningEffortNone, + ChatModelReasoningEffortMinimal, + ChatModelReasoningEffortLow, + ChatModelReasoningEffortMedium, + ChatModelReasoningEffortHigh, + ChatModelReasoningEffortXHigh, + ChatModelReasoningEffortMax, +} + +// ChatModelReasoningEffortValues returns the global reasoning effort scale. +func ChatModelReasoningEffortValues() []string { + return slices.Clone(chatModelReasoningEffortValues) +} + +// ChatModelReasoningEffortConfig configures per-model reasoning effort +// bounds. When configured, Default and Max must both be provided before +// storing. +type ChatModelReasoningEffortConfig struct { + Default *string `json:"default,omitempty" label:"Default Reasoning Effort" description:"Reasoning effort used when the user has not selected one" enum:"none,minimal,low,medium,high,xhigh,max"` + Max *string `json:"max,omitempty" label:"Max Reasoning Effort" description:"Maximum reasoning effort the user may select" enum:"none,minimal,low,medium,high,xhigh,max"` } // ChatModelCallConfig configures per-call model behavior defaults. type ChatModelCallConfig struct { - MaxOutputTokens *int64 `json:"max_output_tokens,omitempty" description:"Upper bound on tokens the model may generate"` - Temperature *float64 `json:"temperature,omitempty" description:"Sampling temperature between 0 and 2"` - TopP *float64 `json:"top_p,omitempty" description:"Nucleus sampling probability cutoff"` - TopK *int64 `json:"top_k,omitempty" description:"Number of highest-probability tokens to keep for sampling"` - PresencePenalty *float64 `json:"presence_penalty,omitempty" description:"Penalty for tokens that have already appeared in the output"` - FrequencyPenalty *float64 `json:"frequency_penalty,omitempty" description:"Penalty for tokens based on their frequency in the output"` - Cost *ModelCostConfig `json:"cost,omitempty" description:"Optional pricing metadata for this model"` - ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty" description:"Provider-specific option overrides"` -} - -// UnmarshalJSON accepts both the current nested cost object and the previous -// top-level pricing keys so legacy stored model_config JSON continues to load. -func (c *ChatModelCallConfig) UnmarshalJSON(data []byte) error { + MaxOutputTokens *int64 `json:"max_output_tokens,omitempty" description:"Upper bound on tokens the model may generate"` + Temperature *float64 `json:"temperature,omitempty" description:"Sampling temperature between 0 and 2"` + TopP *float64 `json:"top_p,omitempty" description:"Nucleus sampling probability cutoff"` + TopK *int64 `json:"top_k,omitempty" description:"Number of highest-probability tokens to keep for sampling"` + PresencePenalty *float64 `json:"presence_penalty,omitempty" description:"Penalty for tokens that have already appeared in the output"` + FrequencyPenalty *float64 `json:"frequency_penalty,omitempty" description:"Penalty for tokens based on their frequency in the output"` + ReasoningEffort *ChatModelReasoningEffortConfig `json:"reasoning_effort,omitempty" description:"Default and max reasoning effort for the model"` + OpenAIConfig *ChatModelOpenAIConfig `json:"openai_config,omitempty" description:"OpenAI client construction settings" providers:"openai"` + ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty" description:"Provider-specific option overrides"` +} + +// ChatModelOpenAIConfig holds settings applied once when the OpenAI client +// is built, not per request. +type ChatModelOpenAIConfig struct { + UseResponsesAPI *bool `json:"use_responses_api,omitempty" label:"Use Responses API" description:"Override which OpenAI API this model uses. Leave unset to decide from the provider SDK's known-model list, true to force the Responses API, false to force Chat Completions. Azure OpenAI providers ignore this and always follow the known-model list."` +} + +// UnmarshalStrict rejects unknown fields except for removed pricing fields, +// which may still be present in stored model configuration JSON: the nested +// cost object and the four top-level per-million-token price keys that +// predate it (see migration 000435, which read both forms). +func (c *ChatModelCallConfig) UnmarshalStrict(data []byte) error { type chatModelCallConfigAlias ChatModelCallConfig aux := struct { *chatModelCallConfigAlias - InputPricePerMillionTokens *decimal.Decimal `json:"input_price_per_million_tokens,omitempty"` - OutputPricePerMillionTokens *decimal.Decimal `json:"output_price_per_million_tokens,omitempty"` - CacheReadPricePerMillionTokens *decimal.Decimal `json:"cache_read_price_per_million_tokens,omitempty"` - CacheWritePricePerMillionTokens *decimal.Decimal `json:"cache_write_price_per_million_tokens,omitempty"` + Cost json.RawMessage `json:"cost"` + InputPrice json.RawMessage `json:"input_price_per_million_tokens"` + OutputPrice json.RawMessage `json:"output_price_per_million_tokens"` + CacheReadPrice json.RawMessage `json:"cache_read_price_per_million_tokens"` + CacheWritePrice json.RawMessage `json:"cache_write_price_per_million_tokens"` }{ chatModelCallConfigAlias: (*chatModelCallConfigAlias)(c), } - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - if aux.InputPricePerMillionTokens == nil && - aux.OutputPricePerMillionTokens == nil && - aux.CacheReadPricePerMillionTokens == nil && - aux.CacheWritePricePerMillionTokens == nil { - return nil - } - - if c.Cost == nil { - c.Cost = &ModelCostConfig{} - } - if c.Cost.InputPricePerMillionTokens == nil { - c.Cost.InputPricePerMillionTokens = aux.InputPricePerMillionTokens - } - if c.Cost.OutputPricePerMillionTokens == nil { - c.Cost.OutputPricePerMillionTokens = aux.OutputPricePerMillionTokens - } - if c.Cost.CacheReadPricePerMillionTokens == nil { - c.Cost.CacheReadPricePerMillionTokens = aux.CacheReadPricePerMillionTokens + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&aux); err != nil { + return err } - if c.Cost.CacheWritePricePerMillionTokens == nil { - c.Cost.CacheWritePricePerMillionTokens = aux.CacheWritePricePerMillionTokens + if _, err := dec.Token(); !errors.Is(err, io.EOF) { + return xerrors.New("unexpected trailing data after JSON value") } return nil } -// CreateChatModelConfigRequest creates a chat model config. -type CreateChatModelConfigRequest struct { +// CreateChatModelRequest is the request body for an organization-scoped +// ChatModel. AIProviderID, Model, and a positive ContextLimit are required. +// Enabled defaults to true. IsDefault defaults to false when the organization +// already has a default model. The first model created in an organization is +// automatically promoted to default. CompressionThreshold defaults to 70. An +// omitted ModelConfig uses the provider defaults. +type CreateChatModelRequest struct { AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"` Model string `json:"model"` DisplayName string `json:"display_name,omitempty"` @@ -1470,8 +1566,10 @@ type CreateChatModelConfigRequest struct { ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"` } -// UpdateChatModelConfigRequest updates a chat model config. -type UpdateChatModelConfigRequest struct { +// UpdateChatModelRequest updates a ChatModel. Empty Model and DisplayName +// values preserve the stored values. Nil pointer fields preserve their stored +// values. This request cannot clear DisplayName. +type UpdateChatModelRequest struct { AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"` Model string `json:"model,omitempty"` DisplayName string `json:"display_name,omitempty"` @@ -1530,7 +1628,7 @@ type ChatDiffContents struct { // Chat git watch error messages. These are the user-visible messages // the server returns in 400 responses from -// /api/experimental/chats/{id}/stream/git when the chat cannot be +// /api/v2/chats/{id}/stream/git when the chat cannot be // observed through a workspace agent. They are exported so the CLI // (and any future consumer) can match them structurally via // IsChatGitWatchFallbackMessage instead of coupling to exact wording. @@ -1538,7 +1636,7 @@ type ChatDiffContents struct { const ( ChatGitWatchNoWorkspaceMessage = "Chat has no workspace to watch." ChatGitWatchWorkspaceNotFoundMessage = "Chat workspace not found." - ChatGitWatchWorkspaceNoAgentsMessage = "Chat workspace has no agents." + ChatGitWatchNoEligibleAgentMessage = "No eligible agent found for chat workspace." // ChatGitWatchAgentStatePrefix is the common prefix of the // message produced by ChatGitWatchAgentStateMessage. The CLI // uses it as a mechanical fingerprint for the "agent not yet @@ -1547,14 +1645,14 @@ const ( ) // ChatGitWatchAgentStateMessage is the user-visible error message -// returned from /api/experimental/chats/{id}/stream/git when the +// returned from /api/v2/chats/{id}/stream/git when the // chat workspace's agent is not in the connected state. func ChatGitWatchAgentStateMessage(actual WorkspaceAgentStatus) string { return fmt.Sprintf("%s%q, it must be in the %q state.", ChatGitWatchAgentStatePrefix, actual, WorkspaceAgentConnected) } // IsChatGitWatchFallbackMessage reports whether msg matches one of -// the 400-response messages /api/experimental/chats/{id}/stream/git +// the 400-response messages /api/v2/chats/{id}/stream/git // emits when the chat cannot be observed through a workspace agent. // Clients should treat these cases as "no diff available" and fall // back to the empty remote diff instead of surfacing a hard error. @@ -1563,7 +1661,7 @@ func IsChatGitWatchFallbackMessage(msg string) bool { switch trimmed { case ChatGitWatchNoWorkspaceMessage, ChatGitWatchWorkspaceNotFoundMessage, - ChatGitWatchWorkspaceNoAgentsMessage: + ChatGitWatchNoEligibleAgentMessage: return true } return strings.HasPrefix(trimmed, ChatGitWatchAgentStatePrefix) @@ -1621,6 +1719,9 @@ const ( ChatErrorKindUsageLimit ChatErrorKind = "usage_limit" ChatErrorKindMissingKey ChatErrorKind = "missing_key" ChatErrorKindProviderDisabled ChatErrorKind = "provider_disabled" + ChatErrorKindContentFilter ChatErrorKind = "content_filter" + ChatErrorKindHookDispatchFailed ChatErrorKind = "hook_dispatch_failed" + ChatErrorKindHookDenied ChatErrorKind = "hook_denied" ) // AllChatErrorKinds contains every ChatErrorKind value. @@ -1636,6 +1737,9 @@ var AllChatErrorKinds = []ChatErrorKind{ ChatErrorKindUsageLimit, ChatErrorKindMissingKey, ChatErrorKindProviderDisabled, + ChatErrorKindContentFilter, + ChatErrorKindHookDispatchFailed, + ChatErrorKindHookDenied, } // ChatError represents a terminal chat error in persisted chat state or the @@ -1718,7 +1822,7 @@ type DynamicTool struct { InputSchema json.RawMessage `json:"input_schema"` // Handler executes the tool when the LLM invokes it. - // Not serialized — this only exists on the client side. + // Not serialized; this only exists on the client side. Handler func(ctx context.Context, call DynamicToolCall) (DynamicToolResponse, error) `json:"-"` } @@ -1762,16 +1866,23 @@ func NewDynamicTool[T any]( type ChatWatchEventKind string const ( - ChatWatchEventKindStatusChange ChatWatchEventKind = "status_change" - ChatWatchEventKindSummaryChange ChatWatchEventKind = "summary_change" - ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change" - ChatWatchEventKindCreated ChatWatchEventKind = "created" - ChatWatchEventKindDeleted ChatWatchEventKind = "deleted" - ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change" - ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required" + ChatWatchEventKindStatusChange ChatWatchEventKind = "status_change" + ChatWatchEventKindSummaryChange ChatWatchEventKind = "summary_change" + // ChatWatchEventKindChatSummaryChange carries the persisted whole-chat + // summary. It is distinct from SummaryChange (bound to last_turn_summary) so + // the frontend updates one field without disturbing the other. + ChatWatchEventKindChatSummaryChange ChatWatchEventKind = "chat_summary_change" + ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change" + ChatWatchEventKindCreated ChatWatchEventKind = "created" + ChatWatchEventKindDeleted ChatWatchEventKind = "deleted" + ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change" + ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required" // ChatWatchEventKindContextDirty signals that the chat's pinned - // workspace context drifted from the agent's latest pushed snapshot. - // The chat stays usable; a refresh re-pins it to the latest snapshot. + // workspace context changed: it drifted from the agent's latest + // pushed snapshot, or hydration first populated it (a first-turn + // pin or an agent push reaching a not-yet-pinned chat). The chat + // stays usable; a refresh re-pins a drifted chat to the latest + // snapshot. ChatWatchEventKindContextDirty ChatWatchEventKind = "context_dirty" ) @@ -1799,306 +1910,35 @@ type ChatStreamEvent struct { ActionRequired *ChatStreamActionRequired `json:"action_required,omitempty"` } -// ChatCostSummaryOptions are optional query parameters for GetChatCostSummary. -type ChatCostSummaryOptions struct { - StartDate time.Time - EndDate time.Time -} - -// ChatCostUsersOptions are optional query parameters for GetChatCostUsers. -type ChatCostUsersOptions struct { - StartDate time.Time - EndDate time.Time - Username string - Pagination -} - -// ChatCostSummary is the response from the chat cost summary endpoint. -type ChatCostSummary struct { - StartDate time.Time `json:"start_date" format:"date-time"` - EndDate time.Time `json:"end_date" format:"date-time"` - TotalCostMicros int64 `json:"total_cost_micros"` - PricedMessageCount int64 `json:"priced_message_count"` - UnpricedMessageCount int64 `json:"unpriced_message_count"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalRuntimeMs int64 `json:"total_runtime_ms"` - ByModel []ChatCostModelBreakdown `json:"by_model"` - ByChat []ChatCostChatBreakdown `json:"by_chat"` - UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"` -} - -// ChatCostModelBreakdown contains per-model cost aggregation. -type ChatCostModelBreakdown struct { - ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"` - DisplayName string `json:"display_name"` - Provider string `json:"provider"` - Model string `json:"model"` - TotalCostMicros int64 `json:"total_cost_micros"` - MessageCount int64 `json:"message_count"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalRuntimeMs int64 `json:"total_runtime_ms"` -} - -// ChatCostChatBreakdown contains per-root-chat cost aggregation. -type ChatCostChatBreakdown struct { - RootChatID uuid.UUID `json:"root_chat_id" format:"uuid"` - ChatTitle string `json:"chat_title"` - TotalCostMicros int64 `json:"total_cost_micros"` - MessageCount int64 `json:"message_count"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalRuntimeMs int64 `json:"total_runtime_ms"` -} - -// ChatCostUserRollup contains per-user cost aggregation for admin views. -type ChatCostUserRollup struct { - UserID uuid.UUID `json:"user_id" format:"uuid"` - Username string `json:"username"` - Name string `json:"name"` - AvatarURL string `json:"avatar_url"` - TotalCostMicros int64 `json:"total_cost_micros"` - MessageCount int64 `json:"message_count"` - ChatCount int64 `json:"chat_count"` - TotalInputTokens int64 `json:"total_input_tokens"` - TotalOutputTokens int64 `json:"total_output_tokens"` - TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` - TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` - TotalRuntimeMs int64 `json:"total_runtime_ms"` -} - -// ChatCostUsersResponse is the response from the admin chat cost users endpoint. -type ChatCostUsersResponse struct { - StartDate time.Time `json:"start_date" format:"date-time"` - EndDate time.Time `json:"end_date" format:"date-time"` - Count int64 `json:"count"` - Users []ChatCostUserRollup `json:"users"` -} - -// ChatUsageLimitExceededResponse is the 409 response body returned when a -// chat operation exceeds the caller's usage limit. The structured fields let -// frontends render user-friendly spend, limit, and reset information without -// parsing debug text. -type ChatUsageLimitExceededResponse struct { +// ChatCost is the AI Gateway cost for the requested chat's whole tree. +// Root and subagent chats report the same total. +// RequestCount counts every finished request in the tree, including ones that +// recorded no billable usage at all, such as a request that failed upstream. +// UnpricedRequestCount counts requests with at least one usage record whose +// model had no recorded price; RequestCount includes them and +// TotalCostMicros omits only their unpriced usage. +type ChatCost struct { + ChatID uuid.UUID `json:"chat_id" format:"uuid"` + TotalCostMicros int64 `json:"total_cost_micros"` + RequestCount int64 `json:"request_count"` + UnpricedRequestCount int64 `json:"unpriced_request_count"` +} + +// ChatHookDispatchFailedResponse is the error body returned when a +// lifecycle hook dispatch fails during a synchronous chat operation. +// Kind lets clients classify the failure without parsing message text. +type ChatHookDispatchFailedResponse struct { Response - SpentMicros int64 `json:"spent_micros"` - LimitMicros int64 `json:"limit_micros"` - ResetsAt time.Time `json:"resets_at" format:"date-time"` -} - -type chatUsageLimitExceededError struct { - err *Error - response ChatUsageLimitExceededResponse -} - -func (e *chatUsageLimitExceededError) Error() string { - if e.err == nil { - return e.response.Message - } - return e.err.Error() + Kind ChatErrorKind `json:"kind"` } -func (e *chatUsageLimitExceededError) Unwrap() error { - return e.err -} - -func readBodyAsChatUsageLimitError(res *http.Response) error { - if res == nil || res.StatusCode != http.StatusConflict { - return ReadBodyAsError(res) - } - defer res.Body.Close() - - rawBody, err := io.ReadAll(res.Body) - if err != nil { - return xerrors.Errorf("read body: %w", err) - } - - if mimeErr := ExpectJSONMime(res); mimeErr != nil { - return readRawBodyAsError(res, rawBody) - } - - var payload ChatUsageLimitExceededResponse - if err := json.NewDecoder(bytes.NewReader(rawBody)).Decode(&payload); err == nil && isChatUsageLimitExceededResponse(payload) { - return &chatUsageLimitExceededError{ - err: newResponseError(res, payload.Response), - response: payload, - } - } - - return readRawBodyAsError(res, rawBody) -} - -func isChatUsageLimitExceededResponse(resp ChatUsageLimitExceededResponse) bool { - return resp.Message != "" && !resp.ResetsAt.IsZero() -} - -func readRawBodyAsError(res *http.Response, rawBody []byte) error { - if mimeErr := ExpectJSONMime(res); mimeErr != nil { - if len(rawBody) > 2048 { - rawBody = append(rawBody[:2048], []byte("...")...) - } - if len(rawBody) == 0 { - rawBody = []byte("no response body") - } - return newResponseError(res, Response{ - Message: mimeErr.Error(), - Detail: string(rawBody), - }) - } - - var response Response - if err := json.NewDecoder(bytes.NewReader(rawBody)).Decode(&response); err != nil { - if errors.Is(err, io.EOF) { - return newResponseError(res, Response{Message: "empty response body"}) - } - return xerrors.Errorf("decode body: %w", err) - } - if response.Message == "" { - if len(rawBody) > 1024 { - rawBody = append(rawBody[:1024], []byte("...")...) - } - response.Message = fmt.Sprintf( - "unexpected status code %d, response has no message", - res.StatusCode, - ) - response.Detail = string(rawBody) - } - return newResponseError(res, response) -} - -func newResponseError(res *http.Response, response Response) *Error { - if res == nil { - return &Error{Response: response} - } - - var requestMethod, requestURL string - if res.Request != nil { - requestMethod = res.Request.Method - if res.Request.URL != nil { - requestURL = res.Request.URL.String() - } - } - - var helpMessage string - if res.StatusCode == http.StatusUnauthorized { - helpMessage = "Try logging in using 'coder login'." - } - - return &Error{ - Response: response, - statusCode: res.StatusCode, - method: requestMethod, - url: requestURL, - Helper: helpMessage, - } -} - -// ChatUsageLimitExceededFrom extracts a structured chat usage limit response -// from an SDK error returned by chat mutation methods. -func ChatUsageLimitExceededFrom(err error) *ChatUsageLimitExceededResponse { - var limitErr *chatUsageLimitExceededError - if !errors.As(err, &limitErr) { - return nil - } - return &limitErr.response -} - -// ChatUsageLimitPeriod represents the time window for usage limits. -type ChatUsageLimitPeriod string - -const ( - ChatUsageLimitPeriodDay ChatUsageLimitPeriod = "day" - ChatUsageLimitPeriodWeek ChatUsageLimitPeriod = "week" - ChatUsageLimitPeriodMonth ChatUsageLimitPeriod = "month" -) - -// Valid reports whether p is a supported chat usage limit period. -func (p ChatUsageLimitPeriod) Valid() bool { - switch p { - case ChatUsageLimitPeriodDay, ChatUsageLimitPeriodWeek, ChatUsageLimitPeriodMonth: - return true - default: - return false - } -} - -// ChatUsageLimitConfig is the deployment-wide default usage limit config. -type ChatUsageLimitConfig struct { - // Nil in the API means no default limit is set. The DB stores 0 when - // limiting is disabled. - SpendLimitMicros *int64 `json:"spend_limit_micros"` - Period ChatUsageLimitPeriod `json:"period"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` -} - -// ChatUsageLimitOverride is a per-user override of the deployment default. -type ChatUsageLimitOverride struct { - UserID uuid.UUID `json:"user_id" format:"uuid"` - Username string `json:"username"` - Name string `json:"name"` - AvatarURL string `json:"avatar_url"` - // Nil in the API means no user override is set. Persisted override rows - // store positive values. - SpendLimitMicros *int64 `json:"spend_limit_micros"` -} - -// ChatUsageLimitGroupOverride represents a group-scoped spend limit override. -type ChatUsageLimitGroupOverride struct { - GroupID uuid.UUID `json:"group_id" format:"uuid"` - GroupName string `json:"group_name"` - GroupDisplayName string `json:"group_display_name"` - GroupAvatarURL string `json:"group_avatar_url"` - MemberCount int64 `json:"member_count"` - // Nil in the API means no group override is set. Persisted override rows - // store positive values. - SpendLimitMicros *int64 `json:"spend_limit_micros"` -} - -// UpsertChatUsageLimitOverrideRequest is the body for creating/updating a -// per-user usage limit override. -type UpsertChatUsageLimitOverrideRequest struct { - SpendLimitMicros int64 `json:"spend_limit_micros"` // Must be greater than 0. -} - -// UpdateChatUsageLimitOverrideRequest is kept as a compatibility alias. -type UpdateChatUsageLimitOverrideRequest = UpsertChatUsageLimitOverrideRequest - -// UpsertChatUsageLimitGroupOverrideRequest is the request to create or update -// a group-level spend limit override. -type UpsertChatUsageLimitGroupOverrideRequest struct { - SpendLimitMicros int64 `json:"spend_limit_micros"` // Must be greater than 0. -} - -// UpdateChatUsageLimitGroupOverrideRequest is kept as a compatibility alias. -type UpdateChatUsageLimitGroupOverrideRequest = UpsertChatUsageLimitGroupOverrideRequest - -// ChatUsageLimitStatus represents the current spend status for a user -// within their active limit period. -type ChatUsageLimitStatus struct { - IsLimited bool `json:"is_limited"` - Period ChatUsageLimitPeriod `json:"period,omitempty"` - SpendLimitMicros *int64 `json:"spend_limit_micros,omitempty"` - CurrentSpend int64 `json:"current_spend"` - PeriodStart time.Time `json:"period_start,omitempty" format:"date-time"` - PeriodEnd time.Time `json:"period_end,omitempty" format:"date-time"` -} - -// ChatUsageLimitConfigResponse is returned from the admin config endpoint -// and includes the config plus a count of models without pricing. -type ChatUsageLimitConfigResponse struct { - ChatUsageLimitConfig - UnpricedModelCount int64 `json:"unpriced_model_count"` - Overrides []ChatUsageLimitOverride `json:"overrides"` - GroupOverrides []ChatUsageLimitGroupOverride `json:"group_overrides"` +// ChatHookDeniedResponse is the error body returned when a lifecycle hook +// denies a synchronous chat operation. Kind lets clients classify the denial +// without parsing message text. +type ChatHookDeniedResponse struct { + Response + Kind ChatErrorKind `json:"kind"` } - type ChatRole string const ( @@ -2148,10 +1988,10 @@ type ListChatsOptions struct { } // ListChats returns all chats for the authenticated user. -func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) { +func (c *Client) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) { var reqOpts []RequestOption if opts != nil { - reqOpts = append(reqOpts, opts.Pagination.asRequestOption()) + reqOpts = append(reqOpts, opts.asRequestOption()) query := opts.Query if opts.Source != "" { if query != "" { @@ -2176,7 +2016,7 @@ func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptio }) } } - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats", nil, reqOpts...) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats", nil, reqOpts...) if err != nil { return nil, err } @@ -2185,22 +2025,7 @@ func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptio return nil, ReadBodyAsError(res) } var chats []Chat - return chats, json.NewDecoder(res.Body).Decode(&chats) -} - -// ListChatModels returns the available chat model catalog. -func (c *ExperimentalClient) ListChatModels(ctx context.Context) (ChatModelsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/models", nil) - if err != nil { - return ChatModelsResponse{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatModelsResponse{}, ReadBodyAsError(res) - } - - var catalog ChatModelsResponse - return catalog, json.NewDecoder(res.Body).Decode(&catalog) + return chats, ReadBodyAsJSON(res, &chats) } // ListChatProviders returns admin-managed chat provider configs. @@ -2215,7 +2040,7 @@ func (c *ExperimentalClient) ListChatProviders(ctx context.Context) ([]ChatProvi } var providers []ChatProviderConfig - return providers, json.NewDecoder(res.Body).Decode(&providers) + return providers, ReadBodyAsJSON(res, &providers) } // CreateChatProvider creates an admin-managed chat provider config. @@ -2230,7 +2055,7 @@ func (c *ExperimentalClient) CreateChatProvider(ctx context.Context, req CreateC } var provider ChatProviderConfig - return provider, json.NewDecoder(res.Body).Decode(&provider) + return provider, ReadBodyAsJSON(res, &provider) } // UpdateChatProvider updates an admin-managed chat provider config. @@ -2245,7 +2070,7 @@ func (c *ExperimentalClient) UpdateChatProvider(ctx context.Context, providerID } var provider ChatProviderConfig - return provider, json.NewDecoder(res.Body).Decode(&provider) + return provider, ReadBodyAsJSON(res, &provider) } // DeleteChatProvider deletes an admin-managed chat provider config. @@ -2262,7 +2087,7 @@ func (c *ExperimentalClient) DeleteChatProvider(ctx context.Context, providerID } // ListUserAIProviderKeyConfigs returns user-scoped AI provider key configs. -func (c *ExperimentalClient) ListUserAIProviderKeyConfigs(ctx context.Context, user string) ([]UserAIProviderKeyConfig, error) { +func (c *Client) ListUserAIProviderKeyConfigs(ctx context.Context, user string) ([]UserAIProviderKeyConfig, error) { res, err := c.Request(ctx, http.MethodGet, userAIProviderKeysPath(user), nil) if err != nil { return nil, xerrors.Errorf("list user AI provider key configs: %w", err) @@ -2272,11 +2097,11 @@ func (c *ExperimentalClient) ListUserAIProviderKeyConfigs(ctx context.Context, u return nil, ReadBodyAsError(res) } var configs []UserAIProviderKeyConfig - return configs, json.NewDecoder(res.Body).Decode(&configs) + return configs, ReadBodyAsJSON(res, &configs) } // UpsertUserAIProviderKey creates or replaces a user API key for an AI provider. -func (c *ExperimentalClient) UpsertUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID, req CreateUserAIProviderKeyRequest) (UserAIProviderKeyConfig, error) { +func (c *Client) UpsertUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID, req CreateUserAIProviderKeyRequest) (UserAIProviderKeyConfig, error) { res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("%s/%s", userAIProviderKeysPath(user), providerID), req) if err != nil { return UserAIProviderKeyConfig{}, xerrors.Errorf("upsert user AI provider key: %w", err) @@ -2286,11 +2111,11 @@ func (c *ExperimentalClient) UpsertUserAIProviderKey(ctx context.Context, user s return UserAIProviderKeyConfig{}, ReadBodyAsError(res) } var config UserAIProviderKeyConfig - return config, json.NewDecoder(res.Body).Decode(&config) + return config, ReadBodyAsJSON(res, &config) } // DeleteUserAIProviderKey deletes a user API key for an AI provider. -func (c *ExperimentalClient) DeleteUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID) error { +func (c *Client) DeleteUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID) error { res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("%s/%s", userAIProviderKeysPath(user), providerID), nil) if err != nil { return xerrors.Errorf("delete user AI provider key: %w", err) @@ -2303,7 +2128,7 @@ func (c *ExperimentalClient) DeleteUserAIProviderKey(ctx context.Context, user s } func userAIProviderKeysPath(user string) string { - return fmt.Sprintf("/api/experimental/users/%s/ai-provider-keys", url.PathEscape(user)) + return fmt.Sprintf("/api/v2/users/%s/ai-provider-keys", url.PathEscape(user)) } // ListUserChatProviderConfigs returns user-scoped chat provider configs. @@ -2317,7 +2142,7 @@ func (c *ExperimentalClient) ListUserChatProviderConfigs(ctx context.Context) ([ return nil, ReadBodyAsError(res) } var configs []UserChatProviderConfig - return configs, json.NewDecoder(res.Body).Decode(&configs) + return configs, ReadBodyAsJSON(res, &configs) } // UpsertUserChatProviderKey creates or replaces a user API key for a provider. @@ -2331,7 +2156,7 @@ func (c *ExperimentalClient) UpsertUserChatProviderKey(ctx context.Context, prov return UserChatProviderConfig{}, ReadBodyAsError(res) } var config UserChatProviderConfig - return config, json.NewDecoder(res.Body).Decode(&config) + return config, ReadBodyAsJSON(res, &config) } // DeleteUserChatProviderKey deletes a user API key for a provider. @@ -2347,132 +2172,182 @@ func (c *ExperimentalClient) DeleteUserChatProviderKey(ctx context.Context, prov return nil } -// ListChatModelConfigs returns admin-managed chat model configs. -func (c *ExperimentalClient) ListChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/model-configs", nil) +// ChatModels returns the chat model configs the caller can read in one +// organization, plus the redacted provider descriptors the authoring page +// needs, for org-scoped management and picker surfaces. +func (c *Client) ChatModels(ctx context.Context, organizationID uuid.UUID) (OrganizationChatModelsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models", organizationID), nil) if err != nil { - return nil, err + return OrganizationChatModelsResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) + return OrganizationChatModelsResponse{}, ReadBodyAsError(res) } - var configs []ChatModelConfig - return configs, json.NewDecoder(res.Body).Decode(&configs) + var resp OrganizationChatModelsResponse + return resp, ReadBodyAsJSON(res, &resp) } -// CreateChatModelConfig creates an admin-managed chat model config. -func (c *ExperimentalClient) CreateChatModelConfig(ctx context.Context, req CreateChatModelConfigRequest) (ChatModelConfig, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/experimental/chats/model-configs", req) +// ChatModel fetches one chat model config by ID in an organization. +func (c *Client) ChatModel(ctx context.Context, organizationID, modelConfigID uuid.UUID) (ChatModel, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelConfigID), nil) if err != nil { - return ChatModelConfig{}, err + return ChatModel{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return ChatModelConfig{}, ReadBodyAsError(res) + if res.StatusCode != http.StatusOK { + return ChatModel{}, ReadBodyAsError(res) } - var config ChatModelConfig - return config, json.NewDecoder(res.Body).Decode(&config) + var config ChatModel + return config, ReadBodyAsJSON(res, &config) } -// UpdateChatModelConfig updates an admin-managed chat model config. -func (c *ExperimentalClient) UpdateChatModelConfig(ctx context.Context, modelConfigID uuid.UUID, req UpdateChatModelConfigRequest) (ChatModelConfig, error) { - res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), req) +// CreateChatModel creates a chat model config in the given organization. +func (c *Client) CreateChatModel(ctx context.Context, organizationID uuid.UUID, req CreateChatModelRequest) (ChatModel, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/chats/models", organizationID), req) if err != nil { - return ChatModelConfig{}, err + return ChatModel{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatModelConfig{}, ReadBodyAsError(res) + if res.StatusCode != http.StatusCreated { + return ChatModel{}, ReadBodyAsError(res) } - var config ChatModelConfig - return config, json.NewDecoder(res.Body).Decode(&config) + var model ChatModel + return model, ReadBodyAsJSON(res, &model) } -// DeleteChatModelConfig deletes an admin-managed chat model config. -func (c *ExperimentalClient) DeleteChatModelConfig(ctx context.Context, modelConfigID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), nil) +// UpdateChatModel updates a ChatModel in an organization. +func (c *Client) UpdateChatModel(ctx context.Context, organizationID, modelID uuid.UUID, req UpdateChatModelRequest) (ChatModel, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelID), req) if err != nil { - return err + return ChatModel{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) + if res.StatusCode != http.StatusOK { + return ChatModel{}, ReadBodyAsError(res) } - return nil + + var model ChatModel + return model, ReadBodyAsJSON(res, &model) } -// GetChatCostSummary returns an aggregate cost summary for the specified -// user. Zero-valued StartDate or EndDate fields are omitted from the -// request, letting the server apply its own defaults (typically the last -// 30 days). -func (c *ExperimentalClient) GetChatCostSummary(ctx context.Context, user string, opts ChatCostSummaryOptions) (ChatCostSummary, error) { - qp := url.Values{} - if !opts.StartDate.IsZero() { - qp.Set("start_date", opts.StartDate.Format(time.RFC3339)) - } - if !opts.EndDate.IsZero() { - qp.Set("end_date", opts.EndDate.Format(time.RFC3339)) - } - reqURL := fmt.Sprintf("/api/experimental/chats/cost/%s/summary", user) - if len(qp) > 0 { - reqURL += "?" + qp.Encode() - } - res, err := c.Request(ctx, http.MethodGet, reqURL, nil) +// ChatModelACL returns the access control list for a chat model in an +// organization. +func (c *Client) ChatModelACL(ctx context.Context, organizationID, modelID uuid.UUID) (ChatModelACL, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl", organizationID, modelID), nil) if err != nil { - return ChatCostSummary{}, err + return ChatModelACL{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatCostSummary{}, ReadBodyAsError(res) + return ChatModelACL{}, ReadBodyAsError(res) } - var summary ChatCostSummary - return summary, json.NewDecoder(res.Body).Decode(&summary) + + var modelACL ChatModelACL + return modelACL, ReadBodyAsJSON(res, &modelACL) } -// GetChatCostUsers returns a per-user cost rollup for the deployment -// (admin only). Zero-valued StartDate or EndDate fields are omitted from -// the request, letting the server apply its own defaults (typically the -// last 30 days). -func (c *ExperimentalClient) GetChatCostUsers(ctx context.Context, opts ChatCostUsersOptions) (ChatCostUsersResponse, error) { - qp := url.Values{} - if !opts.StartDate.IsZero() { - qp.Set("start_date", opts.StartDate.Format(time.RFC3339)) +// ChatModelACLAvailable returns available users and groups that can be assigned +// chat model permissions. The optional request applies q/limit/offset/after_id +// to users. Groups reuse the user search query and q/limit semantics. Pass +// codersdk.UsersRequest{} when no filtering is desired. +func (c *Client) ChatModelACLAvailable(ctx context.Context, organizationID, modelID uuid.UUID, req UsersRequest) (ACLAvailable, error) { + res, err := c.Request( + ctx, + http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl/available", organizationID, modelID), + nil, + req.Pagination.asRequestOption(), + req.asRequestOption(), + ) + if err != nil { + return ACLAvailable{}, err } - if !opts.EndDate.IsZero() { - qp.Set("end_date", opts.EndDate.Format(time.RFC3339)) + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ACLAvailable{}, ReadBodyAsError(res) } - if opts.Username != "" { - qp.Set("username", opts.Username) + + var acl ACLAvailable + return acl, ReadBodyAsJSON(res, &acl) +} + +// UpdateChatModelACL applies a sparse access control list update to a chat +// model in an organization. +func (c *Client) UpdateChatModelACL(ctx context.Context, organizationID, modelID uuid.UUID, req UpdateChatModelACLRequest) error { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl", organizationID, modelID), req) + if err != nil { + return err } - if opts.Limit > 0 { - qp.Set("limit", strconv.Itoa(opts.Limit)) + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) } - if opts.Offset > 0 { - qp.Set("offset", strconv.Itoa(opts.Offset)) + return nil +} + +// DeleteChatModel deletes a ChatModel in an organization. +func (c *Client) DeleteChatModel(ctx context.Context, organizationID, modelID uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelID), nil) + if err != nil { + return err } - reqURL := "/api/experimental/chats/cost/users" - if len(qp) > 0 { - reqURL += "?" + qp.Encode() + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) } - res, err := c.Request(ctx, http.MethodGet, reqURL, nil) + return nil +} + +// ChatModelProviderDescriptor is the redacted view of an AI provider carried +// on the org model collection response. It carries only the capability +// metadata the Models UI needs; key material, base URLs, and headers are +// never exposed. The fields mirror the provider descriptors returned by the +// organization-scoped chat models collection. +type ChatModelProviderDescriptor struct { + ID uuid.UUID `json:"id" format:"uuid"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + Icon string `json:"icon"` + Enabled bool `json:"enabled"` + HasAPIKey bool `json:"has_api_key"` + HasUserAPIKey bool `json:"has_user_api_key"` + HasEffectiveAPIKey bool `json:"has_effective_api_key"` + AllowUserAPIKey bool `json:"allow_user_api_key"` + Available bool `json:"available"` + UnavailableReason ChatModelProviderUnavailableReason `json:"unavailable_reason,omitempty"` +} + +// OrganizationChatModelsResponse is the org chat model config collection: +// the caller-readable configs plus the redacted provider descriptors the +// authoring page needs. +type OrganizationChatModelsResponse struct { + Models []ChatModel `json:"models"` + Providers []ChatModelProviderDescriptor `json:"providers"` + UnsupportedProviders []ChatUnsupportedProvider `json:"unsupported_providers"` +} + +// GetChatCost returns the AI Gateway cost for the whole chat tree that +// contains chatID. +func (c *Client) GetChatCost(ctx context.Context, chatID uuid.UUID) (ChatCost, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/cost", chatID), nil) if err != nil { - return ChatCostUsersResponse{}, err + return ChatCost{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatCostUsersResponse{}, ReadBodyAsError(res) + return ChatCost{}, ReadBodyAsError(res) } - var resp ChatCostUsersResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + var cost ChatCost + return cost, ReadBodyAsJSON(res, &cost) } // GetChatSystemPrompt returns the deployment-wide chat system prompt. -func (c *ExperimentalClient) GetChatSystemPrompt(ctx context.Context) (ChatSystemPromptResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/system-prompt", nil) +func (c *Client) GetChatSystemPrompt(ctx context.Context) (ChatSystemPromptResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/system-prompt", nil) if err != nil { return ChatSystemPromptResponse{}, err } @@ -2481,12 +2356,12 @@ func (c *ExperimentalClient) GetChatSystemPrompt(ctx context.Context) (ChatSyste return ChatSystemPromptResponse{}, ReadBodyAsError(res) } var resp ChatSystemPromptResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatSystemPrompt updates the deployment-wide chat system prompt. -func (c *ExperimentalClient) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSystemPromptRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/system-prompt", req) +func (c *Client) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSystemPromptRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/system-prompt", req) if err != nil { return err } @@ -2498,8 +2373,8 @@ func (c *ExperimentalClient) UpdateChatSystemPrompt(ctx context.Context, req Upd } // GetChatPlanModeInstructions returns the deployment-wide plan mode instructions. -func (c *ExperimentalClient) GetChatPlanModeInstructions(ctx context.Context) (ChatPlanModeInstructionsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/plan-mode-instructions", nil) +func (c *Client) GetChatPlanModeInstructions(ctx context.Context) (ChatPlanModeInstructionsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/plan-mode-instructions", nil) if err != nil { return ChatPlanModeInstructionsResponse{}, err } @@ -2508,12 +2383,12 @@ func (c *ExperimentalClient) GetChatPlanModeInstructions(ctx context.Context) (C return ChatPlanModeInstructionsResponse{}, ReadBodyAsError(res) } var resp ChatPlanModeInstructionsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatPlanModeInstructions updates the deployment-wide plan mode instructions. -func (c *ExperimentalClient) UpdateChatPlanModeInstructions(ctx context.Context, req UpdateChatPlanModeInstructionsRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/plan-mode-instructions", req) +func (c *Client) UpdateChatPlanModeInstructions(ctx context.Context, req UpdateChatPlanModeInstructionsRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/plan-mode-instructions", req) if err != nil { return err } @@ -2524,47 +2399,44 @@ func (c *ExperimentalClient) UpdateChatPlanModeInstructions(ctx context.Context, return nil } -// GetChatModelOverride returns the deployment-wide chat model override for -// the requested context. -func (c *ExperimentalClient) GetChatModelOverride(ctx context.Context, override ChatModelOverrideContext) (ChatModelOverrideResponse, error) { - path := fmt.Sprintf( - "/api/experimental/chats/config/model-override/%s", - url.PathEscape(string(override)), - ) +// OrganizationChatModelOverrides returns the configured chat model overrides for an organization. +func (c *Client) OrganizationChatModelOverrides(ctx context.Context, organizationID uuid.UUID) (ChatModelOverridesResponse, error) { + path := fmt.Sprintf("/api/v2/organizations/%s/chats/model-overrides", organizationID) res, err := c.Request(ctx, http.MethodGet, path, nil) if err != nil { - return ChatModelOverrideResponse{}, err + return ChatModelOverridesResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatModelOverrideResponse{}, ReadBodyAsError(res) + return ChatModelOverridesResponse{}, ReadBodyAsError(res) } - var resp ChatModelOverrideResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + var resp ChatModelOverridesResponse + return resp, ReadBodyAsJSON(res, &resp) } -// UpdateChatModelOverride updates the deployment-wide chat model override for -// the requested context. -func (c *ExperimentalClient) UpdateChatModelOverride(ctx context.Context, override ChatModelOverrideContext, req UpdateChatModelOverrideRequest) error { +// UpdateOrganizationChatModelOverride updates or clears a chat model override for an organization. +func (c *Client) UpdateOrganizationChatModelOverride(ctx context.Context, organizationID uuid.UUID, override ChatModelOverrideContext, req UpdateChatModelOverrideRequest) (ChatModelOverrideResponse, error) { path := fmt.Sprintf( - "/api/experimental/chats/config/model-override/%s", + "/api/v2/organizations/%s/chats/model-overrides/%s", + organizationID, url.PathEscape(string(override)), ) res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { - return err + return ChatModelOverrideResponse{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) + if res.StatusCode != http.StatusOK { + return ChatModelOverrideResponse{}, ReadBodyAsError(res) } - return nil + var resp ChatModelOverrideResponse + return resp, ReadBodyAsJSON(res, &resp) } // GetChatPersonalModelOverridesAdminSettings returns the deployment-wide // personal model override admin settings. -func (c *ExperimentalClient) GetChatPersonalModelOverridesAdminSettings(ctx context.Context) (ChatPersonalModelOverridesAdminSettings, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/personal-model-overrides", nil) +func (c *Client) GetChatPersonalModelOverridesAdminSettings(ctx context.Context) (ChatPersonalModelOverridesAdminSettings, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/personal-model-overrides", nil) if err != nil { return ChatPersonalModelOverridesAdminSettings{}, err } @@ -2573,13 +2445,13 @@ func (c *ExperimentalClient) GetChatPersonalModelOverridesAdminSettings(ctx cont return ChatPersonalModelOverridesAdminSettings{}, ReadBodyAsError(res) } var resp ChatPersonalModelOverridesAdminSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatPersonalModelOverridesAdminSettings updates the deployment-wide // personal model override admin settings. -func (c *ExperimentalClient) UpdateChatPersonalModelOverridesAdminSettings(ctx context.Context, req UpdateChatPersonalModelOverridesAdminSettingsRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/personal-model-overrides", req) +func (c *Client) UpdateChatPersonalModelOverridesAdminSettings(ctx context.Context, req UpdateChatPersonalModelOverridesAdminSettingsRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/personal-model-overrides", req) if err != nil { return err } @@ -2590,10 +2462,14 @@ func (c *ExperimentalClient) UpdateChatPersonalModelOverridesAdminSettings(ctx c return nil } -// GetUserChatPersonalModelOverrides fetches the user's personal model -// override settings. -func (c *ExperimentalClient) GetUserChatPersonalModelOverrides(ctx context.Context) (UserChatPersonalModelOverridesResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-personal-model-overrides", nil) +// UserChatPersonalModelOverrides returns a user's personal model overrides in an organization. +func (c *Client) UserChatPersonalModelOverrides(ctx context.Context, organizationID uuid.UUID, user string) (UserChatPersonalModelOverridesResponse, error) { + path := fmt.Sprintf( + "/api/v2/organizations/%s/members/%s/chats/model-overrides", + organizationID, + url.PathEscape(user), + ) + res, err := c.Request(ctx, http.MethodGet, path, nil) if err != nil { return UserChatPersonalModelOverridesResponse{}, err } @@ -2602,14 +2478,15 @@ func (c *ExperimentalClient) GetUserChatPersonalModelOverrides(ctx context.Conte return UserChatPersonalModelOverridesResponse{}, ReadBodyAsError(res) } var resp UserChatPersonalModelOverridesResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } -// UpdateUserChatPersonalModelOverride updates the user's personal model -// override for the requested context. -func (c *ExperimentalClient) UpdateUserChatPersonalModelOverride(ctx context.Context, override ChatPersonalModelOverrideContext, req UpdateUserChatPersonalModelOverrideRequest) error { +// UpdateUserChatPersonalModelOverride updates a user's personal model override in an organization. +func (c *Client) UpdateUserChatPersonalModelOverride(ctx context.Context, organizationID uuid.UUID, user string, override ChatPersonalModelOverrideContext, req UpdateUserChatPersonalModelOverrideRequest) error { path := fmt.Sprintf( - "/api/experimental/chats/config/user-personal-model-overrides/%s", + "/api/v2/organizations/%s/members/%s/chats/model-overrides/%s", + organizationID, + url.PathEscape(user), url.PathEscape(string(override)), ) res, err := c.Request(ctx, http.MethodPut, path, req) @@ -2624,8 +2501,8 @@ func (c *ExperimentalClient) UpdateUserChatPersonalModelOverride(ctx context.Con } // GetUserChatCustomPrompt fetches the user's custom chat prompt. -func (c *ExperimentalClient) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-prompt", nil) +func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-prompt", nil) if err != nil { return UserChatCustomPrompt{}, err } @@ -2634,7 +2511,7 @@ func (c *ExperimentalClient) GetUserChatCustomPrompt(ctx context.Context) (UserC return UserChatCustomPrompt{}, ReadBodyAsError(res) } var resp UserChatCustomPrompt - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetChatAdvisorConfig returns the deployment-wide advisor configuration. @@ -2648,7 +2525,7 @@ func (c *ExperimentalClient) GetChatAdvisorConfig(ctx context.Context) (AdvisorC return AdvisorConfig{}, ReadBodyAsError(res) } var resp AdvisorConfig - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatAdvisorConfig updates the deployment-wide advisor configuration. @@ -2675,7 +2552,7 @@ func (c *ExperimentalClient) GetChatComputerUseProvider(ctx context.Context) (Ch return ChatComputerUseProviderResponse{}, ReadBodyAsError(res) } var resp ChatComputerUseProviderResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatComputerUseProvider updates the deployment-wide computer use @@ -2693,8 +2570,8 @@ func (c *ExperimentalClient) UpdateChatComputerUseProvider(ctx context.Context, } // GetChatWorkspaceTTL returns the configured chat workspace TTL. -func (c *ExperimentalClient) GetChatWorkspaceTTL(ctx context.Context) (ChatWorkspaceTTLResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/workspace-ttl", nil) +func (c *Client) GetChatWorkspaceTTL(ctx context.Context) (ChatWorkspaceTTLResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/workspace-ttl", nil) if err != nil { return ChatWorkspaceTTLResponse{}, err } @@ -2703,12 +2580,12 @@ func (c *ExperimentalClient) GetChatWorkspaceTTL(ctx context.Context) (ChatWorks return ChatWorkspaceTTLResponse{}, ReadBodyAsError(res) } var resp ChatWorkspaceTTLResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatWorkspaceTTL updates the chat workspace TTL setting. -func (c *ExperimentalClient) UpdateChatWorkspaceTTL(ctx context.Context, req UpdateChatWorkspaceTTLRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/workspace-ttl", req) +func (c *Client) UpdateChatWorkspaceTTL(ctx context.Context, req UpdateChatWorkspaceTTLRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/workspace-ttl", req) if err != nil { return err } @@ -2720,8 +2597,8 @@ func (c *ExperimentalClient) UpdateChatWorkspaceTTL(ctx context.Context, req Upd } // GetChatRetentionDays returns the configured chat retention period. -func (c *ExperimentalClient) GetChatRetentionDays(ctx context.Context) (ChatRetentionDaysResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/retention-days", nil) +func (c *Client) GetChatRetentionDays(ctx context.Context) (ChatRetentionDaysResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/retention-days", nil) if err != nil { return ChatRetentionDaysResponse{}, err } @@ -2730,12 +2607,12 @@ func (c *ExperimentalClient) GetChatRetentionDays(ctx context.Context) (ChatRete return ChatRetentionDaysResponse{}, ReadBodyAsError(res) } var resp ChatRetentionDaysResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatRetentionDays updates the chat retention period. -func (c *ExperimentalClient) UpdateChatRetentionDays(ctx context.Context, req UpdateChatRetentionDaysRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/retention-days", req) +func (c *Client) UpdateChatRetentionDays(ctx context.Context, req UpdateChatRetentionDaysRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/retention-days", req) if err != nil { return err } @@ -2748,8 +2625,8 @@ func (c *ExperimentalClient) UpdateChatRetentionDays(ctx context.Context, req Up // GetChatDebugRetentionDays returns the configured chat debug run // retention period. -func (c *ExperimentalClient) GetChatDebugRetentionDays(ctx context.Context) (ChatDebugRetentionDaysResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/debug-retention-days", nil) +func (c *Client) GetChatDebugRetentionDays(ctx context.Context) (ChatDebugRetentionDaysResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/debug-retention-days", nil) if err != nil { return ChatDebugRetentionDaysResponse{}, err } @@ -2758,12 +2635,12 @@ func (c *ExperimentalClient) GetChatDebugRetentionDays(ctx context.Context) (Cha return ChatDebugRetentionDaysResponse{}, ReadBodyAsError(res) } var resp ChatDebugRetentionDaysResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatDebugRetentionDays updates the chat debug run retention period. -func (c *ExperimentalClient) UpdateChatDebugRetentionDays(ctx context.Context, req UpdateChatDebugRetentionDaysRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/debug-retention-days", req) +func (c *Client) UpdateChatDebugRetentionDays(ctx context.Context, req UpdateChatDebugRetentionDaysRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/debug-retention-days", req) if err != nil { return err } @@ -2775,8 +2652,8 @@ func (c *ExperimentalClient) UpdateChatDebugRetentionDays(ctx context.Context, r } // GetChatAutoArchiveDays returns the configured chat auto-archive period. -func (c *ExperimentalClient) GetChatAutoArchiveDays(ctx context.Context) (ChatAutoArchiveDaysResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/auto-archive-days", nil) +func (c *Client) GetChatAutoArchiveDays(ctx context.Context) (ChatAutoArchiveDaysResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/auto-archive-days", nil) if err != nil { return ChatAutoArchiveDaysResponse{}, err } @@ -2785,39 +2662,12 @@ func (c *ExperimentalClient) GetChatAutoArchiveDays(ctx context.Context) (ChatAu return ChatAutoArchiveDaysResponse{}, ReadBodyAsError(res) } var resp ChatAutoArchiveDaysResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatAutoArchiveDays updates the chat auto-archive period. -func (c *ExperimentalClient) UpdateChatAutoArchiveDays(ctx context.Context, req UpdateChatAutoArchiveDaysRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/auto-archive-days", req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} - -// GetChatTemplateAllowlist returns the deployment-wide chat template allowlist. -func (c *ExperimentalClient) GetChatTemplateAllowlist(ctx context.Context) (ChatTemplateAllowlist, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/template-allowlist", nil) - if err != nil { - return ChatTemplateAllowlist{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatTemplateAllowlist{}, ReadBodyAsError(res) - } - var resp ChatTemplateAllowlist - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// UpdateChatTemplateAllowlist updates the deployment-wide chat template allowlist. -func (c *ExperimentalClient) UpdateChatTemplateAllowlist(ctx context.Context, req ChatTemplateAllowlist) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/template-allowlist", req) +func (c *Client) UpdateChatAutoArchiveDays(ctx context.Context, req UpdateChatAutoArchiveDaysRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/auto-archive-days", req) if err != nil { return err } @@ -2829,8 +2679,8 @@ func (c *ExperimentalClient) UpdateChatTemplateAllowlist(ctx context.Context, re } // UpdateUserChatCustomPrompt updates the user's custom chat prompt. -func (c *ExperimentalClient) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req) +func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/user-prompt", req) if err != nil { return UserChatCustomPrompt{}, err } @@ -2839,13 +2689,13 @@ func (c *ExperimentalClient) UpdateUserChatCustomPrompt(ctx context.Context, req return UserChatCustomPrompt{}, ReadBodyAsError(res) } var resp UserChatCustomPrompt - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetUserChatCompactionThresholds fetches the user's per-model chat // compaction thresholds. -func (c *ExperimentalClient) GetUserChatCompactionThresholds(ctx context.Context) (UserChatCompactionThresholds, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-compaction-thresholds", nil) +func (c *Client) GetUserChatCompactionThresholds(ctx context.Context) (UserChatCompactionThresholds, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-compaction-thresholds", nil) if err != nil { return UserChatCompactionThresholds{}, err } @@ -2854,13 +2704,13 @@ func (c *ExperimentalClient) GetUserChatCompactionThresholds(ctx context.Context return UserChatCompactionThresholds{}, ReadBodyAsError(res) } var thresholds UserChatCompactionThresholds - return thresholds, json.NewDecoder(res.Body).Decode(&thresholds) + return thresholds, ReadBodyAsJSON(res, &thresholds) } // UpdateUserChatCompactionThreshold updates the user's per-model chat // compaction threshold. -func (c *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) { - res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), req) +func (c *Client) UpdateUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/chats/config/user-compaction-thresholds/%s", modelID), req) if err != nil { return UserChatCompactionThreshold{}, err } @@ -2869,13 +2719,13 @@ func (c *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Conte return UserChatCompactionThreshold{}, ReadBodyAsError(res) } var threshold UserChatCompactionThreshold - return threshold, json.NewDecoder(res.Body).Decode(&threshold) + return threshold, ReadBodyAsJSON(res, &threshold) } // DeleteUserChatCompactionThreshold deletes the user's per-model chat // compaction threshold override. -func (c *ExperimentalClient) DeleteUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), nil) +func (c *Client) DeleteUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/chats/config/user-compaction-thresholds/%s", modelID), nil) if err != nil { return err } @@ -2887,17 +2737,17 @@ func (c *ExperimentalClient) DeleteUserChatCompactionThreshold(ctx context.Conte } // CreateChat creates a new chat. -func (c *ExperimentalClient) CreateChat(ctx context.Context, req CreateChatRequest) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/experimental/chats", req) +func (c *Client) CreateChat(ctx context.Context, req CreateChatRequest) (Chat, error) { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/chats", req) if err != nil { return Chat{}, err } if res.StatusCode != http.StatusCreated { - return Chat{}, readBodyAsChatUsageLimitError(res) + return Chat{}, ReadBodyAsError(res) } defer res.Body.Close() var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } // StreamChatOptions are optional parameters for StreamChat. @@ -2914,8 +2764,8 @@ type StreamChatOptions struct { // The returned channel includes initial snapshot events first, followed by // live updates. Callers must close the returned io.Closer to release the // websocket connection when done. -func (c *ExperimentalClient) StreamChat(ctx context.Context, chatID uuid.UUID, opts *StreamChatOptions) (<-chan ChatStreamEvent, io.Closer, error) { - path := fmt.Sprintf("/api/experimental/chats/%s/stream", chatID) +func (c *Client) StreamChat(ctx context.Context, chatID uuid.UUID, opts *StreamChatOptions) (<-chan ChatStreamEvent, io.Closer, error) { + path := fmt.Sprintf("/api/v2/chats/%s/stream", chatID) if opts != nil && opts.AfterID != nil { path += fmt.Sprintf("?after_id=%d", *opts.AfterID) } @@ -2991,10 +2841,10 @@ func (c *ExperimentalClient) StreamChat(ctx context.Context, chatID uuid.UUID, o // deletion, diff-status changes, and action-required notifications. // Callers must close the returned io.Closer to release the websocket // connection when done. -func (c *ExperimentalClient) WatchChats(ctx context.Context) (<-chan ChatWatchEvent, io.Closer, error) { +func (c *Client) WatchChats(ctx context.Context) (<-chan ChatWatchEvent, io.Closer, error) { conn, err := c.Dial( ctx, - "/api/experimental/chats/watch", + "/api/v2/chats/watch", &websocket.DialOptions{CompressionMode: websocket.CompressionDisabled}, ) if err != nil { @@ -3041,8 +2891,8 @@ func (c *ExperimentalClient) WatchChats(ctx context.Context) (<-chan ChatWatchEv // GetChatDebugLogging returns the runtime admin setting that allows // users to opt into chat debug logging. -func (c *ExperimentalClient) GetChatDebugLogging(ctx context.Context) (ChatDebugLoggingAdminSettings, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/debug-logging", nil) +func (c *Client) GetChatDebugLogging(ctx context.Context) (ChatDebugLoggingAdminSettings, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/debug-logging", nil) if err != nil { return ChatDebugLoggingAdminSettings{}, err } @@ -3051,13 +2901,13 @@ func (c *ExperimentalClient) GetChatDebugLogging(ctx context.Context) (ChatDebug return ChatDebugLoggingAdminSettings{}, ReadBodyAsError(res) } var resp ChatDebugLoggingAdminSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChatDebugLogging updates the runtime admin setting that allows // users to opt into chat debug logging. -func (c *ExperimentalClient) UpdateChatDebugLogging(ctx context.Context, req UpdateChatDebugLoggingAllowUsersRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/debug-logging", req) +func (c *Client) UpdateChatDebugLogging(ctx context.Context, req UpdateChatDebugLoggingAllowUsersRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/debug-logging", req) if err != nil { return err } @@ -3070,8 +2920,8 @@ func (c *ExperimentalClient) UpdateChatDebugLogging(ctx context.Context, req Upd // GetUserChatDebugLogging returns whether chat debug logging is active // for the current user and whether the user may change it. -func (c *ExperimentalClient) GetUserChatDebugLogging(ctx context.Context) (UserChatDebugLoggingSettings, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-debug-logging", nil) +func (c *Client) GetUserChatDebugLogging(ctx context.Context) (UserChatDebugLoggingSettings, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-debug-logging", nil) if err != nil { return UserChatDebugLoggingSettings{}, err } @@ -3080,13 +2930,13 @@ func (c *ExperimentalClient) GetUserChatDebugLogging(ctx context.Context) (UserC return UserChatDebugLoggingSettings{}, ReadBodyAsError(res) } var resp UserChatDebugLoggingSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserChatDebugLogging updates the current user's chat debug // logging preference. -func (c *ExperimentalClient) UpdateUserChatDebugLogging(ctx context.Context, req UpdateUserChatDebugLoggingRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-debug-logging", req) +func (c *Client) UpdateUserChatDebugLogging(ctx context.Context, req UpdateUserChatDebugLoggingRequest) error { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/user-debug-logging", req) if err != nil { return err } @@ -3108,7 +2958,7 @@ func (c *ExperimentalClient) GetChatDebugRuns(ctx context.Context, chatID uuid.U return nil, ReadBodyAsError(res) } var resp []ChatDebugRunSummary - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetChatDebugRun returns a single debug run along with its full step @@ -3123,12 +2973,12 @@ func (c *ExperimentalClient) GetChatDebugRun(ctx context.Context, chatID uuid.UU return ChatDebugRun{}, ReadBodyAsError(res) } var resp ChatDebugRun - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetChat returns a chat by ID. -func (c *ExperimentalClient) GetChat(ctx context.Context, chatID uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s", chatID), nil) +func (c *Client) GetChat(ctx context.Context, chatID uuid.UUID) (Chat, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s", chatID), nil) if err != nil { return Chat{}, err } @@ -3137,13 +2987,13 @@ func (c *ExperimentalClient) GetChat(ctx context.Context, chatID uuid.UUID) (Cha return Chat{}, ReadBodyAsError(res) } var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } // RefreshChatContext re-pins the chat to its agent's latest context snapshot // and clears the dirty marker. The request takes no body. -func (c *ExperimentalClient) RefreshChatContext(ctx context.Context, chatID uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/%s/context", chatID), nil) +func (c *Client) RefreshChatContext(ctx context.Context, chatID uuid.UUID) (Chat, error) { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/chats/%s/context", chatID), nil) if err != nil { return Chat{}, err } @@ -3152,11 +3002,11 @@ func (c *ExperimentalClient) RefreshChatContext(ctx context.Context, chatID uuid return Chat{}, ReadBodyAsError(res) } var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } -func (c *ExperimentalClient) GetChatACL(ctx context.Context, chatID uuid.UUID) (ChatACL, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/acl", chatID), nil) +func (c *Client) GetChatACL(ctx context.Context, chatID uuid.UUID) (ChatACL, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/acl", chatID), nil) if err != nil { return ChatACL{}, err } @@ -3165,11 +3015,11 @@ func (c *ExperimentalClient) GetChatACL(ctx context.Context, chatID uuid.UUID) ( return ChatACL{}, ReadBodyAsError(res) } var acl ChatACL - return acl, json.NewDecoder(res.Body).Decode(&acl) + return acl, ReadBodyAsJSON(res, &acl) } -func (c *ExperimentalClient) UpdateChatACL(ctx context.Context, chatID uuid.UUID, req UpdateChatACL) error { - res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/%s/acl", chatID), req) +func (c *Client) UpdateChatACL(ctx context.Context, chatID uuid.UUID, req UpdateChatACL) error { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/chats/%s/acl", chatID), req) if err != nil { return err } @@ -3196,7 +3046,7 @@ type ChatMessagesPaginationOptions struct { } // GetChatMessages returns the messages and queued messages for a chat. -func (c *ExperimentalClient) GetChatMessages(ctx context.Context, chatID uuid.UUID, opts *ChatMessagesPaginationOptions) (ChatMessagesResponse, error) { +func (c *Client) GetChatMessages(ctx context.Context, chatID uuid.UUID, opts *ChatMessagesPaginationOptions) (ChatMessagesResponse, error) { reqOpts := []RequestOption{} if opts != nil { reqOpts = append(reqOpts, func(r *http.Request) { @@ -3213,7 +3063,7 @@ func (c *ExperimentalClient) GetChatMessages(ctx context.Context, chatID uuid.UU r.URL.RawQuery = q.Encode() }) } - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/messages", chatID), nil, reqOpts...) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/messages", chatID), nil, reqOpts...) if err != nil { return ChatMessagesResponse{}, err } @@ -3222,7 +3072,7 @@ func (c *ExperimentalClient) GetChatMessages(ctx context.Context, chatID uuid.UU return ChatMessagesResponse{}, ReadBodyAsError(res) } var resp ChatMessagesResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // ChatPromptsOptions are optional query parameters for GetChatPrompts. @@ -3239,7 +3089,7 @@ type ChatPromptsOptions struct { // only their text parts (concatenated in the original order) are // returned. Whitespace-only prompts are filtered server-side so the // caller never has to skip blank entries while cycling. -func (c *ExperimentalClient) GetChatPrompts(ctx context.Context, chatID uuid.UUID, opts *ChatPromptsOptions) (ChatPromptsResponse, error) { +func (c *Client) GetChatPrompts(ctx context.Context, chatID uuid.UUID, opts *ChatPromptsOptions) (ChatPromptsResponse, error) { reqOpts := []RequestOption{} if opts != nil && opts.Limit > 0 { reqOpts = append(reqOpts, func(r *http.Request) { @@ -3248,7 +3098,7 @@ func (c *ExperimentalClient) GetChatPrompts(ctx context.Context, chatID uuid.UUI r.URL.RawQuery = q.Encode() }) } - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/prompts", chatID), nil, reqOpts...) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/prompts", chatID), nil, reqOpts...) if err != nil { return ChatPromptsResponse{}, err } @@ -3257,12 +3107,12 @@ func (c *ExperimentalClient) GetChatPrompts(ctx context.Context, chatID uuid.UUI return ChatPromptsResponse{}, ReadBodyAsError(res) } var resp ChatPromptsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateChat patches a chat resource. -func (c *ExperimentalClient) UpdateChat(ctx context.Context, chatID uuid.UUID, req UpdateChatRequest) error { - res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/%s", chatID), req) +func (c *Client) UpdateChat(ctx context.Context, chatID uuid.UUID, req UpdateChatRequest) error { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/chats/%s", chatID), req) if err != nil { return err } @@ -3274,21 +3124,21 @@ func (c *ExperimentalClient) UpdateChat(ctx context.Context, chatID uuid.UUID, r } // CreateChatMessage adds a message to a chat. -func (c *ExperimentalClient) CreateChatMessage(ctx context.Context, chatID uuid.UUID, req CreateChatMessageRequest) (CreateChatMessageResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/messages", chatID), req) +func (c *Client) CreateChatMessage(ctx context.Context, chatID uuid.UUID, req CreateChatMessageRequest) (CreateChatMessageResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/messages", chatID), req) if err != nil { return CreateChatMessageResponse{}, err } if res.StatusCode != http.StatusOK { - return CreateChatMessageResponse{}, readBodyAsChatUsageLimitError(res) + return CreateChatMessageResponse{}, ReadBodyAsError(res) } defer res.Body.Close() var resp CreateChatMessageResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // EditChatMessage edits an existing user message in a chat and re-runs from there. -func (c *ExperimentalClient) EditChatMessage( +func (c *Client) EditChatMessage( ctx context.Context, chatID uuid.UUID, messageID int64, @@ -3297,23 +3147,23 @@ func (c *ExperimentalClient) EditChatMessage( res, err := c.Request( ctx, http.MethodPatch, - fmt.Sprintf("/api/experimental/chats/%s/messages/%d", chatID, messageID), + fmt.Sprintf("/api/v2/chats/%s/messages/%d", chatID, messageID), req, ) if err != nil { return EditChatMessageResponse{}, err } if res.StatusCode != http.StatusOK { - return EditChatMessageResponse{}, readBodyAsChatUsageLimitError(res) + return EditChatMessageResponse{}, ReadBodyAsError(res) } defer res.Body.Close() var resp EditChatMessageResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // InterruptChat cancels an in-flight chat run and leaves it waiting. -func (c *ExperimentalClient) InterruptChat(ctx context.Context, chatID uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/interrupt", chatID), nil) +func (c *Client) InterruptChat(ctx context.Context, chatID uuid.UUID) (Chat, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/interrupt", chatID), nil) if err != nil { return Chat{}, err } @@ -3322,14 +3172,15 @@ func (c *ExperimentalClient) InterruptChat(ctx context.Context, chatID uuid.UUID return Chat{}, ReadBodyAsError(res) } var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } -// ReconcileInvalidChatState recovers a chat stuck in an invalid -// execution state, moving it into an error state from which the caller -// can send a new message or edit history to continue. -func (c *ExperimentalClient) ReconcileInvalidChatState(ctx context.Context, chatID uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/reconcile-invalid", chatID), nil) +// CompactChat requests a manual context compaction on an idle or +// errored chat, clearing any stored error. The compaction runs +// asynchronously through the chat worker and bypasses the automatic +// usage threshold. +func (c *Client) CompactChat(ctx context.Context, chatID uuid.UUID) (Chat, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/compact", chatID), nil) if err != nil { return Chat{}, err } @@ -3338,22 +3189,23 @@ func (c *ExperimentalClient) ReconcileInvalidChatState(ctx context.Context, chat return Chat{}, ReadBodyAsError(res) } var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } -// RegenerateChatTitle requests the server to regenerate the chat's -// title using richer conversation context. -func (c *ExperimentalClient) RegenerateChatTitle(ctx context.Context, chatID uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/title/regenerate", chatID), nil) +// ReconcileInvalidChatState recovers a chat stuck in an invalid +// execution state, moving it into an error state from which the caller +// can send a new message or edit history to continue. +func (c *Client) ReconcileInvalidChatState(ctx context.Context, chatID uuid.UUID) (Chat, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/reconcile-invalid", chatID), nil) if err != nil { return Chat{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return Chat{}, readBodyAsChatUsageLimitError(res) + return Chat{}, ReadBodyAsError(res) } var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) + return chat, ReadBodyAsJSON(res, &chat) } // ProposeChatTitleResponse is returned by the propose-title endpoint. @@ -3362,22 +3214,22 @@ type ProposeChatTitleResponse struct { } // ProposeChatTitle requests the server to generate a suggested chat title without persisting it. -func (c *ExperimentalClient) ProposeChatTitle(ctx context.Context, chatID uuid.UUID) (ProposeChatTitleResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/title/propose", chatID), nil) +func (c *Client) ProposeChatTitle(ctx context.Context, chatID uuid.UUID) (ProposeChatTitleResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/title/propose", chatID), nil) if err != nil { return ProposeChatTitleResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ProposeChatTitleResponse{}, readBodyAsChatUsageLimitError(res) + return ProposeChatTitleResponse{}, ReadBodyAsError(res) } var resp ProposeChatTitleResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetChatDiffContents returns resolved diff contents for a chat. -func (c *ExperimentalClient) GetChatDiffContents(ctx context.Context, chatID uuid.UUID) (ChatDiffContents, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/diff", chatID), nil) +func (c *Client) GetChatDiffContents(ctx context.Context, chatID uuid.UUID) (ChatDiffContents, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/diff", chatID), nil) if err != nil { return ChatDiffContents{}, err } @@ -3386,12 +3238,12 @@ func (c *ExperimentalClient) GetChatDiffContents(ctx context.Context, chatID uui return ChatDiffContents{}, ReadBodyAsError(res) } var diff ChatDiffContents - return diff, json.NewDecoder(res.Body).Decode(&diff) + return diff, ReadBodyAsJSON(res, &diff) } // UploadChatFile uploads a file for use in chat messages. -func (c *ExperimentalClient) UploadChatFile(ctx context.Context, organizationID uuid.UUID, contentType string, filename string, rd io.Reader) (UploadChatFileResponse, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/files?organization=%s", organizationID), rd, func(r *http.Request) { +func (c *Client) UploadChatFile(ctx context.Context, organizationID uuid.UUID, contentType string, filename string, rd io.Reader) (UploadChatFileResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/files?organization=%s", organizationID), rd, func(r *http.Request) { r.Header.Set("Content-Type", contentType) if filename != "" { r.Header.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename})) @@ -3405,144 +3257,44 @@ func (c *ExperimentalClient) UploadChatFile(ctx context.Context, organizationID return UploadChatFileResponse{}, ReadBodyAsError(res) } var resp UploadChatFileResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// GetChatFile retrieves a previously uploaded chat file by ID. -func (c *ExperimentalClient) GetChatFile(ctx context.Context, fileID uuid.UUID) ([]byte, string, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/files/%s", fileID), nil) - if err != nil { - return nil, "", err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, "", ReadBodyAsError(res) - } - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, "", err - } - return data, res.Header.Get("Content-Type"), nil -} - -// GetChatUsageLimitConfig returns the deployment-wide chat usage limit config. -func (c *ExperimentalClient) GetChatUsageLimitConfig(ctx context.Context) (ChatUsageLimitConfigResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/usage-limits", nil) - if err != nil { - return ChatUsageLimitConfigResponse{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatUsageLimitConfigResponse{}, ReadBodyAsError(res) - } - var resp ChatUsageLimitConfigResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } -// UpdateChatUsageLimitConfig updates the deployment-wide usage limit config. -func (c *ExperimentalClient) UpdateChatUsageLimitConfig(ctx context.Context, req ChatUsageLimitConfig) (ChatUsageLimitConfig, error) { - res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/usage-limits", req) +// ChatFileDownloadURL creates a short-lived download URL for a chat file. +func (c *Client) ChatFileDownloadURL(ctx context.Context, fileID uuid.UUID) (ChatFileDownloadURLResponse, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/files/%s/download-url", fileID), nil) if err != nil { - return ChatUsageLimitConfig{}, err + return ChatFileDownloadURLResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatUsageLimitConfig{}, ReadBodyAsError(res) + return ChatFileDownloadURLResponse{}, ReadBodyAsError(res) } - var resp ChatUsageLimitConfig - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// UpsertChatUsageLimitOverride creates or updates a per-user usage limit override. -func (c *ExperimentalClient) UpsertChatUsageLimitOverride(ctx context.Context, userID uuid.UUID, req UpsertChatUsageLimitOverrideRequest) (ChatUsageLimitOverride, error) { - res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/usage-limits/overrides/%s", userID), req) - if err != nil { - return ChatUsageLimitOverride{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatUsageLimitOverride{}, ReadBodyAsError(res) - } - var resp ChatUsageLimitOverride - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// UpdateChatUserUsageLimitOverride creates or updates a per-user usage limit override. -func (c *ExperimentalClient) UpdateChatUserUsageLimitOverride(ctx context.Context, userID uuid.UUID, req UpdateChatUsageLimitOverrideRequest) (ChatUsageLimitOverride, error) { - return c.UpsertChatUsageLimitOverride(ctx, userID, req) + var resp ChatFileDownloadURLResponse + return resp, ReadBodyAsJSON(res, &resp) } -// DeleteChatUsageLimitOverride removes a per-user usage limit override. -func (c *ExperimentalClient) DeleteChatUsageLimitOverride(ctx context.Context, userID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/usage-limits/overrides/%s", userID), nil) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} - -// DeleteChatUserUsageLimitOverride removes a per-user usage limit override. -func (c *ExperimentalClient) DeleteChatUserUsageLimitOverride(ctx context.Context, userID uuid.UUID) error { - return c.DeleteChatUsageLimitOverride(ctx, userID) -} - -// UpsertChatUsageLimitGroupOverride creates or updates a group-level -// spend limit override. EXPERIMENTAL: This API is subject to change. -func (c *ExperimentalClient) UpsertChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID, req UpsertChatUsageLimitGroupOverrideRequest) (ChatUsageLimitGroupOverride, error) { - res, err := c.Request(ctx, http.MethodPut, - fmt.Sprintf("/api/experimental/chats/usage-limits/group-overrides/%s", groupID), - req, - ) +// GetChatFile retrieves a previously uploaded chat file by ID. +func (c *Client) GetChatFile(ctx context.Context, fileID uuid.UUID) ([]byte, string, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/files/%s", fileID), nil) if err != nil { - return ChatUsageLimitGroupOverride{}, err + return nil, "", err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatUsageLimitGroupOverride{}, ReadBodyAsError(res) - } - var override ChatUsageLimitGroupOverride - return override, json.NewDecoder(res.Body).Decode(&override) -} - -// DeleteChatUsageLimitGroupOverride removes a group-level spend limit -// override. EXPERIMENTAL: This API is subject to change. -func (c *ExperimentalClient) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, - fmt.Sprintf("/api/experimental/chats/usage-limits/group-overrides/%s", groupID), - nil, - ) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) + return nil, "", ReadBodyAsError(res) } - return nil -} - -// GetMyChatUsageLimitStatus returns the current user's chat usage limit status. -func (c *ExperimentalClient) GetMyChatUsageLimitStatus(ctx context.Context) (ChatUsageLimitStatus, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/usage-limits/status", nil) + data, err := io.ReadAll(res.Body) if err != nil { - return ChatUsageLimitStatus{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return ChatUsageLimitStatus{}, ReadBodyAsError(res) + return nil, "", err } - var resp ChatUsageLimitStatus - return resp, json.NewDecoder(res.Body).Decode(&resp) + return data, res.Header.Get("Content-Type"), nil } // SubmitToolResults submits the results of dynamic tool calls for a chat // that is in requires_action status. -func (c *ExperimentalClient) SubmitToolResults(ctx context.Context, chatID uuid.UUID, req SubmitToolResultsRequest) error { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/tool-results", chatID), req) +func (c *Client) SubmitToolResults(ctx context.Context, chatID uuid.UUID, req SubmitToolResultsRequest) error { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/tool-results", chatID), req) if err != nil { return err } @@ -3556,12 +3308,12 @@ func (c *ExperimentalClient) SubmitToolResults(ctx context.Context, chatID uuid. // GetChatsByWorkspace returns a mapping of workspace ID to the latest // non-archived chat ID for each requested workspace. Workspaces with // no chats are omitted from the response. -func (c *ExperimentalClient) GetChatsByWorkspace(ctx context.Context, workspaceIDs []uuid.UUID) (map[uuid.UUID]uuid.UUID, error) { +func (c *Client) GetChatsByWorkspace(ctx context.Context, workspaceIDs []uuid.UUID) (map[uuid.UUID]uuid.UUID, error) { ids := make([]string, 0, len(workspaceIDs)) for _, id := range workspaceIDs { ids = append(ids, id.String()) } - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/by-workspace?workspace_ids=%s", strings.Join(ids, ",")), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/by-workspace?workspace_ids=%s", strings.Join(ids, ",")), nil) if err != nil { return nil, err } @@ -3570,5 +3322,5 @@ func (c *ExperimentalClient) GetChatsByWorkspace(ctx context.Context, workspaceI return nil, ReadBodyAsError(res) } var result map[uuid.UUID]uuid.UUID - return result, json.NewDecoder(res.Body).Decode(&result) + return result, ReadBodyAsJSON(res, &result) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/client.go b/vendor/github.com/coder/coder/v2/codersdk/client.go index 834dfa465..afa54bdd7 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/client.go +++ b/vendor/github.com/coder/coder/v2/codersdk/client.go @@ -15,6 +15,7 @@ import ( "net/url" "strings" "sync" + "unicode" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" @@ -44,6 +45,11 @@ const ( OAuth2PKCEVerifier = "oauth_pkce_verifier" // OAuth2RedirectCookie is the name of the cookie that stores the oauth2 redirect. OAuth2RedirectCookie = "oauth_redirect" + // OAuth2RedirectURICookie stores the dynamically computed OIDC redirect_uri + // when CODER_OIDC_REDIRECT_ALLOWED_HOSTS is enabled. The same value must be + // used for both the authorization request and the token exchange (RFC 6749 + // section 4.1.3). + OAuth2RedirectURICookie = "oauth_redirect_uri" // PathAppSessionTokenCookie is the name of the cookie that stores an // application-scoped API token on workspace proxy path app domains. @@ -418,21 +424,6 @@ func ReadBodyAsError(res *http.Response) error { } defer res.Body.Close() - var requestMethod, requestURL string - if res.Request != nil { - requestMethod = res.Request.Method - if res.Request.URL != nil { - requestURL = res.Request.URL.String() - } - } - - var helpMessage string - if res.StatusCode == http.StatusUnauthorized { - // 401 means the user is not logged in - // 403 would mean that the user is not authorized - helpMessage = "Try logging in using 'coder login'." - } - resp, err := io.ReadAll(res.Body) if err != nil { return xerrors.Errorf("read body: %w", err) @@ -445,29 +436,19 @@ func ReadBodyAsError(res *http.Response) error { if len(resp) == 0 { resp = []byte("no response body") } - return &Error{ - statusCode: res.StatusCode, - method: requestMethod, - url: requestURL, - Response: Response{ - Message: mimeErr.Error(), - Detail: string(resp), - }, - Helper: helpMessage, - } + return newResponseError(res, Response{ + Message: mimeErr.Error(), + Detail: string(resp), + }) } var m Response err = json.NewDecoder(bytes.NewBuffer(resp)).Decode(&m) if err != nil { if errors.Is(err, io.EOF) { - return &Error{ - statusCode: res.StatusCode, - Response: Response{ - Message: "empty response body", - }, - Helper: helpMessage, - } + return newResponseError(res, Response{ + Message: "empty response body", + }) } return xerrors.Errorf("decode body: %w", err) } @@ -479,8 +460,30 @@ func ReadBodyAsError(res *http.Response) error { m.Detail = string(resp) } + return newResponseError(res, m) +} + +// newResponseError wraps an API response in an *Error annotated with +// the status code, request method, and request URL from res. For 401 +// responses it also sets a helper message suggesting 'coder login'. +func newResponseError(res *http.Response, response Response) *Error { + var requestMethod, requestURL string + if res.Request != nil { + requestMethod = res.Request.Method + if res.Request.URL != nil { + requestURL = res.Request.URL.String() + } + } + + var helpMessage string + if res.StatusCode == http.StatusUnauthorized { + // 401 means the user is not logged in + // 403 would mean that the user is not authorized + helpMessage = "Try logging in using 'coder login'." + } + return &Error{ - Response: m, + Response: response, statusCode: res.StatusCode, method: requestMethod, url: requestURL, @@ -488,14 +491,212 @@ func ReadBodyAsError(res *http.Response) error { } } +// jsonBodySniffLen bounds how much of a response body is retained while +// decoding to identify HTML without buffering the entire response. +const jsonBodySniffLen = 512 + +// htmlResponseHelper suggests the most common causes of receiving HTML +// from what should be a Coder API endpoint: a misconfigured Coder URL, +// or an intermediary such as a reverse proxy or SSO portal intercepting +// API requests. +const htmlResponseHelper = "Ensure the Coder URL is correct and that any reverse proxy or SSO in front of it passes /api/v2 requests through to Coder." + +// ReadBodyAsJSON decodes the response body as JSON into v. It is +// intended for typed API endpoints whose accepted responses are always +// JSON, replacing direct json.NewDecoder(res.Body).Decode(...) calls. +// It must not be used for streaming or non-JSON endpoints such as +// WebSockets, server-sent events, or file downloads. +// +// Bodies that are HTML, empty, or otherwise not valid JSON produce a +// structured *Error describing the response instead of a bare decode +// error such as "invalid character '<' looking for beginning of value". +// Intermediaries like reverse proxies and SSO portals commonly return +// such bodies with a 200 status code. +// +// Valid JSON is decoded even when an intermediary omits or mislabels +// the Content-Type header. The exception is a Content-Type declaring +// HTML, which is always reported as an invalid response since Coder +// API endpoints never serve HTML. The caller remains responsible for +// closing the response body. +func ReadBodyAsJSON(res *http.Response, v any) error { + return decodeBodyAsJSON(res, v, nil) +} + +// ReadBodyAsJSONUseNumber behaves like ReadBodyAsJSON but decodes JSON +// numbers into json.Number instead of float64, preserving integer +// precision for callers that re-serialize or type-assert numeric +// claims, such as license JWT claims. +func ReadBodyAsJSONUseNumber(res *http.Response, v any) error { + return decodeBodyAsJSON(res, v, func(dec *json.Decoder) { + dec.UseNumber() + }) +} + +// decodeBodyAsJSON decodes the response body as JSON into v. When +// configure is non-nil it is called with the decoder before decoding, +// allowing callers to set options such as UseNumber. +func decodeBodyAsJSON(res *http.Response, v any, configure func(*json.Decoder)) error { + if res == nil || res.Body == nil { + return xerrors.New("no response body to decode") + } + + mimeType := parseMimeType(res.Header.Get("Content-Type")) + if isHTMLMimeType(mimeType) { + return htmlBodyError(res) + } + + body := &responseBodyReader{Reader: res.Body} + prefix := &bodyPrefixWriter{} + dec := json.NewDecoder(io.TeeReader(body, prefix)) + if configure != nil { + configure(dec) + } + err := dec.Decode(v) + switch { + case err == nil: + return nil + case body.err != nil && errors.Is(err, body.err): + return xerrors.Errorf("read response body: %w", err) + case len(prefix.bytes) == 0 && errors.Is(err, io.EOF): + return invalidBodyError(res, Response{ + Message: "Received an empty response from the Coder API.", + Detail: invalidBodyDetail(res), + }, "", nil) + case isHTMLBody(mimeType, prefix.bytes): + return htmlBodyError(res) + default: + return invalidBodyError(res, Response{ + Message: "Received an invalid JSON response from the Coder API.", + Detail: fmt.Sprintf("decode body: %s, %s", err.Error(), invalidBodyDetail(res)), + }, "", err) + } +} + +// htmlBodyError returns the structured *Error reported when the Coder API +// response body is HTML rather than JSON. +func htmlBodyError(res *http.Response) *Error { + return invalidBodyError(res, Response{ + Message: "Received an HTML response instead of JSON from the Coder API.", + Detail: invalidBodyDetail(res), + }, htmlResponseHelper, nil) +} + +// responseBodyReader records non-EOF read errors so decode errors from custom +// JSON unmarshallers are not mistaken for transport failures. +type responseBodyReader struct { + io.Reader + err error +} + +func (r *responseBodyReader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if err != nil && !errors.Is(err, io.EOF) && r.err == nil { + r.err = err + } + return n, err +} + +// bodyPrefixWriter retains only the beginning of a body while reporting every +// byte as written so it can be used with io.TeeReader. +type bodyPrefixWriter struct { + bytes []byte +} + +func (w *bodyPrefixWriter) Write(p []byte) (int, error) { + if remaining := jsonBodySniffLen - len(w.bytes); remaining > 0 { + w.bytes = append(w.bytes, p[:min(remaining, len(p))]...) + } + return len(p), nil +} + +// isHTMLBody reports whether a response body is HTML, either by its +// media type or by starting with '<' after optional whitespace. Valid +// JSON can never start with '<', so the sniff also identifies HTML +// bodies mislabeled with a JSON content type. Markup bodies such as +// XML are intentionally classified the same way since the remediation, +// fixing the URL or the intermediary, is identical. +func isHTMLBody(mimeType string, prefix []byte) bool { + if isHTMLMimeType(mimeType) { + return true + } + // Strip a UTF-8 byte order mark, which some intermediaries prepend + // to HTML error pages. + trimmed := bytes.TrimPrefix(prefix, []byte("\xef\xbb\xbf")) + trimmed = bytes.TrimLeftFunc(trimmed, unicode.IsSpace) + return len(trimmed) > 0 && trimmed[0] == '<' +} + +func isHTMLMimeType(mimeType string) bool { + return mimeType == "text/html" || mimeType == "application/xhtml+xml" +} + +// invalidBodyDetail describes an invalid response body and how it was +// reached, for inclusion in an Error Detail. +func invalidBodyDetail(res *http.Response) string { + var sb strings.Builder + if contentType := res.Header.Get("Content-Type"); contentType != "" { + _, _ = fmt.Fprintf(&sb, "content type %q", contentType) + } else { + _, _ = sb.WriteString("no content type") + } + if orig := originalRequestURL(res); orig != "" && res.Request != nil && res.Request.URL != nil && orig != res.Request.URL.String() { + _, _ = fmt.Fprintf(&sb, ", after following redirects from %s", orig) + } + return sb.String() +} + +// originalRequestURL returns the URL of the first request in the +// redirect chain that produced res, or "" when unknown. res.Request +// itself is the final request after any redirects. +func originalRequestURL(res *http.Response) string { + req := res.Request + for req != nil && req.Response != nil && req.Response.Request != nil { + req = req.Response.Request + } + if req == nil || req.URL == nil { + return "" + } + return req.URL.String() +} + +// invalidBodyError builds the *Error returned when a response with an +// accepted status code does not contain the expected JSON body. +func invalidBodyError(res *http.Response, response Response, helper string, cause error) *Error { + var requestMethod, requestURL string + if res.Request != nil { + requestMethod = res.Request.Method + if res.Request.URL != nil { + requestURL = res.Request.URL.String() + } + } + return &Error{ + Response: response, + statusCode: res.StatusCode, + method: requestMethod, + url: requestURL, + contentType: res.Header.Get("Content-Type"), + cause: cause, + invalidBody: true, + Helper: helper, + } +} + // Error represents an unaccepted or invalid request to the API. // @typescript-ignore Error type Error struct { Response - statusCode int - method string - url string + statusCode int + method string + url string + contentType string + // cause is the underlying error, such as a JSON decode failure, + // exposed to errors.Is and errors.As via Unwrap. + cause error + // invalidBody marks errors for responses whose status code was + // accepted but whose body was not the expected JSON. Error() then + // describes the body rather than an unexpected status code. + invalidBody bool Helper string } @@ -512,6 +713,18 @@ func (e *Error) URL() string { return e.url } +// ContentType returns the Content-Type header of the response that +// produced the error, when known. +func (e *Error) ContentType() string { + return e.contentType +} + +// Unwrap returns the underlying cause of the error, if any, such as the +// JSON decode error for a response body that could not be decoded. +func (e *Error) Unwrap() error { + return e.cause +} + func (e *Error) Friendly() string { var sb strings.Builder _, _ = fmt.Fprintf(&sb, "%s. %s", strings.TrimSuffix(e.Message, "."), e.Helper) @@ -526,7 +739,11 @@ func (e *Error) Error() string { if e.method != "" && e.url != "" { _, _ = fmt.Fprintf(&builder, "%v %v: ", e.method, e.url) } - _, _ = fmt.Fprintf(&builder, "unexpected status code %d: %s", e.statusCode, e.Message) + if e.invalidBody { + _, _ = fmt.Fprintf(&builder, "invalid API response (status code %d): %s", e.statusCode, e.Message) + } else { + _, _ = fmt.Fprintf(&builder, "unexpected status code %d: %s", e.statusCode, e.Message) + } if e.Helper != "" { _, _ = fmt.Fprintf(&builder, ": %s", e.Helper) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/connectionlog.go b/vendor/github.com/coder/coder/v2/codersdk/connectionlog.go index 61e1ccbb3..5a5c2bd62 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/connectionlog.go +++ b/vendor/github.com/coder/coder/v2/codersdk/connectionlog.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "net/http" "net/netip" "strings" @@ -26,6 +25,7 @@ type ConnectionLog struct { // WebInfo is only set when `type` is one of: // - `ConnectionTypePortForwarding` // - `ConnectionTypeWorkspaceApp` + // - `ConnectionTypeTunnel` WebInfo *ConnectionLogWebInfo `json:"web_info,omitempty"` // SSHInfo is only set when `type` is one of: @@ -46,6 +46,9 @@ const ( ConnectionTypeReconnectingPTY ConnectionType = "reconnecting_pty" ConnectionTypeWorkspaceApp ConnectionType = "workspace_app" ConnectionTypePortForwarding ConnectionType = "port_forwarding" + // ConnectionTypeTunnel records accepted and denied tailnet tunnel + // requests made by authenticated users. + ConnectionTypeTunnel ConnectionType = "tunnel" ) // ConnectionLogStatus is the status of a connection log entry. @@ -68,10 +71,10 @@ func (s ConnectionLogStatus) Valid() bool { type ConnectionLogWebInfo struct { UserAgent string `json:"user_agent"` - // User is omitted if the connection event was from an unauthenticated user. + // User is omitted if the connection event was unauthenticated. User *User `json:"user"` SlugOrPort string `json:"slug_or_port"` - // StatusCode is the HTTP status code of the request. + // StatusCode is the HTTP status code or tunnel authorization outcome. StatusCode int32 `json:"status_code"` } @@ -100,7 +103,7 @@ type ConnectionLogResponse struct { } func (c *Client) ConnectionLogs(ctx context.Context, req ConnectionLogsRequest) (ConnectionLogResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/connectionlog", nil, req.Pagination.asRequestOption(), func(r *http.Request) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/connectionlog", nil, req.asRequestOption(), func(r *http.Request) { q := r.URL.Query() var params []string if req.SearchQuery != "" { @@ -119,7 +122,7 @@ func (c *Client) ConnectionLogs(ctx context.Context, req ConnectionLogsRequest) } var logRes ConnectionLogResponse - err = json.NewDecoder(res.Body).Decode(&logRes) + err = ReadBodyAsJSON(res, &logRes) if err != nil { return ConnectionLogResponse{}, err } diff --git a/vendor/github.com/coder/coder/v2/codersdk/deployment.go b/vendor/github.com/coder/coder/v2/codersdk/deployment.go index 9a5272c06..9700549eb 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/deployment.go +++ b/vendor/github.com/coder/coder/v2/codersdk/deployment.go @@ -199,6 +199,12 @@ const ( FeatureBoundary FeatureName = "boundary" FeatureServiceAccounts FeatureName = "service_accounts" FeatureAIGovernanceUserLimit FeatureName = "ai_governance_user_limit" + // FeatureAgentRuntimeHours is a usage period feature. It is never a + // license claim itself. It is populated from the + // agent_runtime_hours_allocation, agent_runtime_hours_limit_soft and + // agent_runtime_hours_limit_hard claims. Refer to + // enterprise/coderd/license/license.go for the license format. + FeatureAgentRuntimeHours FeatureName = "agent_runtime_hours" ) var ( @@ -231,6 +237,7 @@ var ( FeatureBoundary, FeatureServiceAccounts, FeatureAIGovernanceUserLimit, + FeatureAgentRuntimeHours, } // FeatureNamesMap is a map of all feature names for quick lookups. @@ -300,6 +307,7 @@ func (n FeatureName) UsesLimit() bool { FeatureUserLimit: true, FeatureManagedAgentLimit: true, FeatureAIGovernanceUserLimit: true, + FeatureAgentRuntimeHours: true, }[n] } @@ -307,6 +315,7 @@ func (n FeatureName) UsesLimit() bool { func (n FeatureName) UsesUsagePeriod() bool { return map[FeatureName]bool{ FeatureManagedAgentLimit: true, + FeatureAgentRuntimeHours: true, }[n] } @@ -371,20 +380,42 @@ func (set FeatureSet) Features() []FeatureName { type Feature struct { Entitlement Entitlement `json:"entitlement"` Enabled bool `json:"enabled"` - Limit *int64 `json:"limit,omitempty"` - Actual *int64 `json:"actual,omitempty"` + // Limit is the maximum value the license grants for the feature, in the + // feature's own unit. For FeatureAgentRuntimeHours, an enabled feature + // with Limit omitted means the license grants unlimited runtime hours. + Limit *int64 `json:"limit,omitempty"` + // SoftLimit is the advisory warning threshold that accompanies Limit for + // features whose license carries it. For these features, Limit carries + // the purchased allocation; an unlimited allocation has no thresholds, + // so SoftLimit is omitted alongside the omitted Limit. Only + // FeatureAgentRuntimeHours sets this field. + SoftLimit *int64 `json:"soft_limit,omitempty"` + // HardLimit is the enforcement threshold that accompanies Limit for + // features whose license carries it. See SoftLimit for the set of + // features that use these thresholds. + HardLimit *int64 `json:"hard_limit,omitempty"` + // Actual is the usage measured against Limit, when known: a + // point-in-time count for most features, or usage accumulated over + // UsagePeriod for features that set one. Its unit matches Limit's; + // FeatureAgentRuntimeHours reports whole hours floored from the + // recorded milliseconds, with the precise value available in + // ActualMs. FeatureAgentRuntimeHours usage can trail by roughly one + // hour because the current hour is not emitted, plus the entitlement + // refresh interval. + Actual *int64 `json:"actual,omitempty"` + // ActualMs is the precise usage backing Actual, in milliseconds, for + // features measured in time. It has the same freshness as Actual. + // Only FeatureAgentRuntimeHours sets this field. + ActualMs *int64 `json:"actual_ms,omitempty"` // Below is only for features that use usage periods. // UsagePeriod denotes that the usage is a counter that accumulates over // this period (and most likely resets with the issuance of the next - // license). - // - // These dates are determined from the license that this entitlement comes - // from, see enterprise/coderd/license/license.go. - // - // Only certain features set these fields: - // - FeatureManagedAgentLimit + // license). These dates are determined from the license that this + // entitlement comes from, see enterprise/coderd/license/license.go. + // Only FeatureManagedAgentLimit and FeatureAgentRuntimeHours set this + // field. UsagePeriod *UsagePeriod `json:"usage_period,omitempty"` } @@ -404,9 +435,11 @@ type UsagePeriod struct { // 2. The usage period has a greater end date (note: only certain features use usage periods) // 3. Graceful & capable > Entitled & not capable (only if both have "Actual" values) // 4. The entitlement is greater -// 5. The limit is greater +// 5. The limit is greater (except a nil limit on a usage period feature means unlimited, outranking any set limit) // 6. Enabled is greater than disabled // 7. The actual is greater +// +// SoftLimit and HardLimit are not comparison inputs. func (f Feature) Compare(b Feature) int { // For features with usage period constraints only, check the issued at and // end dates. @@ -446,11 +479,19 @@ func (f Feature) Compare(b Feature) int { return entitlementDifference } - // If the entitlement is the same, then we can compare the limits. + // If the entitlement is the same, then we can compare the limits. A nil + // limit on a usage period feature means unlimited, so it outranks any set + // limit; on other features a nil limit loses to a set one. if f.Limit == nil && b.Limit != nil { + if bothHaveUsagePeriod { + return 1 + } return -1 } if f.Limit != nil && b.Limit == nil { + if bothHaveUsagePeriod { + return -1 + } return 1 } if f.Limit != nil && b.Limit != nil { @@ -556,7 +597,7 @@ func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { return Entitlements{}, ReadBodyAsError(res) } var ent Entitlements - return ent, json.NewDecoder(res.Body).Decode(&ent) + return ent, ReadBodyAsJSON(res, &ent) } type PostgresAuth string @@ -612,6 +653,31 @@ var AIBudgetPeriods = []string{ string(AIBudgetPeriodMonth), } +// Adjective renders the period as the adjective used in user-facing text (e.g. "monthly"). +func (p AIBudgetPeriod) Adjective() string { + switch p { + case "day": + return "daily" + case "week": + return "weekly" + case AIBudgetPeriodMonth: + return "monthly" + case "year": + return "yearly" + default: + return string(p) + } +} + +// NewAIBudgetPeriodFromString converts s to an AIBudgetPeriod, falling back to +// AIBudgetPeriodMonth when s is empty or not a recognized period. +func NewAIBudgetPeriodFromString(s string) AIBudgetPeriod { + if slices.Contains(AIBudgetPeriods, s) { + return AIBudgetPeriod(s) + } + return AIBudgetPeriodMonth +} + // DeploymentValues is the central configuration values the coder server. type DeploymentValues struct { Verbose serpent.Bool `json:"verbose,omitempty"` @@ -670,7 +736,6 @@ type DeploymentValues struct { DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` DisableWorkspaceSharing serpent.Bool `json:"disable_workspace_sharing,omitempty" typescript:",notnull"` DisableChatSharing serpent.Bool `json:"disable_chat_sharing,omitempty" typescript:",notnull"` - DisableWorkspaceAgentContextSync serpent.Bool `json:"disable_workspace_agent_context_sync,omitempty" typescript:",notnull"` ProxyHealthStatusInterval serpent.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"` EnableTerraformDebugMode serpent.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"` UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"` @@ -684,7 +749,7 @@ type DeploymentValues struct { AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"` WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"` Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` - HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` + EnableAITasks serpent.Bool `json:"enable_ai_tasks,omitempty" typescript:",notnull"` AI AIConfig `json:"ai,omitempty"` StatsCollection StatsCollectionConfig `json:"stats_collection,omitempty" typescript:",notnull"` TemplateBuilder TemplateBuilderConfig `json:"template_builder,omitempty"` @@ -969,7 +1034,7 @@ type OIDCConfig struct { // RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche // situations where the OIDC callback domain is different from the ACCESS_URL - // domain. + // domain. The path component is ignored. RedirectURL serpent.URL `json:"redirect_url" typescript:",notnull"` AutoRepairLinks serpent.Bool `json:"auto_repair_links" typescript:",notnull"` @@ -980,6 +1045,15 @@ type OIDCConfig struct { // brokers that do not issue a stable `sub` for the same user across // connections. EmailFallback serpent.Bool `json:"email_fallback" typescript:",notnull"` + + // RedirectAllowedHosts is an allowlist of hostnames that may be used as + // the host of the OIDC redirect_uri. When non-empty, the redirect_uri is + // constructed from the incoming request's Host header (validated against + // this list) instead of from AccessURL. Every listed host must also be + // registered as a valid redirect URI in the OIDC provider. This setting + // is mutually exclusive with RedirectURL: if RedirectURL is set, this + // allowlist is ignored. + RedirectAllowedHosts serpent.StringArray `json:"redirect_allowed_hosts" typescript:",notnull"` } type TelemetryConfig struct { @@ -1114,10 +1188,14 @@ type ExternalAuthConfig struct { ClientSecret string `json:"-" yaml:"client_secret"` // ID is a unique identifier for the auth config. // It defaults to `type` when not provided. - ID string `json:"id" yaml:"id"` - AuthURL string `json:"auth_url" yaml:"auth_url"` - TokenURL string `json:"token_url" yaml:"token_url"` - ValidateURL string `json:"validate_url" yaml:"validate_url"` + ID string `json:"id" yaml:"id"` + AuthURL string `json:"auth_url" yaml:"auth_url"` + TokenURL string `json:"token_url" yaml:"token_url"` + ValidateURL string `json:"validate_url" yaml:"validate_url"` + // RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche + // situations where the OAuth callback domain is different from the ACCESS_URL + // domain. The path component is ignored. + RedirectURL string `json:"redirect_url" yaml:"redirect_url"` RevokeURL string `json:"revoke_url" yaml:"revoke_url"` AppInstallURL string `json:"app_install_url" yaml:"app_install_url"` AppInstallationsURL string `json:"app_installations_url" yaml:"app_installations_url"` @@ -1898,7 +1976,7 @@ communicating directly.`, } aiGatewayBedrockRegion := serpent.Option{ Name: "AI Gateway Bedrock Region", - Description: aiGatewayProviderSeedingDeprecated + "The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of 'https://bedrock-runtime..amazonaws.com'.", + Description: aiGatewayProviderSeedingDeprecated + "The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of `https://bedrock-runtime..amazonaws.com`.", Flag: "ai-gateway-bedrock-region", Env: "CODER_AI_GATEWAY_BEDROCK_REGION", Value: &c.AI.BridgeConfig.LegacyBedrock.Region, @@ -2114,6 +2192,16 @@ communicating directly.`, Group: &deploymentGroupAIGatewayProxy, YAML: "listen_addr", } + aiGatewayProxyTarget := serpent.Option{ + Name: "AI Gateway Proxy Target", + Description: "Base URL of the AI Gateway to forward intercepted requests to. Defaults to the embedded AI Gateway address at the Coder access URL plus /api/v2/ai-gateway.", + Flag: "ai-gateway-proxy-target", + Env: "CODER_AI_GATEWAY_PROXY_TARGET", + Value: &c.AI.BridgeProxyConfig.Target, + Default: "", + Group: &deploymentGroupAIGatewayProxy, + YAML: "target", + } aiGatewayProxyTLSCertFile := serpent.Option{ Name: "AI Gateway Proxy TLS Certificate File", Description: "Path to the TLS certificate file for the AI Gateway Proxy listener. Must be set together with AI Gateway Proxy TLS Key File.", @@ -2659,7 +2747,7 @@ communicating directly.`, }, { Name: "OAuth2 GitHub Allowed Teams", - Description: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: /.", + Description: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: `/`.", Flag: "oauth2-github-allowed-teams", Env: "CODER_OAUTH2_GITHUB_ALLOWED_TEAMS", Value: &c.OAuth2.Github.AllowedTeams, @@ -3021,9 +3109,6 @@ communicating directly.`, Value: &c.OIDC.RedirectURL, Group: &deploymentGroupOIDC, UseInstead: nil, - // In most deployments, this setting can only complicate and break OIDC. - // So hide it, and only surface it to the small number of users that need it. - Hidden: true, }, { Name: "OIDC Auto Repair Links", @@ -3056,6 +3141,21 @@ communicating directly.`, Group: &deploymentGroupOIDC, Hidden: true, }, + { + Name: "OIDC Redirect Allowed Hosts", + Description: "An allowlist of hostnames that may be used as the host of the OIDC redirect_uri. " + + "When set, the redirect_uri sent to the OIDC provider is built from the incoming request's Host header " + + "(validated against this list) instead of from access-url. Every listed host must also be registered " + + "as a valid redirect URI in the OIDC provider. Ignored when oidc-redirect-url is set.", + Flag: "oidc-redirect-allowed-hosts", + Env: "CODER_OIDC_REDIRECT_ALLOWED_HOSTS", + YAML: "oidcRedirectAllowedHosts", + Default: "", + Value: &c.OIDC.RedirectAllowedHosts, + Group: &deploymentGroupOIDC, + // Niche feature for multi-domain deployments. Surface only to operators who need it. + Hidden: true, + }, // Telemetry settings telemetryEnable, { @@ -3681,15 +3781,6 @@ communicating directly.`, Value: &c.DisableChatSharing, YAML: "disableChatSharing", }, - { - Name: "Disable Workspace Agent Context Sync", - Description: "Stop persisting workspace agent context snapshots (instructions, skills, and MCP state used for pinned chat context). When set, coderd rejects agent context pushes as unimplemented and agents stop sending them; chats cannot pin workspace context. Use this to shed the database write load of context sync on large deployments.", - Flag: "disable-workspace-agent-context-sync", - Env: "CODER_DISABLE_WORKSPACE_AGENT_CONTEXT_SYNC", - - Value: &c.DisableWorkspaceAgentContextSync, - YAML: "disableWorkspaceAgentContextSync", - }, { Name: "Session Duration", Description: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.", @@ -4229,14 +4320,17 @@ Write out the current server config as YAML to stdout.`, Hidden: true, }, { - Name: "Hide AI Tasks", - Description: "Hide AI tasks from the dashboard.", - Flag: "hide-ai-tasks", - Env: "CODER_HIDE_AI_TASKS", + Name: "Enable AI Tasks", + Description: "Enable Coder Tasks. When unset, the Tasks routes are not served, the Tasks UI and its URLs are unavailable, the task RBAC permissions are stripped from built-in roles, and the CLI task commands are hidden.", + Flag: "enable-ai-tasks", + Env: "CODER_ENABLE_AI_TASKS", Default: "false", - Value: &c.HideAITasks, - Group: &deploymentGroupClient, - YAML: "hideAITasks", + Value: &c.EnableAITasks, + YAML: "enableAITasks", + // Hidden keeps Tasks out of the generated CLI and configuration + // reference documentation while the feature is withdrawn from the + // product. + Hidden: true, }, // Chat Options { @@ -4260,9 +4354,65 @@ Write out the current server config as YAML to stdout.`, Group: &deploymentGroupChat, YAML: "debugLoggingEnabled", }, + { + Name: "Chat: Hook URL", + Description: "HTTPS URL to receive chat agent lifecycle hook events (plain HTTP requires --chat-hook-allow-insecure). Hooks are disabled when unset. Requires the agent-lifecycle-hooks experiment.", + Flag: "chat-hook-url", + Hidden: true, + Env: "CODER_CHAT_HOOK_URL", + Value: &c.AI.Chat.HookURL, + Default: "", + Group: &deploymentGroupChat, + YAML: "hookURL", + }, + { + Name: "Chat: Hook Secret", + Description: "Shared secret used to sign chat agent lifecycle hook JWTs.", + Flag: "chat-hook-secret", + Hidden: true, + Env: "CODER_CHAT_HOOK_SECRET", + Value: &c.AI.Chat.HookSecret, + Default: "", + Group: &deploymentGroupChat, + Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"), + }, + { + Name: "Chat: Hook Timeout", + Description: "Maximum time to wait for a chat agent lifecycle hook response.", + Flag: "chat-hook-timeout", + Hidden: true, + Env: "CODER_CHAT_HOOK_TIMEOUT", + Value: &c.AI.Chat.HookTimeout, + Default: (1500 * time.Millisecond).String(), + Group: &deploymentGroupChat, + YAML: "hookTimeout", + Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"), + }, + { + Name: "Chat: Hook Enabled", + Description: "Whether to dispatch chat agent lifecycle hooks when a hook URL is configured. Requires the agent-lifecycle-hooks experiment.", + Flag: "chat-hook-enabled", + Hidden: true, + Env: "CODER_CHAT_HOOK_ENABLED", + Value: &c.AI.Chat.HookEnabled, + Default: "true", + Group: &deploymentGroupChat, + YAML: "hookEnabled", + }, + { + Name: "Chat: Hook Allow Insecure", + Description: "Allow the chat hook URL to use plain HTTP for any host. Plain HTTP exposes sensitive chat data and lets an on-path attacker forge hook responses that control agent execution, so only enable this on a network you fully trust.", + Flag: "chat-hook-allow-insecure", + Hidden: true, + Env: "CODER_CHAT_HOOK_ALLOW_INSECURE", + Value: &c.AI.Chat.HookAllowInsecure, + Default: "false", + Group: &deploymentGroupChat, + YAML: "hookAllowInsecure", + }, { Name: "Chat: AI Gateway Routing Enabled", - Description: "Route chat model requests through AI Gateway when both chat routing and AI Gateway are enabled. Otherwise, chat calls AI providers directly. Pending chats without API key metadata may need a retry or temporary direct routing.", + Description: "Deprecated: AI Gateway routing is now the only routing path. Setting this value has no effect. This option will be removed in a future release.", Flag: "chat-ai-gateway-routing-enabled", Env: "CODER_CHAT_AI_GATEWAY_ROUTING_ENABLED", Value: &c.AI.Chat.AIGatewayRoutingEnabled, @@ -4354,7 +4504,7 @@ Write out the current server config as YAML to stdout.`, { Name: "AI Bridge Bedrock Region", Description: "Deprecated: use --ai-gateway-bedrock-region or CODER_AI_GATEWAY_BEDROCK_REGION instead. The AWS Bedrock API region to use. Constructs a base URL to use for the AWS Bedrock API in the form of " + - "'https://bedrock-runtime..amazonaws.com'.", + "`https://bedrock-runtime..amazonaws.com`.", Flag: "aibridge-bedrock-region", Env: "CODER_AIBRIDGE_BEDROCK_REGION", Value: &c.AI.BridgeConfig.LegacyBedrock.Region, @@ -4627,6 +4777,7 @@ Write out the current server config as YAML to stdout.`, UseInstead: serpent.OptionSet{aiGatewayProxyListenAddr}, }, aiGatewayProxyListenAddr, + aiGatewayProxyTarget, { Name: "AI Bridge Proxy TLS Certificate File", Description: "Deprecated: use --ai-gateway-proxy-tls-cert-file or CODER_AI_GATEWAY_PROXY_TLS_CERT_FILE instead. Path to the TLS certificate file for the AI Bridge Proxy listener. Must be set together with AI Bridge Proxy TLS Key File.", @@ -4840,13 +4991,13 @@ Write out the current server config as YAML to stdout.`, type AIBridgeConfig struct { Enabled serpent.Bool `json:"enabled" typescript:",notnull"` - // Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER__* env vars instead. + // Deprecated: Use Providers with indexed `CODER_AI_GATEWAY_PROVIDER__*` env vars instead. LegacyOpenAI AIBridgeOpenAIConfig `json:"openai" typescript:",notnull"` - // Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER__* env vars instead. + // Deprecated: Use Providers with indexed `CODER_AI_GATEWAY_PROVIDER__*` env vars instead. LegacyAnthropic AIBridgeAnthropicConfig `json:"anthropic" typescript:",notnull"` - // Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER__* env vars instead. + // Deprecated: Use Providers with indexed `CODER_AI_GATEWAY_PROVIDER__*` env vars instead. LegacyBedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"` - // Providers holds provider instances populated from CODER_AI_GATEWAY_PROVIDER__ + // Providers holds provider instances populated from `CODER_AI_GATEWAY_PROVIDER__` // env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above. Providers []AIProviderConfig `json:"providers,omitempty"` // Deprecated: Injected MCP in AI Bridge is deprecated and will be removed in a future release. @@ -4928,6 +5079,7 @@ type AIProviderConfig struct { type AIBridgeProxyConfig struct { Enabled serpent.Bool `json:"enabled" typescript:",notnull"` ListenAddr serpent.String `json:"listen_addr" typescript:",notnull"` + Target serpent.String `json:"target" typescript:",notnull"` TLSCertFile serpent.String `json:"tls_cert_file" typescript:",notnull"` TLSKeyFile serpent.String `json:"tls_key_file" typescript:",notnull"` MITMCertFile serpent.String `json:"cert_file" typescript:",notnull"` @@ -4940,9 +5092,16 @@ type AIBridgeProxyConfig struct { } type ChatConfig struct { - AcquireBatchSize serpent.Int64 `json:"acquire_batch_size" typescript:",notnull"` - DebugLoggingEnabled serpent.Bool `json:"debug_logging_enabled" typescript:",notnull"` - AIGatewayRoutingEnabled serpent.Bool `json:"ai_gateway_routing_enabled" typescript:",notnull" swaggerignore:"true"` + AcquireBatchSize serpent.Int64 `json:"acquire_batch_size" typescript:",notnull"` + DebugLoggingEnabled serpent.Bool `json:"debug_logging_enabled" typescript:",notnull"` + HookURL serpent.URL `json:"hook_url" typescript:",notnull"` + HookSecret serpent.String `json:"hook_secret" typescript:",notnull"` + HookTimeout serpent.Duration `json:"hook_timeout" typescript:",notnull"` + HookEnabled serpent.Bool `json:"hook_enabled" typescript:",notnull"` + HookAllowInsecure serpent.Bool `json:"hook_allow_insecure" typescript:",notnull"` + // Deprecated: AI Gateway routing is now the only routing path. Setting this + // value has no effect. This option will be removed in a future release. + AIGatewayRoutingEnabled serpent.Bool `json:"ai_gateway_routing_enabled" typescript:",notnull" swaggerignore:"true"` } type AIConfig struct { @@ -4989,6 +5148,45 @@ func (c *DeploymentValues) Validate() error { refresh, access, ) } + + // Disabled hooks must not validate inert settings. + if c.AI.Chat.HookEnabled.Value() { + if c.AI.Chat.HookURL.String() != "" { + hookURL := c.AI.Chat.HookURL.Value() + allowInsecure := c.AI.Chat.HookAllowInsecure.Value() + switch { + case hookURL.Scheme == "https": + case hookURL.Scheme == "http" && allowInsecure: + default: + return xerrors.New("chat hook URL must use HTTPS; set --chat-hook-url to an HTTPS URL, or set --chat-hook-allow-insecure to allow plain HTTP") + } + // Hostname() instead of Host: a URL like http://:8080/hooks has a + // non-empty Host (":8080") but no hostname, and the dispatcher + // rejects it on every dispatch. + if hookURL.Hostname() == "" { + return xerrors.New("chat hook URL must include a host; set --chat-hook-url to a complete URL") + } + // The configured string is signed verbatim as the JWT audience, + // and neither component is ever transmitted, so a consumer + // configured with the URL it actually serves would never match. + if hookURL.Fragment != "" || hookURL.RawFragment != "" || hookURL.User != nil { + return xerrors.New("chat hook URL must not contain a fragment or userinfo; set --chat-hook-url to a URL without a fragment or userinfo") + } + if c.AI.Chat.HookSecret.Value() == "" { + return xerrors.New("chat hook secret is required when chat hook URL is set; set --chat-hook-secret") + } + // The hook SDK rejects HS256 secrets shorter than 32 bytes. + if len(c.AI.Chat.HookSecret.Value()) < 32 { + return xerrors.New("chat hook secret must be at least 32 bytes of cryptographically random data; set --chat-hook-secret to a longer value") + } + + hookTimeout := c.AI.Chat.HookTimeout.Value() + if hookTimeout <= 0 || hookTimeout > 5*time.Second { + return xerrors.Errorf("chat hook timeout (%s) must be greater than zero and no more than 5s; set --chat-hook-timeout to a valid duration", hookTimeout) + } + } + } + return nil } @@ -5056,7 +5254,7 @@ func (c *Client) DeploymentConfig(ctx context.Context) (*DeploymentConfig, error Values: conf, Options: conf.Options(), } - return resp, json.NewDecoder(res.Body).Decode(resp) + return resp, ReadBodyAsJSON(res, resp) } func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) { @@ -5071,7 +5269,7 @@ func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) { } var df DeploymentStats - return df, json.NewDecoder(res.Body).Decode(&df) + return df, ReadBodyAsJSON(res, &df) } type AppearanceConfig struct { @@ -5082,6 +5280,7 @@ type AppearanceConfig struct { ServiceBanner BannerConfig `json:"service_banner"` AnnouncementBanners []BannerConfig `json:"announcement_banners"` SupportLinks []LinkConfig `json:"support_links,omitempty"` + CodernautsEnabled bool `json:"codernauts_enabled"` } type UpdateAppearanceConfig struct { @@ -5090,6 +5289,7 @@ type UpdateAppearanceConfig struct { // Deprecated: ServiceBanner has been replaced by AnnouncementBanners. ServiceBanner BannerConfig `json:"service_banner"` AnnouncementBanners []BannerConfig `json:"announcement_banners"` + CodernautsEnabled bool `json:"codernauts_enabled"` } // Deprecated: ServiceBannerConfig has been renamed to BannerConfig. @@ -5113,7 +5313,7 @@ func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) { return AppearanceConfig{}, ReadBodyAsError(res) } var cfg AppearanceConfig - return cfg, json.NewDecoder(res.Body).Decode(&cfg) + return cfg, ReadBodyAsJSON(res, &cfg) } func (c *Client) UpdateAppearance(ctx context.Context, appearance UpdateAppearanceConfig) error { @@ -5190,25 +5390,27 @@ func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { } var buildInfo BuildInfoResponse - return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo) + return buildInfo, ReadBodyAsJSON(res, &buildInfo) } type Experiment string const ( // Add new experiments here! - ExperimentExample Experiment = "example" // This isn't used for anything. - ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature. - ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. - ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. - ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality. - ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality. - ExperimentWorkspaceBuildUpdates Experiment = "workspace-build-updates" // Enables publishing workspace build updates to the all builds pubsub channel. - ExperimentNATSPubsub Experiment = "nats_pubsub" // Enables embedded NATS pubsub. - ExperimentMinimumImplicitMember Experiment = "minimum-implicit-member" // Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts. - ExperimentAIGatewayCostControl Experiment = "ai-gateway-cost-control" // Enables AI Gateway cost control functionality. - ExperimentChatAdvisor Experiment = "chat-advisor" // Enables the advisor tool for root agent chats. - ExperimentChatVirtualDesktop Experiment = "chat-virtual-desktop" // Enables virtual desktop and computer use provider for agents. + ExperimentExample Experiment = "example" // This isn't used for anything. + ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature. + ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. + ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. + ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality. + ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality. + ExperimentMCPToolSearch Experiment = "mcp-tool-search" // Defers MCP tool schemas behind a searchable catalog in agent chats. + ExperimentWorkspaceBuildUpdates Experiment = "workspace-build-updates" // Enables publishing workspace build updates to the all builds pubsub channel. + ExperimentNATSPubsub Experiment = "nats_pubsub" // Enables embedded NATS pubsub. + ExperimentWorkspaceCapableLicensing Experiment = "workspace-capable-licensing" // Counts only users holding the workspace-create permission toward the license seat limit. + ExperimentAIGatewaySeatExclusion Experiment = "ai-gateway-seat-exclusion" // Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption. + ExperimentChatAdvisor Experiment = "chat-advisor" // Enables the advisor tool for root agent chats. + ExperimentChatVirtualDesktop Experiment = "chat-virtual-desktop" // Enables virtual desktop and computer use provider for agents. + ExperimentAgentLifecycleHooks Experiment = "agent-lifecycle-hooks" // Enables chat lifecycle hook webhooks for agent chats. ) func (e Experiment) DisplayName() string { @@ -5229,14 +5431,16 @@ func (e Experiment) DisplayName() string { return "Workspace Build Updates Channel" case ExperimentNATSPubsub: return "NATS Pubsub" - case ExperimentMinimumImplicitMember: - return "Gateway Accounts (minimum implicit member)" - case ExperimentAIGatewayCostControl: - return "AI Gateway Cost Control" + case ExperimentWorkspaceCapableLicensing: + return "Workspace-Capable Licensing" + case ExperimentAIGatewaySeatExclusion: + return "AI Gateway Seat Exclusion" case ExperimentChatAdvisor: return "Chat Advisor" case ExperimentChatVirtualDesktop: return "Chat Virtual Desktop" + case ExperimentAgentLifecycleHooks: + return "Agent Lifecycle Hooks" default: // Split on hyphen and convert to title case // e.g. "mcp-server-http" -> "Mcp Server Http" @@ -5253,21 +5457,21 @@ var ExperimentsKnown = Experiments{ ExperimentWorkspaceUsage, ExperimentOAuth2, ExperimentMCPServerHTTP, + ExperimentMCPToolSearch, ExperimentNATSPubsub, ExperimentWorkspaceBuildUpdates, - ExperimentMinimumImplicitMember, - ExperimentAIGatewayCostControl, + ExperimentWorkspaceCapableLicensing, + ExperimentAIGatewaySeatExclusion, ExperimentChatAdvisor, ExperimentChatVirtualDesktop, + ExperimentAgentLifecycleHooks, } // ExperimentsSafe should include all experiments that are safe for // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here and will be essentially hidden. -var ExperimentsSafe = Experiments{ - ExperimentMinimumImplicitMember, -} +var ExperimentsSafe = Experiments{} // Experiments is a list of experiments. // Multiple experiments may be enabled at the same time. @@ -5293,7 +5497,7 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { return nil, ReadBodyAsError(res) } var exp []Experiment - return exp, json.NewDecoder(res.Body).Decode(&exp) + return exp, ReadBodyAsJSON(res, &exp) } // AvailableExperiments is an expandable type that returns all safe experiments @@ -5312,7 +5516,7 @@ func (c *Client) SafeExperiments(ctx context.Context) (AvailableExperiments, err return AvailableExperiments{}, ReadBodyAsError(res) } var exp AvailableExperiments - return exp, json.NewDecoder(res.Body).Decode(&exp) + return exp, ReadBodyAsJSON(res, &exp) } type DAUsResponse struct { @@ -5377,7 +5581,7 @@ func (c *Client) DeploymentDAUs(ctx context.Context, tzOffset int) (*DAUsRespons } var resp DAUsResponse - return &resp, json.NewDecoder(res.Body).Decode(&resp) + return &resp, ReadBodyAsJSON(res, &resp) } type AppHostResponse struct { @@ -5403,7 +5607,7 @@ func (c *Client) AppHost(ctx context.Context) (AppHostResponse, error) { } var host AppHostResponse - return host, json.NewDecoder(res.Body).Decode(&host) + return host, ReadBodyAsJSON(res, &host) } type WorkspaceConnectionLatencyMS struct { @@ -5485,7 +5689,7 @@ func (c *Client) SSHConfiguration(ctx context.Context) (SSHConfigResponse, error } var sshConfig SSHConfigResponse - return sshConfig, json.NewDecoder(res.Body).Decode(&sshConfig) + return sshConfig, ReadBodyAsJSON(res, &sshConfig) } type CryptoKeyFeature string @@ -5495,7 +5699,14 @@ const ( //nolint:gosec // This denotes a type of key, not a literal. CryptoKeyFeatureWorkspaceAppsToken CryptoKeyFeature = "workspace_apps_token" CryptoKeyFeatureOIDCConvert CryptoKeyFeature = "oidc_convert" + CryptoKeyFeatureChatFilesToken CryptoKeyFeature = "chat_files_token" CryptoKeyFeatureTailnetResume CryptoKeyFeature = "tailnet_resume" + // CryptoKeyFeatureNATSCA is the CA that signs NATS cluster mTLS leaf + // certificates. Its secret is a PEM cert+key bundle (not a hex secret like + // the other features) and contains a private key, so it must never be + // served over the API. It is deliberately excluded from + // whitelistedCryptoKeyFeatures in enterprise/coderd/workspaceproxy.go. + CryptoKeyFeatureNATSCA CryptoKeyFeature = "nats_ca" ) type CryptoKey struct { diff --git a/vendor/github.com/coder/coder/v2/codersdk/externalauth.go b/vendor/github.com/coder/coder/v2/codersdk/externalauth.go index 70c0060a7..35863be25 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/externalauth.go +++ b/vendor/github.com/coder/coder/v2/codersdk/externalauth.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -143,7 +142,7 @@ func (c *Client) ExternalAuthDeviceByID(ctx context.Context, provider string) (E return ExternalAuthDevice{}, ReadBodyAsError(res) } var extAuth ExternalAuthDevice - return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) + return extAuth, ReadBodyAsJSON(res, &extAuth) } // ExchangeGitAuth exchanges a device code for an external auth token. @@ -170,7 +169,7 @@ func (c *Client) ExternalAuthByID(ctx context.Context, provider string) (Externa return ExternalAuth{}, ReadBodyAsError(res) } var extAuth ExternalAuth - return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) + return extAuth, ReadBodyAsJSON(res, &extAuth) } // UnlinkExternalAuthByID deletes the external auth for the given provider by ID @@ -186,7 +185,7 @@ func (c *Client) UnlinkExternalAuthByID(ctx context.Context, provider string) (D return noRevoke, ReadBodyAsError(res) } var resp DeleteExternalAuthByIDResponse - err = json.NewDecoder(res.Body).Decode(&resp) + err = ReadBodyAsJSON(res, &resp) if err != nil { return noRevoke, err } @@ -205,5 +204,5 @@ func (c *Client) ListExternalAuths(ctx context.Context) (ListUserExternalAuthRes return ListUserExternalAuthResponse{}, ReadBodyAsError(res) } var extAuth ListUserExternalAuthResponse - return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) + return extAuth, ReadBodyAsJSON(res, &extAuth) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/files.go b/vendor/github.com/coder/coder/v2/codersdk/files.go index a14f2ca73..4baebad2c 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/files.go +++ b/vendor/github.com/coder/coder/v2/codersdk/files.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -36,7 +35,7 @@ func (c *Client) Upload(ctx context.Context, contentType string, rd io.Reader) ( return UploadResponse{}, ReadBodyAsError(res) } var resp UploadResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // Download fetches a file by uploaded hash. diff --git a/vendor/github.com/coder/coder/v2/codersdk/gitsshkey.go b/vendor/github.com/coder/coder/v2/codersdk/gitsshkey.go index d1b657746..eee8205e0 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/gitsshkey.go +++ b/vendor/github.com/coder/coder/v2/codersdk/gitsshkey.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -34,7 +33,7 @@ func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) } var gitsshkey GitSSHKey - return gitsshkey, json.NewDecoder(res.Body).Decode(&gitsshkey) + return gitsshkey, ReadBodyAsJSON(res, &gitsshkey) } // RegenerateGitSSHKey will create a new SSH key pair for the user and return it. @@ -50,5 +49,5 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKe } var gitsshkey GitSSHKey - return gitsshkey, json.NewDecoder(res.Body).Decode(&gitsshkey) + return gitsshkey, ReadBodyAsJSON(res, &gitsshkey) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/groups.go b/vendor/github.com/coder/coder/v2/codersdk/groups.go index a191b280e..11fac75f5 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/groups.go +++ b/vendor/github.com/coder/coder/v2/codersdk/groups.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "net/url" @@ -48,6 +47,49 @@ type GroupMembersResponse struct { Count int `json:"count"` } +type PaginatedGroupsResponse struct { + Groups []PaginatedGroup `json:"groups"` + Count int `json:"count"` +} + +// PaginatedGroup is a group summary returned by the paginated groups endpoint. +// It deliberately omits the member roster (which the endpoint does not return) +// and exposes only the total member count. Fetch the roster via the group +// members endpoint. +type PaginatedGroup struct { + ID uuid.UUID `json:"id" format:"uuid"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + // TotalMemberCount is the number of members in the group, shown even when + // the caller cannot read individual members. The roster itself is not + // returned by this endpoint. + TotalMemberCount int `json:"total_member_count"` + AvatarURL string `json:"avatar_url" format:"uri"` + QuotaAllowance int `json:"quota_allowance"` + Source GroupSource `json:"source"` + OrganizationName string `json:"organization_name"` + OrganizationDisplayName string `json:"organization_display_name"` +} + +// PaginatedGroupsRequest are the filters for a paginated groups request. +// Groups only support free-text search, so unlike UsersRequest it exposes no +// key:value filters that the endpoint would reject. +type PaginatedGroupsRequest struct { + SearchQuery string `json:"q,omitempty"` + Pagination +} + +func (req PaginatedGroupsRequest) asRequestOption() RequestOption { + return func(r *http.Request) { + q := r.URL.Query() + if req.SearchQuery != "" { + q.Set("q", req.SearchQuery) + } + r.URL.RawQuery = q.Encode() + } +} + func (g Group) IsEveryone() bool { return g.ID == g.OrganizationID } @@ -66,7 +108,7 @@ func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGro return Group{}, ReadBodyAsError(res) } var resp Group - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GroupsByOrganization @@ -115,7 +157,7 @@ func (c *Client) Groups(ctx context.Context, args GroupArguments) ([]Group, erro } var groups []Group - return groups, json.NewDecoder(res.Body).Decode(&groups) + return groups, ReadBodyAsJSON(res, &groups) } func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name string) (Group, error) { @@ -132,7 +174,32 @@ func (c *Client) GroupByOrgAndName(ctx context.Context, orgID uuid.UUID, name st return Group{}, ReadBodyAsError(res) } var resp Group - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) +} + +// OrganizationGroupsPaginated lists filtered and paginated groups in an +// organization. Unlike Groups (GET /groups), which authorizes each group +// individually via its ACL, this endpoint requires organization-wide group +// read permission and does no per-group filtering. It is therefore not a +// drop-in replacement for Groups: callers without org-wide group read will +// receive an error rather than a filtered subset. +func (c *Client) OrganizationGroupsPaginated(ctx context.Context, orgID uuid.UUID, req PaginatedGroupsRequest) (PaginatedGroupsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/paginated-groups", orgID.String()), + nil, + req.Pagination.asRequestOption(), + req.asRequestOption(), + ) + if err != nil { + return PaginatedGroupsResponse{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return PaginatedGroupsResponse{}, ReadBodyAsError(res) + } + var resp PaginatedGroupsResponse + return resp, ReadBodyAsJSON(res, &resp) } type GroupRequest struct { @@ -164,7 +231,7 @@ func (c *Client) Group(ctx context.Context, group uuid.UUID, req GroupRequest) ( return Group{}, ReadBodyAsError(res) } var resp Group - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) GroupMembers(ctx context.Context, group uuid.UUID, req UsersRequest) (GroupMembersResponse, error) { @@ -183,7 +250,7 @@ func (c *Client) GroupMembers(ctx context.Context, group uuid.UUID, req UsersReq return GroupMembersResponse{}, ReadBodyAsError(res) } var resp GroupMembersResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type PatchGroupRequest struct { @@ -209,7 +276,7 @@ func (c *Client) PatchGroup(ctx context.Context, group uuid.UUID, req PatchGroup return Group{}, ReadBodyAsError(res) } var resp Group - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) DeleteGroup(ctx context.Context, group uuid.UUID) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/idpsync.go b/vendor/github.com/coder/coder/v2/codersdk/idpsync.go index 8f92cea68..51c825e0e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/idpsync.go +++ b/vendor/github.com/coder/coder/v2/codersdk/idpsync.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "net/url" @@ -51,7 +50,7 @@ func (c *Client) GroupIDPSyncSettings(ctx context.Context, orgID string) (GroupS return GroupSyncSettings{}, ReadBodyAsError(res) } var resp GroupSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, req GroupSyncSettings) (GroupSyncSettings, error) { @@ -65,7 +64,7 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re return GroupSyncSettings{}, ReadBodyAsError(res) } var resp GroupSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type PatchGroupIDPSyncConfigRequest struct { @@ -85,7 +84,7 @@ func (c *Client) PatchGroupIDPSyncConfig(ctx context.Context, orgID string, req return GroupSyncSettings{}, ReadBodyAsError(res) } var resp GroupSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // If the same mapping is present in both Add and Remove, Remove will take presidence. @@ -105,7 +104,7 @@ func (c *Client) PatchGroupIDPSyncMapping(ctx context.Context, orgID string, req return GroupSyncSettings{}, ReadBodyAsError(res) } var resp GroupSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type RoleSyncSettings struct { @@ -127,7 +126,7 @@ func (c *Client) RoleIDPSyncSettings(ctx context.Context, orgID string) (RoleSyn return RoleSyncSettings{}, ReadBodyAsError(res) } var resp RoleSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req RoleSyncSettings) (RoleSyncSettings, error) { @@ -141,7 +140,7 @@ func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req return RoleSyncSettings{}, ReadBodyAsError(res) } var resp RoleSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type PatchRoleIDPSyncConfigRequest struct { @@ -159,7 +158,7 @@ func (c *Client) PatchRoleIDPSyncConfig(ctx context.Context, orgID string, req P return RoleSyncSettings{}, ReadBodyAsError(res) } var resp RoleSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // If the same mapping is present in both Add and Remove, Remove will take presidence. @@ -179,7 +178,7 @@ func (c *Client) PatchRoleIDPSyncMapping(ctx context.Context, orgID string, req return RoleSyncSettings{}, ReadBodyAsError(res) } var resp RoleSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type OrganizationSyncSettings struct { @@ -205,7 +204,7 @@ func (c *Client) OrganizationIDPSyncSettings(ctx context.Context) (OrganizationS return OrganizationSyncSettings{}, ReadBodyAsError(res) } var resp OrganizationSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req OrganizationSyncSettings) (OrganizationSyncSettings, error) { @@ -219,7 +218,7 @@ func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req Organ return OrganizationSyncSettings{}, ReadBodyAsError(res) } var resp OrganizationSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type PatchOrganizationIDPSyncConfigRequest struct { @@ -238,7 +237,7 @@ func (c *Client) PatchOrganizationIDPSyncConfig(ctx context.Context, req PatchOr return OrganizationSyncSettings{}, ReadBodyAsError(res) } var resp OrganizationSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // If the same mapping is present in both Add and Remove, Remove will take presidence. @@ -258,7 +257,7 @@ func (c *Client) PatchOrganizationIDPSyncMapping(ctx context.Context, req PatchO return OrganizationSyncSettings{}, ReadBodyAsError(res) } var resp OrganizationSyncSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) GetAvailableIDPSyncFields(ctx context.Context) ([]string, error) { @@ -272,7 +271,7 @@ func (c *Client) GetAvailableIDPSyncFields(ctx context.Context) ([]string, error return nil, ReadBodyAsError(res) } var resp []string - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) GetOrganizationAvailableIDPSyncFields(ctx context.Context, orgID string) ([]string, error) { @@ -286,7 +285,7 @@ func (c *Client) GetOrganizationAvailableIDPSyncFields(ctx context.Context, orgI return nil, ReadBodyAsError(res) } var resp []string - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) GetIDPSyncFieldValues(ctx context.Context, claimField string) ([]string, error) { @@ -302,7 +301,7 @@ func (c *Client) GetIDPSyncFieldValues(ctx context.Context, claimField string) ( return nil, ReadBodyAsError(res) } var resp []string - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) GetOrganizationIDPSyncFieldValues(ctx context.Context, orgID string, claimField string) ([]string, error) { @@ -318,5 +317,5 @@ func (c *Client) GetOrganizationIDPSyncFieldValues(ctx context.Context, orgID st return nil, ReadBodyAsError(res) } var resp []string - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/inboxnotification.go b/vendor/github.com/coder/coder/v2/codersdk/inboxnotification.go index 1501f701f..e998658a6 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/inboxnotification.go +++ b/vendor/github.com/coder/coder/v2/codersdk/inboxnotification.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -86,7 +85,7 @@ func (c *Client) ListInboxNotifications(ctx context.Context, req ListInboxNotifi } var listInboxNotificationsResponse ListInboxNotificationsResponse - return listInboxNotificationsResponse, json.NewDecoder(res.Body).Decode(&listInboxNotificationsResponse) + return listInboxNotificationsResponse, ReadBodyAsJSON(res, &listInboxNotificationsResponse) } type UpdateInboxNotificationReadStatusRequest struct { @@ -114,7 +113,7 @@ func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID } var resp UpdateInboxNotificationReadStatusResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/insights.go b/vendor/github.com/coder/coder/v2/codersdk/insights.go index e757f28d1..730a72ccf 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/insights.go +++ b/vendor/github.com/coder/coder/v2/codersdk/insights.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "net/url" @@ -131,7 +130,7 @@ func (c *Client) UserLatencyInsights(ctx context.Context, req UserLatencyInsight return UserLatencyInsightsResponse{}, ReadBodyAsError(resp) } var result UserLatencyInsightsResponse - return result, json.NewDecoder(resp.Body).Decode(&result) + return result, ReadBodyAsJSON(resp, &result) } type UserActivityInsightsRequest struct { @@ -163,7 +162,7 @@ func (c *Client) UserActivityInsights(ctx context.Context, req UserActivityInsig return UserActivityInsightsResponse{}, ReadBodyAsError(resp) } var result UserActivityInsightsResponse - return result, json.NewDecoder(resp.Body).Decode(&result) + return result, ReadBodyAsJSON(resp, &result) } // TemplateInsightsResponse is the response from the template insights endpoint. @@ -281,7 +280,7 @@ func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsReque return TemplateInsightsResponse{}, ReadBodyAsError(resp) } var result TemplateInsightsResponse - return result, json.NewDecoder(resp.Body).Decode(&result) + return result, ReadBodyAsJSON(resp, &result) } type GetUserStatusCountsResponse struct { @@ -318,5 +317,5 @@ func (c *Client) GetUserStatusCounts(ctx context.Context, req GetUserStatusCount return GetUserStatusCountsResponse{}, ReadBodyAsError(resp) } var result GetUserStatusCountsResponse - return result, json.NewDecoder(resp.Body).Decode(&result) + return result, ReadBodyAsJSON(resp, &result) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/licenses.go b/vendor/github.com/coder/coder/v2/codersdk/licenses.go index a5f2853b8..6c160e45f 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/licenses.go +++ b/vendor/github.com/coder/coder/v2/codersdk/licenses.go @@ -17,12 +17,38 @@ const ( LicenseManagedAgentLimitExceededWarningText = "You have built more workspaces with managed agents than your license allows." LicenseAIGovernance90PercentWarningText = "You have used %d%% of your AI Governance add-on seats." LicenseAIGovernanceOverLimitWarningText = "Your organization is using %d of %d AI Governance add-on seats (%d over the limit)." + // The dashboard's LicenseBanner matches this text's pre-placeholder + // prefix to render it muted and without a sales link, so the license + // warning texts must stay pairwise distinct before their first + // placeholder. See TestLicenseAgentRuntimeHoursWarningTexts. + LicenseAgentRuntimeHoursSoftLimitWarningText = "Your deployment is approaching its Coder Agent runtime hours allocation: %d of the %d hours included in the current license term are used, at or above the advisory soft limit of %d hours." + LicenseAgentRuntimeHoursAllocationReachedWarningText = "Your deployment has used %d of the %d Coder Agent runtime hours included in the current license term." + LicenseAgentRuntimeUsageUnavailableErrorText = "Unable to determine Coder Agent runtime usage. Reported runtime hours are unavailable until the next successful refresh; workspaces are unaffected. Check the coderd logs for details." + LicenseAgentRuntimeHoursClaimsIgnoredWarningText = "A license contains unusable Coder Agent runtime hour claims, which were ignored. The rest of that license is unaffected. Check the coderd logs for the affected license and claims, and contact support to have the license re-issued." ) type AddLicenseRequest struct { License string `json:"license" validate:"required"` } +// Defines the input payload for requesting a trial license. +type CreateTrialLicenseRequest struct { + Email string `json:"email" validate:"required,email,max=254" example:"jane.doe@example.com" format:"email" maxLength:"254"` + FirstName string `json:"first_name" validate:"required,min=1,max=60" example:"Jane" minLength:"1" maxLength:"60"` + LastName string `json:"last_name" validate:"required,min=1,max=60" example:"Doe" minLength:"1" maxLength:"60"` + PhoneNumber string `json:"phone_number" validate:"required,min=7,max=20" example:"+14155552671" minLength:"7" maxLength:"20"` + JobTitle string `json:"job_title" validate:"required,min=2,max=100" example:"Engineering Manager" minLength:"2" maxLength:"100"` + CompanyName string `json:"company_name" validate:"required,min=2,max=100" example:"Acme Corp" minLength:"2" maxLength:"100"` + Country string `json:"country" validate:"required" example:"United States"` + Developers string `json:"developers" validate:"required"` + // Source is the premium paywall the request came from, for telemetry. It + // is not forwarded to the licensor. Omit it to report "direct". + Source PremiumFunnelSource `json:"source,omitempty"` + // AttributionID is the ID of the cta_click funnel event that led here, so + // that a signup can be joined back to the paywall that produced it. + AttributionID uuid.UUID `json:"attribution_id,omitempty" format:"uuid"` +} + type License struct { ID int32 `json:"id"` UUID uuid.UUID `json:"uuid" format:"uuid"` @@ -57,8 +83,8 @@ func (l *License) ExpiresAt() (time.Time, error) { } func (l *License) Trial() bool { - if trail, ok := l.Claims["trail"].(bool); ok { - return trail + if trial, ok := l.Claims["trial"].(bool); ok { + return trial } return false } @@ -106,9 +132,21 @@ func (c *Client) AddLicense(ctx context.Context, r AddLicenseRequest) (License, return License{}, ReadBodyAsError(res) } var l License - d := json.NewDecoder(res.Body) - d.UseNumber() - return l, d.Decode(&l) + return l, ReadBodyAsJSONUseNumber(res, &l) +} + +// CreateTrialLicense requests a trial license from the Coder licensor and install it. +func (c *Client) CreateTrialLicense(ctx context.Context, r CreateTrialLicenseRequest) (License, error) { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/licenses/trial", r) + if err != nil { + return License{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return License{}, ReadBodyAsError(res) + } + var l License + return l, ReadBodyAsJSONUseNumber(res, &l) } func (c *Client) Licenses(ctx context.Context) ([]License, error) { @@ -121,9 +159,7 @@ func (c *Client) Licenses(ctx context.Context) ([]License, error) { return nil, ReadBodyAsError(res) } var licenses []License - d := json.NewDecoder(res.Body) - d.UseNumber() - return licenses, d.Decode(&licenses) + return licenses, ReadBodyAsJSONUseNumber(res, &licenses) } func (c *Client) DeleteLicense(ctx context.Context, id int32) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/mcp.go b/vendor/github.com/coder/coder/v2/codersdk/mcp.go index f3d1bd117..d5a0ecb5e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/mcp.go +++ b/vendor/github.com/coder/coder/v2/codersdk/mcp.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -13,31 +12,52 @@ import ( // MCPServerOAuth2ConnectURL returns the URL the user should visit to // start the OAuth2 flow for an MCP server. The frontend opens this // in a new window/popup. -func (c *Client) MCPServerOAuth2ConnectURL(id uuid.UUID) string { - return fmt.Sprintf("%s/api/experimental/mcp/servers/%s/oauth2/connect", c.URL.String(), id) +func (c *Client) MCPServerOAuth2ConnectURL(organizationID, id uuid.UUID) string { + return fmt.Sprintf("%s/api/v2/organizations/%s/mcp-servers/%s/oauth2/connect", c.URL.String(), organizationID, id) +} + +// MCPServerOAuth2DisconnectResponse reports whether the removed token +// was also revoked at the OAuth provider. +type MCPServerOAuth2DisconnectResponse struct { + TokenRevoked bool `json:"token_revoked"` + TokenRevocationError string `json:"token_revocation_error,omitempty"` } // MCPServerOAuth2Disconnect removes the user's OAuth2 token for an -// MCP server. +// MCP server. Use MCPServerOAuth2DisconnectWithResponse for the +// provider revocation outcome. func (c *Client) MCPServerOAuth2Disconnect(ctx context.Context, id uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/mcp/servers/%s/oauth2/disconnect", id), nil) + _, err := c.MCPServerOAuth2DisconnectWithResponse(ctx, id) + return err +} + +// MCPServerOAuth2DisconnectWithResponse removes the user's OAuth2 +// token for an MCP server and reports the provider revocation outcome. +func (c *Client) MCPServerOAuth2DisconnectWithResponse(ctx context.Context, id uuid.UUID) (MCPServerOAuth2DisconnectResponse, error) { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/mcp/servers/%s/oauth2/disconnect", id), nil) if err != nil { - return err + return MCPServerOAuth2DisconnectResponse{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) + // Servers from before provider revocation respond 204 without a body. + if res.StatusCode == http.StatusNoContent { + return MCPServerOAuth2DisconnectResponse{}, nil } - return nil + if res.StatusCode != http.StatusOK { + return MCPServerOAuth2DisconnectResponse{}, ReadBodyAsError(res) + } + var resp MCPServerOAuth2DisconnectResponse + return resp, ReadBodyAsJSON(res, &resp) } // MCPServerConfig represents an admin-configured MCP server. type MCPServerConfig struct { - ID uuid.UUID `json:"id" format:"uuid"` - DisplayName string `json:"display_name"` - Slug string `json:"slug"` - Description string `json:"description"` - IconURL string `json:"icon_url"` + ID uuid.UUID `json:"id" format:"uuid"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + DisplayName string `json:"display_name"` + Slug string `json:"slug"` + Description string `json:"description"` + IconURL string `json:"icon_url"` Transport string `json:"transport"` // "streamable_http" or "sse" URL string `json:"url"` @@ -45,11 +65,12 @@ type MCPServerConfig struct { AuthType string `json:"auth_type"` // "none", "oauth2", "api_key", "custom_headers", "user_oidc" // OAuth2 fields (only populated for admins). - OAuth2ClientID string `json:"oauth2_client_id,omitempty"` - HasOAuth2Secret bool `json:"has_oauth2_secret"` - OAuth2AuthURL string `json:"oauth2_auth_url,omitempty"` - OAuth2TokenURL string `json:"oauth2_token_url,omitempty"` - OAuth2Scopes string `json:"oauth2_scopes,omitempty"` + OAuth2ClientID string `json:"oauth2_client_id,omitempty"` + HasOAuth2Secret bool `json:"has_oauth2_secret"` + OAuth2AuthURL string `json:"oauth2_auth_url,omitempty"` + OAuth2TokenURL string `json:"oauth2_token_url,omitempty"` + OAuth2RevocationURL string `json:"oauth2_revocation_url,omitempty"` + OAuth2Scopes string `json:"oauth2_scopes,omitempty"` // API key fields (only populated for admins). APIKeyHeader string `json:"api_key_header,omitempty"` @@ -81,6 +102,44 @@ type MCPServerConfig struct { AuthConnected bool `json:"auth_connected"` } +// MCPServerConfigRole is a role a user or group holds in an MCP server +// config's access control list. +type MCPServerConfigRole string + +const ( + MCPServerConfigRoleRead MCPServerConfigRole = "read" + // MCPServerConfigRoleDeleted removes the principal's ACL entry when + // used in an update request. + MCPServerConfigRoleDeleted MCPServerConfigRole = "" +) + +// MCPServerConfigACL is the resolved access control list of an MCP server +// config. +type MCPServerConfigACL struct { + Users []MCPServerConfigUser `json:"users"` + Groups []MCPServerConfigGroup `json:"groups"` +} + +// MCPServerConfigUser is a user entry in an MCP server config ACL. +type MCPServerConfigUser struct { + MinimalUser + Role MCPServerConfigRole `json:"role" enums:"read"` +} + +// MCPServerConfigGroup is a group entry in an MCP server config ACL. +type MCPServerConfigGroup struct { + Group + Role MCPServerConfigRole `json:"role" enums:"read"` +} + +// UpdateMCPServerConfigACLRequest is a sparse update of an MCP server +// config ACL: only the listed principals change, and +// MCPServerConfigRoleDeleted removes an entry. +type UpdateMCPServerConfigACLRequest struct { + UserRoles map[string]MCPServerConfigRole `json:"user_roles,omitempty"` + GroupRoles map[string]MCPServerConfigRole `json:"group_roles,omitempty"` +} + // CreateMCPServerConfigRequest is the request to create a new MCP server config. type CreateMCPServerConfigRequest struct { DisplayName string `json:"display_name" validate:"required"` @@ -91,15 +150,18 @@ type CreateMCPServerConfigRequest struct { Transport string `json:"transport" validate:"required,oneof=streamable_http sse"` URL string `json:"url" validate:"required,url"` - AuthType string `json:"auth_type" validate:"required,oneof=none oauth2 api_key custom_headers user_oidc"` - OAuth2ClientID string `json:"oauth2_client_id,omitempty"` - OAuth2ClientSecret string `json:"oauth2_client_secret,omitempty"` - OAuth2AuthURL string `json:"oauth2_auth_url,omitempty" validate:"omitempty,url"` - OAuth2TokenURL string `json:"oauth2_token_url,omitempty" validate:"omitempty,url"` - OAuth2Scopes string `json:"oauth2_scopes,omitempty"` - APIKeyHeader string `json:"api_key_header,omitempty"` - APIKeyValue string `json:"api_key_value,omitempty"` - CustomHeaders map[string]string `json:"custom_headers,omitempty"` + AuthType string `json:"auth_type" validate:"required,oneof=none oauth2 api_key custom_headers user_oidc"` + OAuth2ClientID string `json:"oauth2_client_id,omitempty"` + OAuth2ClientSecret string `json:"oauth2_client_secret,omitempty"` + OAuth2AuthURL string `json:"oauth2_auth_url,omitempty" validate:"omitempty,url"` + OAuth2TokenURL string `json:"oauth2_token_url,omitempty" validate:"omitempty,url"` + // OAuth2RevocationURL is the provider's RFC 7009 revocation + // endpoint; auto-populated by OAuth2 discovery when omitted. + OAuth2RevocationURL string `json:"oauth2_revocation_url,omitempty" validate:"omitempty,url"` + OAuth2Scopes string `json:"oauth2_scopes,omitempty"` + APIKeyHeader string `json:"api_key_header,omitempty"` + APIKeyValue string `json:"api_key_value,omitempty"` + CustomHeaders map[string]string `json:"custom_headers,omitempty"` ToolAllowList []string `json:"tool_allow_list,omitempty"` ToolDenyList []string `json:"tool_deny_list,omitempty"` @@ -124,15 +186,18 @@ type UpdateMCPServerConfigRequest struct { Transport *string `json:"transport,omitempty" validate:"omitempty,oneof=streamable_http sse"` URL *string `json:"url,omitempty" validate:"omitempty,url"` - AuthType *string `json:"auth_type,omitempty" validate:"omitempty,oneof=none oauth2 api_key custom_headers user_oidc"` - OAuth2ClientID *string `json:"oauth2_client_id,omitempty"` - OAuth2ClientSecret *string `json:"oauth2_client_secret,omitempty"` - OAuth2AuthURL *string `json:"oauth2_auth_url,omitempty" validate:"omitempty,url"` - OAuth2TokenURL *string `json:"oauth2_token_url,omitempty" validate:"omitempty,url"` - OAuth2Scopes *string `json:"oauth2_scopes,omitempty"` - APIKeyHeader *string `json:"api_key_header,omitempty"` - APIKeyValue *string `json:"api_key_value,omitempty"` - CustomHeaders *map[string]string `json:"custom_headers,omitempty"` + AuthType *string `json:"auth_type,omitempty" validate:"omitempty,oneof=none oauth2 api_key custom_headers user_oidc"` + OAuth2ClientID *string `json:"oauth2_client_id,omitempty"` + OAuth2ClientSecret *string `json:"oauth2_client_secret,omitempty"` + OAuth2AuthURL *string `json:"oauth2_auth_url,omitempty" validate:"omitempty,url"` + OAuth2TokenURL *string `json:"oauth2_token_url,omitempty" validate:"omitempty,url"` + // OAuth2RevocationURL is validated in the handler because a + // validate tag would reject the pointer to "" that clears it. + OAuth2RevocationURL *string `json:"oauth2_revocation_url,omitempty"` + OAuth2Scopes *string `json:"oauth2_scopes,omitempty"` + APIKeyHeader *string `json:"api_key_header,omitempty"` + APIKeyValue *string `json:"api_key_value,omitempty"` + CustomHeaders *map[string]string `json:"custom_headers,omitempty"` ToolAllowList *[]string `json:"tool_allow_list,omitempty"` ToolDenyList *[]string `json:"tool_deny_list,omitempty"` @@ -147,8 +212,8 @@ type UpdateMCPServerConfigRequest struct { ForwardCoderHeaders *bool `json:"forward_coder_headers,omitempty"` } -func (c *Client) MCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/mcp/servers", nil) +func (c *Client) MCPServerConfigs(ctx context.Context, organizationID uuid.UUID) ([]MCPServerConfig, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers", organizationID), nil) if err != nil { return nil, err } @@ -157,11 +222,11 @@ func (c *Client) MCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error return nil, ReadBodyAsError(res) } var configs []MCPServerConfig - return configs, json.NewDecoder(res.Body).Decode(&configs) + return configs, ReadBodyAsJSON(res, &configs) } -func (c *Client) MCPServerConfigByID(ctx context.Context, id uuid.UUID) (MCPServerConfig, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/mcp/servers/%s", id), nil) +func (c *Client) MCPServerConfigByID(ctx context.Context, organizationID, id uuid.UUID) (MCPServerConfig, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s", organizationID, id), nil) if err != nil { return MCPServerConfig{}, err } @@ -170,11 +235,59 @@ func (c *Client) MCPServerConfigByID(ctx context.Context, id uuid.UUID) (MCPServ return MCPServerConfig{}, ReadBodyAsError(res) } var config MCPServerConfig - return config, json.NewDecoder(res.Body).Decode(&config) + return config, ReadBodyAsJSON(res, &config) +} + +// MCPServerConfigACL returns the resolved ACL of an MCP server config. +func (c *Client) MCPServerConfigACL(ctx context.Context, organizationID, id uuid.UUID) (MCPServerConfigACL, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s/acl", organizationID, id), nil) + if err != nil { + return MCPServerConfigACL{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return MCPServerConfigACL{}, ReadBodyAsError(res) + } + var acl MCPServerConfigACL + return acl, ReadBodyAsJSON(res, &acl) +} + +// MCPServerConfigACLAvailable returns available users and groups that can be +// assigned to an MCP server config ACL. +func (c *Client) MCPServerConfigACLAvailable(ctx context.Context, organizationID, id uuid.UUID, req UsersRequest) (ACLAvailable, error) { + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s/acl/available", organizationID, id), + nil, + req.Pagination.asRequestOption(), + req.asRequestOption(), + ) + if err != nil { + return ACLAvailable{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ACLAvailable{}, ReadBodyAsError(res) + } + var available ACLAvailable + return available, ReadBodyAsJSON(res, &available) +} + +// UpdateMCPServerConfigACL applies a sparse ACL update to an MCP server +// config. +func (c *Client) UpdateMCPServerConfigACL(ctx context.Context, organizationID, id uuid.UUID, req UpdateMCPServerConfigACLRequest) error { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s/acl", organizationID, id), req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil } -func (c *Client) CreateMCPServerConfig(ctx context.Context, req CreateMCPServerConfigRequest) (MCPServerConfig, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/experimental/mcp/servers", req) +func (c *Client) CreateMCPServerConfig(ctx context.Context, organizationID uuid.UUID, req CreateMCPServerConfigRequest) (MCPServerConfig, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers", organizationID), req) if err != nil { return MCPServerConfig{}, err } @@ -183,11 +296,11 @@ func (c *Client) CreateMCPServerConfig(ctx context.Context, req CreateMCPServerC return MCPServerConfig{}, ReadBodyAsError(res) } var config MCPServerConfig - return config, json.NewDecoder(res.Body).Decode(&config) + return config, ReadBodyAsJSON(res, &config) } -func (c *Client) UpdateMCPServerConfig(ctx context.Context, id uuid.UUID, req UpdateMCPServerConfigRequest) (MCPServerConfig, error) { - res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/mcp/servers/%s", id), req) +func (c *Client) UpdateMCPServerConfig(ctx context.Context, organizationID, id uuid.UUID, req UpdateMCPServerConfigRequest) (MCPServerConfig, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s", organizationID, id), req) if err != nil { return MCPServerConfig{}, err } @@ -196,11 +309,11 @@ func (c *Client) UpdateMCPServerConfig(ctx context.Context, id uuid.UUID, req Up return MCPServerConfig{}, ReadBodyAsError(res) } var config MCPServerConfig - return config, json.NewDecoder(res.Body).Decode(&config) + return config, ReadBodyAsJSON(res, &config) } -func (c *Client) DeleteMCPServerConfig(ctx context.Context, id uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/mcp/servers/%s", id), nil) +func (c *Client) DeleteMCPServerConfig(ctx context.Context, organizationID, id uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/mcp-servers/%s", organizationID, id), nil) if err != nil { return err } diff --git a/vendor/github.com/coder/coder/v2/codersdk/notifications.go b/vendor/github.com/coder/coder/v2/codersdk/notifications.go index 559a0116a..7180d62aa 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/notifications.go +++ b/vendor/github.com/coder/coder/v2/codersdk/notifications.go @@ -52,7 +52,7 @@ func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSet return NotificationsSettings{}, ReadBodyAsError(res) } var settings NotificationsSettings - return settings, json.NewDecoder(res.Body).Decode(&settings) + return settings, ReadBodyAsJSON(res, &settings) } // PutNotificationsSettings modifies the notifications settings, which currently just controls whether all diff --git a/vendor/github.com/coder/coder/v2/codersdk/oauth2.go b/vendor/github.com/coder/coder/v2/codersdk/oauth2.go index 3f0db4d75..6a8aa32d7 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/oauth2.go +++ b/vendor/github.com/coder/coder/v2/codersdk/oauth2.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strings" "time" @@ -56,7 +57,7 @@ func (c *Client) OAuth2ProviderApps(ctx context.Context, filter OAuth2ProviderAp return []OAuth2ProviderApp{}, ReadBodyAsError(res) } var apps []OAuth2ProviderApp - return apps, json.NewDecoder(res.Body).Decode(&apps) + return apps, ReadBodyAsJSON(res, &apps) } // OAuth2ProviderApp returns an application configured to authenticate using @@ -71,7 +72,7 @@ func (c *Client) OAuth2ProviderApp(ctx context.Context, id uuid.UUID) (OAuth2Pro return OAuth2ProviderApp{}, ReadBodyAsError(res) } var apps OAuth2ProviderApp - return apps, json.NewDecoder(res.Body).Decode(&apps) + return apps, ReadBodyAsJSON(res, &apps) } type PostOAuth2ProviderAppRequest struct { @@ -92,7 +93,7 @@ func (c *Client) PostOAuth2ProviderApp(ctx context.Context, app PostOAuth2Provid return OAuth2ProviderApp{}, ReadBodyAsError(res) } var resp OAuth2ProviderApp - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type PutOAuth2ProviderAppRequest struct { @@ -113,7 +114,7 @@ func (c *Client) PutOAuth2ProviderApp(ctx context.Context, id uuid.UUID, app Put return OAuth2ProviderApp{}, ReadBodyAsError(res) } var resp OAuth2ProviderApp - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteOAuth2ProviderApp deletes an application, also invalidating any tokens @@ -153,7 +154,7 @@ func (c *Client) OAuth2ProviderAppSecrets(ctx context.Context, appID uuid.UUID) return []OAuth2ProviderAppSecret{}, ReadBodyAsError(res) } var resp []OAuth2ProviderAppSecret - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // PostOAuth2ProviderAppSecret creates a new secret for an OAuth2 application. @@ -168,7 +169,7 @@ func (c *Client) PostOAuth2ProviderAppSecret(ctx context.Context, appID uuid.UUI return OAuth2ProviderAppSecretFull{}, ReadBodyAsError(res) } var resp OAuth2ProviderAppSecretFull - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteOAuth2ProviderAppSecret deletes a secret from an OAuth2 application, @@ -185,6 +186,47 @@ func (c *Client) DeleteOAuth2ProviderAppSecret(ctx context.Context, appID uuid.U return nil } +// OAuth2ProviderSettings controls deployment-wide OAuth2 provider behavior. +// +// DynamicClientRegistrationEnabled is a pointer so a PUT can omit it to leave +// the current value unchanged, rather than a decoded zero value silently +// resetting it to false. This matters once a second field lands in this +// struct (e.g. a future initial-access-token requirement): a client built +// against an older, single-field version of this struct would otherwise +// always encode the newer field's zero value, silently clearing it on every +// unrelated update. GET always returns a non-nil value. +type OAuth2ProviderSettings struct { + DynamicClientRegistrationEnabled *bool `json:"dynamic_client_registration_enabled,omitempty"` +} + +// OAuth2ProviderSettings retrieves the deployment-wide OAuth2 provider settings. +func (c *Client) OAuth2ProviderSettings(ctx context.Context) (OAuth2ProviderSettings, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/oauth2-provider/settings", nil) + if err != nil { + return OAuth2ProviderSettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return OAuth2ProviderSettings{}, ReadBodyAsError(res) + } + var settings OAuth2ProviderSettings + return settings, ReadBodyAsJSON(res, &settings) +} + +// PutOAuth2ProviderSettings modifies the deployment-wide OAuth2 provider settings. +func (c *Client) PutOAuth2ProviderSettings(ctx context.Context, settings OAuth2ProviderSettings) (OAuth2ProviderSettings, error) { + res, err := c.Request(ctx, http.MethodPut, "/api/v2/oauth2-provider/settings", settings) + if err != nil { + return OAuth2ProviderSettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return OAuth2ProviderSettings{}, ReadBodyAsError(res) + } + var updated OAuth2ProviderSettings + return updated, ReadBodyAsJSON(res, &updated) +} + type OAuth2ProviderGrantType string // OAuth2ProviderGrantType values (RFC 6749). @@ -228,11 +270,55 @@ const ( OAuth2TokenEndpointAuthMethodNone OAuth2TokenEndpointAuthMethod = "none" ) -func (m OAuth2TokenEndpointAuthMethod) Valid() bool { - switch m { - case OAuth2TokenEndpointAuthMethodClientSecretBasic, +// AllOAuth2TokenEndpointAuthMethods returns every token endpoint auth method +// registration accepts. Valid() is defined in terms of it, so what +// registration accepts cannot drift from what this function reports. +// +// Discovery does not advertise this list verbatim; see +// AdvertisedOAuth2TokenEndpointAuthMethods for why. +func AllOAuth2TokenEndpointAuthMethods() []OAuth2TokenEndpointAuthMethod { + return []OAuth2TokenEndpointAuthMethod{ + OAuth2TokenEndpointAuthMethodClientSecretBasic, + OAuth2TokenEndpointAuthMethodClientSecretPost, + OAuth2TokenEndpointAuthMethodNone, + } +} + +// AdvertisedOAuth2TokenEndpointAuthMethods returns the token endpoint auth +// methods safe to advertise in discovery metadata (RFC 8414 +// token_endpoint_auth_methods_supported). It excludes "none": registration +// accepts "none" (see AllOAuth2TokenEndpointAuthMethods), but the token +// endpoint still requires a client secret for every authorization_code +// exchange, so advertising "none" would tell a conforming client the server +// accepts an exchange it will reject. Once the token endpoint honors "none", +// this should return the same set as AllOAuth2TokenEndpointAuthMethods. +func AdvertisedOAuth2TokenEndpointAuthMethods() []OAuth2TokenEndpointAuthMethod { + return []OAuth2TokenEndpointAuthMethod{ + OAuth2TokenEndpointAuthMethodClientSecretBasic, OAuth2TokenEndpointAuthMethodClientSecretPost, - OAuth2TokenEndpointAuthMethodNone: + } +} + +func (m OAuth2TokenEndpointAuthMethod) Valid() bool { + return slices.Contains(AllOAuth2TokenEndpointAuthMethods(), m) +} + +// OAuth2ClientType is how a client authenticates at the token endpoint +// (RFC 7591 §2, OAuth 2.1 §2.1). A confidential client authenticates with a +// secret; a public client authenticates with PKCE alone. It is derived from +// the requested token_endpoint_auth_method and stored on the app. A +// follow-up PR wires the token endpoint to read it when deciding whether to +// require a client secret. +type OAuth2ClientType string + +const ( + OAuth2ClientTypeConfidential OAuth2ClientType = "confidential" + OAuth2ClientTypePublic OAuth2ClientType = "public" +) + +func (t OAuth2ClientType) Valid() bool { + switch t { + case OAuth2ClientTypeConfidential, OAuth2ClientTypePublic: return true } return false @@ -486,14 +572,19 @@ func (req OAuth2ClientRegistrationRequest) ApplyDefaults() OAuth2ClientRegistrat return req } -// DetermineClientType determines if client is public or confidential -func (*OAuth2ClientRegistrationRequest) DetermineClientType() string { - // For now, default to confidential - // In the future, we might detect based on: - // - token_endpoint_auth_method == "none" -> public - // - application_type == "native" -> might be public - // - Other heuristics - return "confidential" +// DetermineClientType determines if client is public or confidential, based +// on the requested token_endpoint_auth_method (RFC 7591 §2, OAuth 2.1 §2.1). +// +// Only "none" reads as public; every other value, including an omitted one, +// reads as confidential, so this is safe to call before ApplyDefaults(). A +// caller that also compares the request's auth method against a stored one must +// apply defaults first, or an omitted field compares as "" and looks like a +// change the client did not request. +func (req *OAuth2ClientRegistrationRequest) DetermineClientType() OAuth2ClientType { + if req.TokenEndpointAuthMethod == OAuth2TokenEndpointAuthMethodNone { + return OAuth2ClientTypePublic + } + return OAuth2ClientTypeConfidential } // GenerateClientName generates a client name if not provided @@ -570,7 +661,7 @@ func (c *Client) PostOAuth2ClientRegistration(ctx context.Context, req OAuth2Cli return OAuth2ClientRegistrationResponse{}, ReadBodyAsError(res) } var resp OAuth2ClientRegistrationResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetOAuth2ClientConfiguration retrieves client configuration (RFC 7592) @@ -587,7 +678,7 @@ func (c *Client) GetOAuth2ClientConfiguration(ctx context.Context, clientID stri return OAuth2ClientConfiguration{}, ReadBodyAsError(res) } var resp OAuth2ClientConfiguration - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // PutOAuth2ClientConfiguration updates client configuration (RFC 7592) @@ -604,7 +695,7 @@ func (c *Client) PutOAuth2ClientConfiguration(ctx context.Context, clientID stri return OAuth2ClientConfiguration{}, ReadBodyAsError(res) } var resp OAuth2ClientConfiguration - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteOAuth2ClientConfiguration deletes client registration (RFC 7592) diff --git a/vendor/github.com/coder/coder/v2/codersdk/oauth2_validation.go b/vendor/github.com/coder/coder/v2/codersdk/oauth2_validation.go index 4c6ca0faa..9c61739c7 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/oauth2_validation.go +++ b/vendor/github.com/coder/coder/v2/codersdk/oauth2_validation.go @@ -16,7 +16,9 @@ func (req *OAuth2ClientRegistrationRequest) Validate() error { return xerrors.New("redirect_uris is required for authorization code flow") } - if err := validateRedirectURIs(req.RedirectURIs, req.TokenEndpointAuthMethod); err != nil { + // The client type is derived once, by DetermineClientType, so which RFC 8252 + // rules apply here cannot drift from what gets stored in client_type. + if err := validateRedirectURIs(req.RedirectURIs, req.DetermineClientType()); err != nil { return xerrors.Errorf("invalid redirect_uris: %w", err) } @@ -118,8 +120,11 @@ func validateScheme(u *url.URL) error { return nil } -// validateRedirectURIs validates redirect URIs according to RFC 7591, 8252 -func validateRedirectURIs(uris []string, tokenEndpointAuthMethod OAuth2TokenEndpointAuthMethod) error { +// validateRedirectURIs validates redirect URIs according to RFC 7591, 8252. +// clientType selects which rules apply and is derived by DetermineClientType, +// the single owner of that mapping, so this cannot disagree with the type the +// app is stored as. +func validateRedirectURIs(uris []string, clientType OAuth2ClientType) error { if len(uris) == 0 { return xerrors.New("at least one redirect URI is required") } @@ -144,8 +149,7 @@ func validateRedirectURIs(uris []string, tokenEndpointAuthMethod OAuth2TokenEndp continue } - // Determine if this is a public client based on token endpoint auth method - isPublicClient := tokenEndpointAuthMethod == OAuth2TokenEndpointAuthMethodNone + isPublicClient := clientType == OAuth2ClientTypePublic // Handle different validation for public vs confidential clients if uri.Scheme == "http" || uri.Scheme == "https" { @@ -163,17 +167,31 @@ func validateRedirectURIs(uris []string, tokenEndpointAuthMethod OAuth2TokenEndp } } } - } else { - // Custom scheme validation for public clients (RFC 8252 section 7.1) - if isPublicClient { - // For public clients, custom schemes should follow RFC 8252 recommendations - // Should be reverse domain notation based on domain under their control - if !isValidCustomScheme(uri.Scheme) { - return xerrors.Errorf("redirect URI at index %d: custom scheme %s should use reverse domain notation (e.g. com.example.app)", i, uri.Scheme) - } + } else if isPublicClient { + // mailto, tel, and sms hand off to a mail client, dialer, or SMS + // app rather than returning control to the application that + // started the flow. A public client has no other way to obtain + // its authorization code, so registering one of these would + // produce a client that can never complete authorization. + // + // This check runs only for public clients because that is how + // custom-scheme validation was scoped before this change, not + // because these three schemes are known to be safe for a + // confidential client's redirect; confidential clients were + // never subject to any scheme-shape check beyond validateScheme + // and remain so here. + switch uri.Scheme { + case "mailto", "tel", "sms": + return xerrors.Errorf("redirect URI at index %d: public clients may not use the %s scheme", i, uri.Scheme) } - // For confidential clients, custom schemes are less common but allowed } + // Beyond that, custom schemes need no further check: validateScheme + // already blocked the ones that are dangerous in a redirect context, + // and RFC 8252 §7.1 only recommends reverse-domain notation rather + // than requiring it. Rejecting bare schemes such as vscode:// or + // jetbrains:// would penalize the native and CLI apps this client + // type exists for; PKCE, not the scheme's spelling, is what secures + // the redirect. // Prevent URI fragments (RFC 6749 section 3.1.2) if uri.Fragment != "" || strings.Contains(uriStr, "#") { @@ -295,22 +313,3 @@ func isLoopbackAddress(hostname string) bool { hostname == "127.0.0.1" || hostname == "::1" } - -// isValidCustomScheme validates custom schemes for public clients (RFC 8252) -func isValidCustomScheme(scheme string) bool { - // For security and RFC compliance, require reverse domain notation - // Should contain at least one period and not be a well-known scheme - if !strings.Contains(scheme, ".") { - return false - } - - // Block schemes that look like well-known protocols - wellKnownSchemes := []string{"http", "https", "ftp", "mailto", "tel", "sms"} - for _, wellKnown := range wellKnownSchemes { - if strings.EqualFold(scheme, wellKnown) { - return false - } - } - - return true -} diff --git a/vendor/github.com/coder/coder/v2/codersdk/organizations.go b/vendor/github.com/coder/coder/v2/codersdk/organizations.go index 5d949860d..d50577be1 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/organizations.go +++ b/vendor/github.com/coder/coder/v2/codersdk/organizations.go @@ -229,6 +229,10 @@ type CreateTemplateRequest struct { // CORSBehavior allows optionally specifying the CORS behavior for all shared ports. CORSBehavior *CORSBehavior `json:"cors_behavior"` + + // AgentsAllowed controls whether Coder Agents can create workspaces using + // this template. Defaults to true. + AgentsAllowed *bool `json:"agents_allowed,omitempty"` } // CreateWorkspaceRequest provides options for creating a new workspace. @@ -270,7 +274,7 @@ func (c *Client) OrganizationByName(ctx context.Context, name string) (Organizat } var organization Organization - return organization, json.NewDecoder(res.Body).Decode(&organization) + return organization, ReadBodyAsJSON(res, &organization) } func (c *Client) Organizations(ctx context.Context) ([]Organization, error) { @@ -285,7 +289,7 @@ func (c *Client) Organizations(ctx context.Context) ([]Organization, error) { } var organizations []Organization - return organizations, json.NewDecoder(res.Body).Decode(&organizations) + return organizations, ReadBodyAsJSON(res, &organizations) } func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) { @@ -307,7 +311,7 @@ func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationR } var org Organization - return org, json.NewDecoder(res.Body).Decode(&org) + return org, ReadBodyAsJSON(res, &org) } // UpdateOrganization will update information about the corresponding organization, based on @@ -324,7 +328,7 @@ func (c *Client) UpdateOrganization(ctx context.Context, orgID string, req Updat } var organization Organization - return organization, json.NewDecoder(res.Body).Decode(&organization) + return organization, ReadBodyAsJSON(res, &organization) } // DeleteOrganization will remove the corresponding organization from the deployment, based on @@ -360,7 +364,7 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e } var daemons []ProvisionerDaemon - return daemons, json.NewDecoder(res.Body).Decode(&daemons) + return daemons, ReadBodyAsJSON(res, &daemons) } type OrganizationProvisionerDaemonsOptions struct { @@ -413,7 +417,7 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio } var daemons []ProvisionerDaemon - return daemons, json.NewDecoder(res.Body).Decode(&daemons) + return daemons, ReadBodyAsJSON(res, &daemons) } type OrganizationProvisionerJobsOptions struct { @@ -462,7 +466,7 @@ func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID } var jobs []ProvisionerJob - return jobs, json.NewDecoder(res.Body).Decode(&jobs) + return jobs, ReadBodyAsJSON(res, &jobs) } func (c *Client) OrganizationProvisionerJob(ctx context.Context, organizationID, jobID uuid.UUID) (job ProvisionerJob, err error) { @@ -478,7 +482,7 @@ func (c *Client) OrganizationProvisionerJob(ctx context.Context, organizationID, if res.StatusCode != http.StatusOK { return job, ReadBodyAsError(res) } - return job, json.NewDecoder(res.Body).Decode(&job) + return job, ReadBodyAsJSON(res, &job) } func joinSlice[T ~string](s []T) string { @@ -514,7 +518,7 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid. } var templateVersion TemplateVersion - return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) + return templateVersion, ReadBodyAsJSON(res, &templateVersion) } func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, templateName, versionName string) (TemplateVersion, error) { @@ -532,7 +536,7 @@ func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organ } var templateVersion TemplateVersion - return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) + return templateVersion, ReadBodyAsJSON(res, &templateVersion) } // CreateTemplate creates a new template inside an organization. @@ -551,7 +555,7 @@ func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, r } var template Template - return template, json.NewDecoder(res.Body).Decode(&template) + return template, ReadBodyAsJSON(res, &template) } // TemplatesByOrganization lists all templates inside of an organization. @@ -570,7 +574,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui } var templates []Template - return templates, json.NewDecoder(res.Body).Decode(&templates) + return templates, ReadBodyAsJSON(res, &templates) } type TemplateFilter struct { @@ -631,7 +635,7 @@ func (c *Client) Templates(ctx context.Context, filter TemplateFilter) ([]Templa } var templates []Template - return templates, json.NewDecoder(res.Body).Decode(&templates) + return templates, ReadBodyAsJSON(res, &templates) } // TemplateByName finds a template inside the organization provided with a case-insensitive name. @@ -653,7 +657,7 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n } var template Template - return template, json.NewDecoder(res.Body).Decode(&template) + return template, ReadBodyAsJSON(res, &template) } // CreateWorkspace creates a new workspace for the template specified. @@ -676,5 +680,5 @@ func (c *Client) CreateUserWorkspace(ctx context.Context, user string, request C } var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) + return workspace, ReadBodyAsJSON(res, &workspace) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/prebuilds.go b/vendor/github.com/coder/coder/v2/codersdk/prebuilds.go index 979c61bfb..97f31ea50 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/prebuilds.go +++ b/vendor/github.com/coder/coder/v2/codersdk/prebuilds.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "net/http" ) @@ -29,7 +28,7 @@ func (c *Client) GetPrebuildsSettings(ctx context.Context) (PrebuildsSettings, e return PrebuildsSettings{}, ReadBodyAsError(res) } var settings PrebuildsSettings - return settings, json.NewDecoder(res.Body).Decode(&settings) + return settings, ReadBodyAsJSON(res, &settings) } // PutPrebuildsSettings modifies the prebuilds settings, which currently just controls whether all diff --git a/vendor/github.com/coder/coder/v2/codersdk/premiumfunnel.go b/vendor/github.com/coder/coder/v2/codersdk/premiumfunnel.go new file mode 100644 index 000000000..13e8be226 --- /dev/null +++ b/vendor/github.com/coder/coder/v2/codersdk/premiumfunnel.go @@ -0,0 +1,120 @@ +package codersdk + +import ( + "context" + "net/http" + + "github.com/google/uuid" +) + +// PremiumFunnelSource identifies the premium feature whose paywall the user +// interacted with. This is a fixed enum rather than a page URL because paywall +// routes embed organization and template names. +type PremiumFunnelSource string + +const ( + PremiumFunnelSourceAIBridgeSessionThreads PremiumFunnelSource = "aibridge_session_threads" + PremiumFunnelSourceAIBridgeSessions PremiumFunnelSource = "aibridge_sessions" + PremiumFunnelSourceAIGatewayKeys PremiumFunnelSource = "ai_gateway_keys" + PremiumFunnelSourceAIGovernance PremiumFunnelSource = "ai_governance" + PremiumFunnelSourceAppearance PremiumFunnelSource = "appearance" + PremiumFunnelSourceAuditLog PremiumFunnelSource = "audit_log" + PremiumFunnelSourceBrowserOnly PremiumFunnelSource = "browser_only" + PremiumFunnelSourceConnectionLog PremiumFunnelSource = "connection_log" + PremiumFunnelSourceCustomRoles PremiumFunnelSource = "custom_roles" + PremiumFunnelSourceExternalAuth PremiumFunnelSource = "external_auth" + PremiumFunnelSourceGroups PremiumFunnelSource = "groups" + PremiumFunnelSourceIdpOrgSync PremiumFunnelSource = "idp_org_sync" + PremiumFunnelSourceIdpSync PremiumFunnelSource = "idp_sync" + PremiumFunnelSourceMultipleOrganizations PremiumFunnelSource = "multiple_organizations" + PremiumFunnelSourceObservability PremiumFunnelSource = "observability" + PremiumFunnelSourceProvisionerKeys PremiumFunnelSource = "provisioner_keys" + PremiumFunnelSourceProvisioners PremiumFunnelSource = "provisioners" + PremiumFunnelSourceTemplatePermissions PremiumFunnelSource = "template_permissions" + PremiumFunnelSourceWorkspaceProxies PremiumFunnelSource = "workspace_proxies" + // PremiumFunnelSourceDirect is reported when a trial is requested without + // passing through a paywall, such as from the sidebar or a bookmark. It + // keeps "arrived without attribution" distinguishable from missing data. + PremiumFunnelSourceDirect PremiumFunnelSource = "direct" +) + +// PremiumFunnelSources returns every valid funnel source. +func PremiumFunnelSources() []PremiumFunnelSource { + return []PremiumFunnelSource{ + PremiumFunnelSourceAIBridgeSessionThreads, + PremiumFunnelSourceAIBridgeSessions, + PremiumFunnelSourceAIGatewayKeys, + PremiumFunnelSourceAIGovernance, + PremiumFunnelSourceAppearance, + PremiumFunnelSourceAuditLog, + PremiumFunnelSourceBrowserOnly, + PremiumFunnelSourceConnectionLog, + PremiumFunnelSourceCustomRoles, + PremiumFunnelSourceExternalAuth, + PremiumFunnelSourceGroups, + PremiumFunnelSourceIdpOrgSync, + PremiumFunnelSourceIdpSync, + PremiumFunnelSourceMultipleOrganizations, + PremiumFunnelSourceObservability, + PremiumFunnelSourceProvisionerKeys, + PremiumFunnelSourceProvisioners, + PremiumFunnelSourceTemplatePermissions, + PremiumFunnelSourceWorkspaceProxies, + PremiumFunnelSourceDirect, + } +} + +// Valid reports whether the source is a known funnel source. +func (s PremiumFunnelSource) Valid() bool { + for _, source := range PremiumFunnelSources() { + if s == source { + return true + } + } + return false +} + +// PremiumFunnelVariant identifies which paywall presentation was rendered. +type PremiumFunnelVariant string + +const ( + PremiumFunnelVariantPremium PremiumFunnelVariant = "premium" + PremiumFunnelVariantSmall PremiumFunnelVariant = "small" + PremiumFunnelVariantAIGovernance PremiumFunnelVariant = "ai_governance" +) + +// Valid reports whether the variant is a known paywall presentation. +func (v PremiumFunnelVariant) Valid() bool { + switch v { + case PremiumFunnelVariantPremium, PremiumFunnelVariantSmall, PremiumFunnelVariantAIGovernance: + return true + default: + return false + } +} + +// PremiumFunnelEventRequest is the request body for +// POST /api/v2/deployment/premium-funnel-events. +type PremiumFunnelEventRequest struct { + // ID identifies this click, and doubles as the attribution token that a + // later trial signup reports. + ID uuid.UUID `json:"id" format:"uuid" validate:"required"` + Source PremiumFunnelSource `json:"source" validate:"required"` + Variant PremiumFunnelVariant `json:"variant" validate:"required"` +} + +// ReportPremiumFunnelEvent reports a click on a premium paywall call to action +// for telemetry purposes. It is a no-op on deployments with telemetry +// disabled. Trial signups are reported by coderd when a license is actually +// issued, so a client cannot forge a conversion. +func (c *Client) ReportPremiumFunnelEvent(ctx context.Context, req PremiumFunnelEventRequest) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/deployment/premium-funnel-events", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/presets.go b/vendor/github.com/coder/coder/v2/codersdk/presets.go index eba1b9216..eb069c223 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/presets.go +++ b/vendor/github.com/coder/coder/v2/codersdk/presets.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" @@ -36,5 +35,5 @@ func (c *Client) TemplateVersionPresets(ctx context.Context, templateVersionID u return nil, ReadBodyAsError(res) } var presets []Preset - return presets, json.NewDecoder(res.Body).Decode(&presets) + return presets, ReadBodyAsJSON(res, &presets) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/promptsanitize.go b/vendor/github.com/coder/coder/v2/codersdk/promptsanitize.go new file mode 100644 index 000000000..ec5354ac4 --- /dev/null +++ b/vendor/github.com/coder/coder/v2/codersdk/promptsanitize.go @@ -0,0 +1,165 @@ +package codersdk + +import ( + "strings" + "unicode" +) + +// SanitizePromptText strips invisible Unicode characters that could +// hide prompt-injection content from human reviewers, normalizes line +// endings, collapses excessive blank lines, and trims surrounding +// whitespace. +// +// It lives in codersdk so API consumers, such as terraform-provider-coderd, +// can compare prompt values the same way the server stores them. +// +// The stripped codepoints are truly invisible and have no legitimate +// use in prompt text. An explicit codepoint list is used rather than +// blanket unicode.Cf stripping to avoid breaking subdivision flag +// emoji (🏴󠁧󠁢󠁥󠁮󠁧󠁿) and other legitimate format characters. +// +// Note: U+200D (ZWJ) is stripped even though it joins compound emoji +// (e.g. 👨‍👩‍👦 → 👨👩👦). This is an acceptable trade-off because +// system prompts are not emoji art, and ZWJ is actively exploited in +// zero-width steganography schemes as a delimiter character. +func SanitizePromptText(s string) string { + // 1. Normalize line endings: \r\n → \n, lone \r → \n. + s = strings.ReplaceAll(s, "\r\n", "\n") + s = strings.ReplaceAll(s, "\r", "\n") + + // 2. Strip invisible characters rune-by-rune. + var b strings.Builder + b.Grow(len(s)) + for _, r := range s { + if !isVisible(r) { + continue + } + _, _ = b.WriteRune(r) + } + s = b.String() + + // 3. Collapse 3+ consecutive newlines down to 2 (one blank + // line between paragraphs). This runs after invisible-char + // stripping so that lines containing only stripped chars + // become empty and get collapsed. + s = collapseNewlines(s) + + // 4. Final trim. + return strings.TrimSpace(s) +} + +// isVisible reports whether r is a visible Unicode character that +// should be preserved in prompt text. Each invisible range is +// documented with its Unicode name and rationale. +func isVisible(r rune) bool { + switch { + // Soft hyphen — invisible in most renderers, used to hide + // content boundaries. + case r == 0x00AD: + return false + + // Combining grapheme joiner — invisible, no legitimate + // prompt use. + case r == 0x034F: + return false + + // Arabic letter mark — bidi control, invisible. + case r == 0x061C: + return false + + // Mongolian vowel separator — invisible spacing character. + case r == 0x180E: + return false + + // Zero-width space (U+200B). + case r == 0x200B: + return false + + // U+200C (ZWNJ) is deliberately NOT stripped. It is + // required for correct rendering of Persian, Urdu, and + // Kurdish scripts where it controls cursive joining. + // Stripping ZWS (U+200B) and ZWJ (U+200D) already breaks + // zero-width steganography encodings regardless of whether + // ZWNJ survives. + + // Zero-width joiner (U+200D) — also used in compound emoji, + // but actively exploited in steganography. See + // SanitizePromptText doc comment. + case r == 0x200D: + return false + + // Left-to-right mark (U+200E). + case r == 0x200E: + return false + + // Right-to-left mark (U+200F). + case r == 0x200F: + return false + + // Bidi embedding and override controls (U+202A–U+202E): + // LRE, RLE, PDF, LRO, RLO. + case r >= 0x202A && r <= 0x202E: + return false + + // Word joiner and invisible operators (U+2060–U+2064): + // word joiner, function application, invisible times, + // invisible separator, invisible plus. + case r >= 0x2060 && r <= 0x2064: + return false + + // Bidi isolate controls (U+2066–U+2069): + // LRI, RLI, FSI, PDI. + case r >= 0x2066 && r <= 0x2069: + return false + + // Deprecated format characters (U+206A–U+206F): inhibit + // symmetric swapping through nominal digit shapes. + case r >= 0x206A && r <= 0x206F: + return false + + // Byte order mark / zero-width no-break space (U+FEFF). + // Common at start of Windows-edited files. + case r == 0xFEFF: + return false + + // Interlinear annotation anchor, separator, and + // terminator (U+FFF9–U+FFFB). + case r >= 0xFFF9 && r <= 0xFFFB: + return false + + default: + return true + } +} + +// collapseNewlines replaces runs of 3 or more consecutive newlines +// with exactly 2, preserving single blank lines (paragraph breaks) +// while eliminating scroll-padding attacks. Trailing whitespace on +// each line is stripped first so that whitespace-only lines become +// empty and collapse naturally. +func collapseNewlines(s string) string { + // Step 1: Trim trailing whitespace from each line, preserving + // leading whitespace for indentation. + lines := strings.Split(s, "\n") + for i, line := range lines { + lines[i] = strings.TrimRightFunc(line, unicode.IsSpace) + } + s = strings.Join(lines, "\n") + + // Step 2: Collapse runs of 3+ consecutive newlines down to 2. + var b strings.Builder + b.Grow(len(s)) + consecutiveNewlines := 0 + for _, r := range s { + if r == '\n' { + consecutiveNewlines++ + if consecutiveNewlines <= 2 { + _, _ = b.WriteRune(r) + } + continue + } + consecutiveNewlines = 0 + _, _ = b.WriteRune(r) + } + return b.String() +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/provisionerdaemons.go b/vendor/github.com/coder/coder/v2/codersdk/provisionerdaemons.go index 1dced9ee7..df0eb990a 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/provisionerdaemons.go +++ b/vendor/github.com/coder/coder/v2/codersdk/provisionerdaemons.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -409,6 +408,25 @@ func ReservedProvisionerKeyNames() []string { } } +// IsReservedProvisionerKey reports whether the given ID is one of the reserved +// provisioner keys (built-in, user-auth, PSK). Reserved keys are created by the +// system and cannot be deleted. +func IsReservedProvisionerKey(id uuid.UUID) bool { + switch id { + case ProvisionerKeyUUIDBuiltIn, ProvisionerKeyUUIDUserAuth, ProvisionerKeyUUIDPSK: + return true + default: + return false + } +} + +// IsDeletableProvisionerKey reports whether the given ID identifies a +// provisioner key that can be deleted. The zero value and reserved keys cannot +// be deleted. +func IsDeletableProvisionerKey(id uuid.UUID) bool { + return id != uuid.Nil && !IsReservedProvisionerKey(id) +} + type CreateProvisionerKeyRequest struct { Name string `json:"name"` Tags map[string]string `json:"tags"` @@ -433,7 +451,7 @@ func (c *Client) CreateProvisionerKey(ctx context.Context, organizationID uuid.U return CreateProvisionerKeyResponse{}, ReadBodyAsError(res) } var resp CreateProvisionerKeyResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // ListProvisionerKeys lists all provisioner keys for an organization. @@ -451,7 +469,7 @@ func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UU return nil, ReadBodyAsError(res) } var resp []ProvisionerKey - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetProvisionerKey returns the provisioner key. @@ -471,7 +489,7 @@ func (c *Client) GetProvisionerKey(ctx context.Context, pk string) (ProvisionerK return ProvisionerKey{}, ReadBodyAsError(res) } var resp ProvisionerKey - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // ListProvisionerKeyDaemons lists all provisioner keys with their associated daemons for an organization. @@ -489,7 +507,7 @@ func (c *Client) ListProvisionerKeyDaemons(ctx context.Context, organizationID u return nil, ReadBodyAsError(res) } var resp []ProvisionerKeyDaemons - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // DeleteProvisionerKey deletes a provisioner key. diff --git a/vendor/github.com/coder/coder/v2/codersdk/rbacresources_gen.go b/vendor/github.com/coder/coder/v2/codersdk/rbacresources_gen.go index bc71930ef..4614e4394 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/rbacresources_gen.go +++ b/vendor/github.com/coder/coder/v2/codersdk/rbacresources_gen.go @@ -17,6 +17,7 @@ const ( ResourceBoundaryLog RBACResource = "boundary_log" ResourceBoundaryUsage RBACResource = "boundary_usage" ResourceChat RBACResource = "chat" + ResourceChatModelConfig RBACResource = "chat_model_config" ResourceConnectionLog RBACResource = "connection_log" ResourceCryptoKey RBACResource = "crypto_key" ResourceDebugInfo RBACResource = "debug_info" @@ -28,6 +29,7 @@ const ( ResourceIdpsyncSettings RBACResource = "idpsync_settings" ResourceInboxNotification RBACResource = "inbox_notification" ResourceLicense RBACResource = "license" + ResourceMCPServerConfig RBACResource = "mcp_server_config" ResourceNotificationMessage RBACResource = "notification_message" ResourceNotificationPreference RBACResource = "notification_preference" ResourceNotificationTemplate RBACResource = "notification_template" @@ -52,6 +54,7 @@ const ( ResourceWorkspace RBACResource = "workspace" ResourceWorkspaceAgentDevcontainers RBACResource = "workspace_agent_devcontainers" ResourceWorkspaceAgentResourceMonitor RBACResource = "workspace_agent_resource_monitor" + ResourceWorkspaceBuildOrchestration RBACResource = "workspace_build_orchestration" ResourceWorkspaceDormant RBACResource = "workspace_dormant" ResourceWorkspaceProxy RBACResource = "workspace_proxy" ) @@ -95,6 +98,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceBoundaryLog: {ActionCreate, ActionDelete, ActionRead}, ResourceBoundaryUsage: {ActionDelete, ActionRead, ActionUpdate}, ResourceChat: {ActionCreate, ActionDelete, ActionRead, ActionShare, ActionUpdate}, + ResourceChatModelConfig: {ActionCreate, ActionDelete, ActionRead, ActionShare, ActionUpdate}, ResourceConnectionLog: {ActionRead, ActionUpdate}, ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceDebugInfo: {ActionRead}, @@ -106,6 +110,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceIdpsyncSettings: {ActionRead, ActionUpdate}, ResourceInboxNotification: {ActionCreate, ActionRead, ActionUpdate}, ResourceLicense: {ActionCreate, ActionDelete, ActionRead}, + ResourceMCPServerConfig: {ActionCreate, ActionDelete, ActionRead, ActionShare, ActionUpdate}, ResourceNotificationMessage: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceNotificationPreference: {ActionRead, ActionUpdate}, ResourceNotificationTemplate: {ActionRead, ActionUpdate}, @@ -130,6 +135,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent}, ResourceWorkspaceAgentDevcontainers: {ActionCreate}, ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate}, + ResourceWorkspaceBuildOrchestration: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent}, ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, } diff --git a/vendor/github.com/coder/coder/v2/codersdk/rbacroles.go b/vendor/github.com/coder/coder/v2/codersdk/rbacroles.go index 71b82c634..ea87127a3 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/rbacroles.go +++ b/vendor/github.com/coder/coder/v2/codersdk/rbacroles.go @@ -7,7 +7,10 @@ const ( RoleTemplateAdmin string = "template-admin" RoleUserAdmin string = "user-admin" RoleAuditor string = "auditor" - RoleAgentsAccess string = "agents-access" + // Deprecated: the agents-access role was removed. Coder Agents chat + // access is part of the organization-member permission floor, and + // servers without this built-in role reject assigning it. + RoleAgentsAccess string = "agents-access" RoleOrganizationAdmin string = "organization-admin" RoleOrganizationMember string = "organization-member" diff --git a/vendor/github.com/coder/coder/v2/codersdk/replicas.go b/vendor/github.com/coder/coder/v2/codersdk/replicas.go index 7263aba91..2d3f4d34c 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/replicas.go +++ b/vendor/github.com/coder/coder/v2/codersdk/replicas.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "net/http" "time" @@ -40,5 +39,5 @@ func (c *Client) Replicas(ctx context.Context) ([]Replica, error) { } var replicas []Replica - return replicas, json.NewDecoder(res.Body).Decode(&replicas) + return replicas, ReadBodyAsJSON(res, &replicas) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/richparameters.go b/vendor/github.com/coder/coder/v2/codersdk/richparameters.go index 5df7d2bea..4e064973e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/richparameters.go +++ b/vendor/github.com/coder/coder/v2/codersdk/richparameters.go @@ -31,7 +31,7 @@ func (c *Client) EvaluateTemplateVersion(ctx context.Context, templateVersionID } var dynResp DynamicParametersResponse - return dynResp, json.NewDecoder(res.Body).Decode(&dynResp) + return dynResp, ReadBodyAsJSON(res, &dynResp) } func ValidateNewWorkspaceParameters(richParameters []TemplateVersionParameter, buildParameters []WorkspaceBuildParameter) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/roles.go b/vendor/github.com/coder/coder/v2/codersdk/roles.go index 70162f8f0..c8a95cca5 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/roles.go +++ b/vendor/github.com/coder/coder/v2/codersdk/roles.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" @@ -107,7 +106,7 @@ func (c *Client) CreateOrganizationRole(ctx context.Context, role Role) (Role, e return Role{}, ReadBodyAsError(res) } var r Role - return r, json.NewDecoder(res.Body).Decode(&r) + return r, ReadBodyAsJSON(res, &r) } // UpdateOrganizationRole will update an existing custom organization role @@ -131,7 +130,7 @@ func (c *Client) UpdateOrganizationRole(ctx context.Context, role Role) (Role, e return Role{}, ReadBodyAsError(res) } var r Role - return r, json.NewDecoder(res.Body).Decode(&r) + return r, ReadBodyAsJSON(res, &r) } // DeleteOrganizationRole will delete a custom organization role @@ -159,7 +158,7 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) { return nil, ReadBodyAsError(res) } var roles []AssignableRoles - return roles, json.NewDecoder(res.Body).Decode(&roles) + return roles, ReadBodyAsJSON(res, &roles) } // ListOrganizationRoles lists all assignable roles for a given organization. @@ -173,7 +172,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]As return nil, ReadBodyAsError(res) } var roles []AssignableRoles - return roles, json.NewDecoder(res.Body).Decode(&roles) + return roles, ReadBodyAsJSON(res, &roles) } // CreatePermissions is a helper function to quickly build permissions. diff --git a/vendor/github.com/coder/coder/v2/codersdk/templatebuilder.go b/vendor/github.com/coder/coder/v2/codersdk/templatebuilder.go index 0bcb36ca0..8d8a45008 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/templatebuilder.go +++ b/vendor/github.com/coder/coder/v2/codersdk/templatebuilder.go @@ -78,7 +78,7 @@ func (c *Client) TemplateBuilderBases(ctx context.Context) (TemplateBuilderBases return TemplateBuilderBasesResponse{}, ReadBodyAsError(res) } var resp TemplateBuilderBasesResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // TemplateBuilderModules returns the list of modules available for a given @@ -98,7 +98,7 @@ func (c *Client) TemplateBuilderModules(ctx context.Context, base string) (Templ return TemplateBuilderModulesResponse{}, ReadBodyAsError(res) } var resp TemplateBuilderModulesResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // TemplateBuilderComposeRequest is the request body for @@ -150,6 +150,40 @@ type TemplateBuilderCreateTemplateResponse struct { Template Template `json:"template"` } +// TemplateBuilderSessionEventType enumerates the event types for +// template builder session telemetry. +type TemplateBuilderSessionEventType string + +const ( + TemplateBuilderSessionEventWizardEntry TemplateBuilderSessionEventType = "wizard_entry" + TemplateBuilderSessionEventComposeCompletion TemplateBuilderSessionEventType = "compose_completion" +) + +// TemplateBuilderSessionRequest is the request body for +// POST /api/v2/templatebuilder/sessions. +type TemplateBuilderSessionRequest struct { + SessionID uuid.UUID `json:"session_id" format:"uuid" validate:"required"` + EventType TemplateBuilderSessionEventType `json:"event_type" validate:"required,oneof=wizard_entry compose_completion"` + BaseTemplateID string `json:"base_template_id,omitempty"` + ModuleIDs []string `json:"module_ids,omitempty"` + DurationSeconds float64 `json:"duration_seconds,omitempty"` + Success bool `json:"success,omitempty"` +} + +// TemplateBuilderSession reports a template builder session event for +// telemetry purposes. +func (c *Client) TemplateBuilderSession(ctx context.Context, req TemplateBuilderSessionRequest) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/templatebuilder/sessions", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + // TemplateBuilderCreateTemplate composes a template from a base and modules, // validates it via a provisioner import job, and creates the template. func (c *Client) TemplateBuilderCreateTemplate(ctx context.Context, req TemplateBuilderCreateTemplateRequest) (TemplateBuilderCreateTemplateResponse, error) { @@ -162,5 +196,5 @@ func (c *Client) TemplateBuilderCreateTemplate(ctx context.Context, req Template return TemplateBuilderCreateTemplateResponse{}, ReadBodyAsError(res) } var resp TemplateBuilderCreateTemplateResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/templates.go b/vendor/github.com/coder/coder/v2/codersdk/templates.go index ba1f28770..7b256f38b 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/templates.go +++ b/vendor/github.com/coder/coder/v2/codersdk/templates.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "strings" @@ -69,6 +68,7 @@ type Template struct { CORSBehavior CORSBehavior `json:"cors_behavior"` UseClassicParameterFlow bool `json:"use_classic_parameter_flow"` + AgentsAllowed bool `json:"agents_allowed"` // DisableModuleCache disables the use of cached Terraform modules during // provisioning. @@ -212,7 +212,7 @@ type UpdateTemplateACL struct { GroupPerms map[string]TemplateRole `json:"group_perms,omitempty" example:":admin,8bd26b20-f3e8-48be-a903-46bb920cf671:use"` } -// ACLAvailable is a list of users and groups that can be added to a template +// ACLAvailable is a list of users and groups that can be added to a resource // ACL. type ACLAvailable struct { Users []ReducedUser `json:"users"` @@ -283,6 +283,9 @@ type UpdateTemplateMeta struct { // DisableModuleCache disables the using of cached Terraform modules during // provisioning. It is recommended not to disable this. DisableModuleCache *bool `json:"disable_module_cache,omitempty"` + // AgentsAllowed controls whether Coder Agents can create workspaces using + // this template. If omitted, the current value is preserved. + AgentsAllowed *bool `json:"agents_allowed,omitempty"` } type TemplateExample struct { @@ -306,7 +309,7 @@ func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, er return Template{}, ReadBodyAsError(res) } var resp Template - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) ArchiveTemplateVersions(ctx context.Context, template uuid.UUID, all bool) (ArchiveTemplateVersionsResponse, error) { @@ -324,7 +327,7 @@ func (c *Client) ArchiveTemplateVersions(ctx context.Context, template uuid.UUID return ArchiveTemplateVersionsResponse{}, ReadBodyAsError(res) } var resp ArchiveTemplateVersionsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } //nolint:revive @@ -369,7 +372,7 @@ func (c *Client) UpdateTemplateMeta(ctx context.Context, templateID uuid.UUID, r return Template{}, ReadBodyAsError(res) } var updated Template - return updated, json.NewDecoder(res.Body).Decode(&updated) + return updated, ReadBodyAsJSON(res, &updated) } func (c *Client) UpdateTemplateACL(ctx context.Context, templateID uuid.UUID, req UpdateTemplateACL) error { @@ -405,7 +408,7 @@ func (c *Client) TemplateACLAvailable(ctx context.Context, templateID uuid.UUID, return ACLAvailable{}, ReadBodyAsError(res) } var acl ACLAvailable - return acl, json.NewDecoder(res.Body).Decode(&acl) + return acl, ReadBodyAsJSON(res, &acl) } func (c *Client) TemplateACL(ctx context.Context, templateID uuid.UUID) (TemplateACL, error) { @@ -418,7 +421,7 @@ func (c *Client) TemplateACL(ctx context.Context, templateID uuid.UUID) (Templat return TemplateACL{}, ReadBodyAsError(res) } var acl TemplateACL - return acl, json.NewDecoder(res.Body).Decode(&acl) + return acl, ReadBodyAsJSON(res, &acl) } // UpdateActiveTemplateVersion updates the active template version to the ID provided. @@ -449,7 +452,7 @@ func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVer if req.IncludeArchived { u += "?include_archived=true" } - res, err := c.Request(ctx, http.MethodGet, u, nil, req.Pagination.asRequestOption()) + res, err := c.Request(ctx, http.MethodGet, u, nil, req.asRequestOption()) if err != nil { return nil, err } @@ -458,7 +461,7 @@ func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVer return nil, ReadBodyAsError(res) } var templateVersion []TemplateVersion - return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) + return templateVersion, ReadBodyAsJSON(res, &templateVersion) } // TemplateVersionByName returns a template version by it's friendly name. @@ -473,7 +476,7 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, return TemplateVersion{}, ReadBodyAsError(res) } var templateVersion TemplateVersion - return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) + return templateVersion, ReadBodyAsJSON(res, &templateVersion) } func (c *Client) TemplateDAUsLocalTZ(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) { @@ -496,7 +499,7 @@ func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID, tzOffse } var resp DAUsResponse - return &resp, json.NewDecoder(res.Body).Decode(&resp) + return &resp, ReadBodyAsJSON(res, &resp) } // AgentStatsReportRequest is a WebSocket request by coderd @@ -532,7 +535,7 @@ func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error return nil, ReadBodyAsError(res) } var templateExamples []TemplateExample - return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples) + return templateExamples, ReadBodyAsJSON(res, &templateExamples) } type InvalidatePresetsResponse struct { @@ -563,5 +566,5 @@ func (c *Client) InvalidateTemplatePresets(ctx context.Context, template uuid.UU } var response InvalidatePresetsResponse - return response, json.NewDecoder(res.Body).Decode(&response) + return response, ReadBodyAsJSON(res, &response) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/templateversions.go b/vendor/github.com/coder/coder/v2/codersdk/templateversions.go index 01cd23370..eb8cfa7eb 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/templateversions.go +++ b/vendor/github.com/coder/coder/v2/codersdk/templateversions.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -113,7 +112,7 @@ func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVer return TemplateVersion{}, ReadBodyAsError(res) } var version TemplateVersion - return version, json.NewDecoder(res.Body).Decode(&version) + return version, ReadBodyAsJSON(res, &version) } // CancelTemplateVersion marks a template version job as canceled. @@ -140,12 +139,12 @@ func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid return nil, ReadBodyAsError(res) } var params []TemplateVersionParameter - return params, json.NewDecoder(res.Body).Decode(¶ms) + return params, ReadBodyAsJSON(res, ¶ms) } // TemplateVersionExternalAuth returns authentication providers for the requested template version. -func (c *Client) TemplateVersionExternalAuth(ctx context.Context, version uuid.UUID) ([]TemplateVersionExternalAuth, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/external-auth", version), nil) +func (c *Client) TemplateVersionExternalAuth(ctx context.Context, version uuid.UUID, opts ...RequestOption) ([]TemplateVersionExternalAuth, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/external-auth", version), nil, opts...) if err != nil { return nil, err } @@ -154,7 +153,7 @@ func (c *Client) TemplateVersionExternalAuth(ctx context.Context, version uuid.U return nil, ReadBodyAsError(res) } var extAuth []TemplateVersionExternalAuth - return extAuth, json.NewDecoder(res.Body).Decode(&extAuth) + return extAuth, ReadBodyAsJSON(res, &extAuth) } // TemplateVersionResources returns resources a template version declares. @@ -168,7 +167,7 @@ func (c *Client) TemplateVersionResources(ctx context.Context, version uuid.UUID return nil, ReadBodyAsError(res) } var resources []WorkspaceResource - return resources, json.NewDecoder(res.Body).Decode(&resources) + return resources, ReadBodyAsJSON(res, &resources) } // TemplateVersionVariables returns resources a template version variables. @@ -182,7 +181,7 @@ func (c *Client) TemplateVersionVariables(ctx context.Context, version uuid.UUID return nil, ReadBodyAsError(res) } var variables []TemplateVersionVariable - return variables, json.NewDecoder(res.Body).Decode(&variables) + return variables, ReadBodyAsJSON(res, &variables) } // TemplateVersionLogsAfter streams logs for a template version that occurred after a specific log ID. @@ -211,7 +210,7 @@ func (c *Client) CreateTemplateVersionDryRun(ctx context.Context, version uuid.U } var job ProvisionerJob - return job, json.NewDecoder(res.Body).Decode(&job) + return job, ReadBodyAsJSON(res, &job) } // TemplateVersionDryRun returns the current state of a template version dry-run @@ -227,7 +226,7 @@ func (c *Client) TemplateVersionDryRun(ctx context.Context, version, job uuid.UU } var j ProvisionerJob - return j, json.NewDecoder(res.Body).Decode(&j) + return j, ReadBodyAsJSON(res, &j) } // TemplateVersionDryRunMatchedProvisioners returns the matched provisioners for a @@ -243,7 +242,7 @@ func (c *Client) TemplateVersionDryRunMatchedProvisioners(ctx context.Context, v } var matched MatchedProvisioners - return matched, json.NewDecoder(res.Body).Decode(&matched) + return matched, ReadBodyAsJSON(res, &matched) } // TemplateVersionDryRunResources returns the resources of a finished template @@ -259,7 +258,7 @@ func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, jo } var resources []WorkspaceResource - return resources, json.NewDecoder(res.Body).Decode(&resources) + return resources, ReadBodyAsJSON(res, &resources) } // TemplateVersionDryRunLogsAfter streams logs for a template version dry-run @@ -298,7 +297,7 @@ func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid. return TemplateVersion{}, ReadBodyAsError(res) } var version TemplateVersion - return version, json.NewDecoder(res.Body).Decode(&version) + return version, ReadBodyAsJSON(res, &version) } func (c *Client) UpdateTemplateVersion(ctx context.Context, versionID uuid.UUID, req PatchTemplateVersionRequest) (TemplateVersion, error) { @@ -311,5 +310,5 @@ func (c *Client) UpdateTemplateVersion(ctx context.Context, versionID uuid.UUID, return TemplateVersion{}, ReadBodyAsError(res) } var version TemplateVersion - return version, json.NewDecoder(res.Body).Decode(&version) + return version, ReadBodyAsJSON(res, &version) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/updatecheck.go b/vendor/github.com/coder/coder/v2/codersdk/updatecheck.go index a3652494d..08d81b1c7 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/updatecheck.go +++ b/vendor/github.com/coder/coder/v2/codersdk/updatecheck.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "net/http" ) @@ -30,5 +29,5 @@ func (c *Client) UpdateCheck(ctx context.Context) (UpdateCheckResponse, error) { } var buildInfo UpdateCheckResponse - return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo) + return buildInfo, ReadBodyAsJSON(res, &buildInfo) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/users.go b/vendor/github.com/coder/coder/v2/codersdk/users.go index 351f20d2a..3ac64a736 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/users.go +++ b/vendor/github.com/coder/coder/v2/codersdk/users.go @@ -499,7 +499,7 @@ func (c *Client) UserAutofillParameters(ctx context.Context, user string, templa } var params []UserParameter - return params, json.NewDecoder(res.Body).Decode(¶ms) + return params, ReadBodyAsJSON(res, ¶ms) } // HasFirstUser returns whether the first user has been created. @@ -538,7 +538,7 @@ func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest return CreateFirstUserResponse{}, ReadBodyAsError(res) } var resp CreateFirstUserResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // CreateUser @@ -570,7 +570,7 @@ func (c *Client) CreateUserWithOrgs(ctx context.Context, req CreateUserRequestWi return User{}, ReadBodyAsError(res) } var user User - return user, json.NewDecoder(res.Body).Decode(&user) + return user, ReadBodyAsJSON(res, &user) } // DeleteUser deletes a user. @@ -599,7 +599,7 @@ func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateU return User{}, ReadBodyAsError(res) } var resp User - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // ValidateUserPassword validates the complexity of a user password and that it is secured enough. @@ -613,7 +613,7 @@ func (c *Client) ValidateUserPassword(ctx context.Context, req ValidateUserPassw return ValidateUserPasswordResponse{}, ReadBodyAsError(res) } var resp ValidateUserPasswordResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserStatus sets the user status to the given status @@ -638,7 +638,7 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS } var resp User - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetUserAppearanceSettings fetches the appearance settings for a user. @@ -652,7 +652,7 @@ func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (Us return UserAppearanceSettings{}, ReadBodyAsError(res) } var resp UserAppearanceSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserAppearanceSettings updates the appearance settings for a user. @@ -666,7 +666,7 @@ func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, return UserAppearanceSettings{}, ReadBodyAsError(res) } var resp UserAppearanceSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // GetUserPreferenceSettings fetches the preference settings for a user. @@ -680,7 +680,7 @@ func (c *Client) GetUserPreferenceSettings(ctx context.Context, user string) (Us return UserPreferenceSettings{}, ReadBodyAsError(res) } var resp UserPreferenceSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserPreferenceSettings updates the preference settings for a user. @@ -694,7 +694,7 @@ func (c *Client) UpdateUserPreferenceSettings(ctx context.Context, user string, return UserPreferenceSettings{}, ReadBodyAsError(res) } var resp UserPreferenceSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserPassword updates a user password. @@ -722,7 +722,7 @@ func (c *Client) PostOrganizationMember(ctx context.Context, organizationID uuid return OrganizationMember{}, ReadBodyAsError(res) } var member OrganizationMember - return member, json.NewDecoder(res.Body).Decode(&member) + return member, ReadBodyAsJSON(res, &member) } // DeleteOrganizationMember removes a user from an organization @@ -800,7 +800,7 @@ func (c *Client) OrganizationMember(ctx context.Context, organizationIdent, user return OrganizationMemberWithUserData{}, ReadBodyAsError(res) } var member OrganizationMemberWithUserData - return member, json.NewDecoder(res.Body).Decode(&member) + return member, ReadBodyAsJSON(res, &member) } // OrganizationMembers lists all members in an organization @@ -818,7 +818,7 @@ func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UU return nil, ReadBodyAsError(res) } var members []OrganizationMemberWithUserData - return members, json.NewDecoder(res.Body).Decode(&members) + return members, ReadBodyAsJSON(res, &members) } // OrganizationMembers lists filtered and paginated members in an organization @@ -837,7 +837,7 @@ func (c *Client) OrganizationMembersPaginated(ctx context.Context, organizationI return PaginatedMembersResponse{}, ReadBodyAsError(res) } var membersRes PaginatedMembersResponse - return membersRes, json.NewDecoder(res.Body).Decode(&membersRes) + return membersRes, ReadBodyAsJSON(res, &membersRes) } // UpdateUserRoles grants the userID the specified roles. @@ -852,7 +852,7 @@ func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRol return User{}, ReadBodyAsError(res) } var resp User - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateOrganizationMemberRoles grants the userID the specified roles in an org. @@ -867,7 +867,7 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization return OrganizationMember{}, ReadBodyAsError(res) } var member OrganizationMember - return member, json.NewDecoder(res.Body).Decode(&member) + return member, ReadBodyAsJSON(res, &member) } // UserRoles returns all roles the user has @@ -881,7 +881,7 @@ func (c *Client) UserRoles(ctx context.Context, user string) (UserRoles, error) return UserRoles{}, ReadBodyAsError(res) } var roles UserRoles - return roles, json.NewDecoder(res.Body).Decode(&roles) + return roles, ReadBodyAsJSON(res, &roles) } // UserOIDCClaims returns the merged OIDC claims for the authenticated user. @@ -895,7 +895,7 @@ func (c *Client) UserOIDCClaims(ctx context.Context) (OIDCClaimsResponse, error) return OIDCClaimsResponse{}, ReadBodyAsError(res) } var resp OIDCClaimsResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // LoginWithPassword creates a session token authenticating with an email and password. @@ -910,7 +910,7 @@ func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordReq return LoginWithPasswordResponse{}, ReadBodyAsError(res) } var resp LoginWithPasswordResponse - err = json.NewDecoder(res.Body).Decode(&resp) + err = ReadBodyAsJSON(res, &resp) if err != nil { return LoginWithPasswordResponse{}, err } @@ -965,7 +965,7 @@ func (c *Client) ConvertUserLoginType(ctx context.Context, user string, req Conv return OAuthConversionResponse{}, ReadBodyAsError(res) } var resp OAuthConversionResponse - err = json.NewDecoder(res.Body).Decode(&resp) + err = ReadBodyAsJSON(res, &resp) if err != nil { return OAuthConversionResponse{}, err } @@ -996,7 +996,7 @@ func (c *Client) User(ctx context.Context, userIdent string) (User, error) { return User{}, ReadBodyAsError(res) } var user User - return user, json.NewDecoder(res.Body).Decode(&user) + return user, ReadBodyAsJSON(res, &user) } // UserQuietHoursSchedule returns the quiet hours settings for the user. This @@ -1011,7 +1011,7 @@ func (c *Client) UserQuietHoursSchedule(ctx context.Context, userIdent string) ( return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res) } var resp UserQuietHoursScheduleResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // UpdateUserQuietHoursSchedule updates the quiet hours settings for the user. @@ -1026,7 +1026,7 @@ func (c *Client) UpdateUserQuietHoursSchedule(ctx context.Context, userIdent str return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res) } var resp UserQuietHoursScheduleResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // Users returns all users according to the request parameters. If no parameters are set, @@ -1046,7 +1046,7 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, } var usersRes GetUsersResponse - return usersRes, json.NewDecoder(res.Body).Decode(&usersRes) + return usersRes, ReadBodyAsJSON(res, &usersRes) } // OrganizationsByUser returns all organizations the user is a member of. @@ -1060,7 +1060,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi return nil, ReadBodyAsError(res) } var orgs []Organization - return orgs, json.NewDecoder(res.Body).Decode(&orgs) + return orgs, ReadBodyAsJSON(res, &orgs) } func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, name string) (Organization, error) { @@ -1073,7 +1073,7 @@ func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, nam return Organization{}, ReadBodyAsError(res) } var org Organization - return org, json.NewDecoder(res.Body).Decode(&org) + return org, ReadBodyAsJSON(res, &org) } // AuthMethods returns types of authentication available to the user. @@ -1089,5 +1089,5 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { } var userAuth AuthMethods - return userAuth, json.NewDecoder(res.Body).Decode(&userAuth) + return userAuth, ReadBodyAsJSON(res, &userAuth) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/usersecrets.go b/vendor/github.com/coder/coder/v2/codersdk/usersecrets.go index 43cfd00a4..a340d8e63 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/usersecrets.go +++ b/vendor/github.com/coder/coder/v2/codersdk/usersecrets.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -18,30 +17,42 @@ type UserSecret struct { Description string `json:"description"` EnvName string `json:"env_name"` FilePath string `json:"file_path"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` + // Enabled controls whether the secret is injected into workspaces. + // Disabled secrets remain visible and editable, but are not added + // to the agent manifest, so they are not exposed as environment + // variables or written to secret files. + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` } // CreateUserSecretRequest is the payload for creating a new user -// secret. Name and Value are required. All other fields are optional -// and default to empty string. +// secret. Name and Value are required. An enabled secret must have at +// least one of EnvName or FilePath non-empty so it has an injection +// target; to keep a secret without injecting it, set Enabled to false. +// All other fields are optional and default to empty string. Enabled +// defaults to true when omitted. type CreateUserSecretRequest struct { Name string `json:"name"` Value string `json:"value"` Description string `json:"description,omitempty"` EnvName string `json:"env_name,omitempty"` FilePath string `json:"file_path,omitempty"` + Enabled *bool `json:"enabled,omitempty"` } // UpdateUserSecretRequest is the payload for partially updating a // user secret. At least one field must be non-nil. Pointer fields // distinguish "not sent" (nil) from "set to empty string" (pointer -// to empty string). +// to empty string). If the post-update row is enabled it must still +// have at least one of EnvName or FilePath non-empty; clearing both +// targets is only allowed when the secret is (or becomes) disabled. type UpdateUserSecretRequest struct { Value *string `json:"value,omitempty"` Description *string `json:"description,omitempty"` EnvName *string `json:"env_name,omitempty"` FilePath *string `json:"file_path,omitempty"` + Enabled *bool `json:"enabled,omitempty"` } func (c *Client) CreateUserSecret(ctx context.Context, user string, req CreateUserSecretRequest) (UserSecret, error) { @@ -54,7 +65,7 @@ func (c *Client) CreateUserSecret(ctx context.Context, user string, req CreateUs return UserSecret{}, ReadBodyAsError(res) } var secret UserSecret - return secret, json.NewDecoder(res.Body).Decode(&secret) + return secret, ReadBodyAsJSON(res, &secret) } func (c *Client) UserSecrets(ctx context.Context, user string) ([]UserSecret, error) { @@ -67,7 +78,31 @@ func (c *Client) UserSecrets(ctx context.Context, user string) ([]UserSecret, er return nil, ReadBodyAsError(res) } var secrets []UserSecret - return secrets, json.NewDecoder(res.Body).Decode(&secrets) + return secrets, ReadBodyAsJSON(res, &secrets) +} + +// ImportUserSecretsRequest is the payload for the bulk secret import +// endpoint. Content is the raw file bytes and Format selects the parser. +type ImportUserSecretsRequest struct { + Format SecretsFileFormat `json:"format" validate:"required"` + Content string `json:"content" validate:"required"` +} + +// ImportUserSecrets parses the supplied file content and creates the +// resulting secrets atomically: either all secrets are created or, if +// any entry fails validation, uniqueness, or a per-user limit, none +// are. It returns the created secrets' metadata (never their values). +func (c *Client) ImportUserSecrets(ctx context.Context, user string, req ImportUserSecretsRequest) ([]UserSecret, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/secrets/batch", user), req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return nil, ReadBodyAsError(res) + } + var secrets []UserSecret + return secrets, ReadBodyAsJSON(res, &secrets) } func (c *Client) UserSecretByName(ctx context.Context, user string, name string) (UserSecret, error) { @@ -80,7 +115,7 @@ func (c *Client) UserSecretByName(ctx context.Context, user string, name string) return UserSecret{}, ReadBodyAsError(res) } var secret UserSecret - return secret, json.NewDecoder(res.Body).Decode(&secret) + return secret, ReadBodyAsJSON(res, &secret) } func (c *Client) UpdateUserSecret(ctx context.Context, user string, name string, req UpdateUserSecretRequest) (UserSecret, error) { @@ -93,7 +128,7 @@ func (c *Client) UpdateUserSecret(ctx context.Context, user string, name string, return UserSecret{}, ReadBodyAsError(res) } var secret UserSecret - return secret, json.NewDecoder(res.Body).Decode(&secret) + return secret, ReadBodyAsJSON(res, &secret) } func (c *Client) DeleteUserSecret(ctx context.Context, user string, name string) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/usersecretsimport.go b/vendor/github.com/coder/coder/v2/codersdk/usersecretsimport.go new file mode 100644 index 000000000..f26a725e2 --- /dev/null +++ b/vendor/github.com/coder/coder/v2/codersdk/usersecretsimport.go @@ -0,0 +1,368 @@ +package codersdk + +import ( + "encoding/json" + "errors" + "io" + "strings" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" +) + +// SecretsFileFormat identifies the on-disk format of a secrets file. +type SecretsFileFormat string + +const ( + // SecretsFileFormatEnv is a dotenv-style file of KEY=VALUE lines. + SecretsFileFormatEnv SecretsFileFormat = "env" + // SecretsFileFormatJSON is a flat JSON object of string values. + SecretsFileFormatJSON SecretsFileFormat = "json" + // SecretsFileFormatYAML is a flat YAML mapping of string values. + SecretsFileFormatYAML SecretsFileFormat = "yaml" +) + +// MaxSecretsFileBytes bounds the raw size of a secrets file before parsing. +const MaxSecretsFileBytes = 1 << 20 // 1 MiB + +type secretEntry struct { + key string + value string + line int +} + +// ParseSecretsFile parses a secrets file into CreateUserSecretRequests. +// It checks structure and duplicate keys; per-entry validation is left to +// ValidateCreateUserSecretRequest. EnvName is set only when the key passes +// env-name validation (best-effort; keys like MY-TOKEN or PATH get an empty +// EnvName so they are still imported without env injection). +func ParseSecretsFile(format SecretsFileFormat, content string) ([]CreateUserSecretRequest, error) { + if len(content) > MaxSecretsFileBytes { + return nil, xerrors.Errorf("secrets file exceeds the maximum allowed size of %d bytes", MaxSecretsFileBytes) + } + + switch format { + case SecretsFileFormatEnv, SecretsFileFormatJSON, SecretsFileFormatYAML: + case "": + return nil, xerrors.New("a secrets file format is required") + default: + return nil, xerrors.Errorf("unknown secrets file format %q", format) + } + + if strings.TrimSpace(content) == "" { + return nil, xerrors.New("no secrets found in file") + } + + var ( + entries []secretEntry + err error + ) + switch format { + case SecretsFileFormatEnv: + entries, err = parseEnvSecrets(content) + case SecretsFileFormatJSON: + entries, err = parseJSONSecrets(content) + case SecretsFileFormatYAML: + entries, err = parseYAMLSecrets(content) + } + if err != nil { + return nil, err + } + + if len(entries) == 0 { + return nil, xerrors.New("no secrets found in file") + } + if len(entries) > MaxUserSecretsPerUserCount { + return nil, xerrors.Errorf("secrets file contains %d secrets, which exceeds the maximum of %d secrets per user", len(entries), MaxUserSecretsPerUserCount) + } + + if err := detectDuplicateKeys(entries); err != nil { + return nil, err + } + + reqs := make([]CreateUserSecretRequest, 0, len(entries)) + for _, e := range entries { + req := CreateUserSecretRequest{Name: e.key, Value: e.value} + // env_name uses a partial unique index (WHERE env_name != ''), + // so multiple empty env_names are allowed. + if UserSecretEnvNameValid(e.key) == nil { + req.EnvName = e.key + } else { + // Keys that cannot be env-injected (reserved names, invalid + // identifiers) are imported without an injection target, so + // they must be disabled: an enabled secret always has at + // least one of env_name or file_path set. The user can add + // a target and re-enable the secret afterwards. + disabled := false + req.Enabled = &disabled + } + reqs = append(reqs, req) + } + return reqs, nil +} + +// Duplicate keys are rejected up front (citing the line for env files) +// instead of surfacing as a per-row uniqueness violation later. +func detectDuplicateKeys(entries []secretEntry) error { + seen := make(map[string]struct{}, len(entries)) + for _, e := range entries { + if _, ok := seen[e.key]; ok { + if e.line > 0 { + return xerrors.Errorf("duplicate key %q on line %d", e.key, e.line) + } + return xerrors.Errorf("duplicate key %q", e.key) + } + seen[e.key] = struct{}{} + } + return nil +} + +// parseEnvSecrets parses a dotenv-style file into ordered entries. It supports a +// deliberately small subset of dotenv: +// - KEY=VALUE lines, an optional "export " prefix, and full-line "#" comments. +// - Single-quoted values are literal; double-quoted values support \n, \t, +// \r, \\, and \" escapes and keep unknown escapes literal. +// +// It intentionally does NOT: +// - expand $VAR or ${VAR}. Secrets frequently contain "$", and expansion would +// silently corrupt them. +// - strip inline comments, so PASS=abc#123 keeps the trailing #123. +// - support multiline values. Use the JSON or YAML format for PEM keys or +// certs. +// +// Duplicate keys are an error (see detectDuplicateKeys); a silent last-wins +// would drop a secret. +// +// This is hand-rolled rather than using joho/godotenv or hashicorp/go-envparse +// because those expand variables and/or strip inline comments (silent secret +// corruption) and return unordered maps, so we lose source order, line numbers, +// and duplicate detection. They would also add a dependency to the public +// codersdk package. +func parseEnvSecrets(content string) ([]secretEntry, error) { + content = strings.ReplaceAll(content, "\r\n", "\n") + content = strings.TrimPrefix(content, "\ufeff") + + var entries []secretEntry + for i, raw := range strings.Split(content, "\n") { + lineNum := i + 1 + + if t := strings.TrimSpace(raw); t == "" || strings.HasPrefix(t, "#") { + continue + } + + work := stripExportPrefix(strings.TrimLeft(raw, " \t")) + + eq := strings.IndexByte(work, '=') + if eq < 0 { + return nil, xerrors.Errorf("line %d: expected KEY=VALUE but found no '='", lineNum) + } + + key := strings.TrimSpace(work[:eq]) + if key == "" { + return nil, xerrors.Errorf("line %d: missing key before '='", lineNum) + } + + value, err := parseEnvValue(work[eq+1:], lineNum) + if err != nil { + return nil, err + } + entries = append(entries, secretEntry{key: key, value: value, line: lineNum}) + } + return entries, nil +} + +// stripExportPrefix removes a leading "export " (the word export +// followed by whitespace). A line like "export=foo" is left untouched +// so the key becomes "export". +func stripExportPrefix(s string) string { + const kw = "export" + if !strings.HasPrefix(s, kw) { + return s + } + rest := s[len(kw):] + if rest == "" || (rest[0] != ' ' && rest[0] != '\t') { + return s + } + return strings.TrimLeft(rest, " \t") +} + +func parseEnvValue(rhs string, lineNum int) (string, error) { + v := strings.TrimLeft(rhs, " \t") + if v == "" { + return "", nil + } + switch v[0] { + case '"': + inner, err := doubleQuotedInner(v, lineNum) + if err != nil { + return "", err + } + return unescapeDoubleQuoted(inner), nil + case '\'': + return singleQuotedInner(v, lineNum) + default: + return strings.TrimSpace(v), nil + } +} + +func doubleQuotedInner(v string, lineNum int) (string, error) { + for i := 1; i < len(v); i++ { + if v[i] != '"' || hasOddBackslashRun(v, i) { + continue + } + if strings.Trim(v[i+1:], " \t") != "" { + return "", xerrors.Errorf("line %d: unexpected data after closing double quote", lineNum) + } + return v[1:i], nil + } + return "", xerrors.Errorf("line %d: missing closing double quote", lineNum) +} + +func hasOddBackslashRun(s string, before int) bool { + count := 0 + for i := before - 1; i >= 0 && s[i] == '\\'; i-- { + count++ + } + return count%2 == 1 +} + +// singleQuotedInner returns the content between the opening single +// quote at index 0 and the first closing single quote. Single quotes +// have no escape sequences, so the first quote after the opener always +// closes the value. Only whitespace may follow the closing quote. +func singleQuotedInner(v string, lineNum int) (string, error) { + for i := 1; i < len(v); i++ { + if v[i] != '\'' { + continue + } + if strings.Trim(v[i+1:], " \t") != "" { + return "", xerrors.Errorf("line %d: unexpected data after closing single quote", lineNum) + } + return v[1:i], nil + } + return "", xerrors.Errorf("line %d: missing closing single quote", lineNum) +} + +func unescapeDoubleQuoted(s string) string { + if !strings.Contains(s, "\\") { + return s + } + buf := make([]byte, 0, len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if c != '\\' || i == len(s)-1 { + buf = append(buf, c) + continue + } + switch next := s[i+1]; next { + case 'n': + buf = append(buf, '\n') + case 't': + buf = append(buf, '\t') + case 'r': + buf = append(buf, '\r') + case '\\': + buf = append(buf, '\\') + case '"': + buf = append(buf, '"') + default: + buf = append(buf, '\\', next) + } + i++ + } + return string(buf) +} + +func parseJSONSecrets(content string) ([]secretEntry, error) { + dec := json.NewDecoder(strings.NewReader(content)) + + tok, err := dec.Token() + if err != nil { + return nil, xerrors.Errorf("invalid JSON: %w", err) + } + if delim, ok := tok.(json.Delim); !ok || delim != '{' { + return nil, xerrors.New("JSON content must be an object mapping secret names to string values") + } + + var entries []secretEntry + for dec.More() { + keyTok, err := dec.Token() + if err != nil { + return nil, xerrors.Errorf("invalid JSON: %w", err) + } + key, ok := keyTok.(string) + if !ok { + return nil, xerrors.New("invalid JSON object key") + } + + valTok, err := dec.Token() + if err != nil { + return nil, xerrors.Errorf("invalid JSON: %w", err) + } + switch val := valTok.(type) { + case string: + entries = append(entries, secretEntry{key: key, value: val}) + case json.Delim: + return nil, xerrors.Errorf("value for key %q must be a string, not a nested object or array", key) + default: + return nil, xerrors.Errorf("value for key %q must be a string", key) + } + } + + if _, err := dec.Token(); err != nil { + return nil, xerrors.Errorf("invalid JSON: %w", err) + } + var extra any + if err := dec.Decode(&extra); errors.Is(err, io.EOF) { + return entries, nil + } else if err != nil { + return nil, xerrors.Errorf("invalid JSON: %w", err) + } + return nil, xerrors.New("unexpected trailing data after JSON object") +} + +func parseYAMLSecrets(content string) ([]secretEntry, error) { + dec := yaml.NewDecoder(strings.NewReader(content)) + + var root yaml.Node + if err := dec.Decode(&root); err != nil { + if errors.Is(err, io.EOF) { + return nil, nil + } + return nil, xerrors.Errorf("invalid YAML: %w", err) + } + + var extra yaml.Node + if err := dec.Decode(&extra); err == nil { + return nil, xerrors.New("YAML content must be a single document mapping secret names to string values") + } else if !errors.Is(err, io.EOF) { + return nil, xerrors.Errorf("invalid YAML: %w", err) + } + + if root.Kind == 0 || len(root.Content) == 0 { + return nil, nil + } + + doc := root.Content[0] + if doc.Kind != yaml.MappingNode { + return nil, xerrors.New("YAML content must be a mapping of secret names to string values") + } + + entries := make([]secretEntry, 0, len(doc.Content)/2) + for i := 0; i+1 < len(doc.Content); i += 2 { + keyNode := doc.Content[i] + valNode := doc.Content[i+1] + + if keyNode.Kind != yaml.ScalarNode || (keyNode.Tag != "" && keyNode.Tag != "!!str") { + return nil, xerrors.New("YAML keys must be strings") + } + if valNode.Kind != yaml.ScalarNode { + return nil, xerrors.Errorf("value for key %q must be a string, not a nested mapping or sequence", keyNode.Value) + } + if valNode.Tag != "" && valNode.Tag != "!!str" { + return nil, xerrors.Errorf("value for key %q must be a string (quote the value if it is numeric or boolean)", keyNode.Value) + } + entries = append(entries, secretEntry{key: keyNode.Value, value: valNode.Value, line: keyNode.Line}) + } + return entries, nil +} diff --git a/vendor/github.com/coder/coder/v2/codersdk/usersecretvalidation.go b/vendor/github.com/coder/coder/v2/codersdk/usersecretvalidation.go index d43626e8e..6fdc73058 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/usersecretvalidation.go +++ b/vendor/github.com/coder/coder/v2/codersdk/usersecretvalidation.go @@ -209,6 +209,54 @@ var ( } ) +// UserSecret*Field constants are the canonical ValidationError.Field values +// for user secret fields. UserSecretNameField is also the chi URL parameter +// name used in coderd route segments. +const ( + UserSecretNameField = "name" + UserSecretValueField = "value" + UserSecretEnvNameField = "env_name" + UserSecretFilePathField = "file_path" +) + +// ValidateCreateUserSecretRequest validates a single create-secret request. +func ValidateCreateUserSecretRequest(req CreateUserSecretRequest) []ValidationError { + var validations []ValidationError + if err := UserSecretNameValid(req.Name); err != nil { + validations = append(validations, ValidationError{Field: UserSecretNameField, Detail: err.Error()}) + } + if req.Value == "" { + validations = append(validations, ValidationError{Field: UserSecretValueField, Detail: "Value is required."}) + } else if err := UserSecretValueValid(req.Value); err != nil { + validations = append(validations, ValidationError{Field: UserSecretValueField, Detail: err.Error()}) + } + if err := UserSecretEnvNameValid(req.EnvName); err != nil { + validations = append(validations, ValidationError{Field: UserSecretEnvNameField, Detail: err.Error()}) + } + if err := UserSecretFilePathValid(req.FilePath); err != nil { + validations = append(validations, ValidationError{Field: UserSecretFilePathField, Detail: err.Error()}) + } + // An enabled secret must have an injection target. The agent + // manifest layer relies on this invariant so it can gate injection + // solely on the enabled flag; "stored but not injected" is + // expressed by enabled=false, not by clearing both targets. + // Disabled secrets may have no target (e.g. bulk imports of keys + // that cannot be env-injected). + if req.EnvName == "" && req.FilePath == "" && (req.Enabled == nil || *req.Enabled) { + validations = append(validations, ValidationError{ + Field: UserSecretEnvNameField, + Detail: UserSecretInjectionTargetRequiredDetail, + }) + } + return validations +} + +// UserSecretInjectionTargetRequiredDetail explains the injection-target +// invariant. It is shared by the create validator above and the PATCH +// handler's post-state check in coderd. The value is a user-facing +// validation message, not a credential. +const UserSecretInjectionTargetRequiredDetail = "An enabled secret must have at least one of env_name or file_path set. To keep a secret without injecting it, set enabled to false instead of clearing both targets." //nolint:gosec // G101: message text, not a hardcoded credential. + // UserSecretNameValid validates a user secret name. Names are used in // API route path segments, so they must not include route separators. func UserSecretNameValid(s string) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/userskills.go b/vendor/github.com/coder/coder/v2/codersdk/userskills.go index 796e9d504..5586b7623 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/userskills.go +++ b/vendor/github.com/coder/coder/v2/codersdk/userskills.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "net/url" @@ -61,7 +60,7 @@ func (c *ExperimentalClient) CreateUserSkill(ctx context.Context, user string, r return UserSkill{}, ReadBodyAsError(res) } var skill UserSkill - return skill, json.NewDecoder(res.Body).Decode(&skill) + return skill, ReadBodyAsJSON(res, &skill) } // UserSkills lists user skill metadata for the specified user. @@ -75,7 +74,7 @@ func (c *ExperimentalClient) UserSkills(ctx context.Context, user string) ([]Use return nil, ReadBodyAsError(res) } var skills []UserSkillMetadata - return skills, json.NewDecoder(res.Body).Decode(&skills) + return skills, ReadBodyAsJSON(res, &skills) } // UserSkillByName returns a user skill by name. @@ -89,7 +88,7 @@ func (c *ExperimentalClient) UserSkillByName(ctx context.Context, user string, n return UserSkill{}, ReadBodyAsError(res) } var skill UserSkill - return skill, json.NewDecoder(res.Body).Decode(&skill) + return skill, ReadBodyAsJSON(res, &skill) } // UpdateUserSkill replaces a user skill's raw Markdown content. @@ -103,7 +102,7 @@ func (c *ExperimentalClient) UpdateUserSkill(ctx context.Context, user string, n return UserSkill{}, ReadBodyAsError(res) } var skill UserSkill - return skill, json.NewDecoder(res.Body).Decode(&skill) + return skill, ReadBodyAsJSON(res, &skill) } // DeleteUserSkill deletes a user skill by name. diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspaceagentportshare.go b/vendor/github.com/coder/coder/v2/codersdk/workspaceagentportshare.go index fe5509451..72549ce44 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspaceagentportshare.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspaceagentportshare.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" @@ -118,7 +117,7 @@ func (c *Client) GetWorkspaceAgentPortShares(ctx context.Context, workspaceID uu return shares, ReadBodyAsError(res) } - return shares, json.NewDecoder(res.Body).Decode(&shares) + return shares, ReadBodyAsJSON(res, &shares) } func (c *Client) UpsertWorkspaceAgentPortShare(ctx context.Context, workspaceID uuid.UUID, req UpsertWorkspaceAgentPortShareRequest) (WorkspaceAgentPortShare, error) { @@ -132,7 +131,7 @@ func (c *Client) UpsertWorkspaceAgentPortShare(ctx context.Context, workspaceID return share, ReadBodyAsError(res) } - return share, json.NewDecoder(res.Body).Decode(&share) + return share, ReadBodyAsJSON(res, &share) } func (c *Client) DeleteWorkspaceAgentPortShare(ctx context.Context, workspaceID uuid.UUID, req DeleteWorkspaceAgentPortShareRequest) error { diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspaceagents.go b/vendor/github.com/coder/coder/v2/codersdk/workspaceagents.go index fa246fc39..653919c2f 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspaceagents.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspaceagents.go @@ -170,6 +170,11 @@ type WorkspaceAgent struct { DisplayApps []DisplayApp `json:"display_apps"` LogSources []WorkspaceAgentLogSource `json:"log_sources"` Scripts []WorkspaceAgentScript `json:"scripts"` + // Metadata is only populated on the workspaces list endpoint when the + // request opts in with the include_agent_metadata search key, and it + // only carries the requested keys. The description's script is always + // empty here: it can be long, and list consumers want values. + Metadata []WorkspaceAgentMetadata `json:"metadata,omitempty"` // StartupScriptBehavior is a legacy field that is deprecated in favor // of the `coder_script` resource. It's only referenced by old clients. @@ -365,7 +370,7 @@ func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAge return WorkspaceAgent{}, ReadBodyAsError(res) } var workspaceAgent WorkspaceAgent - err = json.NewDecoder(res.Body).Decode(&workspaceAgent) + err = ReadBodyAsJSON(res, &workspaceAgent) if err != nil { return WorkspaceAgent{}, err } @@ -392,7 +397,7 @@ func (c *Client) IssueReconnectingPTYSignedToken(ctx context.Context, req IssueR return IssueReconnectingPTYSignedTokenResponse{}, ReadBodyAsError(res) } var resp IssueReconnectingPTYSignedTokenResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } type WorkspaceAgentListeningPortsResponse struct { @@ -421,7 +426,7 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid. return WorkspaceAgentListeningPortsResponse{}, ReadBodyAsError(res) } var listeningPorts WorkspaceAgentListeningPortsResponse - return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts) + return listeningPorts, ReadBodyAsJSON(res, &listeningPorts) } // WorkspaceAgentDevcontainerStatus is the status of a devcontainer. @@ -582,7 +587,7 @@ func (c *Client) WorkspaceAgentListContainers(ctx context.Context, agentID uuid. } var cr WorkspaceAgentListContainersResponse - return cr, json.NewDecoder(res.Body).Decode(&cr) + return cr, ReadBodyAsJSON(res, &cr) } func (c *Client) WatchWorkspaceAgentContainers(ctx context.Context, agentID uuid.UUID) (<-chan WorkspaceAgentListContainersResponse, io.Closer, error) { @@ -643,7 +648,7 @@ func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID return Response{}, ReadBodyAsError(res) } var m Response - if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + if err := ReadBodyAsJSON(res, &m); err != nil { return Response{}, xerrors.Errorf("decode response body: %w", err) } return m, nil @@ -679,7 +684,7 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID, } var logs []WorkspaceAgentLog - err = json.NewDecoder(resp.Body).Decode(&logs) + err = ReadBodyAsJSON(resp, &logs) if err != nil { return nil, nil, xerrors.Errorf("decode startup logs: %w", err) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspacebuilds.go b/vendor/github.com/coder/coder/v2/codersdk/workspacebuilds.go index 518088575..2b8110d74 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspacebuilds.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspacebuilds.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -147,7 +146,7 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild - return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) + return workspaceBuild, ReadBodyAsJSON(res, &workspaceBuild) } type CancelWorkspaceBuildStatus string @@ -233,7 +232,7 @@ func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx cont return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild - return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) + return workspaceBuild, ReadBodyAsJSON(res, &workspaceBuild) } func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) ([]WorkspaceBuildParameter, error) { @@ -246,7 +245,7 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) return nil, ReadBodyAsError(res) } var params []WorkspaceBuildParameter - return params, json.NewDecoder(res.Body).Decode(¶ms) + return params, ReadBodyAsJSON(res, ¶ms) } type TimingStage string @@ -313,5 +312,5 @@ func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (Wo return WorkspaceBuildTimings{}, ReadBodyAsError(res) } var timings WorkspaceBuildTimings - return timings, json.NewDecoder(res.Body).Decode(&timings) + return timings, ReadBodyAsJSON(res, &timings) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspaceproxy.go b/vendor/github.com/coder/coder/v2/codersdk/workspaceproxy.go index 6c780988a..bc0279552 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspaceproxy.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspaceproxy.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -86,7 +85,7 @@ func (c *Client) CreateWorkspaceProxy(ctx context.Context, req CreateWorkspacePr return UpdateWorkspaceProxyResponse{}, ReadBodyAsError(res) } var resp UpdateWorkspaceProxyResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) WorkspaceProxies(ctx context.Context) (RegionsResponse[WorkspaceProxy], error) { @@ -104,7 +103,7 @@ func (c *Client) WorkspaceProxies(ctx context.Context) (RegionsResponse[Workspac } var proxies RegionsResponse[WorkspaceProxy] - return proxies, json.NewDecoder(res.Body).Decode(&proxies) + return proxies, ReadBodyAsJSON(res, &proxies) } type PatchWorkspaceProxy struct { @@ -129,7 +128,7 @@ func (c *Client) PatchWorkspaceProxy(ctx context.Context, req PatchWorkspaceProx return UpdateWorkspaceProxyResponse{}, ReadBodyAsError(res) } var resp UpdateWorkspaceProxyResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) DeleteWorkspaceProxyByName(ctx context.Context, name string) error { @@ -168,7 +167,7 @@ func (c *Client) WorkspaceProxyByName(ctx context.Context, name string) (Workspa } var resp WorkspaceProxy - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } func (c *Client) WorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) { @@ -217,5 +216,5 @@ func (c *Client) Regions(ctx context.Context) ([]Region, error) { } var regions RegionsResponse[Region] - return regions.Regions, json.NewDecoder(res.Body).Decode(®ions) + return regions.Regions, ReadBodyAsJSON(res, ®ions) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspaces.go b/vendor/github.com/coder/coder/v2/codersdk/workspaces.go index 074acb6f9..94ab3dee8 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspaces.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspaces.go @@ -136,6 +136,34 @@ type CreateWorkspaceBuildRequest struct { TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` // Reason sets the reason for the workspace build. Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection task_manual_pause"` + // OnSuccess queues a follow-up workspace build after this build succeeds. + // It currently supports restarting a workspace by starting it after a + // successful stop build. + OnSuccess *CreateWorkspaceBuildOnSuccessRequest `json:"on_success,omitempty"` +} + +// CreateWorkspaceBuildOnSuccessRequest queues a follow-up build that +// runs after the parent build succeeds. It currently supports +// restarting a workspace: the parent build must be a "stop" and this +// child build a "start". The child build inherits LogLevel and Reason +// from the parent CreateWorkspaceBuildRequest. +type CreateWorkspaceBuildOnSuccessRequest struct { + // TemplateVersionID pins the child build to a specific template + // version. Pinning requires permission to update the template, + // since the active version may change before the child build + // runs. When empty, the child build uses the template's active + // version at the time it runs. + TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"` + // Transition must be "start". The parent build's transition must + // be "stop". + Transition WorkspaceTransition `json:"transition" validate:"oneof=start,required"` + // RichParameterValues are applied to the child build. Parameters + // not listed here fall back to their values from the previous + // build, matching normal build behavior. + RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` + // TemplateVersionPresetID selects a preset for the child build. + // It requires TemplateVersionID to also be set. + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } type WorkspaceOptions struct { @@ -177,7 +205,7 @@ func (c *Client) getWorkspace(ctx context.Context, id uuid.UUID, opts ...Request return Workspace{}, ReadBodyAsError(res) } var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) + return workspace, ReadBodyAsJSON(res, &workspace) } type WorkspaceBuildsRequest struct { @@ -190,7 +218,7 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, req WorkspaceBuildsRequest res, err := c.Request( ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", req.WorkspaceID), - nil, req.Pagination.asRequestOption(), WithQueryParam("since", req.Since.Format(time.RFC3339)), + nil, req.asRequestOption(), WithQueryParam("since", req.Since.Format(time.RFC3339)), ) if err != nil { return nil, err @@ -200,7 +228,7 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, req WorkspaceBuildsRequest return nil, ReadBodyAsError(res) } var workspaceBuild []WorkspaceBuild - return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) + return workspaceBuild, ReadBodyAsJSON(res, &workspaceBuild) } // CreateWorkspaceBuild queues a new build to occur for a workspace. @@ -214,7 +242,7 @@ func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, return WorkspaceBuild{}, ReadBodyAsError(res) } var workspaceBuild WorkspaceBuild - return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) + return workspaceBuild, ReadBodyAsJSON(res, &workspaceBuild) } func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Workspace, error) { @@ -532,6 +560,9 @@ type WorkspaceFilter struct { SharedWithUser string `json:"shared_with_user,omitempty" typescript:"-"` // SharedWithGroup is the group name, group ID, or / of the group that the workspace is shared with SharedWithGroup string `json:"shared_with_group,omitempty" typescript:"-"` + // IncludeAgentMetadata expands each agent in the response with the + // named metadata keys. It does not filter the returned workspaces. + IncludeAgentMetadata []string `json:"include_agent_metadata,omitempty" typescript:"-"` // FilterQuery supports a raw filter query string FilterQuery string `json:"q,omitempty"` } @@ -567,6 +598,9 @@ func (f WorkspaceFilter) asRequestOption() RequestOption { if f.SharedWithGroup != "" { params = append(params, fmt.Sprintf("shared_with_group:%q", f.SharedWithGroup)) } + for _, key := range f.IncludeAgentMetadata { + params = append(params, fmt.Sprintf("include_agent_metadata:%q", key)) + } if f.FilterQuery != "" { // If custom stuff is added, just add it on here. params = append(params, f.FilterQuery) @@ -595,7 +629,7 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) (Worksp } var wres WorkspacesResponse - return wres, json.NewDecoder(res.Body).Decode(&wres) + return wres, ReadBodyAsJSON(res, &wres) } // WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. @@ -615,7 +649,7 @@ func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, owner string, name } var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) + return workspace, ReadBodyAsJSON(res, &workspace) } // SplitWorkspaceIdentifier splits an identifier into owner and @@ -680,7 +714,7 @@ func (c *Client) WorkspaceQuota(ctx context.Context, organizationID string, user return WorkspaceQuota{}, ReadBodyAsError(res) } var quota WorkspaceQuota - return quota, json.NewDecoder(res.Body).Decode("a) + return quota, ReadBodyAsJSON(res, "a) } type ResolveAutostartResponse struct { @@ -697,7 +731,7 @@ func (c *Client) ResolveAutostart(ctx context.Context, workspaceID string) (Reso return ResolveAutostartResponse{}, ReadBodyAsError(res) } var response ResolveAutostartResponse - return response, json.NewDecoder(res.Body).Decode(&response) + return response, ReadBodyAsJSON(res, &response) } func (c *Client) FavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) error { @@ -735,7 +769,7 @@ func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceB return WorkspaceBuildTimings{}, ReadBodyAsError(res) } var timings WorkspaceBuildTimings - return timings, json.NewDecoder(res.Body).Decode(&timings) + return timings, ReadBodyAsJSON(res, &timings) } type WorkspaceACL struct { @@ -786,7 +820,7 @@ func (c *Client) WorkspaceACL(ctx context.Context, workspaceID uuid.UUID) (Works return WorkspaceACL{}, ReadBodyAsError(res) } var acl WorkspaceACL - return acl, json.NewDecoder(res.Body).Decode(&acl) + return acl, ReadBodyAsJSON(res, &acl) } type UpdateWorkspaceACL struct { @@ -841,7 +875,7 @@ func (c *Client) WorkspaceExternalAgentCredentials(ctx context.Context, workspac return ExternalAgentCredentials{}, ReadBodyAsError(res) } var credentials ExternalAgentCredentials - return credentials, json.NewDecoder(res.Body).Decode(&credentials) + return credentials, ReadBodyAsJSON(res, &credentials) } // WorkspaceBuildUpdate contains information about a workspace build state change. @@ -913,5 +947,5 @@ func (c *Client) WorkspaceAvailableUsers(ctx context.Context, organizationID uui return nil, ReadBodyAsError(res) } var users []MinimalUser - return users, json.NewDecoder(res.Body).Decode(&users) + return users, ReadBodyAsJSON(res, &users) } diff --git a/vendor/github.com/coder/coder/v2/codersdk/workspacesharing.go b/vendor/github.com/coder/coder/v2/codersdk/workspacesharing.go index b4e9dc662..43a16817e 100644 --- a/vendor/github.com/coder/coder/v2/codersdk/workspacesharing.go +++ b/vendor/github.com/coder/coder/v2/codersdk/workspacesharing.go @@ -2,7 +2,6 @@ package codersdk import ( "context" - "encoding/json" "fmt" "net/http" ) @@ -56,7 +55,7 @@ func (c *Client) WorkspaceSharingSettings(ctx context.Context, orgID string) (Wo return WorkspaceSharingSettings{}, ReadBodyAsError(res) } var resp WorkspaceSharingSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } // PatchWorkspaceSharingSettings modifies the workspace sharing settings for an organization. @@ -71,5 +70,5 @@ func (c *Client) PatchWorkspaceSharingSettings(ctx context.Context, orgID string return WorkspaceSharingSettings{}, ReadBodyAsError(res) } var resp WorkspaceSharingSettings - return resp, json.NewDecoder(res.Body).Decode(&resp) + return resp, ReadBodyAsJSON(res, &resp) } diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go new file mode 100644 index 000000000..f443bf154 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go @@ -0,0 +1,65 @@ +// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. +// +// Construct a [Provider] and [oauth2.Config] through the identity provider's +// discovery document with [NewProvider], and construct an ID Token verifier +// with [Provider.Verifier]: +// +// provider, err := oidc.NewProvider(ctx, "https://accounts.google.com") +// if err != nil { +// // handle error +// } +// +// // Configure an OpenID Connect aware OAuth2 client. +// oauth2Config := oauth2.Config{ +// ClientID: clientID, +// ClientSecret: clientSecret, +// RedirectURL: redirectURL, +// // Discovery returns the OAuth2 endpoints. +// Endpoint: provider.Endpoint(), +// // "openid" is a required scope for OpenID Connect flows. +// Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}, +// } +// +// idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: clientID}) +// +// OAuth 2.0 redirects then opt into the OpenID Connect flow with [ScopeOpenID]: +// +// func handleRedirect(w http.ResponseWriter, r *http.Request) { +// http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound) +// } +// +// When handling an OAuth 2.0 response, an [IDTokenVerifier] can be used to +// validate the "id_token" payload, containing well-known fields such as the +// user's email, name, and picture URL: +// +// func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { +// // Verify state and other OAuth 2.0 responses. +// +// // Perform standard token exchange. +// oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code")) +// if err != nil { +// // ... +// } +// // Extract the ID Token from OAuth2 token. +// rawIDToken, ok := oauth2Token.Extra("id_token").(string) +// if !ok { +// // ... +// } +// // Parse and verify ID Token payload. +// idToken, err := idTokenVerifier.Verify(r.Context(), rawIDToken) +// if err != nil { +// // ... +// } +// // Parse well-known claims from the token. +// // https://openid.net/specs/openid-connect-core-1_0.html#Claims +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// Name string `json:"name"` +// Picture string `json:"picture"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // ... +// } +// } +package oidc diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go index f42d37d48..fbcc5406c 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go @@ -30,3 +30,11 @@ var allAlgs = []jose.SignatureAlgorithm{ jose.PS512, jose.EdDSA, } + +var supportedJOSEAlgs = map[string]bool{} + +func init() { + for _, alg := range allAlgs { + supportedJOSEAlgs[string(alg)] = true + } +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go index d892d54e6..eab30aabb 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "encoding/json" "errors" "fmt" "io" @@ -17,8 +18,8 @@ import ( // StaticKeySet is a verifier that validates JWT against a static set of public keys. type StaticKeySet struct { - // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and - // *ecdsa.PublicKey. + // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey, + // *ecdsa.PublicKey, and ed25519.PublicKey. PublicKeys []crypto.PublicKey } @@ -53,8 +54,10 @@ func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, // exposed for providers that don't support discovery or to prevent round trips to the // discovery URL. // -// The returned KeySet is a long lived verifier that caches keys based on any -// keys change. Reuse a common remote key set instead of creating new ones as needed. +// The returned KeySet is a long lived verifier that caches keys in memory, +// re-fetching from the remote URL when it encounters a key ID it hasn't seen. +// Reuse a single remote key set rather than creating a new one for each +// verification. func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet { return newRemoteKeySet(ctx, jwksURL) } @@ -123,7 +126,7 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { return i.keys, i.err } -// paresdJWTKey is a context key that allows common setups to avoid parsing the +// parsedJWTKey is a context key that allows common setups to avoid parsing the // JWT twice. It holds a *jose.JSONWebSignature value. var parsedJWTKey contextKey @@ -233,6 +236,55 @@ func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e } } +// jwkJSON implements custom logic for unmarshaling JSON Web Key Sets. +type jwkJSON struct { + Keys []jose.JSONWebKey +} + +func (j *jwkJSON) UnmarshalJSON(data []byte) error { + var raw struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + // For each key, attempt to determine if the algorithm is supported before + // unmarshaling the key. This allows us to ignore keys with unsupported + // algorithms, rather than failing to load the entire set. + for _, key := range raw.Keys { + var metadata struct { + Alg string `json:"alg"` + } + if err := json.Unmarshal(key, &metadata); err != nil { + return err + } + if metadata.Alg != "" { + // Algorithms are technically optional, but if the key advertises an + // algorithm we don't support, ignore it rather than failing to load + // the entire key set. + // + // https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + // + // This skip is currently implemented due to secp256k1, which some + // providers use for signing with "ES256K". While we could also + // ignore the curve, this seems like a more general check in case + // providers start throwing in post-quantum algorithims or something + // like that. + // + // https://github.com/coreos/go-oidc/issues/490 + if !supportedJOSEAlgs[metadata.Alg] { + continue + } + } + var jwk jose.JSONWebKey + if err := json.Unmarshal(key, &jwk); err != nil { + return err + } + j.Keys = append(j.Keys, jwk) + } + return nil +} + func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { req, err := http.NewRequest("GET", r.jwksURL, nil) if err != nil { @@ -255,7 +307,7 @@ func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body) } - var keySet jose.JSONWebKeySet + var keySet jwkJSON err = unmarshalResp(resp, body, &keySet) if err != nil { return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body) diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go b/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go index 2edeaa244..41f077208 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "time" ) @@ -37,7 +38,7 @@ type LogoutToken struct { Expiry time.Time // Optional session ID claim ("sid"). // - // The exact semantics of session IDs very between identity providers. Use + // The exact semantics of session IDs vary between identity providers. Use // your provider's documentation to determine what this correlates to and // how it should be handled. SessionID string @@ -98,7 +99,7 @@ type logoutTokenJSON struct { // } // verifier := provider.Verifier(oidcConfig) // -// mux.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Reequest) { +// mux.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Request) { // rawLogoutToken := r.PostFormValue("logout_token") // if rawLogoutToken == "" { // // ... @@ -163,7 +164,7 @@ func (v *IDTokenVerifier) VerifyLogout(ctx context.Context, rawLogoutToken strin if !v.config.SkipClientIDCheck { if v.config.ClientID != "" { - if !contains(t.Audience, v.config.ClientID) { + if !slices.Contains(t.Audience, v.config.ClientID) { return nil, fmt.Errorf("oidc: expected logout token audience %q got %q", v.config.ClientID, t.Audience) } } else { diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go index 2659518cc..15c3a97f0 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go @@ -1,4 +1,3 @@ -// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. package oidc import ( @@ -24,6 +23,26 @@ const ( // ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests. ScopeOpenID = "openid" + // ScopeProfile can be used to request information about the user's profile, + // such as "name", "picture", etc. + // + // The exact set of claims supported by identity providers differs widely, + // though "name" and "picture" are commonly returned. + // + // See: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + ScopeProfile = "profile" + + // ScopeEmail can be used to request the user's email address through the + // "email" and "email_verified" claims. + // + // What it means to verify an email isn't well defined. Clients can + // generally throw out emails when the "emvail_verified" claim is false, but + // should consult identity provider specific docs if attempting to ensure + // that the user controls the returned email address. + // + // See: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + ScopeEmail = "email" + // ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting // OAuth2 refresh tokens. // @@ -92,7 +111,24 @@ func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { return client.Do(req.WithContext(ctx)) } -// Provider represents an OpenID Connect server's configuration. +// Provider represents an OpenID Connect server's configuration, fetched from +// the discovery document. +// +// To access fields in the discovery document that aren't exposed directly +// through this package's API, use the [Provider.Claims] method. For example, to +// access the registration or end session endpoints: +// +// p, err := oidc.NewProvider(ctx, "https://issuer.example.com") +// if err != nil { +// // ... +// } +// var metadata struct { +// EndSessionEndpoint string `json:"end_session_endpoint"` +// RegistrationEndpoint string `json:"registration_endpoint"` +// } +// if err := p.Claims(&metadata); err != nil { +// // ... +// } type Provider struct { issuer string authURL string @@ -213,6 +249,8 @@ type ProviderConfig struct { // // The provided context is only used for [http.Client] configuration through // [ClientContext], not cancelation. +// +// For providers that implement discovery, use [NewProvider] instead. func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { return &Provider{ issuer: p.IssuerURL, @@ -226,12 +264,36 @@ func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { } } +// IssuerMismatchError is returned by [NewProvider] when the "iss" value +// reported by the upstream is different than the expected value. +// +// Issuer mismatches can occur due to trailing slashes ("https://example.com" +// vs. "https://example.com/") or represent significant misconfiguration for +// multi-tenant issuers. +// +// Issuers must match exactly as they are also used to validate ID Tokens. +// +// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +type IssuerMismatchError struct { + // The value provided to this package. The expected value. + Provided string + // The value advertised by the discovery document. + Discovered string +} + +func (e *IssuerMismatchError) Error() string { + return fmt.Sprintf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", e.Provided, e.Discovered) +} + // NewProvider uses the OpenID Connect discovery mechanism to construct a Provider. // The issuer is the URL identifier for the service. For example: "https://accounts.google.com" // or "https://login.salesforce.com". // +// If the "iss" value returned in the discovery document doesn't match the value +// provided here, [IssuerMismatchError] is returned. +// // OpenID Connect providers that don't implement discovery or host the discovery -// document at a non-spec complaint path (such as requiring a URL parameter), +// document at a non-spec compliant path (such as requiring a URL parameter), // should use [ProviderConfig] instead. // // See: https://openid.net/specs/openid-connect-discovery-1_0.html @@ -267,7 +329,10 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) { issuerURL = issuer } if p.Issuer != issuerURL && !skipIssuerValidation { - return nil, fmt.Errorf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", issuer, p.Issuer) + return nil, &IssuerMismatchError{ + Provided: issuerURL, + Discovered: p.Issuer, + } } var algs []string for _, a := range p.Algorithms { @@ -301,7 +366,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) { // // For a list of fields defined by the OpenID Connect spec see: // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata -func (p *Provider) Claims(v interface{}) error { +func (p *Provider) Claims(v any) error { if p.rawClaims == nil { return errors.New("oidc: claims not set") } @@ -340,7 +405,7 @@ type userInfoRaw struct { } // Claims unmarshals the raw JSON object claims into the provided object. -func (u *UserInfo) Claims(v interface{}) error { +func (u *UserInfo) Claims(v any) error { if u.claims == nil { return errors.New("oidc: claims not set") } @@ -348,6 +413,44 @@ func (u *UserInfo) Claims(v interface{}) error { } // UserInfo uses the token source to query the provider's user info endpoint. +// +// It's fewer round trips and better supported to validate the ID Token with +// [Provider.Verifier], rather than using the UserInfo endpoint. The ID Token +// contains all information [UserInfo] provides: +// +// p, err := oidc.NewProvider(ctx, "https://issuer.example.com") +// if err != nil { +// // ... +// } +// config := &oidc.Config{ +// ClientID: clientID, +// } +// v := p.Verifier(config) +// http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { +// oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code")) +// if err != nil { +// // ... +// } +// rawIDToken, ok := oauth2Token.Extra("id_token").(string) +// if !ok { +// // ... +// } +// idToken, err := verifier.Verify(ctx, rawIDToken) +// if err != nil { +// // ... +// } +// // https://openid.net/specs/openid-connect-core-1_0.html#Claims +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// Name string `json:"name"` +// Picture string `json:"picture"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // ... +// } +// // Use claims... +// }) func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) { if p.userInfoURL == "" { return nil, errors.New("oidc: user info endpoint is not supported by this provider") @@ -425,7 +528,7 @@ type IDToken struct { // A unique string which identifies the end user. Subject string - // Expiry of the token. Ths package will not process tokens that have + // Expiry of the token. This package will not process tokens that have // expired unless that validation is explicitly turned off. Expiry time.Time // When the token was issued by the provider. @@ -433,7 +536,7 @@ type IDToken struct { // Initial nonce provided during the authentication redirect. // - // This package does NOT provided verification on the value of this field + // This package does NOT provide verification on the value of this field // and it's the user's responsibility to ensure it contains a valid value. Nonce string @@ -465,15 +568,15 @@ type IDToken struct { // if err := idToken.Claims(&claims); err != nil { // // handle error // } -func (i *IDToken) Claims(v interface{}) error { +func (i *IDToken) Claims(v any) error { if i.claims == nil { return errors.New("oidc: claims not set") } return json.Unmarshal(i.claims, v) } -// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token -// matches the hash in the id token. It returns an error if the hashes don't match. +// VerifyAccessToken verifies that the hash of the access token that corresponds to the ID token +// matches the hash in the ID token. It returns an error if the hashes don't match. // It is the caller's responsibility to ensure that the optional access token hash is present for the ID token // before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken func (i *IDToken) VerifyAccessToken(accessToken string) error { @@ -570,7 +673,7 @@ func (j *jsonTime) UnmarshalJSON(b []byte) error { return nil } -func unmarshalResp(r *http.Response, body []byte, v interface{}) error { +func unmarshalResp(r *http.Response, body []byte, v any) error { err := json.Unmarshal(body, &v) if err == nil { return nil diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go index 895721e2b..1aa2bd740 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "slices" "time" jose "github.com/go-jose/go-jose/v4" @@ -17,10 +18,10 @@ const ( issuerGoogleAccountsNoScheme = "accounts.google.com" ) -// TokenExpiredError indicates that Verify or VerifyLogout failed because the -// token was expired. This error does NOT indicate that the token is not also -// invalid for other reasons. Other checks might have failed if the expiration -// check had not failed. +// TokenExpiredError indicates that [IDTokenVerifier.Verify] or +// [IDTokenVerifier.VerifyLogout] failed because the token was expired. This +// error does NOT indicate that the token is not also invalid for other reasons. +// Other checks might have failed if the expiration check had not failed. type TokenExpiredError struct { // Expiry is the time when the token expired. Expiry time.Time @@ -30,7 +31,7 @@ func (e *TokenExpiredError) Error() string { return fmt.Sprintf("oidc: token is expired (Token Expiry: %v)", e.Expiry) } -// KeySet is a set of publc JSON Web Keys that can be used to validate the signature +// KeySet is a set of public JSON Web Keys that can be used to validate the signature // of JSON web tokens. This is expected to be backed by a remote key set through // provider metadata discovery or an in-memory set of keys delivered out-of-band. type KeySet interface { @@ -55,8 +56,8 @@ type IDTokenVerifier struct { // NewVerifier returns a verifier manually constructed from a key set and issuer URL. // // It's easier to use provider discovery to construct an IDTokenVerifier than creating -// one directly. This method is intended to be used with provider that don't support -// metadata discovery, or avoiding round trips when the key set URL is already known. +// one directly. This method is intended to be used with providers that don't support +// metadata discovery, or to avoid round trips when the key set URL is already known. // // This constructor can be used to create a verifier directly using the issuer URL and // JSON Web Key Set URL without using discovery: @@ -82,8 +83,8 @@ type Config struct { ClientID string // If specified, only this set of algorithms may be used to sign the JWT. // - // If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this - // defaults to the set of algorithms the provider supports. Otherwise this values + // If the IDTokenVerifier is created from a provider with [Provider.Verifier], this + // defaults to the set of algorithms the provider supports. Otherwise this value // defaults to RS256. SupportedSigningAlgs []string @@ -92,7 +93,7 @@ type Config struct { // If true, token expiry is not checked. SkipExpiryCheck bool - // SkipIssuerCheck is intended for specialized cases where the the caller wishes to + // SkipIssuerCheck is intended for specialized cases where the caller wishes to // defer issuer validation. When enabled, callers MUST independently verify the Token's // Issuer is a known good value. // @@ -117,8 +118,9 @@ type Config struct { } // VerifierContext returns an IDTokenVerifier that uses the provider's key set to -// verify JWTs. As opposed to Verifier, the context is used to configure requests -// to the upstream JWKs endpoint. The provided context's cancellation is ignored. +// verify JWTs. As opposed to [Provider.Verifier], the context is used to configure +// requests to the upstream JWKs endpoint. The provided context's cancellation is +// ignored. func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDTokenVerifier { return p.newVerifier(NewRemoteKeySet(ctx, p.jwksURL), config) } @@ -126,7 +128,7 @@ func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDToken // Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs. // // The returned verifier uses a background context for all requests to the upstream -// JWKs endpoint. To control that context, use VerifierContext instead. +// JWKs endpoint. To control that context, use [Provider.VerifierContext] instead. func (p *Provider) Verifier(config *Config) *IDTokenVerifier { return p.newVerifier(p.remoteKeySet(), config) } @@ -142,15 +144,6 @@ func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier { return NewVerifier(p.issuer, keySet, config) } -func contains(sli []string, ele string) bool { - for _, s := range sli { - if s == ele { - return true - } - } - return false -} - // Returns the Claims from the distributed JWT token func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) { req, err := http.NewRequest("GET", src.Endpoint, nil) @@ -187,7 +180,7 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src // Verify parses a raw ID Token, verifies it's been signed by the provider, performs // any additional checks depending on the Config, and returns the payload. // -// Verify does NOT do nonce validation, which is the callers responsibility. +// Verify does NOT do nonce validation, which is the caller's responsibility. // // See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // @@ -257,7 +250,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok // This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party). if !v.config.SkipClientIDCheck { if v.config.ClientID != "" { - if !contains(t.Audience, v.config.ClientID) { + if !slices.Contains(t.Audience, v.config.ClientID) { return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) } } else { diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_freebsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_freebsd.go index d69775596..11af74db6 100644 --- a/vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_freebsd.go +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/zsymbols_freebsd.go @@ -16,16 +16,16 @@ import "unsafe" //go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so.7" //go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so.7" //go:cgo_import_dynamic purego_abort abort "libc.so.7" -//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libpthread.so" -//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so" -//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libpthread.so" -//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libpthread.so" -//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so" -//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libpthread.so" -//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libpthread.so" -//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libpthread.so" -//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libpthread.so" -//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_create pthread_create "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libthr.so.3" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libthr.so.3" //go:nosplit //go:norace diff --git a/vendor/github.com/google/jsonschema-go/jsonschema/infer.go b/vendor/github.com/google/jsonschema-go/jsonschema/infer.go index 9c195f579..1b3d975df 100644 --- a/vendor/github.com/google/jsonschema-go/jsonschema/infer.go +++ b/vendor/github.com/google/jsonschema-go/jsonschema/infer.go @@ -7,6 +7,7 @@ package jsonschema import ( + "encoding" "fmt" "log/slog" "maps" @@ -199,14 +200,12 @@ func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas ma // Unrestricted case reflect.Map: - if t.Key().Kind() != reflect.String { + if t.Key().Kind() != reflect.String && !t.Key().Implements(reflect.TypeFor[encoding.TextMarshaler]()) { if ignore { return nil, nil // ignore } return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind()) } - if t.Key().Kind() != reflect.String { - } s.Type = "object" s.AdditionalProperties, err = forType(t.Elem(), seen, ignore, schemas) if err != nil { diff --git a/vendor/github.com/google/jsonschema-go/jsonschema/validate.go b/vendor/github.com/google/jsonschema-go/jsonschema/validate.go index bbef5906c..03fe40936 100644 --- a/vendor/github.com/google/jsonschema-go/jsonschema/validate.go +++ b/vendor/github.com/google/jsonschema-go/jsonschema/validate.go @@ -262,14 +262,15 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an } if schema.AnyOf != nil { // We must visit them all, to collect annotations. - ok := false + var errs []error for _, ss := range schema.AnyOf { - if valid(ss, &anns) { - ok = true + if err := st.validate(instance, ss, &anns); err != nil { + errs = append(errs, err) } } - if !ok { - return fmt.Errorf("anyOf: did not validate against any of %v", schema.AnyOf) + if len(errs) == len(schema.AnyOf) { + return fmt.Errorf("anyOf: did not validate against any of %v:\n%v", + schema.AnyOf, errors.Join(errs...)) } } if schema.OneOf != nil { diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/LICENSE b/vendor/github.com/hashicorp/terraform-plugin-log/LICENSE index 84cd06439..b002499e9 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/LICENSE +++ b/vendor/github.com/hashicorp/terraform-plugin-log/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2021 HashiCorp, Inc. +Copyright IBM Corp. 2021, 2026 Mozilla Public License, version 2.0 diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/fieldutils/field_maps.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/fieldutils/field_maps.go index 0bdcd47d6..bfa7d56bc 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/fieldutils/field_maps.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/fieldutils/field_maps.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package fieldutils diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/args.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/args.go index 43ac23bf3..13455147d 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/args.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/args.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package hclogutils diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/logger_options.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/logger_options.go index 69dad9eb7..083297a30 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/logger_options.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/hclogutils/logger_options.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package hclogutils diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/filtering.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/filtering.go index c1eceb1d8..4c0236957 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/filtering.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/filtering.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/log.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/log.go index 6ad6c1d2d..32aa48590 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/log.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/log.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/options.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/options.go index 42a79d9c7..1b4adf0b0 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/options.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/options.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/provider.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/provider.go index 7353cc6c0..c07c9a520 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/provider.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/provider.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sdk.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sdk.go index db84026b2..bb2d2e6cf 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sdk.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sdk.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sink.go b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sink.go index b1e42de0e..27dbf9e2d 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sink.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/internal/logging/sink.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package logging diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/doc.go b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/doc.go index a168965de..f69f6e128 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/doc.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 // Package tflog provides helper functions for writing log output and creating diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/options.go b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/options.go index 953786a06..d02deebd6 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/options.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/options.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tflog diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/provider.go b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/provider.go index b5a083588..e5a9e8f86 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/provider.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/provider.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tflog @@ -140,6 +140,51 @@ func Error(ctx context.Context, msg string, additionalFields ...map[string]inter logger.Error(msg, additionalArgs...) } +// IsTrace returns true if the logger in ctx would emit a trace-level log. +func IsTrace(ctx context.Context) bool { + logger := logging.GetProviderRootLogger(ctx) + if logger == nil { + return false + } + return logger.IsTrace() +} + +// IsDebug returns true if the logger in ctx would emit a debug-level log. +func IsDebug(ctx context.Context) bool { + logger := logging.GetProviderRootLogger(ctx) + if logger == nil { + return false + } + return logger.IsDebug() +} + +// IsInfo returns true if the logger in ctx would emit an info-level log. +func IsInfo(ctx context.Context) bool { + logger := logging.GetProviderRootLogger(ctx) + if logger == nil { + return false + } + return logger.IsInfo() +} + +// IsWarn returns true if the logger in ctx would emit a warn-level log. +func IsWarn(ctx context.Context) bool { + logger := logging.GetProviderRootLogger(ctx) + if logger == nil { + return false + } + return logger.IsWarn() +} + +// IsError returns true if the logger in ctx would emit an error-level log. +func IsError(ctx context.Context) bool { + logger := logging.GetProviderRootLogger(ctx) + if logger == nil { + return false + } + return logger.IsError() +} + // OmitLogWithFieldKeys returns a new context.Context that has a modified logger // that will omit to write any log when any of the given keys is found // within its fields. diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/subsystem.go b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/subsystem.go index 2d22ef1bb..c14801234 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tflog/subsystem.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tflog/subsystem.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tflog @@ -219,6 +219,51 @@ func SubsystemError(ctx context.Context, subsystem, msg string, additionalFields logger.Error(msg, additionalArgs...) } +// SubsystemIsTrace returns true if the subsystem logger in ctx would emit a trace-level log. +func SubsystemIsTrace(ctx context.Context, subsystem string) bool { + logger := logging.GetProviderSubsystemLogger(ctx, subsystem) + if logger == nil { + return false + } + return logger.IsTrace() +} + +// SubsystemIsDebug returns true if the subsystem logger in ctx would emit a debug-level log. +func SubsystemIsDebug(ctx context.Context, subsystem string) bool { + logger := logging.GetProviderSubsystemLogger(ctx, subsystem) + if logger == nil { + return false + } + return logger.IsDebug() +} + +// SubsystemIsInfo returns true if the subsystem logger in ctx would emit an info-level log. +func SubsystemIsInfo(ctx context.Context, subsystem string) bool { + logger := logging.GetProviderSubsystemLogger(ctx, subsystem) + if logger == nil { + return false + } + return logger.IsInfo() +} + +// SubsystemIsWarn returns true if the subsystem logger in ctx would emit a warn-level log. +func SubsystemIsWarn(ctx context.Context, subsystem string) bool { + logger := logging.GetProviderSubsystemLogger(ctx, subsystem) + if logger == nil { + return false + } + return logger.IsWarn() +} + +// SubsystemIsError returns true if the subsystem logger in ctx would emit an error-level log. +func SubsystemIsError(ctx context.Context, subsystem string) bool { + logger := logging.GetProviderSubsystemLogger(ctx, subsystem) + if logger == nil { + return false + } + return logger.IsError() +} + // SubsystemOmitLogWithFieldKeys returns a new context.Context that has a modified logger // that will omit to write any log when any of the given keys is found // within its fields. diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/doc.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/doc.go index 13eb4c636..312850217 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/doc.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 // Package tfsdklog provides helper functions for logging from SDKs and diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/levels.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/levels.go index 71bce01be..c9a967708 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/levels.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/levels.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tfsdklog diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/options.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/options.go index 09cedb1ca..b3abc817e 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/options.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/options.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tfsdklog diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sdk.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sdk.go index 9c7d4c24d..015eaed98 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sdk.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sdk.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tfsdklog @@ -231,6 +231,36 @@ func Error(ctx context.Context, msg string, additionalFields ...map[string]inter logger.Error(msg, additionalArgs...) } +// IsTrace returns true if the root SDK logger would emit a trace-level log. +// The check is performed against a cached level for performance. +func IsTrace(_ context.Context) bool { + return rootWouldLog(hclog.Trace) +} + +// IsDebug returns true if the root SDK logger would emit a debug-level log. +// The check is performed against a cached level for performance. +func IsDebug(_ context.Context) bool { + return rootWouldLog(hclog.Debug) +} + +// IsInfo returns true if the root SDK logger would emit an info-level log. +// The check is performed against a cached level for performance. +func IsInfo(_ context.Context) bool { + return rootWouldLog(hclog.Info) +} + +// IsWarn returns true if the root SDK logger would emit a warn-level log. +// The check is performed against a cached level for performance. +func IsWarn(_ context.Context) bool { + return rootWouldLog(hclog.Warn) +} + +// IsError returns true if the root SDK logger would emit an error-level log. +// The check is performed against a cached level for performance. +func IsError(_ context.Context) bool { + return rootWouldLog(hclog.Error) +} + // OmitLogWithFieldKeys returns a new context.Context that has a modified logger // that will omit to write any log when any of the given keys is found // within its fields. diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sink.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sink.go index e4b3e6c93..22b62b962 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sink.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/sink.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tfsdklog diff --git a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/subsystem.go b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/subsystem.go index bfed8aca7..fc799bde4 100644 --- a/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/subsystem.go +++ b/vendor/github.com/hashicorp/terraform-plugin-log/tfsdklog/subsystem.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package tfsdklog @@ -246,6 +246,36 @@ func SubsystemError(ctx context.Context, subsystem, msg string, additionalFields logger.Error(msg, additionalArgs...) } +// SubsystemIsTrace returns true if the subsystem SDK logger would emit a trace-level log. +// The check is performed against a cached level for performance. +func SubsystemIsTrace(_ context.Context, subsystem string) bool { + return subsystemWouldLog(subsystem, hclog.Trace) +} + +// SubsystemIsDebug returns true if the subsystem SDK logger would emit a debug-level log. +// The check is performed against a cached level for performance. +func SubsystemIsDebug(_ context.Context, subsystem string) bool { + return subsystemWouldLog(subsystem, hclog.Debug) +} + +// SubsystemIsInfo returns true if the subsystem SDK logger would emit an info-level log. +// The check is performed against a cached level for performance. +func SubsystemIsInfo(_ context.Context, subsystem string) bool { + return subsystemWouldLog(subsystem, hclog.Info) +} + +// SubsystemIsWarn returns true if the subsystem SDK logger would emit a warn-level log. +// The check is performed against a cached level for performance. +func SubsystemIsWarn(_ context.Context, subsystem string) bool { + return subsystemWouldLog(subsystem, hclog.Warn) +} + +// SubsystemIsError returns true if the subsystem SDK logger would emit an error-level log. +// The check is performed against a cached level for performance. +func SubsystemIsError(_ context.Context, subsystem string) bool { + return subsystemWouldLog(subsystem, hclog.Error) +} + // SubsystemOmitLogWithFieldKeys returns a new context.Context that has a modified logger // that will omit to write any log when any of the given keys is found // within its fields. diff --git a/vendor/github.com/klauspost/compress/README.md b/vendor/github.com/klauspost/compress/README.md index fb023f2cf..0e9f170d0 100644 --- a/vendor/github.com/klauspost/compress/README.md +++ b/vendor/github.com/klauspost/compress/README.md @@ -27,6 +27,18 @@ Use the links above for more information on each. # changelog +* Jul 1st, 2026 [1.19.0](https://github.com/klauspost/compress/releases/tag/v1.19.0) + * zstd: Add true concurrent stream encodingin https://github.com/klauspost/compress/pull/1136 + * zstd: arm64 decoder asm by @lizthegrey in https://github.com/klauspost/compress/pull/1160 + * flate: Add inflate checkpoints in https://github.com/klauspost/compress/pull/1154 + * zstd: avoid unused BuildDict encoder allocation by @snissn in https://github.com/klauspost/compress/pull/1147 + * snappy/s2: Limit length of varint in `decodedLen` by @eustas in https://github.com/klauspost/compress/pull/1148 + * gzhttp: match qvalue parameter case-insensitively (RFC 7231) by @z9z in https://github.com/klauspost/compress/pull/1149 + * zip: add NameDecoder callback for legacy encoding rewrite by @SAY-5 in https://github.com/klauspost/compress/pull/1150 + * huff0: Allow building tables from histogram in https://github.com/klauspost/compress/pull/1155 + * huff0: Allow building table from oversized histogram in https://github.com/klauspost/compress/pull/1156 + * s2sx: Clean symlink targets in https://github.com/klauspost/compress/pull/1163 + * Feb 9th, 2026 [1.18.4](https://github.com/klauspost/compress/releases/tag/v1.18.4) * gzhttp: Add zstandard to server handler wrapper https://github.com/klauspost/compress/pull/1121 * zstd: Add ResetWithOptions to encoder/decoder https://github.com/klauspost/compress/pull/1122 diff --git a/vendor/github.com/klauspost/compress/flate/dict_decoder.go b/vendor/github.com/klauspost/compress/flate/dict_decoder.go index cb855abc4..37861cf6a 100644 --- a/vendor/github.com/klauspost/compress/flate/dict_decoder.go +++ b/vendor/github.com/klauspost/compress/flate/dict_decoder.go @@ -28,9 +28,10 @@ type dictDecoder struct { hist []byte // Sliding window history // Invariant: 0 <= rdPos <= wrPos <= len(hist) - wrPos int // Current output position in buffer - rdPos int // Have emitted hist[:rdPos] already - full bool // Has a full window length been written yet? + wrPos int // Current output position in buffer + rdPos int // Have emitted hist[:rdPos] already + flushed int64 // Total bytes returned by readFlush since init + full bool // Has a full window length been written yet? } // init initializes dictDecoder to have a sliding window dictionary of the given @@ -167,11 +168,22 @@ loop: return dstPos - dstBase } +// appendWindow appends the current sliding window (up to len(hist) most recent +// bytes, oldest first) to dst. +func (dd *dictDecoder) appendWindow(dst []byte) []byte { + if dd.full { + dst = append(dst, dd.hist[dd.wrPos:]...) + return append(dst, dd.hist[:dd.wrPos]...) + } + return append(dst, dd.hist[:dd.wrPos]...) +} + // readFlush returns a slice of the historical buffer that is ready to be // emitted to the user. The data returned by readFlush must be fully consumed // before calling any other dictDecoder methods. func (dd *dictDecoder) readFlush() []byte { toRead := dd.hist[dd.rdPos:dd.wrPos] + dd.flushed += int64(len(toRead)) dd.rdPos = dd.wrPos if dd.wrPos == len(dd.hist) { dd.wrPos, dd.rdPos = 0, 0 @@ -179,3 +191,9 @@ func (dd *dictDecoder) readFlush() []byte { } return toRead } + +// decoded reports the total number of bytes written into the dictionary since +// init (i.e. excluding any preset dict bytes). +func (dd *dictDecoder) decoded() int64 { + return dd.flushed + int64(dd.wrPos-dd.rdPos) +} diff --git a/vendor/github.com/klauspost/compress/flate/inflate.go b/vendor/github.com/klauspost/compress/flate/inflate.go index 6e90126db..39dd683b2 100644 --- a/vendor/github.com/klauspost/compress/flate/inflate.go +++ b/vendor/github.com/klauspost/compress/flate/inflate.go @@ -342,6 +342,11 @@ type decompressor struct { final bool flushMode flushMode + cb func(InflateCheckpoint) + cp InflateCheckpoint + hasCP bool // WithResumeFrom was supplied + uncOffset int64 // baseline uncompressed offset (from a resume checkpoint) + cpBuf []byte } func (f *decompressor) nextBlock() { @@ -676,6 +681,18 @@ func (f *decompressor) finishBlock() { f.toRead = f.dict.readFlush() } + if f.cb != nil { + bitPos := f.roffset*8 - int64(f.nb) + f.cpBuf = f.dict.appendWindow(f.cpBuf[:0]) + f.cb(InflateCheckpoint{ + UncompressedOffset: f.uncOffset + f.dict.decoded(), + CompressedOffset: bitPos / 8, + Final: f.final, + BitOffset: uint8(bitPos & 7), + Window: f.cpBuf, + }) + } + f.step = nextBlock } @@ -806,6 +823,45 @@ func (f *decompressor) Reset(r io.Reader, dict []byte) error { return nil } +// ResetCP will adjust the input to the provided checkpoint. +// It is assumed the input stream is forwarded to cp.CompressedOffset. +func (f *decompressor) ResetCP(r io.Reader, cp InflateCheckpoint) error { + *f = decompressor{ + r: makeReader(r), + bits: f.bits, + codebits: f.codebits, + h1: f.h1, + h2: f.h2, + dict: f.dict, + step: nextBlock, + cpBuf: f.cpBuf, + } + return f.applyCP(cp) +} + +// applyCP seeds the decompressor state from a resume checkpoint: +// loads the sliding window, sets the absolute compressed/uncompressed +// offsets, and skips cp.BitOffset bits into the first input byte so +// the next decode aligns with the start of a deflate block. +func (f *decompressor) applyCP(cp InflateCheckpoint) error { + f.dict.init(maxMatchOffset, cp.Window) + f.roffset = cp.CompressedOffset + f.uncOffset = cp.UncompressedOffset + f.final = cp.Final + f.b = 0 + f.nb = 0 + if cp.BitOffset > 0 { + c, err := f.r.ReadByte() + if err != nil { + return noEOF(err) + } + f.roffset++ + f.b = uint32(c) >> cp.BitOffset + f.nb = 8 - uint(cp.BitOffset) + } + return nil +} + type ReaderOpt func(*decompressor) // WithPartialBlock tells decompressor to return after each block, @@ -823,6 +879,36 @@ func WithDict(dict []byte) ReaderOpt { } } +// InflateCheckpoint provides a resumable checkpoint for inflate. +type InflateCheckpoint struct { + UncompressedOffset int64 // Byte offset in the decompressed stream + CompressedOffset int64 // Byte offset in the compressed stream + Final bool // True if this is the final block + BitOffset uint8 // 0-7 bits + Window []byte // 32KB sliding window dictionary +} + +// WithEobCallback will call the provided function after each block +// with the current gzip checkpoint. +// After returning the provided window can no longer be referenced. +// The callback will not be triggered after a block is marked "final". +// The callback is not retained after Reset. +func WithEobCallback(cb func(InflateCheckpoint)) ReaderOpt { + return func(f *decompressor) { + f.cb = cb + } +} + +// WithResumeFrom will adjust the input to the provided checkpoint. +// It is assumed the input stream is forwarded to the provided offset. +// The checkpoint is removed when Reset is called. +func WithResumeFrom(cp InflateCheckpoint) ReaderOpt { + return func(f *decompressor) { + f.cp = cp + f.hasCP = true + } +} + // NewReaderOpts returns new reader with provided options func NewReaderOpts(r io.Reader, opts ...ReaderOpt) io.ReadCloser { fixedHuffmanDecoderInit() @@ -838,6 +924,12 @@ func NewReaderOpts(r io.Reader, opts ...ReaderOpt) io.ReadCloser { opt(&f) } + if f.hasCP { + if err := f.applyCP(f.cp); err != nil { + f.err = err + } + } + return &f } diff --git a/vendor/github.com/klauspost/compress/flate/inflate_gen.go b/vendor/github.com/klauspost/compress/flate/inflate_gen.go index 2b2f993f7..e80f19656 100644 --- a/vendor/github.com/klauspost/compress/flate/inflate_gen.go +++ b/vendor/github.com/klauspost/compress/flate/inflate_gen.go @@ -10,6 +10,15 @@ import ( "strings" ) +func peekBufio(fr *bufio.Reader) ([]byte, error) { + if fr.Buffered() == 0 { + if _, err := fr.Peek(1); err != nil && fr.Buffered() == 0 { + return nil, err + } + } + return fr.Peek(fr.Buffered()) +} + // Decode a single Huffman block from f. // hl and hd are the Huffman states for the lit/length values // and the distance values, respectively. If hd == nil, using the @@ -527,6 +536,8 @@ func (f *decompressor) huffmanBufioReader() { // but is smart enough to keep local variables in registers, so use nb and b, // inline call to moreBits and reassign b,nb back to f on return. fnb, fb, dict := f.nb, f.b, &f.dict + pbuf, _ := fr.Peek(fr.Buffered()) + pos := 0 switch f.stepState { case stateInit: @@ -548,12 +559,19 @@ readLiteral: n := uint(f.hl.maxRead) for { for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return + if pos >= len(pbuf) { + fr.Discard(pos) + var err error + pbuf, err = peekBufio(fr) + pos = 0 + if len(pbuf) == 0 { + f.b, f.nb = fb, fnb + f.err = noEOF(err) + return + } } + c := pbuf[pos] + pos++ f.roffset++ fb |= uint32(c) << (fnb & regSizeMaskUint32) fnb += 8 @@ -566,6 +584,7 @@ readLiteral: } if n <= fnb { if n == 0 { + fr.Discard(pos) f.b, f.nb = fb, fnb if debugDecode { fmt.Println("huffsym: n==0") @@ -586,6 +605,7 @@ readLiteral: case v < 256: dict.writeByte(byte(v)) if dict.availWrite() == 0 { + fr.Discard(pos) f.toRead = dict.readFlush() f.step = huffmanBufioReader f.stepState = stateInit @@ -594,6 +614,7 @@ readLiteral: } goto readLiteral case v == 256: + fr.Discard(pos) f.b, f.nb = fb, fnb f.finishBlock() return @@ -605,15 +626,22 @@ readLiteral: length = int(val.length) + 3 n := uint(val.extra) for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits n>0:", err) + if pos >= len(pbuf) { + fr.Discard(pos) + var err error + pbuf, err = peekBufio(fr) + pos = 0 + if len(pbuf) == 0 { + f.b, f.nb = fb, fnb + if debugDecode { + fmt.Println("morebits n>0:", err) + } + f.err = err + return } - f.err = err - return } + c := pbuf[pos] + pos++ f.roffset++ fb |= uint32(c) << (fnb & regSizeMaskUint32) fnb += 8 @@ -622,6 +650,7 @@ readLiteral: fb >>= n & regSizeMaskUint32 fnb -= n default: + fr.Discard(pos) if debugDecode { fmt.Println(v, ">= maxNumLit") } @@ -633,15 +662,22 @@ readLiteral: var dist uint32 if f.hd == nil { for fnb < 5 { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb<5:", err) + if pos >= len(pbuf) { + fr.Discard(pos) + var err error + pbuf, err = peekBufio(fr) + pos = 0 + if len(pbuf) == 0 { + f.b, f.nb = fb, fnb + if debugDecode { + fmt.Println("morebits f.nb<5:", err) + } + f.err = err + return } - f.err = err - return } + c := pbuf[pos] + pos++ f.roffset++ fb |= uint32(c) << (fnb & regSizeMaskUint32) fnb += 8 @@ -660,12 +696,19 @@ readLiteral: // inline call to moreBits and reassign b,nb back to f on return. for { for fnb < n { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - f.err = noEOF(err) - return + if pos >= len(pbuf) { + fr.Discard(pos) + var err error + pbuf, err = peekBufio(fr) + pos = 0 + if len(pbuf) == 0 { + f.b, f.nb = fb, fnb + f.err = noEOF(err) + return + } } + c := pbuf[pos] + pos++ f.roffset++ fb |= uint32(c) << (fnb & regSizeMaskUint32) fnb += 8 @@ -678,6 +721,7 @@ readLiteral: } if n <= fnb { if n == 0 { + fr.Discard(pos) f.b, f.nb = fb, fnb if debugDecode { fmt.Println("huffsym: n==0") @@ -701,15 +745,22 @@ readLiteral: // have 1 bit in bottom of dist, need nb more. extra := (dist & 1) << (nb & regSizeMaskUint32) for fnb < nb { - c, err := fr.ReadByte() - if err != nil { - f.b, f.nb = fb, fnb - if debugDecode { - fmt.Println("morebits f.nb= len(pbuf) { + fr.Discard(pos) + var err error + pbuf, err = peekBufio(fr) + pos = 0 + if len(pbuf) == 0 { + f.b, f.nb = fb, fnb + if debugDecode { + fmt.Println("morebits f.nb uint32(dict.histSize()) { + fr.Discard(pos) f.b, f.nb = fb, fnb if debugDecode { fmt.Println("dist > dict.histSize():", dist, dict.histSize()) @@ -752,6 +805,7 @@ copyHistory: f.copyLen -= cnt if dict.availWrite() == 0 || f.copyLen > 0 { + fr.Discard(pos) f.toRead = dict.readFlush() f.step = huffmanBufioReader // We need to continue this work f.stepState = stateDict diff --git a/vendor/github.com/klauspost/compress/huff0/build_table.go b/vendor/github.com/klauspost/compress/huff0/build_table.go new file mode 100644 index 000000000..e3757c87e --- /dev/null +++ b/vendor/github.com/klauspost/compress/huff0/build_table.go @@ -0,0 +1,168 @@ +package huff0 + +import "errors" + +// BuildCTable builds a Huffman compression table from a precomputed symbol +// histogram and installs it as the previous (reuse) table on s. +// +// After this call: +// - EstimateSize/CanUseTable can probe the table against other histograms. +// - Compress1X/Compress4X with Reuse = ReusePolicyMust will encode without +// emitting a new table header. +// - TransferCTable can hand the table to a sibling Scratch. +// +// count[i] is the number of occurrences of symbol i. The histogram must have +// at least 2 distinct non-zero symbols; ErrUseRLE is returned for a single +// symbol and an error is returned for an empty histogram. +func (s *Scratch) BuildCTable(count *[256]uint32) error { + if s == nil { + return errors.New("huff0: BuildCTable on nil Scratch") + } + if count == nil { + return errors.New("huff0: nil count passed to BuildCTable") + } + var err error + s, err = s.prepare(nil) + if err != nil { + return err + } + s.count = *count + var total, maxCount int + var symLen uint16 + for i, v := range s.count { + total += int(v) + if int(v) > maxCount { + maxCount = int(v) + } + if v != 0 { + symLen = uint16(i) + 1 + } + } + if total == 0 { + return errors.New("huff0: empty histogram") + } + if symLen < 2 || maxCount == total { + return ErrUseRLE + } + // huff0's internal rank table assumes total ≤ BlockSizeMax (it uses + // highBit32(count+1) + 1 as a rank index into a fixed-size array). + // Histograms summed across multiple blocks can exceed that; scale the + // counts down preserving the distribution. Non-zero entries round up so + // rare symbols stay representable. + if total > BlockSizeMax { + shift := uint(0) + for total>>shift > BlockSizeMax { + shift++ + } + round := uint32(1<> shift + if scaled == 0 { + scaled = 1 + } + s.count[i] = scaled + newTotal += int(scaled) + if int(scaled) > newMax { + newMax = int(scaled) + } + } + total = newTotal + maxCount = newMax + if maxCount == total { + return ErrUseRLE + } + } + s.symbolLen = symLen + s.maxCount = maxCount + s.srcLen = total + if err := s.buildCTable(); err != nil { + return err + } + if cap(s.prevTable) < len(s.cTable) { + s.prevTable = make(cTable, 0, maxSymbolValue+1) + } + s.prevTable = s.prevTable[:len(s.cTable)] + copy(s.prevTable, s.cTable) + s.prevTableLog = s.actualTableLog + // Force the next Compress* to recount from real input. + s.clearCount = true + s.maxCount = 0 + return nil +} + +// EstimateSize returns an estimated compressed payload size in bytes for the +// supplied histogram using the table currently stored in prevTable. It returns +// -1 when the table cannot encode every non-zero symbol of hist (i.e. when +// CanUseTable would return false). The estimate excludes the table header. +func (s *Scratch) EstimateSize(hist *[256]uint32) int { + if s == nil || hist == nil || len(s.prevTable) == 0 { + return -1 + } + pt := s.prevTable + nbBits := uint32(7) + for i, v := range hist { + if v == 0 { + continue + } + if i >= len(pt) || pt[i].nBits == 0 { + return -1 + } + nbBits += uint32(pt[i].nBits) * v + } + return int(nbBits >> 3) +} + +// CanUseTable reports whether the table in prevTable can encode every +// non-zero symbol present in hist. +func (s *Scratch) CanUseTable(hist *[256]uint32) bool { + if s == nil || hist == nil || len(s.prevTable) == 0 { + return false + } + pt := s.prevTable + for i, v := range hist { + if v == 0 { + continue + } + if i >= len(pt) || pt[i].nBits == 0 { + return false + } + } + return true +} + +// AppendTable serializes the table currently stored in prevTable (e.g. as +// installed by BuildCTable or carried over from a previous Compress call) +// into a self-delimiting zstd-style header and appends it to dst. The +// returned slice can be parsed back by ReadTable. +func (s *Scratch) AppendTable(dst []byte) ([]byte, error) { + if s == nil || len(s.prevTable) == 0 { + return dst, errors.New("huff0: AppendTable with empty table") + } + // cTable.write reads s.actualTableLog, s.symbolLen, s.huffWeight, s.fse + // and writes into s.Out. Save/restore Out so we don't disturb in-flight + // compression buffers. + saveOut := s.Out + saveTL := s.actualTableLog + saveSL := s.symbolLen + if s.fse == nil { + // Lazily init in case AppendTable is called on a fresh Scratch. + if _, err := s.prepare(nil); err != nil { + return dst, err + } + saveOut = s.Out + } + s.Out = s.Out[:0] + s.actualTableLog = s.prevTableLog + s.symbolLen = uint16(len(s.prevTable)) + if err := s.prevTable.write(s); err != nil { + s.Out, s.actualTableLog, s.symbolLen = saveOut, saveTL, saveSL + return dst, err + } + dst = append(dst, s.Out...) + s.Out, s.actualTableLog, s.symbolLen = saveOut, saveTL, saveSL + return dst, nil +} diff --git a/vendor/github.com/klauspost/compress/internal/snapref/decode.go b/vendor/github.com/klauspost/compress/internal/snapref/decode.go index a2c82fcd2..584b7574b 100644 --- a/vendor/github.com/klauspost/compress/internal/snapref/decode.go +++ b/vendor/github.com/klauspost/compress/internal/snapref/decode.go @@ -31,7 +31,7 @@ func DecodedLen(src []byte) (int, error) { // that the length header occupied. func decodedLen(src []byte) (blockLen, headerLen int, err error) { v, n := binary.Uvarint(src) - if n <= 0 || v > 0xffffffff { + if n <= 0 || n > 5 || v > 0xffffffff { return 0, 0, ErrCorrupt } diff --git a/vendor/github.com/klauspost/compress/s2/decode.go b/vendor/github.com/klauspost/compress/s2/decode.go index 264ffd0a9..17abb515a 100644 --- a/vendor/github.com/klauspost/compress/s2/decode.go +++ b/vendor/github.com/klauspost/compress/s2/decode.go @@ -35,7 +35,7 @@ func DecodedLen(src []byte) (int, error) { // that the length header occupied. func decodedLen(src []byte) (blockLen, headerLen int, err error) { v, n := binary.Uvarint(src) - if n <= 0 || v > 0xffffffff { + if n <= 0 || n > 5 || v > 0xffffffff { return 0, 0, ErrCorrupt } diff --git a/vendor/github.com/klauspost/compress/s2/encode_all.go b/vendor/github.com/klauspost/compress/s2/encode_all.go index 9d12c44f3..794ec8a68 100644 --- a/vendor/github.com/klauspost/compress/s2/encode_all.go +++ b/vendor/github.com/klauspost/compress/s2/encode_all.go @@ -981,7 +981,7 @@ searchDict: cv = load64(src, s) continue } - } else if uint32(cv>>(checkRep*8)) == load32(src, s-repeat+checkRep) { + } else if repeat > 0 && uint32(cv>>(checkRep*8)) == load32(src, s-repeat+checkRep) { base := s + checkRep // Extend back for i := base - repeat; base > nextEmit && i > 0 && src[i-1] == src[base-1]; { diff --git a/vendor/github.com/klauspost/compress/s2/hashtable_pool.go b/vendor/github.com/klauspost/compress/s2/hashtable_pool.go index bc7cabd5c..ec972132b 100644 --- a/vendor/github.com/klauspost/compress/s2/hashtable_pool.go +++ b/vendor/github.com/klauspost/compress/s2/hashtable_pool.go @@ -25,7 +25,7 @@ type betterTables struct { sTable [betterShortTableSize]uint32 } -var betterTablePool = sync.Pool{New: func() interface{} { return &betterTables{} }} +var betterTablePool = sync.Pool{New: func() any { return &betterTables{} }} // betterSnappyTables holds better-snappy compression hash tables. type betterSnappyTables struct { @@ -33,7 +33,7 @@ type betterSnappyTables struct { sTable [betterShortTableSize]uint32 } -var betterSnappyTablePool = sync.Pool{New: func() interface{} { return &betterSnappyTables{} }} +var betterSnappyTablePool = sync.Pool{New: func() any { return &betterSnappyTables{} }} // bestTables holds best compression hash tables. type bestTables struct { @@ -41,7 +41,7 @@ type bestTables struct { sTable [bestShortTableSize]uint64 } -var bestTablePool = sync.Pool{New: func() interface{} { return &bestTables{} }} +var bestTablePool = sync.Pool{New: func() any { return &bestTables{} }} // getBetterTables gets a zeroed betterTables from the pool. func getBetterTables() *betterTables { diff --git a/vendor/github.com/klauspost/compress/s2/reader.go b/vendor/github.com/klauspost/compress/s2/reader.go index 4d01c4190..17443e2c1 100644 --- a/vendor/github.com/klauspost/compress/s2/reader.go +++ b/vendor/github.com/klauspost/compress/s2/reader.go @@ -216,13 +216,18 @@ func (r *Reader) skippable(tmp []byte, n int, allowEOF bool, id uint8) (ok bool) return r.err == nil } if rs, ok := r.r.(io.ReadSeeker); ok { - _, err := rs.Seek(int64(n), io.SeekCurrent) - if err == nil { - return true - } - if err == io.ErrUnexpectedEOF || (r.err == io.EOF && !allowEOF) { - r.err = ErrCorrupt - return false + if cur, err := rs.Seek(0, io.SeekCurrent); err == nil { + if end, err := rs.Seek(0, io.SeekEnd); err == nil { + if cur+int64(n) <= end { + if _, err := rs.Seek(cur+int64(n), io.SeekStart); err == nil { + return true + } + } + if _, err := rs.Seek(cur, io.SeekStart); err != nil { + r.err = ErrCorrupt + return false + } + } } } for n > 0 { diff --git a/vendor/github.com/klauspost/compress/zstd/README.md b/vendor/github.com/klauspost/compress/zstd/README.md index c11d7fa28..a5aeeaed0 100644 --- a/vendor/github.com/klauspost/compress/zstd/README.md +++ b/vendor/github.com/klauspost/compress/zstd/README.md @@ -75,14 +75,47 @@ The above is fine for big encodes. However, whenever possible try to *reuse* the To reuse the encoder, you can use the `Reset(io.Writer)` function to change to another output. This will allow the encoder to reuse all resources and avoid wasteful allocations. -Currently stream encoding has 'light' concurrency, meaning up to 2 goroutines can be working on part -of a stream. This is independent of the `WithEncoderConcurrency(n)`, but that is likely to change +By default, stream encoding has 'light' concurrency, meaning up to 2 goroutines can be working on part +of a stream. This is independent of the `WithEncoderConcurrency(n)`, but that is likely to change in the future. So if you want to limit concurrency for future updates, specify the concurrency you would like. If you would like stream encoding to be done without spawning async goroutines, use `WithEncoderConcurrency(1)` which will compress input as each block is completed, blocking on writes until each has completed. +#### Parallel Stream Compression + +For maximum throughput on large streams, use `WithConcurrentBlocks(true)` together with +`WithEncoderConcurrency(n)` where n is the number of CPU cores you want to use. +This splits the input into large sections (jobs) that are compressed simultaneously by multiple goroutines, +similar to how the C zstd library does multithreaded compression. + +```Go +enc, err := zstd.NewWriter(out, + zstd.WithEncoderLevel(zstd.SpeedDefault), + zstd.WithEncoderConcurrency(runtime.GOMAXPROCS(0)), + zstd.WithConcurrentBlocks(true), +) +``` + +Each non-first job receives an overlap prefix from the previous job for match context, +so compression ratio is only marginally affected. Output is flushed in order, +producing a valid single-frame zstd stream. + +Benchmark on 1.8GB GOB stream (AMD Ryzen 9 9950X): + +| Level | 1 thread | 4 threads | 16 threads | 1T ratio | 16T ratio | +|---------|:----------:|:------------------:|:-------------------:|:--------:|:---------:| +| fastest | 783 MB/s | 2950 MB/s (3.8×) | 6939 MB/s (8.9×) | 12.24% | 12.26% | +| default | 728 MB/s | 2533 MB/s (3.5×) | 5340 MB/s (7.3×) | 10.67% | 10.68% | +| better | 434 MB/s | 1105 MB/s (2.5×) | 2206 MB/s (5.1×) | 9.14% | 9.21% | +| best | 129 MB/s | 367 MB/s (2.8×) | 884 MB/s (6.8×) | 8.48% | 8.63% | + +Notes: +* Not compatible with dictionary encoding. +* `Flush()` dispatches the current partial job, so latency-sensitive callers can force output. +* `EncodeAll` is unaffected — it uses its own concurrency via the encoder pool. + You can specify your desired compression level using `WithEncoderLevel()` option. Currently only pre-defined compression settings can be specified. diff --git a/vendor/github.com/klauspost/compress/zstd/dict.go b/vendor/github.com/klauspost/compress/zstd/dict.go index 2ffbfdf37..4f1c4938c 100644 --- a/vendor/github.com/klauspost/compress/zstd/dict.go +++ b/vendor/github.com/klauspost/compress/zstd/dict.go @@ -230,7 +230,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) { } block := blockEnc{lowMem: false} block.init() - enc := encoder(&bestFastEncoder{fastBase: fastBase{maxMatchOff: int32(maxMatchLen), bufferReset: math.MaxInt32 - int32(maxMatchLen*2), lowMem: false}}) + var enc encoder if o.Level != 0 { eOpts := encoderOptions{ level: o.Level, @@ -242,6 +242,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) { enc = eOpts.encoder() } else { o.Level = SpeedBestCompression + enc = encoder(&bestFastEncoder{fastBase: fastBase{maxMatchOff: int32(maxMatchLen), bufferReset: math.MaxInt32 - int32(maxMatchLen*2), lowMem: false}}) } var ( remain [256]int diff --git a/vendor/github.com/klauspost/compress/zstd/enc_base.go b/vendor/github.com/klauspost/compress/zstd/enc_base.go index c4de134a7..c4fea575d 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_base.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_base.go @@ -128,6 +128,34 @@ func (e *fastBase) matchlen(s, t int32, src []byte) int32 { return int32(matchLen(src[s:], src[t:])) } +// resetBasePrefix resets the encoder state and loads prefix as initial history. +// This is used for parallel job encoding where non-first jobs need overlap context. +// Rep offsets are set to defaults [1,4,8] (invalidated, matching C behavior). +func (e *fastBase) resetBasePrefix(prefix []byte) { + if e.blk == nil { + e.blk = &blockEnc{lowMem: e.lowMem} + e.blk.init() + } else { + e.blk.reset(nil) + } + e.blk.initNewEncode() + if e.crc == nil { + e.crc = xxhash.New() + } else { + e.crc.Reset() + } + e.blk.dictLitEnc = nil + e.ensureHist(len(prefix) + maxCompressedBlockSize) + // Bump cur so old table entries fall outside the window. + // When cur >= bufferReset, leave it; the first Encode call + // will shift/clear tables, preserving valid prefix entries. + if e.cur < e.bufferReset { + e.cur += e.maxMatchOff + int32(len(e.hist)) + } + e.hist = e.hist[:0] + e.hist = append(e.hist, prefix...) +} + // Reset the encoding table. func (e *fastBase) resetBase(d *dict, singleBlock bool) { if e.blk == nil { diff --git a/vendor/github.com/klauspost/compress/zstd/enc_best.go b/vendor/github.com/klauspost/compress/zstd/enc_best.go index 851799322..c71382dde 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_best.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_best.go @@ -551,3 +551,18 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) { // Reset table to initial state copy(e.table[:], e.dictTable) } + +func (e *bestFastEncoder) ResetPrefix(prefix []byte) { + e.resetBasePrefix(prefix) + if len(prefix) < 8 { + return + } + end := e.cur + int32(len(prefix)) - 8 + for i := e.cur; i < end; i++ { + cv := load6432(prefix, i-e.cur) + h := hashLen(cv, bestLongTableBits, bestLongLen) + e.longTable[h] = prevEntry{offset: i, prev: e.longTable[h].offset} + h0 := hashLen(cv, bestShortTableBits, bestShortLen) + e.table[h0] = prevEntry{offset: i, prev: e.table[h0].offset} + } +} diff --git a/vendor/github.com/klauspost/compress/zstd/enc_better.go b/vendor/github.com/klauspost/compress/zstd/enc_better.go index 3305f0924..523d57f3a 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_better.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_better.go @@ -1096,6 +1096,20 @@ func (e *betterFastEncoder) Reset(d *dict, singleBlock bool) { } } +func (e *betterFastEncoder) ResetPrefix(prefix []byte) { + e.resetBasePrefix(prefix) + if len(prefix) < 8 { + return + } + end := e.cur + int32(len(prefix)) - 8 + for i := e.cur; i < end; i += 2 { + cv := load6432(prefix, i-e.cur) + h := hashLen(cv, betterLongTableBits, betterLongLen) + e.longTable[h] = prevEntry{offset: i, prev: e.longTable[h].offset} + e.table[hashLen(cv>>8, betterShortTableBits, betterShortLen)] = tableEntry{val: uint32(cv >> 8), offset: i + 1} + } +} + // ResetDict will reset and set a dictionary if not nil func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) { e.resetBase(d, singleBlock) @@ -1229,6 +1243,10 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) { e.allDirty = false } +func (e *betterFastEncoderDict) ResetPrefix([]byte) { + panic("ResetPrefix not supported for dict encoders") +} + func (e *betterFastEncoderDict) markLongShardDirty(entryNum uint32) { e.longTableShardDirty[entryNum/betterLongTableShardSize] = true } diff --git a/vendor/github.com/klauspost/compress/zstd/enc_dfast.go b/vendor/github.com/klauspost/compress/zstd/enc_dfast.go index 2fb6da112..712ba7ab5 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_dfast.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_dfast.go @@ -1037,6 +1037,18 @@ func (e *doubleFastEncoder) Reset(d *dict, singleBlock bool) { } } +func (e *doubleFastEncoder) ResetPrefix(prefix []byte) { + e.fastEncoder.ResetPrefix(prefix) + if len(prefix) < 8 { + return + } + end := e.cur + int32(len(prefix)) - 8 + for i := e.cur + 1; i < end; i += 2 { + cv := load6432(prefix, i-e.cur) + e.longTable[hashLen(cv, dFastLongTableBits, dFastLongLen)] = tableEntry{val: uint32(cv), offset: i} + } +} + // ResetDict will reset and set a dictionary if not nil func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) { allDirty := e.allDirty @@ -1102,6 +1114,10 @@ func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) { } } +func (e *doubleFastEncoderDict) ResetPrefix([]byte) { + panic("ResetPrefix not supported for dict encoders") +} + func (e *doubleFastEncoderDict) markLongShardDirty(entryNum uint32) { e.longTableShardDirty[entryNum/dLongTableShardSize] = true } diff --git a/vendor/github.com/klauspost/compress/zstd/enc_fast.go b/vendor/github.com/klauspost/compress/zstd/enc_fast.go index 5e104f1a4..06045e246 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_fast.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_fast.go @@ -797,6 +797,19 @@ func (e *fastEncoder) Reset(d *dict, singleBlock bool) { } } +func (e *fastEncoder) ResetPrefix(prefix []byte) { + e.resetBasePrefix(prefix) + if len(prefix) < 8 { + return + } + end := e.cur + int32(len(prefix)) - 8 + // Index every 4th + for i := e.cur + 1; i < end; i += 4 { + cv := load6432(prefix, i-e.cur) + e.table[hashLen(cv, tableBits, tableFastHashLen)] = tableEntry{val: uint32(cv), offset: i} + } +} + // ResetDict will reset and set a dictionary if not nil func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) { e.resetBase(d, singleBlock) @@ -866,6 +879,10 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) { e.allDirty = false } +func (e *fastEncoderDict) ResetPrefix([]byte) { + panic("ResetPrefix not supported for dict encoders") +} + func (e *fastEncoderDict) markAllShardsDirty() { e.allDirty = true } diff --git a/vendor/github.com/klauspost/compress/zstd/enc_jobs.go b/vendor/github.com/klauspost/compress/zstd/enc_jobs.go new file mode 100644 index 000000000..95ce67ac0 --- /dev/null +++ b/vendor/github.com/klauspost/compress/zstd/enc_jobs.go @@ -0,0 +1,352 @@ +// Copyright 2019+ Klaus Post. All rights reserved. +// License information can be found in the LICENSE file. +// Based on work by Yann Collet, released under BSD License. + +package zstd + +import ( + "fmt" + rdebug "runtime/debug" + "sync" +) + +type encJob struct { + prefix []byte // overlap from previous job (nil for first) + input []byte // job's own input data (swapped from filling) + last bool // last block of last job gets last=true + output []byte // compressed blocks (filled by worker) + err error // encoding error + done chan struct{} // closed when complete +} + +type jobState struct { + jobSize int + overlapSize int + filling []byte // accumulates input up to jobSize + nextPrefix []byte // overlap prefix prepared for the next dispatched job + + jobSeq int // next job sequence number + + jobCh chan *encJob // dispatch to workers + resultCh chan *encJob // ordered results to flusher + + workerWg sync.WaitGroup + flusherWg sync.WaitGroup + + mu sync.Mutex + flushedSeq int // last flushed sequence number + cond *sync.Cond + + flusherErr error + started bool + + inputPool sync.Pool // *[]byte buffers of jobSize cap + outputPool sync.Pool // *[]byte buffers for compressed output + overlapPool sync.Pool // *[]byte buffers for overlap prefixes +} + +func (e *Encoder) startJobWorkers() { + js := &e.state.jobs + n := e.o.concurrent + js.jobCh = make(chan *encJob, n) + js.resultCh = make(chan *encJob, n) + js.flushedSeq = 0 + js.cond = sync.NewCond(&js.mu) + + // Workers borrow encoders from the shared e.encoders pool per-job. + // Ensure the pool is initialized before any worker tries to borrow. + e.init.Do(e.initialize) + + for range n { + js.workerWg.Add(1) + go e.jobWorker() + } + js.flusherWg.Add(1) + go e.jobFlusher() + js.started = true +} + +func (e *Encoder) jobWorker() { + js := &e.state.jobs + defer js.workerWg.Done() + for job := range js.jobCh { + enc := <-e.encoders + e.compressJob(enc, job) + e.encoders <- enc + close(job.done) + } +} + +func (e *Encoder) compressJob(enc encoder, job *encJob) { + defer func() { + if r := recover(); r != nil { + job.err = fmt.Errorf("panic in parallel job: %v", r) + rdebug.PrintStack() + } + }() + + if len(job.prefix) > 0 { + enc.ResetPrefix(job.prefix) + } else { + enc.Reset(nil, false) + } + + data := job.input + if len(data) == 0 && job.last { + blk := enc.Block() + blk.reset(nil) + blk.last = true + blk.encodeRaw(nil) + job.output = append(job.output, blk.output...) + return + } + + blk := enc.Block() + for len(data) > 0 { + todo := data + if len(todo) > e.o.blockSize { + todo = todo[:e.o.blockSize] + } + data = data[len(todo):] + + blk.pushOffsets() + enc.Encode(blk, todo) + blk.last = len(data) == 0 && job.last + + err := blk.encode(todo, e.o.noEntropy, !e.o.allLitEntropy) + if err != nil { + job.err = err + return + } + job.output = append(job.output, blk.output...) + blk.reset(nil) + } +} + +func (js *jobState) getInputBuf(size int) []byte { + if v := js.inputPool.Get(); v != nil { + bp := v.(*[]byte) + b := *bp + if cap(b) >= size { + return b[:0] + } + } + return make([]byte, 0, size) +} + +func (js *jobState) putInputBuf(b []byte) { + if cap(b) > 0 { + b = b[:0] + js.inputPool.Put(&b) + } +} + +func (js *jobState) getOutputBuf(size int) []byte { + if v := js.outputPool.Get(); v != nil { + bp := v.(*[]byte) + b := *bp + if cap(b) >= size { + return b[:0] + } + } + return make([]byte, 0, size) +} + +func (js *jobState) putOutputBuf(b []byte) { + if cap(b) > 0 { + b = b[:0] + js.outputPool.Put(&b) + } +} + +func (js *jobState) getOverlapBuf(size int) []byte { + if v := js.overlapPool.Get(); v != nil { + bp := v.(*[]byte) + b := *bp + if cap(b) >= size { + return b[:size] + } + } + return make([]byte, size) +} + +func (js *jobState) putOverlapBuf(b []byte) { + if cap(b) > 0 { + b = b[:0] + js.overlapPool.Put(&b) + } +} + +func (e *Encoder) jobFlusher() { + js := &e.state.jobs + defer js.flusherWg.Done() + for job := range js.resultCh { + <-job.done + // Worker has fully exited compressJob, so the prefix is no longer + // in use. Return it to the pool regardless of outcome. + if job.prefix != nil { + js.putOverlapBuf(job.prefix) + job.prefix = nil + } + if job.err != nil { + js.mu.Lock() + js.flusherErr = job.err + js.cond.Broadcast() + js.mu.Unlock() + for range js.resultCh { + } + return + } + if len(job.output) > 0 { + _, err := e.state.w.Write(job.output) + if err != nil { + js.mu.Lock() + js.flusherErr = err + js.cond.Broadcast() + js.mu.Unlock() + for range js.resultCh { + } + return + } + e.state.nWritten += int64(len(job.output)) + } + // Return buffers to pools. + js.putInputBuf(job.input) + js.putOutputBuf(job.output) + job.input = nil + job.output = nil + + js.mu.Lock() + js.flushedSeq++ + js.cond.Broadcast() + js.mu.Unlock() + } +} + +func (e *Encoder) shutdownJobWorkers() { + js := &e.state.jobs + if !js.started { + return + } + close(js.jobCh) + js.workerWg.Wait() + close(js.resultCh) + js.flusherWg.Wait() + js.started = false +} + +// waitAllJobs blocks until all dispatched jobs have been flushed. +func (e *Encoder) waitAllJobs() { + js := &e.state.jobs + if !js.started { + return + } + js.mu.Lock() + for js.flushedSeq < js.jobSeq && js.flusherErr == nil { + js.cond.Wait() + } + js.mu.Unlock() +} + +func (e *Encoder) dispatchJob(final bool) error { + s := &e.state + js := &s.jobs + + js.mu.Lock() + fErr := js.flusherErr + js.mu.Unlock() + if fErr != nil { + return fErr + } + + if !s.headerWritten { + // Single-block optimization: fall through to encodeAll path. + if final && len(js.filling) > 0 && len(js.filling) <= e.o.blockSize { + s.current = e.encodeAll(s.encoder, js.filling, s.current[:0]) + var n2 int + n2, s.err = s.w.Write(s.current) + if s.err != nil { + return s.err + } + s.nWritten += int64(n2) + s.nInput += int64(len(js.filling)) + s.current = s.current[:0] + js.filling = js.filling[:0] + s.headerWritten = true + s.fullFrameWritten = true + s.eofWritten = true + return nil + } + if final && len(js.filling) == 0 && !e.o.fullZero { + s.headerWritten = true + s.fullFrameWritten = true + s.eofWritten = true + return nil + } + + var tmp [maxHeaderSize]byte + fh := frameHeader{ + ContentSize: uint64(s.frameContentSize), + WindowSize: uint32(s.encoder.WindowSize(s.frameContentSize)), + SingleSegment: false, + Checksum: e.o.crc, + DictID: 0, + } + dst := fh.appendTo(tmp[:0]) + var n2 int + n2, s.err = s.w.Write(dst) + if s.err != nil { + return s.err + } + s.nWritten += int64(n2) + s.headerWritten = true + } + + if len(js.filling) == 0 && !final { + return nil + } + + if !js.started { + e.startJobWorkers() + } + + // Estimate output size for pooled buffer. + outputEst := max(len(js.filling)/2, 512) + + job := &encJob{ + last: final, + done: make(chan struct{}), + output: js.getOutputBuf(outputEst), + } + + // Each job owns its prefix slice; the flusher returns it to the pool + // after <-job.done, so workers and dispatch never share a buffer. + if js.nextPrefix != nil { + job.prefix = js.nextPrefix + js.nextPrefix = nil + } + + // Build the next job's prefix from the tail of this job's input. + if !final && len(js.filling) > 0 { + overlapLen := min(js.overlapSize, len(js.filling)) + np := js.getOverlapBuf(overlapLen) + copy(np, js.filling[len(js.filling)-overlapLen:]) + js.nextPrefix = np + } + + // Swap filling buffer into job — zero-copy for the input data. + job.input = js.filling + js.filling = js.getInputBuf(js.jobSize) + + s.nInput += int64(len(job.input)) + js.jobSeq++ + + if final { + s.eofWritten = true + } + + js.resultCh <- job + js.jobCh <- job + + return nil +} diff --git a/vendor/github.com/klauspost/compress/zstd/encoder.go b/vendor/github.com/klauspost/compress/zstd/encoder.go index 0f2a00a00..6ee96d873 100644 --- a/vendor/github.com/klauspost/compress/zstd/encoder.go +++ b/vendor/github.com/klauspost/compress/zstd/encoder.go @@ -38,6 +38,7 @@ type encoder interface { WindowSize(size int64) int32 UseBlock(*blockEnc) Reset(d *dict, singleBlock bool) + ResetPrefix(prefix []byte) } type encoderState struct { @@ -60,6 +61,9 @@ type encoderState struct { wg sync.WaitGroup // This waitgroup indicates we have a block encoding/writing. wWg sync.WaitGroup + + // Parallel job state (used when concurrentBlocks is enabled). + jobs jobState } // NewWriter will create a new Zstandard encoder. @@ -74,6 +78,9 @@ func NewWriter(w io.Writer, opts ...EOption) (*Encoder, error) { return nil, err } } + if e.o.concurrentBlocks && (e.o.dict != nil || e.o.concurrent <= 1) { + e.o.concurrentBlocks = false + } if w != nil { e.Reset(w) } @@ -95,12 +102,31 @@ func (e *Encoder) initialize() { // as a new, independent stream. func (e *Encoder) Reset(w io.Writer) { s := &e.state + + if e.o.concurrentBlocks { + e.shutdownJobWorkers() + js := &s.jobs + js.jobSize = e.o.jobSize() + js.overlapSize = e.o.overlapSize() + // js.filling is allocated lazily on first Write/ReadFrom so callers + // that only use EncodeAll don't pay the (up to ~32 MB) jobSize cost. + js.filling = js.filling[:0] + if js.nextPrefix != nil { + js.putOverlapBuf(js.nextPrefix) + js.nextPrefix = nil + } + js.jobSeq = 0 + js.flushedSeq = 0 + js.flusherErr = nil + js.started = false + } + s.wg.Wait() s.wWg.Wait() if cap(s.filling) == 0 { s.filling = make([]byte, 0, e.o.blockSize) } - if e.o.concurrent > 1 { + if e.o.concurrent > 1 && !e.o.concurrentBlocks { if cap(s.current) == 0 { s.current = make([]byte, 0, e.o.blockSize) } @@ -145,6 +171,9 @@ func (e *Encoder) ResetWithOptions(w io.Writer, opts ...EOption) error { } } hasDict := e.o.dict != nil + if e.o.concurrentBlocks && hasDict { + e.o.concurrentBlocks = false + } if hadDict != hasDict { // Dict presence changed — encoder type must be recreated. e.state.encoder = nil @@ -176,6 +205,49 @@ func (e *Encoder) Write(p []byte) (n int, err error) { if s.eofWritten { return 0, ErrEncoderClosed } + if e.o.concurrentBlocks { + return e.writeJobs(p) + } + return e.writeBlocks(p) +} + +func (e *Encoder) writeJobs(p []byte) (n int, err error) { + s := &e.state + js := &s.jobs + jobSize := js.jobSize + if cap(js.filling) == 0 && len(p) > 0 { + js.filling = make([]byte, 0, jobSize) + } + for len(p) > 0 { + if len(p)+len(js.filling) < jobSize { + if e.o.crc { + _, _ = s.encoder.CRC().Write(p) + } + js.filling = append(js.filling, p...) + return n + len(p), nil + } + add := p + if len(p)+len(js.filling) > jobSize { + add = add[:jobSize-len(js.filling)] + } + if e.o.crc { + _, _ = s.encoder.CRC().Write(add) + } + js.filling = append(js.filling, add...) + p = p[len(add):] + n += len(add) + if len(js.filling) < jobSize { + return n, nil + } + if err := e.dispatchJob(false); err != nil { + return n, err + } + } + return n, nil +} + +func (e *Encoder) writeBlocks(p []byte) (n int, err error) { + s := &e.state for len(p) > 0 { if len(p)+len(s.filling) < e.o.blockSize { if e.o.crc { @@ -374,6 +446,10 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) { println("Using ReadFrom") } + if e.o.concurrentBlocks { + return e.readFromJobs(r) + } + // Flush any current writes. if len(e.state.filling) > 0 { if err := e.nextBlock(false); err != nil { @@ -387,7 +463,6 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) { if e.o.crc { _, _ = e.state.encoder.CRC().Write(src[:n2]) } - // src is now the unfilled part... src = src[n2:] n += int64(n2) switch err { @@ -420,15 +495,63 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) { } } +func (e *Encoder) readFromJobs(r io.Reader) (n int64, err error) { + js := &e.state.jobs + jobSize := js.jobSize + + // Flush any current filling. + if len(js.filling) > 0 { + if err := e.dispatchJob(false); err != nil { + return 0, err + } + } + + if cap(js.filling) < jobSize { + js.filling = make([]byte, 0, jobSize) + } + js.filling = js.filling[:jobSize] + src := js.filling + for { + n2, err := r.Read(src) + if e.o.crc { + _, _ = e.state.encoder.CRC().Write(src[:n2]) + } + src = src[n2:] + n += int64(n2) + switch err { + case io.EOF: + js.filling = js.filling[:len(js.filling)-len(src)] + return n, nil + case nil: + default: + e.state.err = err + return n, err + } + if len(src) > 0 { + continue + } + if err = e.dispatchJob(false); err != nil { + return n, err + } + if cap(js.filling) < jobSize { + js.filling = make([]byte, 0, jobSize) + } + js.filling = js.filling[:jobSize] + src = js.filling + } +} + // Flush will send the currently written data to output // and block until everything has been written. // This should only be used on rare occasions where pushing the currently queued data is critical. func (e *Encoder) Flush() error { s := &e.state + if e.o.concurrentBlocks { + return e.flushJobs() + } if len(s.filling) > 0 { err := e.nextBlock(false) if err != nil { - // Ignore Flush after Close. if errors.Is(s.err, ErrEncoderClosed) { return nil } @@ -438,7 +561,6 @@ func (e *Encoder) Flush() error { s.wg.Wait() s.wWg.Wait() if s.err != nil { - // Ignore Flush after Close. if errors.Is(s.err, ErrEncoderClosed) { return nil } @@ -447,6 +569,20 @@ func (e *Encoder) Flush() error { return s.writeErr } +func (e *Encoder) flushJobs() error { + js := &e.state.jobs + if len(js.filling) > 0 { + if err := e.dispatchJob(false); err != nil { + return err + } + } + e.waitAllJobs() + js.mu.Lock() + fErr := js.flusherErr + js.mu.Unlock() + return fErr +} + // Close will flush the final output and close the stream. // The function will block until everything has been written. // The Encoder can still be re-used after calling this. @@ -455,12 +591,16 @@ func (e *Encoder) Close() error { if s.encoder == nil { return nil } + if e.o.concurrentBlocks { + return e.closeJobs() + } if s.w == nil { if len(s.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 { return nil } return errors.New("zstd: encoder has no writer") } + err := e.nextBlock(true) if err != nil { if errors.Is(s.err, ErrEncoderClosed) { @@ -511,6 +651,68 @@ func (e *Encoder) Close() error { return s.err } +func (e *Encoder) closeJobs() error { + s := &e.state + js := &s.jobs + + if errors.Is(s.err, ErrEncoderClosed) { + return nil + } + + if s.w == nil { + if len(js.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 { + return nil + } + return errors.New("zstd: encoder has no writer") + } + + if err := e.dispatchJob(true); err != nil { + e.shutdownJobWorkers() + if errors.Is(s.err, ErrEncoderClosed) { + return nil + } + return err + } + + if s.frameContentSize > 0 && s.nInput != s.frameContentSize { + e.shutdownJobWorkers() + return fmt.Errorf("frame content size %d given, but %d bytes was written", s.frameContentSize, s.nInput) + } + + if s.fullFrameWritten { + e.shutdownJobWorkers() + s.err = ErrEncoderClosed + return nil + } + + e.shutdownJobWorkers() + if js.flusherErr != nil { + return js.flusherErr + } + + // Write CRC + if e.o.crc { + var tmp [4]byte + _, s.err = s.w.Write(s.encoder.AppendCRC(tmp[:0])) + s.nWritten += 4 + } + + // Add padding + if s.err == nil && e.o.pad > 0 { + add := calcSkippableFrame(s.nWritten, int64(e.o.pad)) + frame, err := skippableFrame(js.filling[:0], add, rand.Reader) + if err != nil { + return err + } + _, s.err = s.w.Write(frame) + } + if s.err == nil { + s.err = ErrEncoderClosed + return nil + } + return s.err +} + // EncodeAll will encode all input in src and append it to dst. // This function can be called concurrently, but each call will only run on a single goroutine. // If empty input is given, nothing is returned, unless WithZeroFrames is specified. diff --git a/vendor/github.com/klauspost/compress/zstd/encoder_options.go b/vendor/github.com/klauspost/compress/zstd/encoder_options.go index e217be0a1..a80814967 100644 --- a/vendor/github.com/klauspost/compress/zstd/encoder_options.go +++ b/vendor/github.com/klauspost/compress/zstd/encoder_options.go @@ -14,22 +14,23 @@ type EOption func(*encoderOptions) error // options retains accumulated state of multiple options. type encoderOptions struct { - resetOpt bool - concurrent int - level EncoderLevel - single *bool - pad int - blockSize int - windowSize int - crc bool - fullZero bool - noEntropy bool - allLitEntropy bool - customWindow bool - customALEntropy bool - customBlockSize bool - lowMem bool - dict *dict + resetOpt bool + concurrent int + level EncoderLevel + single *bool + pad int + blockSize int + windowSize int + crc bool + fullZero bool + noEntropy bool + allLitEntropy bool + customWindow bool + customALEntropy bool + customBlockSize bool + lowMem bool + dict *dict + concurrentBlocks bool } func (o *encoderOptions) setDefault() { @@ -333,6 +334,42 @@ func WithLowerEncoderMem(b bool) EOption { } } +// WithConcurrentBlocks enables job-based parallel compression for streams. +// When enabled and concurrent > 1, input is split into large sections (jobs) +// that are compressed simultaneously by multiple goroutines. +// Each non-first job receives an overlap prefix from the previous job for match context. +// Output is flushed in order, producing a valid single-frame zstd stream. +// +// Currently disabled when used with dictionary encoding. +// Cannot be changed with ResetWithOptions. +func WithConcurrentBlocks(b bool) EOption { + return func(o *encoderOptions) error { + if o.resetOpt && b != o.concurrentBlocks { + return errors.New("WithConcurrentBlocks cannot be changed on Reset") + } + o.concurrentBlocks = b + return nil + } +} + +// jobSize returns the input section size per parallel job. +func (o *encoderOptions) jobSize() int { + s := max(o.windowSize*4, 512<<10) + return s +} + +// overlapSize returns the overlap prefix size for parallel jobs. +func (o *encoderOptions) overlapSize() int { + switch o.level { + case SpeedBestCompression: + return o.windowSize / 2 + case SpeedBetterCompression: + return o.windowSize / 4 + default: + return o.windowSize / 8 + } +} + // WithEncoderDict allows to register a dictionary that will be used for the encode. // // The slice dict must be in the [dictionary format] produced by diff --git a/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.s b/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.s index bcde39869..deeadc49e 100644 --- a/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.s +++ b/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.s @@ -1,4 +1,4 @@ -// Code generated by command: go run gen_fse.go -out ../fse_decoder_amd64.s -pkg=zstd. DO NOT EDIT. +// Code generated by command: go run gen_fse.go -out ../fse_decoder.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT. //go:build !appengine && !noasm && gc && !noasm diff --git a/vendor/github.com/klauspost/compress/zstd/fse_decoder_arm64.s b/vendor/github.com/klauspost/compress/zstd/fse_decoder_arm64.s new file mode 100644 index 000000000..77ee3913f --- /dev/null +++ b/vendor/github.com/klauspost/compress/zstd/fse_decoder_arm64.s @@ -0,0 +1,153 @@ +// Code generated by command: go run gen_fse.go -out ../fse_decoder.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT. +// EXPERIMENTAL arm64 output lowered from an amd64 avo program. + +//go:build arm64 && !appengine && !noasm && gc && !noasm + +// func buildDtable_asm(s *fseDecoder, ctx *buildDtableAsmContext) int +TEXT ·buildDtable_asm(SB), $0-24 + MOVD ctx+8(FP), R1 + MOVD s+0(FP), R6 + + // Load values + MOVBU 4098(R6), R2 + MOVD $0, R0 + MOVD $1, R16 + LSL R2, R16, R16 + ORR R16, R0, R0 + MOVD (R1), R3 + MOVD 16(R1), R5 + SUB $1, R0, R7 + MOVD 8(R1), R1 + MOVHU 4096(R6), R6 + + // End load values + // Init, lay down lowprob symbols + MOVD $0, R8 + JMP init_main_loop_condition + +init_main_loop: + ADD R8<<1, R1, R15 + MOVH (R15), R9 + AND $0xffff, R9, R15 + MOVD $-1, R16 + AND $0xffff, R16, R16 + CMP R16, R15 + BNE do_not_update_high_threshold + ADD R7<<3, R5, R15 + MOVB R8, 1(R15) + SUB $1, R7, R7 + MOVD $0x0000000000000001, R9 + +do_not_update_high_threshold: + ADD R8<<1, R3, R15 + MOVH R9, (R15) + ADD $1, R8, R8 + +init_main_loop_condition: + CMP R6, R8 + BLT init_main_loop + + // Spread symbols + // Calculate table step + MOVD R0, R8 + LSR $0x01, R8, R8 + MOVD R0, R9 + LSR $0x03, R9, R9 + ADD R9, R8, R8 + ADD $3, R8, R8 + + // Fill add bits values + SUB $1, R0, R9 + MOVD $0, R10 + MOVD $0, R11 + JMP spread_main_loop_condition + +spread_main_loop: + MOVD $0, R12 + ADD R11<<1, R1, R15 + MOVH (R15), R13 + JMP spread_inner_loop_condition + +spread_inner_loop: + ADD R10<<3, R5, R15 + MOVB R11, 1(R15) + +adjust_position: + ADD R8, R10, R10 + AND R9, R10, R10 + CMP R7, R10 + BGT adjust_position + ADD $1, R12, R12 + +spread_inner_loop_condition: + CMP R13, R12 + BLT spread_inner_loop + ADD $1, R11, R11 + +spread_main_loop_condition: + CMP R6, R11 + BLT spread_main_loop + TST R10, R10 + BEQ spread_check_ok + MOVD ctx+8(FP), R0 + MOVD R10, 24(R0) + MOVD $+1, R16 + MOVD R16, ret+16(FP) + RET + +spread_check_ok: + // Build Decoding table + MOVD $0, R6 + +build_table_main_table: + ADD R6<<3, R5, R15 + MOVBU 1(R15), R1 + ADD R1<<1, R3, R15 + MOVHU (R15), R7 + ADD $1, R7, R8 + ADD R1<<1, R3, R15 + MOVH R8, (R15) + MOVD R7, R8 + CLZ R8, R16 + MOVD $63, R8 + SUB R16, R8, R8 + MOVD R2, R1 + SUB R8, R1, R1 + LSL R1, R7, R7 + SUB R0, R7, R7 + ADD R6<<3, R5, R15 + MOVB R1, (R15) + ADD R6<<3, R5, R15 + MOVH R7, 2(R15) + CMP R0, R7 + BLE build_table_check1_ok + MOVD ctx+8(FP), R1 + MOVD R7, 24(R1) + MOVD R0, 32(R1) + MOVD $+2, R16 + MOVD R16, ret+16(FP) + RET + +build_table_check1_ok: + AND $0xff, R1, R15 + AND $0xff, R1, R16 + TST R16, R15 + BNE build_table_check2_ok + AND $0xffff, R7, R15 + AND $0xffff, R6, R16 + CMP R16, R15 + BNE build_table_check2_ok + MOVD ctx+8(FP), R0 + MOVD R7, 24(R0) + MOVD R6, 32(R0) + MOVD $+3, R16 + MOVD R16, ret+16(FP) + RET + +build_table_check2_ok: + ADD $1, R6, R6 + CMP R0, R6 + BLT build_table_main_table + MOVD $+0, R16 + MOVD R16, ret+16(FP) + RET diff --git a/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go b/vendor/github.com/klauspost/compress/zstd/fse_decoder_asm.go similarity index 81% rename from vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go rename to vendor/github.com/klauspost/compress/zstd/fse_decoder_asm.go index b8c8607b5..4ffc7e3c9 100644 --- a/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go +++ b/vendor/github.com/klauspost/compress/zstd/fse_decoder_asm.go @@ -1,4 +1,4 @@ -//go:build amd64 && !appengine && !noasm && gc +//go:build (amd64 || arm64) && !appengine && !noasm && gc package zstd @@ -6,6 +6,10 @@ import ( "fmt" ) +// buildDtable_asm is generated by _generate/gen_fse.go and lowered to each +// architecture (amd64 by goasm, arm64 by the avo arm64 lowering printer). The +// Go side is identical across architectures, so it lives here. + type buildDtableAsmContext struct { // inputs stateTable *uint16 @@ -18,7 +22,7 @@ type buildDtableAsmContext struct { errParam2 uint64 } -// buildDtable_asm is an x86 assembly implementation of fseDecoder.buildDtable. +// buildDtable_asm is an assembly implementation of fseDecoder.buildDtable. // Function returns non-zero exit code on error. // //go:noescape diff --git a/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go b/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go index 2138f8091..38fd2ccb2 100644 --- a/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go +++ b/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go @@ -1,4 +1,4 @@ -//go:build !amd64 || appengine || !gc || noasm +//go:build (!amd64 && !arm64) || appengine || !gc || noasm package zstd diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go index 18c3703dd..1281da885 100644 --- a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go @@ -3,321 +3,83 @@ package zstd import ( - "fmt" - "io" - "github.com/klauspost/compress/internal/cpuinfo" ) -type decodeSyncAsmContext struct { - llTable []decSymbol - mlTable []decSymbol - ofTable []decSymbol - llState uint64 - mlState uint64 - ofState uint64 - iteration int - litRemain int - out []byte - outPosition int - literals []byte - litPosition int - history []byte - windowSize int - ll int // set on error (not for all errors, please refer to _generate/gen.go) - ml int // set on error (not for all errors, please refer to _generate/gen.go) - mo int // set on error (not for all errors, please refer to _generate/gen.go) -} +// The shared decode/decodeSync/executeSimple wrappers and context structs live +// in seqdec_asm.go; this file only declares the amd64 asm routines and the +// dispatch helpers that pick the BMI2 / non-BMI2 (and 56-bit / safe) variant. -// sequenceDecs_decodeSync_amd64 implements the main loop of sequenceDecs.decodeSync in x86 asm. +// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm. // // Please refer to seqdec_generic.go for the reference implementation. // //go:noescape -func sequenceDecs_decodeSync_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int +func sequenceDecs_decode_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int -// sequenceDecs_decodeSync_bmi2 implements the main loop of sequenceDecs.decodeSync in x86 asm with BMI2 extensions. +// sequenceDecs_decode_56_amd64 implements the main loop of sequenceDecs in x86 asm. // //go:noescape -func sequenceDecs_decodeSync_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int +func sequenceDecs_decode_56_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int -// sequenceDecs_decodeSync_safe_amd64 does the same as above, but does not write more than output buffer. +// sequenceDecs_decode_bmi2 implements the main loop of sequenceDecs in x86 asm with BMI2 extensions. // //go:noescape -func sequenceDecs_decodeSync_safe_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int +func sequenceDecs_decode_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int -// sequenceDecs_decodeSync_safe_bmi2 does the same as above, but does not write more than output buffer. +// sequenceDecs_decode_56_bmi2 implements the main loop of sequenceDecs in x86 asm with BMI2 extensions. // //go:noescape -func sequenceDecs_decodeSync_safe_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int - -// decode sequences from the stream with the provided history but without a dictionary. -func (s *sequenceDecs) decodeSyncSimple(hist []byte) (bool, error) { - if len(s.dict) > 0 { - return false, nil - } - if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSize { - return false, nil - } - - // FIXME: Using unsafe memory copies leads to rare, random crashes - // with fuzz testing. It is therefore disabled for now. - const useSafe = true - /* - useSafe := false - if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSizeAlloc { - useSafe = true - } - if s.maxSyncLen > 0 && cap(s.out)-len(s.out)-compressedBlockOverAlloc < int(s.maxSyncLen) { - useSafe = true - } - if cap(s.literals) < len(s.literals)+compressedBlockOverAlloc { - useSafe = true - } - */ - - br := s.br - - maxBlockSize := min(s.windowSize, maxCompressedBlockSize) - - ctx := decodeSyncAsmContext{ - llTable: s.litLengths.fse.dt[:maxTablesize], - mlTable: s.matchLengths.fse.dt[:maxTablesize], - ofTable: s.offsets.fse.dt[:maxTablesize], - llState: uint64(s.litLengths.state.state), - mlState: uint64(s.matchLengths.state.state), - ofState: uint64(s.offsets.state.state), - iteration: s.nSeqs - 1, - litRemain: len(s.literals), - out: s.out, - outPosition: len(s.out), - literals: s.literals, - windowSize: s.windowSize, - history: hist, - } - - s.seqSize = 0 - startSize := len(s.out) +func sequenceDecs_decode_56_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int - var errCode int +// decodeAsm runs the sequenceDecs decode loop, choosing the BMI2 / 56-bit variant. +func decodeAsm(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext, lte56bits bool) int { if cpuinfo.HasBMI2() { - if useSafe { - errCode = sequenceDecs_decodeSync_safe_bmi2(s, br, &ctx) - } else { - errCode = sequenceDecs_decodeSync_bmi2(s, br, &ctx) - } - } else { - if useSafe { - errCode = sequenceDecs_decodeSync_safe_amd64(s, br, &ctx) - } else { - errCode = sequenceDecs_decodeSync_amd64(s, br, &ctx) - } - } - switch errCode { - case noError: - break - - case errorMatchLenOfsMismatch: - return true, fmt.Errorf("zero matchoff and matchlen (%d) > 0", ctx.ml) - - case errorMatchLenTooBig: - return true, fmt.Errorf("match len (%d) bigger than max allowed length", ctx.ml) - - case errorMatchOffTooBig: - return true, fmt.Errorf("match offset (%d) bigger than current history (%d)", - ctx.mo, ctx.outPosition+len(hist)-startSize) - - case errorNotEnoughLiterals: - return true, fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", - ctx.ll, ctx.litRemain+ctx.ll) - - case errorOverread: - return true, io.ErrUnexpectedEOF - - case errorNotEnoughSpace: - size := ctx.outPosition + ctx.ll + ctx.ml - if debugDecoder { - println("msl:", s.maxSyncLen, "cap", cap(s.out), "bef:", startSize, "sz:", size-startSize, "mbs:", maxBlockSize, "outsz:", cap(s.out)-startSize) + if lte56bits { + return sequenceDecs_decode_56_bmi2(s, br, ctx) } - return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) - - default: - return true, fmt.Errorf("sequenceDecs_decode returned erroneous code %d", errCode) - } - - s.seqSize += ctx.litRemain - if s.seqSize > maxBlockSize { - return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) + return sequenceDecs_decode_bmi2(s, br, ctx) } - err := br.close() - if err != nil { - printf("Closing sequences: %v, %+v\n", err, *br) - return true, err + if lte56bits { + return sequenceDecs_decode_56_amd64(s, br, ctx) } - - s.literals = s.literals[ctx.litPosition:] - t := ctx.outPosition - s.out = s.out[:t] - - // Add final literals - s.out = append(s.out, s.literals...) - if debugDecoder { - t += len(s.literals) - if t != len(s.out) { - panic(fmt.Errorf("length mismatch, want %d, got %d", len(s.out), t)) - } - } - - return true, nil + return sequenceDecs_decode_amd64(s, br, ctx) } -// -------------------------------------------------------------------------------- - -type decodeAsmContext struct { - llTable []decSymbol - mlTable []decSymbol - ofTable []decSymbol - llState uint64 - mlState uint64 - ofState uint64 - iteration int - seqs []seqVals - litRemain int -} - -const noError = 0 - -// error reported when mo == 0 && ml > 0 -const errorMatchLenOfsMismatch = 1 - -// error reported when ml > maxMatchLen -const errorMatchLenTooBig = 2 - -// error reported when mo > available history or mo > s.windowSize -const errorMatchOffTooBig = 3 - -// error reported when the sum of literal lengths exeeceds the literal buffer size -const errorNotEnoughLiterals = 4 - -// error reported when capacity of `out` is too small -const errorNotEnoughSpace = 5 - -// error reported when bits are overread. -const errorOverread = 6 - -// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm. +// sequenceDecs_decodeSync_amd64 implements the main loop of sequenceDecs.decodeSync in x86 asm. // // Please refer to seqdec_generic.go for the reference implementation. // //go:noescape -func sequenceDecs_decode_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int +func sequenceDecs_decodeSync_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int -// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm. -// -// Please refer to seqdec_generic.go for the reference implementation. +// sequenceDecs_decodeSync_bmi2 implements the main loop of sequenceDecs.decodeSync in x86 asm with BMI2 extensions. // //go:noescape -func sequenceDecs_decode_56_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int +func sequenceDecs_decodeSync_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int -// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm with BMI2 extensions. +// sequenceDecs_decodeSync_safe_amd64 does the same as above, but does not write more than output buffer. // //go:noescape -func sequenceDecs_decode_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int +func sequenceDecs_decodeSync_safe_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int -// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm with BMI2 extensions. +// sequenceDecs_decodeSync_safe_bmi2 does the same as above, but does not write more than output buffer. // //go:noescape -func sequenceDecs_decode_56_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int - -// decode sequences from the stream without the provided history. -func (s *sequenceDecs) decode(seqs []seqVals) error { - br := s.br - - maxBlockSize := min(s.windowSize, maxCompressedBlockSize) - - ctx := decodeAsmContext{ - llTable: s.litLengths.fse.dt[:maxTablesize], - mlTable: s.matchLengths.fse.dt[:maxTablesize], - ofTable: s.offsets.fse.dt[:maxTablesize], - llState: uint64(s.litLengths.state.state), - mlState: uint64(s.matchLengths.state.state), - ofState: uint64(s.offsets.state.state), - seqs: seqs, - iteration: len(seqs) - 1, - litRemain: len(s.literals), - } - - if debugDecoder { - println("decode: decoding", len(seqs), "sequences", br.remain(), "bits remain on stream") - } +func sequenceDecs_decodeSync_safe_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int - s.seqSize = 0 - lte56bits := s.maxBits+s.offsets.fse.actualTableLog+s.matchLengths.fse.actualTableLog+s.litLengths.fse.actualTableLog <= 56 - var errCode int +// decodeSyncAsm runs the decodeSync loop, choosing the BMI2 / safe variant. +func decodeSyncAsm(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext, safe bool) int { if cpuinfo.HasBMI2() { - if lte56bits { - errCode = sequenceDecs_decode_56_bmi2(s, br, &ctx) - } else { - errCode = sequenceDecs_decode_bmi2(s, br, &ctx) - } - } else { - if lte56bits { - errCode = sequenceDecs_decode_56_amd64(s, br, &ctx) - } else { - errCode = sequenceDecs_decode_amd64(s, br, &ctx) + if safe { + return sequenceDecs_decodeSync_safe_bmi2(s, br, ctx) } + return sequenceDecs_decodeSync_bmi2(s, br, ctx) } - if errCode != 0 { - i := len(seqs) - ctx.iteration - 1 - switch errCode { - case errorMatchLenOfsMismatch: - ml := ctx.seqs[i].ml - return fmt.Errorf("zero matchoff and matchlen (%d) > 0", ml) - - case errorMatchLenTooBig: - ml := ctx.seqs[i].ml - return fmt.Errorf("match len (%d) bigger than max allowed length", ml) - - case errorNotEnoughLiterals: - ll := ctx.seqs[i].ll - return fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", ll, ctx.litRemain+ll) - case errorOverread: - return io.ErrUnexpectedEOF - } - - return fmt.Errorf("sequenceDecs_decode_amd64 returned erroneous code %d", errCode) + if safe { + return sequenceDecs_decodeSync_safe_amd64(s, br, ctx) } - - if ctx.litRemain < 0 { - return fmt.Errorf("literal count is too big: total available %d, total requested %d", - len(s.literals), len(s.literals)-ctx.litRemain) - } - - s.seqSize += ctx.litRemain - if s.seqSize > maxBlockSize { - return fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) - } - if debugDecoder { - println("decode: ", br.remain(), "bits remain on stream. code:", errCode) - } - err := br.close() - if err != nil { - printf("Closing sequences: %v, %+v\n", err, *br) - } - return err -} - -// -------------------------------------------------------------------------------- - -type executeAsmContext struct { - seqs []seqVals - seqIndex int - out []byte - history []byte - literals []byte - outPosition int - litPosition int - windowSize int + return sequenceDecs_decodeSync_amd64(s, br, ctx) } // sequenceDecs_executeSimple_amd64 implements the main loop of sequenceDecs.executeSimple in x86 asm. @@ -334,54 +96,10 @@ func sequenceDecs_executeSimple_amd64(ctx *executeAsmContext) bool //go:noescape func sequenceDecs_executeSimple_safe_amd64(ctx *executeAsmContext) bool -// executeSimple handles cases when dictionary is not used. -func (s *sequenceDecs) executeSimple(seqs []seqVals, hist []byte) error { - // Ensure we have enough output size... - if len(s.out)+s.seqSize+compressedBlockOverAlloc > cap(s.out) { - addBytes := s.seqSize + len(s.out) + compressedBlockOverAlloc - s.out = append(s.out, make([]byte, addBytes)...) - s.out = s.out[:len(s.out)-addBytes] - } - - if debugDecoder { - printf("Execute %d seqs with literals: %d into %d bytes\n", len(seqs), len(s.literals), s.seqSize) - } - - var t = len(s.out) - out := s.out[:t+s.seqSize] - - ctx := executeAsmContext{ - seqs: seqs, - seqIndex: 0, - out: out, - history: hist, - outPosition: t, - litPosition: 0, - literals: s.literals, - windowSize: s.windowSize, +// executeSimpleAsm runs the executeSimple loop, choosing the safe variant. +func executeSimpleAsm(ctx *executeAsmContext, safe bool) bool { + if safe { + return sequenceDecs_executeSimple_safe_amd64(ctx) } - var ok bool - if cap(s.literals) < len(s.literals)+compressedBlockOverAlloc { - ok = sequenceDecs_executeSimple_safe_amd64(&ctx) - } else { - ok = sequenceDecs_executeSimple_amd64(&ctx) - } - if !ok { - return fmt.Errorf("match offset (%d) bigger than current history (%d)", - seqs[ctx.seqIndex].mo, ctx.outPosition+len(hist)) - } - s.literals = s.literals[ctx.litPosition:] - t = ctx.outPosition - - // Add final literals - copy(out[t:], s.literals) - if debugDecoder { - t += len(s.literals) - if t != len(out) { - panic(fmt.Errorf("length mismatch, want %d, got %d, ss: %d", len(out), t, s.seqSize)) - } - } - s.out = out - - return nil + return sequenceDecs_executeSimple_amd64(ctx) } diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s index a708ca6d3..3fc381c7a 100644 --- a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s @@ -1,4 +1,4 @@ -// Code generated by command: go run gen.go -out ../seqdec_amd64.s -pkg=zstd. DO NOT EDIT. +// Code generated by command: go run gen.go -out ../seqdec.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT. //go:build !appengine && !noasm && gc && !noasm diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.go b/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.go new file mode 100644 index 000000000..5ad262acf --- /dev/null +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.go @@ -0,0 +1,70 @@ +//go:build arm64 && !appengine && !noasm && gc + +package zstd + +// The shared decode/decodeSync/executeSimple wrappers and context structs live +// in seqdec_asm.go; this file only declares the arm64 asm routines (generated +// by the avo arm64 lowering printer) and the dispatch helpers. arm64 has no +// BMI2, so each helper selects only between the 56-bit / safe variants. + +// sequenceDecs_decode_arm64 implements the main loop of sequenceDecs in arm64 asm. +// +// Please refer to seqdec_generic.go for the reference implementation. +// +//go:noescape +func sequenceDecs_decode_arm64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int + +// sequenceDecs_decode_56_arm64 implements the main loop of sequenceDecs in arm64 asm. +// +//go:noescape +func sequenceDecs_decode_56_arm64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int + +// decodeAsm runs the sequenceDecs decode loop, choosing the 56-bit variant. +func decodeAsm(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext, lte56bits bool) int { + if lte56bits { + return sequenceDecs_decode_56_arm64(s, br, ctx) + } + return sequenceDecs_decode_arm64(s, br, ctx) +} + +// sequenceDecs_decodeSync_arm64 implements the main loop of sequenceDecs.decodeSync in arm64 asm. +// +// Please refer to seqdec_generic.go for the reference implementation. +// +//go:noescape +func sequenceDecs_decodeSync_arm64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int + +// sequenceDecs_decodeSync_safe_arm64 does the same as above, but does not write more than output buffer. +// +//go:noescape +func sequenceDecs_decodeSync_safe_arm64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int + +// decodeSyncAsm runs the decodeSync loop, choosing the safe variant. +func decodeSyncAsm(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext, safe bool) int { + if safe { + return sequenceDecs_decodeSync_safe_arm64(s, br, ctx) + } + return sequenceDecs_decodeSync_arm64(s, br, ctx) +} + +// sequenceDecs_executeSimple_arm64 implements the main loop of sequenceDecs.executeSimple in arm64 asm. +// +// Returns false if a match offset is too big. +// +// Please refer to seqdec_generic.go for the reference implementation. +// +//go:noescape +func sequenceDecs_executeSimple_arm64(ctx *executeAsmContext) bool + +// Same as above, but with safe memcopies +// +//go:noescape +func sequenceDecs_executeSimple_safe_arm64(ctx *executeAsmContext) bool + +// executeSimpleAsm runs the executeSimple loop, choosing the safe variant. +func executeSimpleAsm(ctx *executeAsmContext, safe bool) bool { + if safe { + return sequenceDecs_executeSimple_safe_arm64(ctx) + } + return sequenceDecs_executeSimple_arm64(ctx) +} diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.s b/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.s new file mode 100644 index 000000000..a468e5fc2 --- /dev/null +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_arm64.s @@ -0,0 +1,2705 @@ +// Code generated by command: go run gen.go -out ../seqdec.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT. +// EXPERIMENTAL arm64 output lowered from an amd64 avo program. + +//go:build arm64 && !appengine && !noasm && gc && !noasm + +// func sequenceDecs_decode_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int +// Requires: CMOV +TEXT ·sequenceDecs_decode_arm64(SB), $8-32 + MOVD br+8(FP), R1 + MOVD 24(R1), R2 + MOVBU 40(R1), R3 + MOVD (R1), R0 + MOVD 32(R1), R5 + ADD R5, R0, R0 + MOVD R0, (RSP) + MOVD ctx+16(FP), R0 + MOVD 72(R0), R6 + MOVD 80(R0), R7 + MOVD 88(R0), R8 + MOVD 104(R0), R9 + MOVD s+0(FP), R0 + MOVD 144(R0), R10 + MOVD 152(R0), R11 + MOVD 160(R0), R12 + +sequenceDecs_decode_amd64_main_loop: + MOVD (RSP), R13 + + // Fill bitreader to have enough for the offset and match length. + CMP $0x08, R5 + BLT sequenceDecs_decode_amd64_fill_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R13, R13 + MOVD (R13), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decode_amd64_fill_end + +sequenceDecs_decode_amd64_fill_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decode_amd64_fill_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decode_amd64_fill_end + LSL $0x08, R2, R2 + SUB $0x01, R13, R13 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R13), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decode_amd64_fill_byte_by_byte + +sequenceDecs_decode_amd64_fill_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decode_amd64_fill_end: + // Update offset + MOVD R8, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_amd64_of_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_amd64_of_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_amd64_of_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_amd64_of_update_zero: + MOVD R0, 16(R9) + + // Update match length + MOVD R7, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_amd64_ml_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_amd64_ml_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_amd64_ml_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_amd64_ml_update_zero: + MOVD R0, 8(R9) + + // Fill bitreader to have enough for the remaining + CMP $0x08, R5 + BLT sequenceDecs_decode_amd64_fill_2_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R13, R13 + MOVD (R13), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decode_amd64_fill_2_end + +sequenceDecs_decode_amd64_fill_2_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decode_amd64_fill_2_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decode_amd64_fill_2_end + LSL $0x08, R2, R2 + SUB $0x01, R13, R13 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R13), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decode_amd64_fill_2_byte_by_byte + +sequenceDecs_decode_amd64_fill_2_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decode_amd64_fill_2_end: + // Update literal length + MOVD R6, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_amd64_ll_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_amd64_ll_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_amd64_ll_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_amd64_ll_update_zero: + MOVD R0, (R9) + + // Fill bitreader for state updates + MOVD R13, (RSP) + MOVD R8, R0 + LSR $0x08, R0, R0 + MOVBU R0, R0 + MOVD ctx+16(FP), R1 + MOVD 96(R1), R16 + CMP $0x00, R16 + BEQ sequenceDecs_decode_amd64_skip_update + + // Update Literal Length State + MOVBU R6, R13 + LSRW $0x10, R6, R6 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R6, R6 + + // Load ctx.llTable + MOVD ctx+16(FP), R1 + MOVD (R1), R1 + ADD R6<<3, R1, R15 + MOVD (R15), R6 + + // Update Match Length State + MOVBU R7, R13 + LSRW $0x10, R7, R7 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R7, R7 + + // Load ctx.mlTable + MOVD ctx+16(FP), R1 + MOVD 24(R1), R1 + ADD R7<<3, R1, R15 + MOVD (R15), R7 + + // Update Offset State + MOVBU R8, R13 + LSRW $0x10, R8, R8 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R8, R8 + + // Load ctx.ofTable + MOVD ctx+16(FP), R1 + MOVD 48(R1), R1 + ADD R8<<3, R1, R15 + MOVD (R15), R8 + +sequenceDecs_decode_amd64_skip_update: + // Adjust offset + MOVD 16(R9), R1 + CMP $0x01, R0 + BLS sequenceDecs_decode_amd64_adjust_offsetB_1_or_0 + MOVD R11, R12 + MOVD R10, R11 + MOVD R1, R10 + JMP sequenceDecs_decode_amd64_after_adjust + +sequenceDecs_decode_amd64_adjust_offsetB_1_or_0: + MOVD (R9), R16 + CMP $0x00000000, R16 + BNE sequenceDecs_decode_amd64_adjust_offset_maybezero + ADD $1, R1, R1 + JMP sequenceDecs_decode_amd64_adjust_offset_nonzero + +sequenceDecs_decode_amd64_adjust_offset_maybezero: + TST R1, R1 + BNE sequenceDecs_decode_amd64_adjust_offset_nonzero + MOVD R10, R1 + JMP sequenceDecs_decode_amd64_after_adjust + +sequenceDecs_decode_amd64_adjust_offset_nonzero: + CMP $0x01, R1 + BLO sequenceDecs_decode_amd64_adjust_zero + BEQ sequenceDecs_decode_amd64_adjust_one + CMP $0x02, R1 + BHI sequenceDecs_decode_amd64_adjust_three + JMP sequenceDecs_decode_amd64_adjust_two + +sequenceDecs_decode_amd64_adjust_zero: + MOVD R10, R0 + JMP sequenceDecs_decode_amd64_adjust_test_temp_valid + +sequenceDecs_decode_amd64_adjust_one: + MOVD R11, R0 + JMP sequenceDecs_decode_amd64_adjust_test_temp_valid + +sequenceDecs_decode_amd64_adjust_two: + MOVD R12, R0 + JMP sequenceDecs_decode_amd64_adjust_test_temp_valid + +sequenceDecs_decode_amd64_adjust_three: + SUB $1, R10, R0 + +sequenceDecs_decode_amd64_adjust_test_temp_valid: + TST R0, R0 + BNE sequenceDecs_decode_amd64_adjust_temp_valid + MOVD $0x00000001, R0 + +sequenceDecs_decode_amd64_adjust_temp_valid: + CMP $0x01, R1 + CSEL NE, R11, R12, R12 + MOVD R10, R11 + MOVD R0, R10 + MOVD R0, R1 + +sequenceDecs_decode_amd64_after_adjust: + MOVD R1, 16(R9) + + // Check values + MOVD 8(R9), R0 + MOVD (R9), R13 + ADD R13, R0, R14 + MOVD s+0(FP), R4 + MOVD 256(R4), R16 + ADD R14, R16, R16 + MOVD R16, 256(R4) + MOVD ctx+16(FP), R14 + MOVD 128(R14), R16 + SUBS R13, R16, R16 + MOVD R16, 128(R14) + BMI error_not_enough_literals + CMP $0x00020002, R0 + BHI sequenceDecs_decode_amd64_error_match_len_too_big + TST R1, R1 + BNE sequenceDecs_decode_amd64_match_len_ofs_ok + TST R0, R0 + BNE sequenceDecs_decode_amd64_error_match_len_ofs_mismatch + +sequenceDecs_decode_amd64_match_len_ofs_ok: + ADD $0x18, R9, R9 + MOVD ctx+16(FP), R0 + MOVD 96(R0), R16 + SUBS $1, R16, R16 + MOVD R16, 96(R0) + BPL sequenceDecs_decode_amd64_main_loop + MOVD s+0(FP), R0 + MOVD R10, 144(R0) + MOVD R11, 152(R0) + MOVD R12, 160(R0) + MOVD br+8(FP), R0 + MOVD R2, 24(R0) + MOVB R3, 40(R0) + MOVD R5, 32(R0) + + // Return success + MOVD $0x00000000, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match length error +sequenceDecs_decode_amd64_error_match_len_ofs_mismatch: + MOVD $0x00000001, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match too long error +sequenceDecs_decode_amd64_error_match_len_too_big: + MOVD $0x00000002, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match offset too long error + MOVD $0x00000003, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough literals error +error_not_enough_literals: + MOVD $0x00000004, R16 + MOVD R16, ret+24(FP) + RET + + // Return with overread error +error_overread: + MOVD $0x00000006, R16 + MOVD R16, ret+24(FP) + RET + +// func sequenceDecs_decode_56_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int +// Requires: CMOV +TEXT ·sequenceDecs_decode_56_arm64(SB), $8-32 + MOVD br+8(FP), R1 + MOVD 24(R1), R2 + MOVBU 40(R1), R3 + MOVD (R1), R0 + MOVD 32(R1), R5 + ADD R5, R0, R0 + MOVD R0, (RSP) + MOVD ctx+16(FP), R0 + MOVD 72(R0), R6 + MOVD 80(R0), R7 + MOVD 88(R0), R8 + MOVD 104(R0), R9 + MOVD s+0(FP), R0 + MOVD 144(R0), R10 + MOVD 152(R0), R11 + MOVD 160(R0), R12 + +sequenceDecs_decode_56_amd64_main_loop: + MOVD (RSP), R13 + + // Fill bitreader to have enough for the offset and match length. + CMP $0x08, R5 + BLT sequenceDecs_decode_56_amd64_fill_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R13, R13 + MOVD (R13), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decode_56_amd64_fill_end + +sequenceDecs_decode_56_amd64_fill_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decode_56_amd64_fill_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decode_56_amd64_fill_end + LSL $0x08, R2, R2 + SUB $0x01, R13, R13 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R13), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decode_56_amd64_fill_byte_by_byte + +sequenceDecs_decode_56_amd64_fill_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decode_56_amd64_fill_end: + // Update offset + MOVD R8, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_56_amd64_of_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_56_amd64_of_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_56_amd64_of_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_56_amd64_of_update_zero: + MOVD R0, 16(R9) + + // Update match length + MOVD R7, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_56_amd64_ml_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_56_amd64_ml_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_56_amd64_ml_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_56_amd64_ml_update_zero: + MOVD R0, 8(R9) + + // Update literal length + MOVD R6, R0 + MOVD R3, R1 + MOVD R2, R14 + LSL R1, R14, R14 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decode_56_amd64_ll_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decode_56_amd64_ll_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decode_56_amd64_ll_update_zero + NEG R1, R1 + LSR R1, R14, R14 + ADD R14, R0, R0 + +sequenceDecs_decode_56_amd64_ll_update_zero: + MOVD R0, (R9) + + // Fill bitreader for state updates + MOVD R13, (RSP) + MOVD R8, R0 + LSR $0x08, R0, R0 + MOVBU R0, R0 + MOVD ctx+16(FP), R1 + MOVD 96(R1), R16 + CMP $0x00, R16 + BEQ sequenceDecs_decode_56_amd64_skip_update + + // Update Literal Length State + MOVBU R6, R13 + LSRW $0x10, R6, R6 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R6, R6 + + // Load ctx.llTable + MOVD ctx+16(FP), R1 + MOVD (R1), R1 + ADD R6<<3, R1, R15 + MOVD (R15), R6 + + // Update Match Length State + MOVBU R7, R13 + LSRW $0x10, R7, R7 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R7, R7 + + // Load ctx.mlTable + MOVD ctx+16(FP), R1 + MOVD 24(R1), R1 + ADD R7<<3, R1, R15 + MOVD (R15), R7 + + // Update Offset State + MOVBU R8, R13 + LSRW $0x10, R8, R8 + ADD R13, R3, R1 + MOVD R2, R14 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R14, R14 + MOVD $0x00000001, R4 + MOVB R13, R1 + LSLW R1, R4, R4 + SUBW $1, R4, R4 + AND R4, R14, R14 + ADD R14, R8, R8 + + // Load ctx.ofTable + MOVD ctx+16(FP), R1 + MOVD 48(R1), R1 + ADD R8<<3, R1, R15 + MOVD (R15), R8 + +sequenceDecs_decode_56_amd64_skip_update: + // Adjust offset + MOVD 16(R9), R1 + CMP $0x01, R0 + BLS sequenceDecs_decode_56_amd64_adjust_offsetB_1_or_0 + MOVD R11, R12 + MOVD R10, R11 + MOVD R1, R10 + JMP sequenceDecs_decode_56_amd64_after_adjust + +sequenceDecs_decode_56_amd64_adjust_offsetB_1_or_0: + MOVD (R9), R16 + CMP $0x00000000, R16 + BNE sequenceDecs_decode_56_amd64_adjust_offset_maybezero + ADD $1, R1, R1 + JMP sequenceDecs_decode_56_amd64_adjust_offset_nonzero + +sequenceDecs_decode_56_amd64_adjust_offset_maybezero: + TST R1, R1 + BNE sequenceDecs_decode_56_amd64_adjust_offset_nonzero + MOVD R10, R1 + JMP sequenceDecs_decode_56_amd64_after_adjust + +sequenceDecs_decode_56_amd64_adjust_offset_nonzero: + CMP $0x01, R1 + BLO sequenceDecs_decode_56_amd64_adjust_zero + BEQ sequenceDecs_decode_56_amd64_adjust_one + CMP $0x02, R1 + BHI sequenceDecs_decode_56_amd64_adjust_three + JMP sequenceDecs_decode_56_amd64_adjust_two + +sequenceDecs_decode_56_amd64_adjust_zero: + MOVD R10, R0 + JMP sequenceDecs_decode_56_amd64_adjust_test_temp_valid + +sequenceDecs_decode_56_amd64_adjust_one: + MOVD R11, R0 + JMP sequenceDecs_decode_56_amd64_adjust_test_temp_valid + +sequenceDecs_decode_56_amd64_adjust_two: + MOVD R12, R0 + JMP sequenceDecs_decode_56_amd64_adjust_test_temp_valid + +sequenceDecs_decode_56_amd64_adjust_three: + SUB $1, R10, R0 + +sequenceDecs_decode_56_amd64_adjust_test_temp_valid: + TST R0, R0 + BNE sequenceDecs_decode_56_amd64_adjust_temp_valid + MOVD $0x00000001, R0 + +sequenceDecs_decode_56_amd64_adjust_temp_valid: + CMP $0x01, R1 + CSEL NE, R11, R12, R12 + MOVD R10, R11 + MOVD R0, R10 + MOVD R0, R1 + +sequenceDecs_decode_56_amd64_after_adjust: + MOVD R1, 16(R9) + + // Check values + MOVD 8(R9), R0 + MOVD (R9), R13 + ADD R13, R0, R14 + MOVD s+0(FP), R4 + MOVD 256(R4), R16 + ADD R14, R16, R16 + MOVD R16, 256(R4) + MOVD ctx+16(FP), R14 + MOVD 128(R14), R16 + SUBS R13, R16, R16 + MOVD R16, 128(R14) + BMI error_not_enough_literals + CMP $0x00020002, R0 + BHI sequenceDecs_decode_56_amd64_error_match_len_too_big + TST R1, R1 + BNE sequenceDecs_decode_56_amd64_match_len_ofs_ok + TST R0, R0 + BNE sequenceDecs_decode_56_amd64_error_match_len_ofs_mismatch + +sequenceDecs_decode_56_amd64_match_len_ofs_ok: + ADD $0x18, R9, R9 + MOVD ctx+16(FP), R0 + MOVD 96(R0), R16 + SUBS $1, R16, R16 + MOVD R16, 96(R0) + BPL sequenceDecs_decode_56_amd64_main_loop + MOVD s+0(FP), R0 + MOVD R10, 144(R0) + MOVD R11, 152(R0) + MOVD R12, 160(R0) + MOVD br+8(FP), R0 + MOVD R2, 24(R0) + MOVB R3, 40(R0) + MOVD R5, 32(R0) + + // Return success + MOVD $0x00000000, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match length error +sequenceDecs_decode_56_amd64_error_match_len_ofs_mismatch: + MOVD $0x00000001, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match too long error +sequenceDecs_decode_56_amd64_error_match_len_too_big: + MOVD $0x00000002, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match offset too long error + MOVD $0x00000003, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough literals error +error_not_enough_literals: + MOVD $0x00000004, R16 + MOVD R16, ret+24(FP) + RET + + // Return with overread error +error_overread: + MOVD $0x00000006, R16 + MOVD R16, ret+24(FP) + RET + +// skipped sequenceDecs_decode_bmi2 (generic twin preferred on arm64) + +// skipped sequenceDecs_decode_56_bmi2 (generic twin preferred on arm64) + +// func sequenceDecs_executeSimple_amd64(ctx *executeAsmContext) bool +// Requires: SSE +TEXT ·sequenceDecs_executeSimple_arm64(SB), $8-9 + MOVD ctx+0(FP), R9 + MOVD 8(R9), R1 + TST R1, R1 + BEQ empty_seqs + MOVD (R9), R0 + MOVD 24(R9), R2 + MOVD 32(R9), R3 + MOVD 80(R9), R5 + MOVD 104(R9), R6 + MOVD 120(R9), R7 + MOVD 56(R9), R8 + MOVD 64(R9), R9 + ADD R9, R8, R8 + + // seqsBase += 24 * seqIndex + ADD R2<<1, R2, R10 + LSL $0x03, R10, R10 + ADD R10, R0, R0 + + // outBase += outPosition + ADD R6, R3, R3 + +main_loop: + MOVD (R0), R10 + MOVD 16(R0), R11 + MOVD 8(R0), R12 + + // Copy literals + TST R10, R10 + BEQ check_offset + MOVD $0, R13 + +copy_1: + ADD R13, R5, R15 + VLD1 (R15), [V0.B16] + ADD R13, R3, R15 + VST1 [V0.B16], (R15) + ADD $0x10, R13, R13 + CMP R10, R13 + BLO copy_1 + ADD R10, R5, R5 + ADD R10, R3, R3 + ADD R10, R6, R6 + + // Malformed input if seq.mo > t+len(hist) || seq.mo > s.windowSize) +check_offset: + ADD R9, R6, R10 + CMP R10, R11 + BGT error_match_off_too_big + CMP R7, R11 + BGT error_match_off_too_big + + // Copy match from history + MOVD R11, R10 + SUBS R6, R10, R10 + BLS copy_match + MOVD R8, R13 + SUB R10, R13, R13 + CMP R10, R12 + BGT copy_all_from_history + MOVD R12, R10 + SUBS $0x10, R10, R10 + BLO copy_4_small + +copy_4_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R13, R13 + ADD $0x10, R3, R3 + SUBS $0x10, R10, R10 + BHS copy_4_loop + ADD R10, R13, R13 + ADD $16, R13, R13 + ADD R10, R3, R3 + ADD $16, R3, R3 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_4_end + +copy_4_small: + CMP $0x03, R12 + BEQ copy_4_move_3 + CMP $0x08, R12 + BLO copy_4_move_4through7 + JMP copy_4_move_8through16 + +copy_4_move_3: + MOVH (R13), R10 + MOVB 2(R13), R11 + MOVH R10, (R3) + MOVB R11, 2(R3) + ADD R12, R13, R13 + ADD R12, R3, R3 + JMP copy_4_end + +copy_4_move_4through7: + MOVWU (R13), R10 + ADD R12, R13, R15 + MOVWU -4(R15), R11 + MOVW R10, (R3) + ADD R12, R3, R15 + MOVW R11, -4(R15) + ADD R12, R13, R13 + ADD R12, R3, R3 + JMP copy_4_end + +copy_4_move_8through16: + MOVD (R13), R10 + ADD R12, R13, R15 + MOVD -8(R15), R11 + MOVD R10, (R3) + ADD R12, R3, R15 + MOVD R11, -8(R15) + ADD R12, R13, R13 + ADD R12, R3, R3 + +copy_4_end: + ADD R12, R6, R6 + ADD $0x18, R0, R0 + ADD $1, R2, R2 + CMP R1, R2 + BLO main_loop + JMP loop_finished + +copy_all_from_history: + MOVD R10, R14 + SUBS $0x10, R14, R14 + BLO copy_5_small + +copy_5_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R13, R13 + ADD $0x10, R3, R3 + SUBS $0x10, R14, R14 + BHS copy_5_loop + ADD R14, R13, R13 + ADD $16, R13, R13 + ADD R14, R3, R3 + ADD $16, R3, R3 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_5_end + +copy_5_small: + CMP $0x03, R10 + BEQ copy_5_move_3 + BLO copy_5_move_1or2 + CMP $0x08, R10 + BLO copy_5_move_4through7 + JMP copy_5_move_8through16 + +copy_5_move_1or2: + MOVB (R13), R14 + ADD R10, R13, R15 + MOVB -1(R15), R4 + MOVB R14, (R3) + ADD R10, R3, R15 + MOVB R4, -1(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_3: + MOVH (R13), R14 + MOVB 2(R13), R4 + MOVH R14, (R3) + MOVB R4, 2(R3) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_4through7: + MOVWU (R13), R14 + ADD R10, R13, R15 + MOVWU -4(R15), R4 + MOVW R14, (R3) + ADD R10, R3, R15 + MOVW R4, -4(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_8through16: + MOVD (R13), R14 + ADD R10, R13, R15 + MOVD -8(R15), R4 + MOVD R14, (R3) + ADD R10, R3, R15 + MOVD R4, -8(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + +copy_5_end: + ADD R10, R6, R6 + SUB R10, R12, R12 + + // Copy match from the current buffer +copy_match: + MOVD R3, R10 + SUB R11, R10, R10 + + // ml <= mo + CMP R11, R12 + BHI copy_overlapping_match + + // Copy non-overlapping match + ADD R12, R6, R6 + MOVD R3, R11 + ADD R12, R3, R3 + +copy_2: + VLD1 (R10), [V0.B16] + VST1 [V0.B16], (R11) + ADD $0x10, R10, R10 + ADD $0x10, R11, R11 + SUBS $0x10, R12, R12 + BHI copy_2 + JMP handle_loop + + // Copy overlapping match +copy_overlapping_match: + ADD R12, R6, R6 + +copy_slow_3: + MOVB (R10), R11 + MOVB R11, (R3) + ADD $1, R10, R10 + ADD $1, R3, R3 + SUBS $1, R12, R12 + BNE copy_slow_3 + +handle_loop: + ADD $0x18, R0, R0 + ADD $1, R2, R2 + CMP R1, R2 + BLO main_loop + +loop_finished: + // Return value + MOVD $0x01, R16 + MOVB R16, ret+8(FP) + + // Update the context + MOVD ctx+0(FP), R0 + MOVD R2, 24(R0) + MOVD R6, 104(R0) + MOVD 80(R0), R16 + SUB R16, R5, R5 + MOVD R5, 112(R0) + RET + +error_match_off_too_big: + // Return value + MOVD $0x00, R16 + MOVB R16, ret+8(FP) + + // Update the context + MOVD ctx+0(FP), R0 + MOVD R2, 24(R0) + MOVD R6, 104(R0) + MOVD 80(R0), R16 + SUB R16, R5, R5 + MOVD R5, 112(R0) + RET + +empty_seqs: + // Return value + MOVD $0x01, R16 + MOVB R16, ret+8(FP) + RET + +// func sequenceDecs_executeSimple_safe_amd64(ctx *executeAsmContext) bool +// Requires: SSE +TEXT ·sequenceDecs_executeSimple_safe_arm64(SB), $8-9 + MOVD ctx+0(FP), R9 + MOVD 8(R9), R1 + TST R1, R1 + BEQ empty_seqs + MOVD (R9), R0 + MOVD 24(R9), R2 + MOVD 32(R9), R3 + MOVD 80(R9), R5 + MOVD 104(R9), R6 + MOVD 120(R9), R7 + MOVD 56(R9), R8 + MOVD 64(R9), R9 + ADD R9, R8, R8 + + // seqsBase += 24 * seqIndex + ADD R2<<1, R2, R10 + LSL $0x03, R10, R10 + ADD R10, R0, R0 + + // outBase += outPosition + ADD R6, R3, R3 + +main_loop: + MOVD (R0), R10 + MOVD 16(R0), R11 + MOVD 8(R0), R12 + + // Copy literals + TST R10, R10 + BEQ check_offset + MOVD R10, R13 + SUBS $0x10, R13, R13 + BLO copy_1_small + +copy_1_loop: + VLD1 (R5), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R5, R5 + ADD $0x10, R3, R3 + SUBS $0x10, R13, R13 + BHS copy_1_loop + ADD R13, R5, R5 + ADD $16, R5, R5 + ADD R13, R3, R3 + ADD $16, R3, R3 + ADD $-16, R5, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_1_end + +copy_1_small: + CMP $0x03, R10 + BEQ copy_1_move_3 + BLO copy_1_move_1or2 + CMP $0x08, R10 + BLO copy_1_move_4through7 + JMP copy_1_move_8through16 + +copy_1_move_1or2: + MOVB (R5), R13 + ADD R10, R5, R15 + MOVB -1(R15), R14 + MOVB R13, (R3) + ADD R10, R3, R15 + MOVB R14, -1(R15) + ADD R10, R5, R5 + ADD R10, R3, R3 + JMP copy_1_end + +copy_1_move_3: + MOVH (R5), R13 + MOVB 2(R5), R14 + MOVH R13, (R3) + MOVB R14, 2(R3) + ADD R10, R5, R5 + ADD R10, R3, R3 + JMP copy_1_end + +copy_1_move_4through7: + MOVWU (R5), R13 + ADD R10, R5, R15 + MOVWU -4(R15), R14 + MOVW R13, (R3) + ADD R10, R3, R15 + MOVW R14, -4(R15) + ADD R10, R5, R5 + ADD R10, R3, R3 + JMP copy_1_end + +copy_1_move_8through16: + MOVD (R5), R13 + ADD R10, R5, R15 + MOVD -8(R15), R14 + MOVD R13, (R3) + ADD R10, R3, R15 + MOVD R14, -8(R15) + ADD R10, R5, R5 + ADD R10, R3, R3 + +copy_1_end: + ADD R10, R6, R6 + + // Malformed input if seq.mo > t+len(hist) || seq.mo > s.windowSize) +check_offset: + ADD R9, R6, R10 + CMP R10, R11 + BGT error_match_off_too_big + CMP R7, R11 + BGT error_match_off_too_big + + // Copy match from history + MOVD R11, R10 + SUBS R6, R10, R10 + BLS copy_match + MOVD R8, R13 + SUB R10, R13, R13 + CMP R10, R12 + BGT copy_all_from_history + MOVD R12, R10 + SUBS $0x10, R10, R10 + BLO copy_4_small + +copy_4_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R13, R13 + ADD $0x10, R3, R3 + SUBS $0x10, R10, R10 + BHS copy_4_loop + ADD R10, R13, R13 + ADD $16, R13, R13 + ADD R10, R3, R3 + ADD $16, R3, R3 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_4_end + +copy_4_small: + CMP $0x03, R12 + BEQ copy_4_move_3 + CMP $0x08, R12 + BLO copy_4_move_4through7 + JMP copy_4_move_8through16 + +copy_4_move_3: + MOVH (R13), R10 + MOVB 2(R13), R11 + MOVH R10, (R3) + MOVB R11, 2(R3) + ADD R12, R13, R13 + ADD R12, R3, R3 + JMP copy_4_end + +copy_4_move_4through7: + MOVWU (R13), R10 + ADD R12, R13, R15 + MOVWU -4(R15), R11 + MOVW R10, (R3) + ADD R12, R3, R15 + MOVW R11, -4(R15) + ADD R12, R13, R13 + ADD R12, R3, R3 + JMP copy_4_end + +copy_4_move_8through16: + MOVD (R13), R10 + ADD R12, R13, R15 + MOVD -8(R15), R11 + MOVD R10, (R3) + ADD R12, R3, R15 + MOVD R11, -8(R15) + ADD R12, R13, R13 + ADD R12, R3, R3 + +copy_4_end: + ADD R12, R6, R6 + ADD $0x18, R0, R0 + ADD $1, R2, R2 + CMP R1, R2 + BLO main_loop + JMP loop_finished + +copy_all_from_history: + MOVD R10, R14 + SUBS $0x10, R14, R14 + BLO copy_5_small + +copy_5_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R13, R13 + ADD $0x10, R3, R3 + SUBS $0x10, R14, R14 + BHS copy_5_loop + ADD R14, R13, R13 + ADD $16, R13, R13 + ADD R14, R3, R3 + ADD $16, R3, R3 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_5_end + +copy_5_small: + CMP $0x03, R10 + BEQ copy_5_move_3 + BLO copy_5_move_1or2 + CMP $0x08, R10 + BLO copy_5_move_4through7 + JMP copy_5_move_8through16 + +copy_5_move_1or2: + MOVB (R13), R14 + ADD R10, R13, R15 + MOVB -1(R15), R4 + MOVB R14, (R3) + ADD R10, R3, R15 + MOVB R4, -1(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_3: + MOVH (R13), R14 + MOVB 2(R13), R4 + MOVH R14, (R3) + MOVB R4, 2(R3) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_4through7: + MOVWU (R13), R14 + ADD R10, R13, R15 + MOVWU -4(R15), R4 + MOVW R14, (R3) + ADD R10, R3, R15 + MOVW R4, -4(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + JMP copy_5_end + +copy_5_move_8through16: + MOVD (R13), R14 + ADD R10, R13, R15 + MOVD -8(R15), R4 + MOVD R14, (R3) + ADD R10, R3, R15 + MOVD R4, -8(R15) + ADD R10, R13, R13 + ADD R10, R3, R3 + +copy_5_end: + ADD R10, R6, R6 + SUB R10, R12, R12 + + // Copy match from the current buffer +copy_match: + MOVD R3, R10 + SUB R11, R10, R10 + + // ml <= mo + CMP R11, R12 + BHI copy_overlapping_match + + // Copy non-overlapping match + ADD R12, R6, R6 + MOVD R12, R11 + SUBS $0x10, R11, R11 + BLO copy_2_small + +copy_2_loop: + VLD1 (R10), [V0.B16] + VST1 [V0.B16], (R3) + ADD $0x10, R10, R10 + ADD $0x10, R3, R3 + SUBS $0x10, R11, R11 + BHS copy_2_loop + ADD R11, R10, R10 + ADD $16, R10, R10 + ADD R11, R3, R3 + ADD $16, R3, R3 + ADD $-16, R10, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R3, R15 + VST1 [V0.B16], (R15) + JMP copy_2_end + +copy_2_small: + CMP $0x03, R12 + BEQ copy_2_move_3 + BLO copy_2_move_1or2 + CMP $0x08, R12 + BLO copy_2_move_4through7 + JMP copy_2_move_8through16 + +copy_2_move_1or2: + MOVB (R10), R11 + ADD R12, R10, R15 + MOVB -1(R15), R13 + MOVB R11, (R3) + ADD R12, R3, R15 + MOVB R13, -1(R15) + ADD R12, R10, R10 + ADD R12, R3, R3 + JMP copy_2_end + +copy_2_move_3: + MOVH (R10), R11 + MOVB 2(R10), R13 + MOVH R11, (R3) + MOVB R13, 2(R3) + ADD R12, R10, R10 + ADD R12, R3, R3 + JMP copy_2_end + +copy_2_move_4through7: + MOVWU (R10), R11 + ADD R12, R10, R15 + MOVWU -4(R15), R13 + MOVW R11, (R3) + ADD R12, R3, R15 + MOVW R13, -4(R15) + ADD R12, R10, R10 + ADD R12, R3, R3 + JMP copy_2_end + +copy_2_move_8through16: + MOVD (R10), R11 + ADD R12, R10, R15 + MOVD -8(R15), R13 + MOVD R11, (R3) + ADD R12, R3, R15 + MOVD R13, -8(R15) + ADD R12, R10, R10 + ADD R12, R3, R3 + +copy_2_end: + JMP handle_loop + + // Copy overlapping match +copy_overlapping_match: + ADD R12, R6, R6 + +copy_slow_3: + MOVB (R10), R11 + MOVB R11, (R3) + ADD $1, R10, R10 + ADD $1, R3, R3 + SUBS $1, R12, R12 + BNE copy_slow_3 + +handle_loop: + ADD $0x18, R0, R0 + ADD $1, R2, R2 + CMP R1, R2 + BLO main_loop + +loop_finished: + // Return value + MOVD $0x01, R16 + MOVB R16, ret+8(FP) + + // Update the context + MOVD ctx+0(FP), R0 + MOVD R2, 24(R0) + MOVD R6, 104(R0) + MOVD 80(R0), R16 + SUB R16, R5, R5 + MOVD R5, 112(R0) + RET + +error_match_off_too_big: + // Return value + MOVD $0x00, R16 + MOVB R16, ret+8(FP) + + // Update the context + MOVD ctx+0(FP), R0 + MOVD R2, 24(R0) + MOVD R6, 104(R0) + MOVD 80(R0), R16 + SUB R16, R5, R5 + MOVD R5, 112(R0) + RET + +empty_seqs: + // Return value + MOVD $0x01, R16 + MOVB R16, ret+8(FP) + RET + +// func sequenceDecs_decodeSync_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int +// Requires: CMOV, SSE +TEXT ·sequenceDecs_decodeSync_arm64(SB), $64-32 + MOVD br+8(FP), R1 + MOVD 24(R1), R2 + MOVBU 40(R1), R3 + MOVD (R1), R0 + MOVD 32(R1), R5 + ADD R5, R0, R0 + MOVD R0, (RSP) + MOVD ctx+16(FP), R0 + MOVD 72(R0), R6 + MOVD 80(R0), R7 + MOVD 88(R0), R8 + MOVD $0, R1 + MOVD R1, 8(RSP) + MOVD R1, 16(RSP) + MOVD R1, 24(RSP) + MOVD 112(R0), R9 + MOVD 128(R0), R1 + MOVD R1, 32(RSP) + MOVD 144(R0), R10 + MOVD 136(R0), R11 + MOVD 200(R0), R1 + MOVD R1, 56(RSP) + MOVD 176(R0), R1 + MOVD R1, 48(RSP) + MOVD 184(R0), R0 + MOVD R0, 40(RSP) + MOVD 40(RSP), R0 + MOVD 48(RSP), R16 + ADD R0, R16, R16 + MOVD R16, 48(RSP) + + // Calculate pointer to s.out[cap(s.out)] (a past-end pointer) + MOVD 32(RSP), R16 + ADD R9, R16, R16 + MOVD R16, 32(RSP) + + // outBase += outPosition + ADD R11, R9, R9 + +sequenceDecs_decodeSync_amd64_main_loop: + MOVD (RSP), R12 + + // Fill bitreader to have enough for the offset and match length. + CMP $0x08, R5 + BLT sequenceDecs_decodeSync_amd64_fill_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R12, R12 + MOVD (R12), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decodeSync_amd64_fill_end + +sequenceDecs_decodeSync_amd64_fill_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decodeSync_amd64_fill_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decodeSync_amd64_fill_end + LSL $0x08, R2, R2 + SUB $0x01, R12, R12 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R12), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decodeSync_amd64_fill_byte_by_byte + +sequenceDecs_decodeSync_amd64_fill_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decodeSync_amd64_fill_end: + // Update offset + MOVD R8, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_amd64_of_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_amd64_of_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_amd64_of_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_amd64_of_update_zero: + MOVD R0, 8(RSP) + + // Update match length + MOVD R7, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_amd64_ml_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_amd64_ml_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_amd64_ml_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_amd64_ml_update_zero: + MOVD R0, 16(RSP) + + // Fill bitreader to have enough for the remaining + CMP $0x08, R5 + BLT sequenceDecs_decodeSync_amd64_fill_2_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R12, R12 + MOVD (R12), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decodeSync_amd64_fill_2_end + +sequenceDecs_decodeSync_amd64_fill_2_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decodeSync_amd64_fill_2_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decodeSync_amd64_fill_2_end + LSL $0x08, R2, R2 + SUB $0x01, R12, R12 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R12), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decodeSync_amd64_fill_2_byte_by_byte + +sequenceDecs_decodeSync_amd64_fill_2_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decodeSync_amd64_fill_2_end: + // Update literal length + MOVD R6, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_amd64_ll_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_amd64_ll_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_amd64_ll_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_amd64_ll_update_zero: + MOVD R0, 24(RSP) + + // Fill bitreader for state updates + MOVD R12, (RSP) + MOVD R8, R0 + LSR $0x08, R0, R0 + MOVBU R0, R0 + MOVD ctx+16(FP), R1 + MOVD 96(R1), R16 + CMP $0x00, R16 + BEQ sequenceDecs_decodeSync_amd64_skip_update + + // Update Literal Length State + MOVBU R6, R12 + LSRW $0x10, R6, R6 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R6, R6 + + // Load ctx.llTable + MOVD ctx+16(FP), R1 + MOVD (R1), R1 + ADD R6<<3, R1, R15 + MOVD (R15), R6 + + // Update Match Length State + MOVBU R7, R12 + LSRW $0x10, R7, R7 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R7, R7 + + // Load ctx.mlTable + MOVD ctx+16(FP), R1 + MOVD 24(R1), R1 + ADD R7<<3, R1, R15 + MOVD (R15), R7 + + // Update Offset State + MOVBU R8, R12 + LSRW $0x10, R8, R8 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R8, R8 + + // Load ctx.ofTable + MOVD ctx+16(FP), R1 + MOVD 48(R1), R1 + ADD R8<<3, R1, R15 + MOVD (R15), R8 + +sequenceDecs_decodeSync_amd64_skip_update: + // Adjust offset + MOVD s+0(FP), R1 + MOVD 8(RSP), R12 + CMP $0x01, R0 + BLS sequenceDecs_decodeSync_amd64_adjust_offsetB_1_or_0 + ADD $144, R1, R15 + VLD1 (R15), [V0.B16] + MOVD R12, 144(R1) + ADD $152, R1, R15 + VST1 [V0.B16], (R15) + JMP sequenceDecs_decodeSync_amd64_after_adjust + +sequenceDecs_decodeSync_amd64_adjust_offsetB_1_or_0: + MOVD 24(RSP), R16 + CMP $0x00000000, R16 + BNE sequenceDecs_decodeSync_amd64_adjust_offset_maybezero + ADD $1, R12, R12 + JMP sequenceDecs_decodeSync_amd64_adjust_offset_nonzero + +sequenceDecs_decodeSync_amd64_adjust_offset_maybezero: + TST R12, R12 + BNE sequenceDecs_decodeSync_amd64_adjust_offset_nonzero + MOVD 144(R1), R12 + JMP sequenceDecs_decodeSync_amd64_after_adjust + +sequenceDecs_decodeSync_amd64_adjust_offset_nonzero: + MOVD R12, R0 + MOVD $0, R13 + MOVD $-1, R14 + CMP $0x03, R12 + CSEL EQ, R13, R0, R0 + CSEL EQ, R14, R13, R13 + ADD R0<<3, R1, R15 + MOVD 144(R15), R16 + ADDS R16, R13, R13 + BNE sequenceDecs_decodeSync_amd64_adjust_temp_valid + MOVD $0x00000001, R13 + +sequenceDecs_decodeSync_amd64_adjust_temp_valid: + CMP $0x01, R12 + BEQ sequenceDecs_decodeSync_amd64_adjust_skip + MOVD 152(R1), R0 + MOVD R0, 160(R1) + +sequenceDecs_decodeSync_amd64_adjust_skip: + MOVD 144(R1), R0 + MOVD R0, 152(R1) + MOVD R13, 144(R1) + MOVD R13, R12 + +sequenceDecs_decodeSync_amd64_after_adjust: + MOVD R12, 8(RSP) + + // Check values + MOVD 16(RSP), R0 + MOVD 24(RSP), R1 + ADD R1, R0, R13 + MOVD s+0(FP), R14 + MOVD 256(R14), R16 + ADD R13, R16, R16 + MOVD R16, 256(R14) + MOVD ctx+16(FP), R13 + MOVD 104(R13), R16 + SUBS R1, R16, R16 + MOVD R16, 104(R13) + BMI error_not_enough_literals + CMP $0x00020002, R0 + BHI sequenceDecs_decodeSync_amd64_error_match_len_too_big + TST R12, R12 + BNE sequenceDecs_decodeSync_amd64_match_len_ofs_ok + TST R0, R0 + BNE sequenceDecs_decodeSync_amd64_error_match_len_ofs_mismatch + +sequenceDecs_decodeSync_amd64_match_len_ofs_ok: + MOVD 24(RSP), R0 + MOVD 8(RSP), R1 + MOVD 16(RSP), R12 + + // Check if we have enough space in s.out + ADD R12, R0, R13 + ADD R9, R13, R13 + MOVD 32(RSP), R16 + CMP R16, R13 + BHI error_not_enough_space + + // Copy literals + TST R0, R0 + BEQ check_offset + MOVD $0, R13 + +copy_1: + ADD R13, R10, R15 + VLD1 (R15), [V0.B16] + ADD R13, R9, R15 + VST1 [V0.B16], (R15) + ADD $0x10, R13, R13 + CMP R0, R13 + BLO copy_1 + ADD R0, R10, R10 + ADD R0, R9, R9 + ADD R0, R11, R11 + + // Malformed input if seq.mo > t+len(hist) || seq.mo > s.windowSize) +check_offset: + MOVD R11, R0 + MOVD 40(RSP), R16 + ADD R16, R0, R0 + CMP R0, R1 + BGT error_match_off_too_big + MOVD 56(RSP), R16 + CMP R16, R1 + BGT error_match_off_too_big + + // Copy match from history + MOVD R1, R0 + SUBS R11, R0, R0 + BLS copy_match + MOVD 48(RSP), R13 + SUB R0, R13, R13 + CMP R0, R12 + BGT copy_all_from_history + MOVD R12, R0 + SUBS $0x10, R0, R0 + BLO copy_4_small + +copy_4_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R13, R13 + ADD $0x10, R9, R9 + SUBS $0x10, R0, R0 + BHS copy_4_loop + ADD R0, R13, R13 + ADD $16, R13, R13 + ADD R0, R9, R9 + ADD $16, R9, R9 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_4_end + +copy_4_small: + CMP $0x03, R12 + BEQ copy_4_move_3 + CMP $0x08, R12 + BLO copy_4_move_4through7 + JMP copy_4_move_8through16 + +copy_4_move_3: + MOVH (R13), R0 + MOVB 2(R13), R1 + MOVH R0, (R9) + MOVB R1, 2(R9) + ADD R12, R13, R13 + ADD R12, R9, R9 + JMP copy_4_end + +copy_4_move_4through7: + MOVWU (R13), R0 + ADD R12, R13, R15 + MOVWU -4(R15), R1 + MOVW R0, (R9) + ADD R12, R9, R15 + MOVW R1, -4(R15) + ADD R12, R13, R13 + ADD R12, R9, R9 + JMP copy_4_end + +copy_4_move_8through16: + MOVD (R13), R0 + ADD R12, R13, R15 + MOVD -8(R15), R1 + MOVD R0, (R9) + ADD R12, R9, R15 + MOVD R1, -8(R15) + ADD R12, R13, R13 + ADD R12, R9, R9 + +copy_4_end: + ADD R12, R11, R11 + JMP handle_loop + JMP loop_finished + +copy_all_from_history: + MOVD R0, R14 + SUBS $0x10, R14, R14 + BLO copy_5_small + +copy_5_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R13, R13 + ADD $0x10, R9, R9 + SUBS $0x10, R14, R14 + BHS copy_5_loop + ADD R14, R13, R13 + ADD $16, R13, R13 + ADD R14, R9, R9 + ADD $16, R9, R9 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_5_end + +copy_5_small: + CMP $0x03, R0 + BEQ copy_5_move_3 + BLO copy_5_move_1or2 + CMP $0x08, R0 + BLO copy_5_move_4through7 + JMP copy_5_move_8through16 + +copy_5_move_1or2: + MOVB (R13), R14 + ADD R0, R13, R15 + MOVB -1(R15), R4 + MOVB R14, (R9) + ADD R0, R9, R15 + MOVB R4, -1(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_3: + MOVH (R13), R14 + MOVB 2(R13), R4 + MOVH R14, (R9) + MOVB R4, 2(R9) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_4through7: + MOVWU (R13), R14 + ADD R0, R13, R15 + MOVWU -4(R15), R4 + MOVW R14, (R9) + ADD R0, R9, R15 + MOVW R4, -4(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_8through16: + MOVD (R13), R14 + ADD R0, R13, R15 + MOVD -8(R15), R4 + MOVD R14, (R9) + ADD R0, R9, R15 + MOVD R4, -8(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + +copy_5_end: + ADD R0, R11, R11 + SUB R0, R12, R12 + + // Copy match from the current buffer +copy_match: + MOVD R9, R0 + SUB R1, R0, R0 + + // ml <= mo + CMP R1, R12 + BHI copy_overlapping_match + + // Copy non-overlapping match + ADD R12, R11, R11 + MOVD R9, R1 + ADD R12, R9, R9 + +copy_2: + VLD1 (R0), [V0.B16] + VST1 [V0.B16], (R1) + ADD $0x10, R0, R0 + ADD $0x10, R1, R1 + SUBS $0x10, R12, R12 + BHI copy_2 + JMP handle_loop + + // Copy overlapping match +copy_overlapping_match: + ADD R12, R11, R11 + +copy_slow_3: + MOVB (R0), R1 + MOVB R1, (R9) + ADD $1, R0, R0 + ADD $1, R9, R9 + SUBS $1, R12, R12 + BNE copy_slow_3 + +handle_loop: + MOVD ctx+16(FP), R0 + MOVD 96(R0), R16 + SUBS $1, R16, R16 + MOVD R16, 96(R0) + BPL sequenceDecs_decodeSync_amd64_main_loop + +loop_finished: + MOVD br+8(FP), R0 + MOVD R2, 24(R0) + MOVB R3, 40(R0) + MOVD R5, 32(R0) + + // Update the context + MOVD ctx+16(FP), R0 + MOVD R11, 136(R0) + MOVD 144(R0), R1 + SUB R1, R10, R10 + MOVD R10, 168(R0) + + // Return success + MOVD $0x00000000, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match length error +sequenceDecs_decodeSync_amd64_error_match_len_ofs_mismatch: + MOVD 16(RSP), R0 + MOVD ctx+16(FP), R1 + MOVD R0, 216(R1) + MOVD $0x00000001, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match too long error +sequenceDecs_decodeSync_amd64_error_match_len_too_big: + MOVD ctx+16(FP), R0 + MOVD 16(RSP), R1 + MOVD R1, 216(R0) + MOVD $0x00000002, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match offset too long error +error_match_off_too_big: + MOVD ctx+16(FP), R0 + MOVD 8(RSP), R1 + MOVD R1, 224(R0) + MOVD R11, 136(R0) + MOVD $0x00000003, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough literals error +error_not_enough_literals: + MOVD ctx+16(FP), R0 + MOVD 24(RSP), R1 + MOVD R1, 208(R0) + MOVD $0x00000004, R16 + MOVD R16, ret+24(FP) + RET + + // Return with overread error +error_overread: + MOVD $0x00000006, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough output space error +error_not_enough_space: + MOVD ctx+16(FP), R0 + MOVD 24(RSP), R1 + MOVD R1, 208(R0) + MOVD 16(RSP), R1 + MOVD R1, 216(R0) + MOVD R11, 136(R0) + MOVD $0x00000005, R16 + MOVD R16, ret+24(FP) + RET + +// skipped sequenceDecs_decodeSync_bmi2 (generic twin preferred on arm64) + +// func sequenceDecs_decodeSync_safe_amd64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int +// Requires: CMOV, SSE +TEXT ·sequenceDecs_decodeSync_safe_arm64(SB), $64-32 + MOVD br+8(FP), R1 + MOVD 24(R1), R2 + MOVBU 40(R1), R3 + MOVD (R1), R0 + MOVD 32(R1), R5 + ADD R5, R0, R0 + MOVD R0, (RSP) + MOVD ctx+16(FP), R0 + MOVD 72(R0), R6 + MOVD 80(R0), R7 + MOVD 88(R0), R8 + MOVD $0, R1 + MOVD R1, 8(RSP) + MOVD R1, 16(RSP) + MOVD R1, 24(RSP) + MOVD 112(R0), R9 + MOVD 128(R0), R1 + MOVD R1, 32(RSP) + MOVD 144(R0), R10 + MOVD 136(R0), R11 + MOVD 200(R0), R1 + MOVD R1, 56(RSP) + MOVD 176(R0), R1 + MOVD R1, 48(RSP) + MOVD 184(R0), R0 + MOVD R0, 40(RSP) + MOVD 40(RSP), R0 + MOVD 48(RSP), R16 + ADD R0, R16, R16 + MOVD R16, 48(RSP) + + // Calculate pointer to s.out[cap(s.out)] (a past-end pointer) + MOVD 32(RSP), R16 + ADD R9, R16, R16 + MOVD R16, 32(RSP) + + // outBase += outPosition + ADD R11, R9, R9 + +sequenceDecs_decodeSync_safe_amd64_main_loop: + MOVD (RSP), R12 + + // Fill bitreader to have enough for the offset and match length. + CMP $0x08, R5 + BLT sequenceDecs_decodeSync_safe_amd64_fill_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R12, R12 + MOVD (R12), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decodeSync_safe_amd64_fill_end + +sequenceDecs_decodeSync_safe_amd64_fill_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decodeSync_safe_amd64_fill_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decodeSync_safe_amd64_fill_end + LSL $0x08, R2, R2 + SUB $0x01, R12, R12 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R12), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decodeSync_safe_amd64_fill_byte_by_byte + +sequenceDecs_decodeSync_safe_amd64_fill_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decodeSync_safe_amd64_fill_end: + // Update offset + MOVD R8, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_safe_amd64_of_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_safe_amd64_of_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_safe_amd64_of_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_safe_amd64_of_update_zero: + MOVD R0, 8(RSP) + + // Update match length + MOVD R7, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_safe_amd64_ml_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_safe_amd64_ml_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_safe_amd64_ml_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_safe_amd64_ml_update_zero: + MOVD R0, 16(RSP) + + // Fill bitreader to have enough for the remaining + CMP $0x08, R5 + BLT sequenceDecs_decodeSync_safe_amd64_fill_2_byte_by_byte + MOVD R3, R0 + LSR $0x03, R0, R0 + SUB R0, R12, R12 + MOVD (R12), R2 + SUB R0, R5, R5 + AND $0x07, R3, R3 + JMP sequenceDecs_decodeSync_safe_amd64_fill_2_end + +sequenceDecs_decodeSync_safe_amd64_fill_2_byte_by_byte: + CMP $0x00, R5 + BLE sequenceDecs_decodeSync_safe_amd64_fill_2_check_overread + CMP $0x07, R3 + BLE sequenceDecs_decodeSync_safe_amd64_fill_2_end + LSL $0x08, R2, R2 + SUB $0x01, R12, R12 + SUB $0x01, R5, R5 + SUB $0x08, R3, R3 + MOVBU (R12), R0 + ORR R0, R2, R2 + JMP sequenceDecs_decodeSync_safe_amd64_fill_2_byte_by_byte + +sequenceDecs_decodeSync_safe_amd64_fill_2_check_overread: + CMP $0x40, R3 + BHI error_overread + +sequenceDecs_decodeSync_safe_amd64_fill_2_end: + // Update literal length + MOVD R6, R0 + MOVD R3, R1 + MOVD R2, R13 + LSL R1, R13, R13 + UBFX $8, R0, $8, R1 + LSR $0x20, R0, R0 + TST R1, R1 + BEQ sequenceDecs_decodeSync_safe_amd64_ll_update_zero + ADD R1, R3, R3 + CMP $0x40, R3 + BHI sequenceDecs_decodeSync_safe_amd64_ll_update_zero + CMP $0x40, R1 + BHS sequenceDecs_decodeSync_safe_amd64_ll_update_zero + NEG R1, R1 + LSR R1, R13, R13 + ADD R13, R0, R0 + +sequenceDecs_decodeSync_safe_amd64_ll_update_zero: + MOVD R0, 24(RSP) + + // Fill bitreader for state updates + MOVD R12, (RSP) + MOVD R8, R0 + LSR $0x08, R0, R0 + MOVBU R0, R0 + MOVD ctx+16(FP), R1 + MOVD 96(R1), R16 + CMP $0x00, R16 + BEQ sequenceDecs_decodeSync_safe_amd64_skip_update + + // Update Literal Length State + MOVBU R6, R12 + LSRW $0x10, R6, R6 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R6, R6 + + // Load ctx.llTable + MOVD ctx+16(FP), R1 + MOVD (R1), R1 + ADD R6<<3, R1, R15 + MOVD (R15), R6 + + // Update Match Length State + MOVBU R7, R12 + LSRW $0x10, R7, R7 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R7, R7 + + // Load ctx.mlTable + MOVD ctx+16(FP), R1 + MOVD 24(R1), R1 + ADD R7<<3, R1, R15 + MOVD (R15), R7 + + // Update Offset State + MOVBU R8, R12 + LSRW $0x10, R8, R8 + ADD R12, R3, R1 + MOVD R2, R13 + MOVD R1, R3 + NEG R1, R16 + ROR R16, R13, R13 + MOVD $0x00000001, R14 + MOVB R12, R1 + LSLW R1, R14, R14 + SUBW $1, R14, R14 + AND R14, R13, R13 + ADD R13, R8, R8 + + // Load ctx.ofTable + MOVD ctx+16(FP), R1 + MOVD 48(R1), R1 + ADD R8<<3, R1, R15 + MOVD (R15), R8 + +sequenceDecs_decodeSync_safe_amd64_skip_update: + // Adjust offset + MOVD s+0(FP), R1 + MOVD 8(RSP), R12 + CMP $0x01, R0 + BLS sequenceDecs_decodeSync_safe_amd64_adjust_offsetB_1_or_0 + ADD $144, R1, R15 + VLD1 (R15), [V0.B16] + MOVD R12, 144(R1) + ADD $152, R1, R15 + VST1 [V0.B16], (R15) + JMP sequenceDecs_decodeSync_safe_amd64_after_adjust + +sequenceDecs_decodeSync_safe_amd64_adjust_offsetB_1_or_0: + MOVD 24(RSP), R16 + CMP $0x00000000, R16 + BNE sequenceDecs_decodeSync_safe_amd64_adjust_offset_maybezero + ADD $1, R12, R12 + JMP sequenceDecs_decodeSync_safe_amd64_adjust_offset_nonzero + +sequenceDecs_decodeSync_safe_amd64_adjust_offset_maybezero: + TST R12, R12 + BNE sequenceDecs_decodeSync_safe_amd64_adjust_offset_nonzero + MOVD 144(R1), R12 + JMP sequenceDecs_decodeSync_safe_amd64_after_adjust + +sequenceDecs_decodeSync_safe_amd64_adjust_offset_nonzero: + MOVD R12, R0 + MOVD $0, R13 + MOVD $-1, R14 + CMP $0x03, R12 + CSEL EQ, R13, R0, R0 + CSEL EQ, R14, R13, R13 + ADD R0<<3, R1, R15 + MOVD 144(R15), R16 + ADDS R16, R13, R13 + BNE sequenceDecs_decodeSync_safe_amd64_adjust_temp_valid + MOVD $0x00000001, R13 + +sequenceDecs_decodeSync_safe_amd64_adjust_temp_valid: + CMP $0x01, R12 + BEQ sequenceDecs_decodeSync_safe_amd64_adjust_skip + MOVD 152(R1), R0 + MOVD R0, 160(R1) + +sequenceDecs_decodeSync_safe_amd64_adjust_skip: + MOVD 144(R1), R0 + MOVD R0, 152(R1) + MOVD R13, 144(R1) + MOVD R13, R12 + +sequenceDecs_decodeSync_safe_amd64_after_adjust: + MOVD R12, 8(RSP) + + // Check values + MOVD 16(RSP), R0 + MOVD 24(RSP), R1 + ADD R1, R0, R13 + MOVD s+0(FP), R14 + MOVD 256(R14), R16 + ADD R13, R16, R16 + MOVD R16, 256(R14) + MOVD ctx+16(FP), R13 + MOVD 104(R13), R16 + SUBS R1, R16, R16 + MOVD R16, 104(R13) + BMI error_not_enough_literals + CMP $0x00020002, R0 + BHI sequenceDecs_decodeSync_safe_amd64_error_match_len_too_big + TST R12, R12 + BNE sequenceDecs_decodeSync_safe_amd64_match_len_ofs_ok + TST R0, R0 + BNE sequenceDecs_decodeSync_safe_amd64_error_match_len_ofs_mismatch + +sequenceDecs_decodeSync_safe_amd64_match_len_ofs_ok: + MOVD 24(RSP), R0 + MOVD 8(RSP), R1 + MOVD 16(RSP), R12 + + // Check if we have enough space in s.out + ADD R12, R0, R13 + ADD R9, R13, R13 + MOVD 32(RSP), R16 + CMP R16, R13 + BHI error_not_enough_space + + // Copy literals + TST R0, R0 + BEQ check_offset + MOVD R0, R13 + SUBS $0x10, R13, R13 + BLO copy_1_small + +copy_1_loop: + VLD1 (R10), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R10, R10 + ADD $0x10, R9, R9 + SUBS $0x10, R13, R13 + BHS copy_1_loop + ADD R13, R10, R10 + ADD $16, R10, R10 + ADD R13, R9, R9 + ADD $16, R9, R9 + ADD $-16, R10, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_1_end + +copy_1_small: + CMP $0x03, R0 + BEQ copy_1_move_3 + BLO copy_1_move_1or2 + CMP $0x08, R0 + BLO copy_1_move_4through7 + JMP copy_1_move_8through16 + +copy_1_move_1or2: + MOVB (R10), R13 + ADD R0, R10, R15 + MOVB -1(R15), R14 + MOVB R13, (R9) + ADD R0, R9, R15 + MOVB R14, -1(R15) + ADD R0, R10, R10 + ADD R0, R9, R9 + JMP copy_1_end + +copy_1_move_3: + MOVH (R10), R13 + MOVB 2(R10), R14 + MOVH R13, (R9) + MOVB R14, 2(R9) + ADD R0, R10, R10 + ADD R0, R9, R9 + JMP copy_1_end + +copy_1_move_4through7: + MOVWU (R10), R13 + ADD R0, R10, R15 + MOVWU -4(R15), R14 + MOVW R13, (R9) + ADD R0, R9, R15 + MOVW R14, -4(R15) + ADD R0, R10, R10 + ADD R0, R9, R9 + JMP copy_1_end + +copy_1_move_8through16: + MOVD (R10), R13 + ADD R0, R10, R15 + MOVD -8(R15), R14 + MOVD R13, (R9) + ADD R0, R9, R15 + MOVD R14, -8(R15) + ADD R0, R10, R10 + ADD R0, R9, R9 + +copy_1_end: + ADD R0, R11, R11 + + // Malformed input if seq.mo > t+len(hist) || seq.mo > s.windowSize) +check_offset: + MOVD R11, R0 + MOVD 40(RSP), R16 + ADD R16, R0, R0 + CMP R0, R1 + BGT error_match_off_too_big + MOVD 56(RSP), R16 + CMP R16, R1 + BGT error_match_off_too_big + + // Copy match from history + MOVD R1, R0 + SUBS R11, R0, R0 + BLS copy_match + MOVD 48(RSP), R13 + SUB R0, R13, R13 + CMP R0, R12 + BGT copy_all_from_history + MOVD R12, R0 + SUBS $0x10, R0, R0 + BLO copy_4_small + +copy_4_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R13, R13 + ADD $0x10, R9, R9 + SUBS $0x10, R0, R0 + BHS copy_4_loop + ADD R0, R13, R13 + ADD $16, R13, R13 + ADD R0, R9, R9 + ADD $16, R9, R9 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_4_end + +copy_4_small: + CMP $0x03, R12 + BEQ copy_4_move_3 + CMP $0x08, R12 + BLO copy_4_move_4through7 + JMP copy_4_move_8through16 + +copy_4_move_3: + MOVH (R13), R0 + MOVB 2(R13), R1 + MOVH R0, (R9) + MOVB R1, 2(R9) + ADD R12, R13, R13 + ADD R12, R9, R9 + JMP copy_4_end + +copy_4_move_4through7: + MOVWU (R13), R0 + ADD R12, R13, R15 + MOVWU -4(R15), R1 + MOVW R0, (R9) + ADD R12, R9, R15 + MOVW R1, -4(R15) + ADD R12, R13, R13 + ADD R12, R9, R9 + JMP copy_4_end + +copy_4_move_8through16: + MOVD (R13), R0 + ADD R12, R13, R15 + MOVD -8(R15), R1 + MOVD R0, (R9) + ADD R12, R9, R15 + MOVD R1, -8(R15) + ADD R12, R13, R13 + ADD R12, R9, R9 + +copy_4_end: + ADD R12, R11, R11 + JMP handle_loop + JMP loop_finished + +copy_all_from_history: + MOVD R0, R14 + SUBS $0x10, R14, R14 + BLO copy_5_small + +copy_5_loop: + VLD1 (R13), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R13, R13 + ADD $0x10, R9, R9 + SUBS $0x10, R14, R14 + BHS copy_5_loop + ADD R14, R13, R13 + ADD $16, R13, R13 + ADD R14, R9, R9 + ADD $16, R9, R9 + ADD $-16, R13, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_5_end + +copy_5_small: + CMP $0x03, R0 + BEQ copy_5_move_3 + BLO copy_5_move_1or2 + CMP $0x08, R0 + BLO copy_5_move_4through7 + JMP copy_5_move_8through16 + +copy_5_move_1or2: + MOVB (R13), R14 + ADD R0, R13, R15 + MOVB -1(R15), R4 + MOVB R14, (R9) + ADD R0, R9, R15 + MOVB R4, -1(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_3: + MOVH (R13), R14 + MOVB 2(R13), R4 + MOVH R14, (R9) + MOVB R4, 2(R9) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_4through7: + MOVWU (R13), R14 + ADD R0, R13, R15 + MOVWU -4(R15), R4 + MOVW R14, (R9) + ADD R0, R9, R15 + MOVW R4, -4(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + JMP copy_5_end + +copy_5_move_8through16: + MOVD (R13), R14 + ADD R0, R13, R15 + MOVD -8(R15), R4 + MOVD R14, (R9) + ADD R0, R9, R15 + MOVD R4, -8(R15) + ADD R0, R13, R13 + ADD R0, R9, R9 + +copy_5_end: + ADD R0, R11, R11 + SUB R0, R12, R12 + + // Copy match from the current buffer +copy_match: + MOVD R9, R0 + SUB R1, R0, R0 + + // ml <= mo + CMP R1, R12 + BHI copy_overlapping_match + + // Copy non-overlapping match + ADD R12, R11, R11 + MOVD R12, R1 + SUBS $0x10, R1, R1 + BLO copy_2_small + +copy_2_loop: + VLD1 (R0), [V0.B16] + VST1 [V0.B16], (R9) + ADD $0x10, R0, R0 + ADD $0x10, R9, R9 + SUBS $0x10, R1, R1 + BHS copy_2_loop + ADD R1, R0, R0 + ADD $16, R0, R0 + ADD R1, R9, R9 + ADD $16, R9, R9 + ADD $-16, R0, R15 + VLD1 (R15), [V0.B16] + ADD $-16, R9, R15 + VST1 [V0.B16], (R15) + JMP copy_2_end + +copy_2_small: + CMP $0x03, R12 + BEQ copy_2_move_3 + BLO copy_2_move_1or2 + CMP $0x08, R12 + BLO copy_2_move_4through7 + JMP copy_2_move_8through16 + +copy_2_move_1or2: + MOVB (R0), R1 + ADD R12, R0, R15 + MOVB -1(R15), R13 + MOVB R1, (R9) + ADD R12, R9, R15 + MOVB R13, -1(R15) + ADD R12, R0, R0 + ADD R12, R9, R9 + JMP copy_2_end + +copy_2_move_3: + MOVH (R0), R1 + MOVB 2(R0), R13 + MOVH R1, (R9) + MOVB R13, 2(R9) + ADD R12, R0, R0 + ADD R12, R9, R9 + JMP copy_2_end + +copy_2_move_4through7: + MOVWU (R0), R1 + ADD R12, R0, R15 + MOVWU -4(R15), R13 + MOVW R1, (R9) + ADD R12, R9, R15 + MOVW R13, -4(R15) + ADD R12, R0, R0 + ADD R12, R9, R9 + JMP copy_2_end + +copy_2_move_8through16: + MOVD (R0), R1 + ADD R12, R0, R15 + MOVD -8(R15), R13 + MOVD R1, (R9) + ADD R12, R9, R15 + MOVD R13, -8(R15) + ADD R12, R0, R0 + ADD R12, R9, R9 + +copy_2_end: + JMP handle_loop + + // Copy overlapping match +copy_overlapping_match: + ADD R12, R11, R11 + +copy_slow_3: + MOVB (R0), R1 + MOVB R1, (R9) + ADD $1, R0, R0 + ADD $1, R9, R9 + SUBS $1, R12, R12 + BNE copy_slow_3 + +handle_loop: + MOVD ctx+16(FP), R0 + MOVD 96(R0), R16 + SUBS $1, R16, R16 + MOVD R16, 96(R0) + BPL sequenceDecs_decodeSync_safe_amd64_main_loop + +loop_finished: + MOVD br+8(FP), R0 + MOVD R2, 24(R0) + MOVB R3, 40(R0) + MOVD R5, 32(R0) + + // Update the context + MOVD ctx+16(FP), R0 + MOVD R11, 136(R0) + MOVD 144(R0), R1 + SUB R1, R10, R10 + MOVD R10, 168(R0) + + // Return success + MOVD $0x00000000, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match length error +sequenceDecs_decodeSync_safe_amd64_error_match_len_ofs_mismatch: + MOVD 16(RSP), R0 + MOVD ctx+16(FP), R1 + MOVD R0, 216(R1) + MOVD $0x00000001, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match too long error +sequenceDecs_decodeSync_safe_amd64_error_match_len_too_big: + MOVD ctx+16(FP), R0 + MOVD 16(RSP), R1 + MOVD R1, 216(R0) + MOVD $0x00000002, R16 + MOVD R16, ret+24(FP) + RET + + // Return with match offset too long error +error_match_off_too_big: + MOVD ctx+16(FP), R0 + MOVD 8(RSP), R1 + MOVD R1, 224(R0) + MOVD R11, 136(R0) + MOVD $0x00000003, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough literals error +error_not_enough_literals: + MOVD ctx+16(FP), R0 + MOVD 24(RSP), R1 + MOVD R1, 208(R0) + MOVD $0x00000004, R16 + MOVD R16, ret+24(FP) + RET + + // Return with overread error +error_overread: + MOVD $0x00000006, R16 + MOVD R16, ret+24(FP) + RET + + // Return with not enough output space error +error_not_enough_space: + MOVD ctx+16(FP), R0 + MOVD 24(RSP), R1 + MOVD R1, 208(R0) + MOVD 16(RSP), R1 + MOVD R1, 216(R0) + MOVD R11, 136(R0) + MOVD $0x00000005, R16 + MOVD R16, ret+24(FP) + RET + +// skipped sequenceDecs_decodeSync_safe_bmi2 (generic twin preferred on arm64) diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_asm.go b/vendor/github.com/klauspost/compress/zstd/seqdec_asm.go new file mode 100644 index 000000000..55405f391 --- /dev/null +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_asm.go @@ -0,0 +1,289 @@ +//go:build (amd64 || arm64) && !appengine && !noasm && gc + +package zstd + +import ( + "fmt" + "io" +) + +// This file holds the parts of the assembly sequence decoder that are identical +// across architectures: the context structs exchanged with the asm, the error +// codes, and the decode/decodeSync/executeSimple wrappers. Each architecture +// supplies the small dispatch helpers (decodeAsm, decodeSyncAsm, +// executeSimpleAsm) that select the concrete asm routine — amd64 also chooses a +// BMI2 variant, arm64 has a single implementation. + +type decodeSyncAsmContext struct { + llTable []decSymbol + mlTable []decSymbol + ofTable []decSymbol + llState uint64 + mlState uint64 + ofState uint64 + iteration int + litRemain int + out []byte + outPosition int + literals []byte + litPosition int + history []byte + windowSize int + ll int // set on error (not for all errors, please refer to _generate/gen.go) + ml int // set on error (not for all errors, please refer to _generate/gen.go) + mo int // set on error (not for all errors, please refer to _generate/gen.go) +} + +type decodeAsmContext struct { + llTable []decSymbol + mlTable []decSymbol + ofTable []decSymbol + llState uint64 + mlState uint64 + ofState uint64 + iteration int + seqs []seqVals + litRemain int +} + +type executeAsmContext struct { + seqs []seqVals + seqIndex int + out []byte + history []byte + literals []byte + outPosition int + litPosition int + windowSize int +} + +const noError = 0 + +// error reported when mo == 0 && ml > 0 +const errorMatchLenOfsMismatch = 1 + +// error reported when ml > maxMatchLen +const errorMatchLenTooBig = 2 + +// error reported when mo > available history or mo > s.windowSize +const errorMatchOffTooBig = 3 + +// error reported when the sum of literal lengths exeeceds the literal buffer size +const errorNotEnoughLiterals = 4 + +// error reported when capacity of `out` is too small +const errorNotEnoughSpace = 5 + +// error reported when bits are overread. +const errorOverread = 6 + +// decode sequences from the stream with the provided history but without a dictionary. +func (s *sequenceDecs) decodeSyncSimple(hist []byte) (bool, error) { + if len(s.dict) > 0 { + return false, nil + } + if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSize { + return false, nil + } + + // FIXME: Using unsafe memory copies leads to rare, random crashes + // with fuzz testing. It is therefore disabled for now. + const useSafe = true + + br := s.br + + maxBlockSize := min(s.windowSize, maxCompressedBlockSize) + + ctx := decodeSyncAsmContext{ + llTable: s.litLengths.fse.dt[:maxTablesize], + mlTable: s.matchLengths.fse.dt[:maxTablesize], + ofTable: s.offsets.fse.dt[:maxTablesize], + llState: uint64(s.litLengths.state.state), + mlState: uint64(s.matchLengths.state.state), + ofState: uint64(s.offsets.state.state), + iteration: s.nSeqs - 1, + litRemain: len(s.literals), + out: s.out, + outPosition: len(s.out), + literals: s.literals, + windowSize: s.windowSize, + history: hist, + } + + s.seqSize = 0 + startSize := len(s.out) + + errCode := decodeSyncAsm(s, br, &ctx, useSafe) + switch errCode { + case noError: + break + + case errorMatchLenOfsMismatch: + return true, fmt.Errorf("zero matchoff and matchlen (%d) > 0", ctx.ml) + + case errorMatchLenTooBig: + return true, fmt.Errorf("match len (%d) bigger than max allowed length", ctx.ml) + + case errorMatchOffTooBig: + return true, fmt.Errorf("match offset (%d) bigger than current history (%d)", + ctx.mo, ctx.outPosition+len(hist)-startSize) + + case errorNotEnoughLiterals: + return true, fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", + ctx.ll, ctx.litRemain+ctx.ll) + + case errorOverread: + return true, io.ErrUnexpectedEOF + + case errorNotEnoughSpace: + size := ctx.outPosition + ctx.ll + ctx.ml + if debugDecoder { + println("msl:", s.maxSyncLen, "cap", cap(s.out), "bef:", startSize, "sz:", size-startSize, "mbs:", maxBlockSize, "outsz:", cap(s.out)-startSize) + } + return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) + + default: + return true, fmt.Errorf("sequenceDecs_decode returned erroneous code %d", errCode) + } + + s.seqSize += ctx.litRemain + if s.seqSize > maxBlockSize { + return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) + } + err := br.close() + if err != nil { + printf("Closing sequences: %v, %+v\n", err, *br) + return true, err + } + + s.literals = s.literals[ctx.litPosition:] + t := ctx.outPosition + s.out = s.out[:t] + + // Add final literals + s.out = append(s.out, s.literals...) + if debugDecoder { + t += len(s.literals) + if t != len(s.out) { + panic(fmt.Errorf("length mismatch, want %d, got %d", len(s.out), t)) + } + } + + return true, nil +} + +// decode sequences from the stream without the provided history. +func (s *sequenceDecs) decode(seqs []seqVals) error { + br := s.br + + maxBlockSize := min(s.windowSize, maxCompressedBlockSize) + + ctx := decodeAsmContext{ + llTable: s.litLengths.fse.dt[:maxTablesize], + mlTable: s.matchLengths.fse.dt[:maxTablesize], + ofTable: s.offsets.fse.dt[:maxTablesize], + llState: uint64(s.litLengths.state.state), + mlState: uint64(s.matchLengths.state.state), + ofState: uint64(s.offsets.state.state), + seqs: seqs, + iteration: len(seqs) - 1, + litRemain: len(s.literals), + } + + if debugDecoder { + println("decode: decoding", len(seqs), "sequences", br.remain(), "bits remain on stream") + } + + s.seqSize = 0 + lte56bits := s.maxBits+s.offsets.fse.actualTableLog+s.matchLengths.fse.actualTableLog+s.litLengths.fse.actualTableLog <= 56 + errCode := decodeAsm(s, br, &ctx, lte56bits) + if errCode != 0 { + i := len(seqs) - ctx.iteration - 1 + switch errCode { + case errorMatchLenOfsMismatch: + ml := ctx.seqs[i].ml + return fmt.Errorf("zero matchoff and matchlen (%d) > 0", ml) + + case errorMatchLenTooBig: + ml := ctx.seqs[i].ml + return fmt.Errorf("match len (%d) bigger than max allowed length", ml) + + case errorNotEnoughLiterals: + ll := ctx.seqs[i].ll + return fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", ll, ctx.litRemain+ll) + case errorOverread: + return io.ErrUnexpectedEOF + } + + return fmt.Errorf("sequenceDecs_decode_amd64 returned erroneous code %d", errCode) + } + + if ctx.litRemain < 0 { + return fmt.Errorf("literal count is too big: total available %d, total requested %d", + len(s.literals), len(s.literals)-ctx.litRemain) + } + + s.seqSize += ctx.litRemain + if s.seqSize > maxBlockSize { + return fmt.Errorf("output bigger than max block size (%d)", maxBlockSize) + } + if debugDecoder { + println("decode: ", br.remain(), "bits remain on stream. code:", errCode) + } + err := br.close() + if err != nil { + printf("Closing sequences: %v, %+v\n", err, *br) + } + return err +} + +// executeSimple handles cases when dictionary is not used. +func (s *sequenceDecs) executeSimple(seqs []seqVals, hist []byte) error { + // Ensure we have enough output size... + if len(s.out)+s.seqSize+compressedBlockOverAlloc > cap(s.out) { + addBytes := s.seqSize + len(s.out) + compressedBlockOverAlloc + s.out = append(s.out, make([]byte, addBytes)...) + s.out = s.out[:len(s.out)-addBytes] + } + + if debugDecoder { + printf("Execute %d seqs with literals: %d into %d bytes\n", len(seqs), len(s.literals), s.seqSize) + } + + var t = len(s.out) + out := s.out[:t+s.seqSize] + + ctx := executeAsmContext{ + seqs: seqs, + seqIndex: 0, + out: out, + history: hist, + outPosition: t, + litPosition: 0, + literals: s.literals, + windowSize: s.windowSize, + } + // useSafe avoids overwriting the output buffer when the literals slice has + // not been allocated with the required over-allocation slack. + useSafe := cap(s.literals) < len(s.literals)+compressedBlockOverAlloc + + ok := executeSimpleAsm(&ctx, useSafe) + if !ok { + return fmt.Errorf("match offset (%d) bigger than current history (%d)", + seqs[ctx.seqIndex].mo, ctx.outPosition+len(hist)) + } + s.literals = s.literals[ctx.litPosition:] + t = ctx.outPosition + + // Add final literals + copy(out[t:], s.literals) + if debugDecoder { + t += len(s.literals) + if t != len(out) { + panic(fmt.Errorf("length mismatch, want %d, got %d, ss: %d", len(out), t, s.seqSize)) + } + } + s.out = out + + return nil +} diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go b/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go index 516cd9b07..8a3db6ba2 100644 --- a/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go @@ -1,4 +1,4 @@ -//go:build !amd64 || appengine || !gc || noasm +//go:build (!amd64 && !arm64) || appengine || !gc || noasm package zstd diff --git a/vendor/github.com/klauspost/compress/zstd/snappy.go b/vendor/github.com/klauspost/compress/zstd/snappy.go index 336c28893..36c56f36d 100644 --- a/vendor/github.com/klauspost/compress/zstd/snappy.go +++ b/vendor/github.com/klauspost/compress/zstd/snappy.go @@ -334,9 +334,10 @@ func decodeSnappy(blk *blockEnc, src []byte) error { return errUnsupportedLiteralLength } - //if length > snappyMaxBlockSize-d || uint32(length) > len(src)-s { - // return ErrSnappyCorrupt - //} + if length > len(src)-s { + println("length > len(src)-s", length, len(src)-s) + return ErrSnappyCorrupt + } blk.literals = append(blk.literals, src[s:s+length]...) //println(length, "litLen") diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go index 36ff259e9..dc02d7d66 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "slices" "strings" @@ -46,6 +47,34 @@ type RequireBearerTokenOptions struct { ResourceMetadataURL string // The required scopes. Scopes []string + // AllowMissingExpiration opts the middleware out of the + // `tokenInfo.Expiration.IsZero()` reject. Default false preserves the + // existing strict behaviour (every TokenInfo must carry an Expiration). + // + // Some IdPs emit session-bound bearer tokens that do not carry a standalone + // `exp` claim — the token's lifetime is bounded by an external session and + // is not advertised in-band. Resource servers integrating with such IdPs + // need to opt in to validating the rest of the token (scopes, signature + // via the verifier callback, etc.) without requiring the expiration field + // to be present. + // + // When enabled, the verifier is still responsible for any session-level + // validity check it can perform; this option only relaxes the middleware's + // own expiration enforcement. + AllowMissingExpiration bool + // ClockSkew bounds the tolerance applied to a token's Expiration when + // deciding whether it has elapsed. A token is rejected only if + // Expiration + ClockSkew is before the current time. Zero (the default) + // preserves strict comparison: any expired token is rejected immediately. + // + // Resource servers running behind a CDN, in distributed deployments, or + // communicating with an authorization server whose clock drifts a few + // seconds (common with cloud-managed IdPs) need a small positive value + // here to avoid rejecting tokens that are valid by the issuer's clock + // but momentarily appear expired by the verifier's. The same tolerance + // guards against an issuer's clock running slightly fast at /token + // issuance time. + ClockSkew time.Duration } type tokenInfoKey struct{} @@ -73,8 +102,17 @@ func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) tokenInfo, errmsg, code := verify(r, verifier, opts) if code != 0 { if code == http.StatusUnauthorized || code == http.StatusForbidden { - if opts != nil && opts.ResourceMetadataURL != "" { - w.Header().Add("WWW-Authenticate", "Bearer resource_metadata="+opts.ResourceMetadataURL) + if opts != nil { + var params []string + if opts.ResourceMetadataURL != "" { + params = append(params, fmt.Sprintf("resource_metadata=%q", opts.ResourceMetadataURL)) + } + if len(opts.Scopes) > 0 { + params = append(params, fmt.Sprintf("scope=%q", strings.Join(opts.Scopes, " "))) + } + if len(params) > 0 { + w.Header().Add("WWW-Authenticate", "Bearer "+strings.Join(params, ", ")) + } } } http.Error(w, errmsg, code) @@ -119,11 +157,17 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO } } - // Check expiration. - if tokenInfo.Expiration.IsZero() { - return nil, "token missing expiration", http.StatusUnauthorized + if opts == nil { + opts = &RequireBearerTokenOptions{} } - if tokenInfo.Expiration.Before(time.Now()) { + // Check expiration, with optional clock-skew tolerance. Skew only applies + // when an expiration is present; a missing expiration is governed solely by + // AllowMissingExpiration. + if tokenInfo.Expiration.IsZero() { + if !opts.AllowMissingExpiration { + return nil, "token missing expiration", http.StatusUnauthorized + } + } else if tokenInfo.Expiration.Add(opts.ClockSkew).Before(time.Now()) { return nil, "token expired", http.StatusUnauthorized } return tokenInfo, "", 0 diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go index 1190836e0..d897d8485 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go @@ -2,8 +2,6 @@ // Use of this source code is governed by the license // that can be found in the LICENSE file. -//go:build mcp_go_client_oauth - package auth import ( @@ -11,27 +9,19 @@ import ( "crypto/rand" "errors" "fmt" + "io" "net/http" "net/url" "slices" "strings" + "sync" + "github.com/modelcontextprotocol/go-sdk/internal/authutil" + "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/oauthex" "golang.org/x/oauth2" ) -// ClientSecretAuthConfig is used to configure client authentication using client_secret. -// Authentication method will be selected based on the authorization server's supported methods, -// according to the following preference order: -// 1. client_secret_post -// 2. client_secret_basic -type ClientSecretAuthConfig struct { - // ClientID is the client ID to be used for client authentication. - ClientID string - // ClientSecret is the client secret to be used for client authentication. - ClientSecret string -} - // ClientIDMetadataDocumentConfig is used to configure the Client ID Metadata Document // based client registration per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents. @@ -42,19 +32,15 @@ type ClientIDMetadataDocumentConfig struct { URL string } -// PreregisteredClientConfig is used to configure a pre-registered client per -// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration. -// Currently only "client_secret_basic" and "client_secret_post" authentication methods are supported. -type PreregisteredClientConfig struct { - // ClientSecretAuthConfig is the client_secret based configuration to be used for client authentication. - ClientSecretAuthConfig *ClientSecretAuthConfig -} - // DynamicClientRegistrationConfig is used to configure dynamic client registration per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration. type DynamicClientRegistrationConfig struct { // Metadata to be used in dynamic client registration request as per // https://datatracker.ietf.org/doc/html/rfc7591#section-2. + // + // If Metadata.ApplicationType is empty, it will be inferred from + // Metadata.RedirectURIs. When set, it will be validated against the inferred type + // and an error will be returned if they conflict. Metadata *oauthex.ClientRegistrationMetadata } @@ -65,14 +51,26 @@ type AuthorizationResult struct { Code string // State string returned by the authorization server. State string + // Iss is the issuer identifier returned by the authorization server in the + // authorization response per [RFC 9207]. The AuthorizationCodeFetcher should + // populate this from the "iss" query parameter in the redirect URI if present. + // + // [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207 + Iss string } -// AuthorizationArgs is the input to [AuthorizationCodeHandlerConfig].AuthorizationCodeFetcher. +// AuthorizationArgs is the input to [AuthorizationCodeFetcher]. type AuthorizationArgs struct { // Authorization URL to be opened in a browser for the user to start the authorization process. URL string } +// AuthorizationCodeFetcher is called to initiate the OAuth authorization flow. +// It is responsible for directing the user to the authorization URL (e.g., opening +// in a browser) and returning the authorization code and state once the Authorization +// Server redirects back to the configured RedirectURL. +type AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) + // AuthorizationCodeHandlerConfig is the configuration for [AuthorizationCodeHandler]. type AuthorizationCodeHandlerConfig struct { // Client registration configuration. @@ -82,7 +80,7 @@ type AuthorizationCodeHandlerConfig struct { // 3. Dynamic Client Registration // At least one method must be configured. ClientIDMetadataDocumentConfig *ClientIDMetadataDocumentConfig - PreregisteredClientConfig *PreregisteredClientConfig + PreregisteredClient *oauthex.ClientCredentials DynamicClientRegistrationConfig *DynamicClientRegistrationConfig // RedirectURL is a required URL to redirect to after authorization. @@ -97,10 +95,25 @@ type AuthorizationCodeHandlerConfig struct { RedirectURL string // AuthorizationCodeFetcher is a required function called to initiate the authorization flow. - // It is responsible for opening the URL in a browser for the user to start the authorization process. - // It should return the authorization code and state once the Authorization Server - // redirects back to the RedirectURL. - AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) + // See [AuthorizationCodeFetcher] for details. + AuthorizationCodeFetcher AuthorizationCodeFetcher + + // RequestRefreshToken indicates that the client intends to use refresh + // tokens and is capable of storing them securely. + // + // When true and the Authorization Server metadata contains "offline_access" + // in its scopes_supported, the client adds "offline_access" to the + // requested scopes. + // + // When using Dynamic Client Registration, callers should include + // "refresh_token" in [DynamicClientRegistrationConfig].Metadata.GrantTypes + // directly to advertise refresh token support to the Authorization Server. + // + // When using Client ID Metadata Document, the document hosted at the + // Client ID URL should include "refresh_token" in its grant_types. + // + // See https://modelcontextprotocol.io/seps/2207-oidc-refresh-token-guidance. + RequestRefreshToken bool // Client is an optional HTTP client to use for HTTP requests. // It is used for the following requests: @@ -114,6 +127,24 @@ type AuthorizationCodeHandlerConfig struct { // https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#server-side-request-forgery-ssrf // If not provided, http.DefaultClient will be used. Client *http.Client + + // NewTokenSource is an optional function that can be set to construct the + // token source that will be used by the [AuthorizationCodeHandler]. If + // non-nil, it is called after the authorization code is successfully + // exchanged for a token in [AuthorizationCodeHandler.Authorize] + // to obtain the [oauth2.TokenSource] returned by + // [AuthorizationCodeHandler.TokenSource]. Implementations must use the + // provided context, which is properly configured for constructing a + // TokenSource. The default is to call [oauth2.Config.TokenSource]. + NewTokenSource func(context.Context, *oauth2.Config, *oauth2.Token) (oauth2.TokenSource, error) + + // InitialTokenSource is an optional field that can be set to inject the + // token source that will be used by the [AuthorizationCodeHandler]. If + // non-nil, it is set as the token source that will be returned by + // [AuthorizationCodeHandler.TokenSource] during handler initialization. + // The default is nil, which means no token source has been set initially, + // and will trigger a call to [AuthorizationCodeHandler.Authorize]. + InitialTokenSource oauth2.TokenSource } // AuthorizationCodeHandler is an implementation of [OAuthHandler] that uses @@ -121,15 +152,21 @@ type AuthorizationCodeHandlerConfig struct { type AuthorizationCodeHandler struct { config *AuthorizationCodeHandlerConfig + // mu protects concurrent access to tokenSource and grantedScopes. + mu sync.RWMutex + // tokenSource is the token source to use for authorization. tokenSource oauth2.TokenSource + + // grantedScopes maps authorization server issuer to the list of scopes granted by that issuer. + grantedScopes map[string][]string } var _ OAuthHandler = (*AuthorizationCodeHandler)(nil) -func (h *AuthorizationCodeHandler) isOAuthHandler() {} - func (h *AuthorizationCodeHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { + h.mu.RLock() + defer h.mu.RUnlock() return h.tokenSource, nil } @@ -141,7 +178,7 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho return nil, errors.New("config must be provided") } if config.ClientIDMetadataDocumentConfig == nil && - config.PreregisteredClientConfig == nil && + config.PreregisteredClient == nil && config.DynamicClientRegistrationConfig == nil { return nil, errors.New("at least one client registration configuration must be provided") } @@ -151,19 +188,15 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho if config.ClientIDMetadataDocumentConfig != nil && !isNonRootHTTPSURL(config.ClientIDMetadataDocumentConfig.URL) { return nil, fmt.Errorf("client ID metadata document URL must be a non-root HTTPS URL") } - preCfg := config.PreregisteredClientConfig - if preCfg != nil { - if preCfg.ClientSecretAuthConfig == nil { - return nil, errors.New("ClientSecretAuthConfig is required for pre-registered client") - } - if preCfg.ClientSecretAuthConfig.ClientID == "" || preCfg.ClientSecretAuthConfig.ClientSecret == "" { - return nil, fmt.Errorf("pre-registered client ID or secret is empty") + if config.PreregisteredClient != nil { + if err := config.PreregisteredClient.Validate(); err != nil { + return nil, fmt.Errorf("invalid PreregisteredClient configuration: %w", err) } } dCfg := config.DynamicClientRegistrationConfig if dCfg != nil { if dCfg.Metadata == nil { - return nil, errors.New("Metadata is required for dynamic client registration") + return nil, errors.New("dynamic client registration requires non-nil Metadata") } if len(dCfg.Metadata.RedirectURIs) == 0 { return nil, errors.New("Metadata.RedirectURIs is required for dynamic client registration") @@ -173,6 +206,12 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho } else if !slices.Contains(dCfg.Metadata.RedirectURIs, config.RedirectURL) { return nil, fmt.Errorf("RedirectURL %q is not in the list of allowed redirect URIs for dynamic client registration", config.RedirectURL) } + applicationType := inferApplicationType(dCfg.Metadata.RedirectURIs) + if dCfg.Metadata.ApplicationType == "" { + dCfg.Metadata.ApplicationType = applicationType + } else if dCfg.Metadata.ApplicationType != applicationType { + return nil, fmt.Errorf("application type %q conflicts with the application type inferred from redirect URIs", dCfg.Metadata.ApplicationType) + } } if config.RedirectURL == "" { // If the RedirectURL was supposed to be set by the dynamic client registration, @@ -182,7 +221,11 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho if config.Client == nil { config.Client = http.DefaultClient } - return &AuthorizationCodeHandler{config: config}, nil + return &AuthorizationCodeHandler{ + config: config, + tokenSource: config.InitialTokenSource, + grantedScopes: make(map[string][]string), + }, nil } func isNonRootHTTPSURL(u string) bool { @@ -193,11 +236,42 @@ func isNonRootHTTPSURL(u string) bool { return pu.Scheme == "https" && pu.Path != "" } +// inferApplicationType returns an application type based on the redirect URIs. +func inferApplicationType(redirectURIs []string) string { + hasNative := false + hasWeb := false + for _, uri := range redirectURIs { + u, err := url.Parse(uri) + if err != nil { + return "" + } + switch u.Scheme { + case "http", "https": + if util.IsLoopback(u.Hostname()) { + hasNative = true + } else { + hasWeb = true + } + default: + hasNative = true + } + } + + if hasNative && hasWeb { + return "" + } + if hasNative { + return "native" + } + return "web" +} + // Authorize performs the authorization flow. // It is designed to perform the whole Authorization Code Grant flow. // On success, [AuthorizationCodeHandler.TokenSource] will return a token source with the fetched token. func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Request, resp *http.Response) error { defer resp.Body.Close() + defer io.Copy(io.Discard, resp.Body) wwwChallenges, err := oauthex.ParseWWWAuthenticate(resp.Header[http.CanonicalHeaderKey("WWW-Authenticate")]) if err != nil { @@ -218,9 +292,20 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ return err } - asm, err := h.getAuthServerMetadata(ctx, prm) + asm, err := GetAuthServerMetadata(ctx, prm.AuthorizationServers[0], h.config.Client) if err != nil { - return err + return fmt.Errorf("failed to get authorization server metadata: %w", err) + } + if asm == nil { + // Fallback to 2025-03-26 spec: predefined endpoints. + // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery + authServerURL := prm.AuthorizationServers[0] + asm = &oauthex.AuthServerMeta{ + Issuer: authServerURL, + AuthorizationEndpoint: authServerURL + "/authorize", + TokenEndpoint: authServerURL + "/token", + RegistrationEndpoint: authServerURL + "/register", + } } resolvedClientConfig, err := h.handleRegistration(ctx, asm) @@ -228,11 +313,27 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ return err } - scps := scopesFromChallenges(wwwChallenges) - if len(scps) == 0 && len(prm.ScopesSupported) > 0 { - scps = prm.ScopesSupported + requestedScopes := scopesFromChallenges(wwwChallenges) + if len(requestedScopes) == 0 && len(prm.ScopesSupported) > 0 { + requestedScopes = prm.ScopesSupported + } + + // SEP-2207: when the client desires refresh tokens and the Authorization + // Server advertises offline_access support, add it to the requested scopes. + if h.config.RequestRefreshToken && + slices.Contains(asm.ScopesSupported, "offline_access") && + !slices.Contains(requestedScopes, "offline_access") { + requestedScopes = append(requestedScopes, "offline_access") } + // Accumulate scopes: union previously granted scopes with the newly + // challenged scopes so that step-up authorization does not lose + // permissions granted in earlier rounds (SEP-2350). + h.mu.RLock() + granted := h.grantedScopes[asm.Issuer] + h.mu.RUnlock() + requestedScopes = authutil.UnionScopes(granted, requestedScopes) + cfg := &oauth2.Config{ ClientID: resolvedClientConfig.clientID, ClientSecret: resolvedClientConfig.clientSecret, @@ -243,16 +344,24 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ AuthStyle: resolvedClientConfig.authStyle, }, RedirectURL: h.config.RedirectURL, - Scopes: scps, + Scopes: requestedScopes, } - authRes, err := h.getAuthorizationCode(ctx, cfg, req.URL.String()) + authRes, err := h.getAuthorizationCode(ctx, cfg, prm.Resource) if err != nil { // Purposefully leaving the error unwrappable so it can be handled by the caller. return err } + if err := validateIssuerResponse(authRes.Iss, asm.Issuer, asm.AuthorizationResponseIssParameterSupported); err != nil { + return err + } - return h.exchangeAuthorizationCode(ctx, cfg, authRes, prm.Resource) + err = h.exchangeAuthorizationCode(ctx, cfg, authRes, prm.Resource) + if err != nil { + return err + } + + return h.updateGrantedScopes(asm.Issuer, requestedScopes) } // resourceMetadataURLFromChallenges returns a resource metadata URL from the given "WWW-Authenticate" header challenges, @@ -292,22 +401,34 @@ func errorFromChallenges(cs []oauthex.Challenge) string { // If no metadata was found or the fetched metadata fails security checks, // it returns an error. func (h *AuthorizationCodeHandler) getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string) (*oauthex.ProtectedResourceMetadata, error) { - var errs []error // Use MCP server URL as the resource URI per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri. for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) { prm, err := oauthex.GetProtectedResourceMetadata(ctx, url.URL, url.Resource, h.config.Client) if err != nil { - errs = append(errs, err) continue } if prm == nil { - errs = append(errs, fmt.Errorf("protected resource metadata is nil")) continue } + if len(prm.AuthorizationServers) == 0 { + // If we found PRM, we enforce the 2025-11-25 spec and not search further. + return nil, fmt.Errorf("protected resource metadata has no authorization servers specified") + } return prm, nil } - return nil, fmt.Errorf("failed to get protected resource metadata: %v", errors.Join(errs...)) + // Fallback to 2025-03-26 spec MCP server root is the Authorization Server: + // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery + u, err := url.Parse(mcpServerURL) + if err != nil { + return nil, fmt.Errorf("failed to parse MCP server URL: %v", err) + } + u.Path = "" + prm := &oauthex.ProtectedResourceMetadata{ + AuthorizationServers: []string{u.String()}, + Resource: mcpServerURL, + } + return prm, nil } type prmURL struct { @@ -350,82 +471,6 @@ func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL { return urls } -// getAuthServerMetadata returns the authorization server metadata. -// The provided Protected Resource Metadata must not be nil. -// It returns an error if the metadata request fails with non-4xx HTTP status code -// or the fetched metadata fails security checks. -// If no metadata was found, it returns a minimal set of endpoints -// as a fallback to 2025-03-26 spec. -func (h *AuthorizationCodeHandler) getAuthServerMetadata(ctx context.Context, prm *oauthex.ProtectedResourceMetadata) (*oauthex.AuthServerMeta, error) { - var authServerURL string - if len(prm.AuthorizationServers) > 0 { - // Use the first authorization server, similarly to other SDKs. - authServerURL = prm.AuthorizationServers[0] - } else { - // Fallback to 2025-03-26 spec: MCP server base URL acts as Authorization Server. - authURL, err := url.Parse(prm.Resource) - if err != nil { - return nil, fmt.Errorf("failed to parse resource URL: %v", err) - } - authURL.Path = "" - authServerURL = authURL.String() - } - - for _, u := range authorizationServerMetadataURLs(authServerURL) { - asm, err := oauthex.GetAuthServerMeta(ctx, u, authServerURL, h.config.Client) - if err != nil { - return nil, fmt.Errorf("failed to get authorization server metadata: %w", err) - } - if asm != nil { - return asm, nil - } - } - - // Fallback to 2025-03-26 spec: predefined endpoints. - // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery - asm := &oauthex.AuthServerMeta{ - Issuer: authServerURL, - AuthorizationEndpoint: authServerURL + "/authorize", - TokenEndpoint: authServerURL + "/token", - RegistrationEndpoint: authServerURL + "/register", - } - return asm, nil -} - -// authorizationServerMetadataURLs returns a list of URLs to try when looking for -// authorization server metadata as mandated by the MCP specification: -// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery. -func authorizationServerMetadataURLs(issuerURL string) []string { - var urls []string - - baseURL, err := url.Parse(issuerURL) - if err != nil { - return nil - } - - if baseURL.Path == "" { - // "OAuth 2.0 Authorization Server Metadata". - baseURL.Path = "/.well-known/oauth-authorization-server" - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0". - baseURL.Path = "/.well-known/openid-configuration" - urls = append(urls, baseURL.String()) - return urls - } - - originalPath := baseURL.Path - // "OAuth 2.0 Authorization Server Metadata with path insertion". - baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/") - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0 with path insertion". - baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/") - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0 with path appending". - baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration" - urls = append(urls, baseURL.String()) - return urls -} - type registrationType int const ( @@ -488,13 +533,20 @@ func (h *AuthorizationCodeHandler) handleRegistration(ctx context.Context, asm * }, nil } // 2. Attempt to use pre-registered client configuration. - pCfg := h.config.PreregisteredClientConfig - if pCfg != nil { + preCfg := h.config.PreregisteredClient + if preCfg != nil { + if preCfg.Issuer != "" && !authutil.IssuersEqual(preCfg.Issuer, asm.Issuer) { + return nil, fmt.Errorf("authorization server issuer %q does not match pre-registered credentials issuer %q", asm.Issuer, preCfg.Issuer) + } authStyle := selectTokenAuthMethod(asm.TokenEndpointAuthMethodsSupported) + clientSecret := "" + if preCfg.ClientSecretAuth != nil { + clientSecret = preCfg.ClientSecretAuth.ClientSecret + } return &resolvedClientConfig{ registrationType: registrationTypePreregistered, - clientID: pCfg.ClientSecretAuthConfig.ClientID, - clientSecret: pCfg.ClientSecretAuthConfig.ClientSecret, + clientID: preCfg.ClientID, + clientSecret: clientSecret, authStyle: authStyle, }, nil } @@ -548,6 +600,27 @@ func (h *AuthorizationCodeHandler) getAuthorizationCode(ctx context.Context, cfg }, nil } +// validateIssuerResponse validates the "iss" parameter in an authorization response +// per [RFC 9207]. +// +// [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207 +func validateIssuerResponse(iss, expectedIssuer string, issParameterSupported bool) error { + if issParameterSupported { + if iss == "" { + return fmt.Errorf("authorization server advertises RFC 9207 iss parameter support but none was received in the authorization response") + } + if iss != expectedIssuer { + return fmt.Errorf("authorization response issuer %q does not match expected issuer %q", iss, expectedIssuer) + } + } else { + if iss != "" { + return fmt.Errorf("authorization server does not advertise RFC 9207 iss parameter support but iss was received in the authorization response") + } + } + + return nil +} + // exchangeAuthorizationCode exchanges the authorization code for a token // and stores it in a token source. func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context, cfg *oauth2.Config, authResult *authResult, resourceURL string) error { @@ -560,6 +633,52 @@ func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context if err != nil { return fmt.Errorf("token exchange failed: %w", err) } - h.tokenSource = cfg.TokenSource(clientCtx, token) + // The token source outlives this authorization request: it is stored on the + // handler and used by the transport for the lifetime of the connection. The + // oauth2 library captures the context passed to TokenSource and reuses it for + // every subsequent token refresh (see golang.org/x/oauth2: tokenRefresher + // retains the context and passes it to each refresh round-trip). Binding it to + // the per-request ctx makes all later refreshes fail with "context canceled" + // once that request (or the connect operation that triggered authorization) + // completes. Use a background context that still carries the configured HTTP + // client so refreshes keep working for the life of the token source. + refreshCtx := context.WithValue(context.Background(), oauth2.HTTPClient, h.config.Client) + var ts oauth2.TokenSource + if h.config.NewTokenSource == nil { + ts = cfg.TokenSource(refreshCtx, token) + } else { + var err error + ts, err = h.config.NewTokenSource(refreshCtx, cfg, token) + if err != nil { + return fmt.Errorf("constructing token source failed: %w", err) + } + } + h.mu.Lock() + h.tokenSource = ts + h.mu.Unlock() + return nil +} + +// updateGrantedScopes updates the granted scopes based on the token source and requested scopes. +func (h *AuthorizationCodeHandler) updateGrantedScopes(issuer string, requestedScopes []string) error { + h.mu.RLock() + ts := h.tokenSource + h.mu.RUnlock() + + if ts == nil { + return nil + } + tok, err := ts.Token() + if err != nil { + return err + } + + h.mu.Lock() + if tokenScopes := authutil.ScopesFromToken(tok); tokenScopes == nil { + h.grantedScopes[issuer] = requestedScopes + } else { + h.grantedScopes[issuer] = tokenScopes + } + h.mu.Unlock() return nil } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go index 0af6963fc..db32d97a1 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go @@ -22,8 +22,6 @@ import ( // [github.com/modelcontextprotocol/go-sdk/mcp.StreamableClientTransport] // for an example. type OAuthHandler interface { - isOAuthHandler() - // TokenSource returns a token source to be used for outgoing requests. // Returned token source might be nil. In that case, the transport will not // add any authorization headers to the request. diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go deleted file mode 100644 index 767c59eea..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2025 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -//go:build mcp_go_client_oauth - -package auth - -import ( - "bytes" - "errors" - "io" - "net/http" - "sync" - - "golang.org/x/oauth2" -) - -// An OAuthHandlerLegacy conducts an OAuth flow and returns a [oauth2.TokenSource] if the authorization -// is approved, or an error if not. -// The handler receives the HTTP request and response that triggered the authentication flow. -// To obtain the protected resource metadata, call [oauthex.GetProtectedResourceMetadataFromHeader]. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type OAuthHandlerLegacy func(req *http.Request, res *http.Response) (oauth2.TokenSource, error) - -// HTTPTransport is an [http.RoundTripper] that follows the MCP -// OAuth protocol when it encounters a 401 Unauthorized response. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type HTTPTransport struct { - handler OAuthHandlerLegacy - mu sync.Mutex // protects opts.Base - opts HTTPTransportOptions -} - -// NewHTTPTransport returns a new [*HTTPTransport]. -// The handler is invoked when an HTTP request results in a 401 Unauthorized status. -// It is called only once per transport. Once a TokenSource is obtained, it is used -// for the lifetime of the transport; subsequent 401s are not processed. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -func NewHTTPTransport(handler OAuthHandlerLegacy, opts *HTTPTransportOptions) (*HTTPTransport, error) { - if handler == nil { - return nil, errors.New("handler cannot be nil") - } - t := &HTTPTransport{ - handler: handler, - } - if opts != nil { - t.opts = *opts - } - if t.opts.Base == nil { - t.opts.Base = http.DefaultTransport - } - return t, nil -} - -// HTTPTransportOptions are options to [NewHTTPTransport]. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type HTTPTransportOptions struct { - // Base is the [http.RoundTripper] to use. - // If nil, [http.DefaultTransport] is used. - Base http.RoundTripper -} - -func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { - t.mu.Lock() - base := t.opts.Base - t.mu.Unlock() - - var ( - // If haveBody is set, the request has a nontrivial body, and we need avoid - // reading (or closing) it multiple times. In that case, bodyBytes is its - // content. - haveBody bool - bodyBytes []byte - ) - if req.Body != nil && req.Body != http.NoBody { - // if we're setting Body, we must mutate first. - req = req.Clone(req.Context()) - haveBody = true - var err error - bodyBytes, err = io.ReadAll(req.Body) - if err != nil { - return nil, err - } - // Now that we've read the request body, http.RoundTripper requires that we - // close it. - req.Body.Close() // ignore error - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - } - - resp, err := base.RoundTrip(req) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusUnauthorized { - return resp, nil - } - if _, ok := base.(*oauth2.Transport); ok { - // We failed to authorize even with a token source; give up. - return resp, nil - } - - resp.Body.Close() - // Try to authorize. - t.mu.Lock() - defer t.mu.Unlock() - // If we don't have a token source, get one by following the OAuth flow. - // (We may have obtained one while t.mu was not held above.) - // TODO: We hold the lock for the entire OAuth flow. This could be a long - // time. Is there a better way? - if _, ok := t.opts.Base.(*oauth2.Transport); !ok { - ts, err := t.handler(req, resp) - if err != nil { - return nil, err - } - t.opts.Base = &oauth2.Transport{Base: t.opts.Base, Source: ts} - } - - // If we don't have a body, the request is reusable, though it will be cloned - // by the base. However, if we've had to read the body, we must clone. - if haveBody { - req = req.Clone(req.Context()) - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - } - - return t.opts.Base.RoundTrip(req) -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go new file mode 100644 index 000000000..8c5e10cfe --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go @@ -0,0 +1,69 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +// This file contains shared utilities for OAuth handlers. + +package auth + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/modelcontextprotocol/go-sdk/oauthex" +) + +// GetAuthServerMetadata fetches authorization server metadata for the given issuer URL. +// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result. +// +// Returns (nil, nil) when no metadata endpoints respond (404s), allowing callers to implement +// fallback logic. Returns an error for any non-client error (network failures, invalid JSON, etc.). +func GetAuthServerMetadata(ctx context.Context, issuerURL string, httpClient *http.Client) (*oauthex.AuthServerMeta, error) { + for _, metadataURL := range authorizationServerMetadataURLs(issuerURL) { + asm, err := oauthex.GetAuthServerMeta(ctx, metadataURL, issuerURL, httpClient) + if err != nil { + return nil, err + } + if asm != nil { + return asm, nil + } + } + return nil, nil +} + +// authorizationServerMetadataURLs returns a list of URLs to try when looking for +// authorization server metadata as mandated by the MCP specification: +// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery. +func authorizationServerMetadataURLs(issuerURL string) []string { + var urls []string + + baseURL, err := url.Parse(issuerURL) + if err != nil { + return nil + } + + if baseURL.Path == "" { + // "OAuth 2.0 Authorization Server Metadata". + baseURL.Path = "/.well-known/oauth-authorization-server" + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0". + baseURL.Path = "/.well-known/openid-configuration" + urls = append(urls, baseURL.String()) + return urls + } + + originalPath := baseURL.Path + // "OAuth 2.0 Authorization Server Metadata with path insertion". + baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/") + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0 with path insertion". + baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/") + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0 with path appending". + baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration" + urls = append(urls, baseURL.String()) + + return urls +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/scopes.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/scopes.go new file mode 100644 index 000000000..4b5de73af --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/scopes.go @@ -0,0 +1,36 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. +package authutil + +import ( + "maps" + "slices" + "strings" + + "golang.org/x/oauth2" +) + +// UnionScopes returns the union of the existing and challenged scope sets. +// It is used during step-up authorization to accumulate scopes across +// authorization rounds (SEP-2350). +func UnionScopes(existing, challenged []string) []string { + combined := make(map[string]struct{}) + for _, s := range existing { + combined[s] = struct{}{} + } + for _, s := range challenged { + combined[s] = struct{}{} + } + return slices.Collect(maps.Keys(combined)) +} + +// ScopesFromToken extracts the granted scopes from an OAuth2 token response. +// Per RFC 6749 §5.1, the scope parameter is optional; returns nil if absent. +func ScopesFromToken(token *oauth2.Token) []string { + scope, ok := token.Extra("scope").(string) + if !ok { + return nil + } + return strings.Fields(scope) +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/util.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/util.go new file mode 100644 index 000000000..880d1bfa0 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/authutil/util.go @@ -0,0 +1,13 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package authutil + +import "strings" + +// IssuersEqual reports whether two OAuth 2.0 authorization server issuer +// identifiers refer to the same server comparing them without the final trailing slash. +func IssuersEqual(a, b string) bool { + return strings.TrimSuffix(a, "/") == strings.TrimSuffix(b, "/") +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go index 1148770e3..b4c897fa8 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go @@ -8,12 +8,25 @@ package json import ( "bytes" + "io" "github.com/segmentio/encoding/json" ) -func Unmarshal(data []byte, v any) error { - dec := json.NewDecoder(bytes.NewReader(data)) +type Decoder struct { + dec *json.Decoder +} + +func NewDecoder(r io.Reader) *Decoder { + dec := json.NewDecoder(r) dec.DontMatchCaseInsensitiveStructFields() - return dec.Decode(v) + return &Decoder{dec: dec} +} + +func (d *Decoder) Decode(v any) error { + return d.dec.Decode(v) +} + +func Unmarshal(data []byte, v any) error { + return NewDecoder(bytes.NewReader(data)).Decode(v) } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/conn.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/conn.go index 571df63aa..4994c63b0 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/conn.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/conn.go @@ -14,46 +14,24 @@ import ( "time" "github.com/modelcontextprotocol/go-sdk/internal/json" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" ) -// Binder builds a connection configuration. -// This may be used in servers to generate a new configuration per connection. -// ConnectionOptions itself implements Binder returning itself unmodified, to -// allow for the simple cases where no per connection information is needed. -type Binder interface { - // Bind returns the ConnectionOptions to use when establishing the passed-in - // Connection. - // - // The connection is not ready to use when Bind is called, - // but Bind may close it without reading or writing to it. - Bind(context.Context, *Connection) ConnectionOptions -} - -// A BinderFunc implements the Binder interface for a standalone Bind function. -type BinderFunc func(context.Context, *Connection) ConnectionOptions - -func (f BinderFunc) Bind(ctx context.Context, c *Connection) ConnectionOptions { - return f(ctx, c) -} - -var _ Binder = BinderFunc(nil) - -// ConnectionOptions holds the options for new connections. -type ConnectionOptions struct { - // Framer allows control over the message framing and encoding. - // If nil, HeaderFramer will be used. - Framer Framer - // Preempter allows registration of a pre-queue message handler. - // If nil, no messages will be preempted. - Preempter Preempter - // Handler is used as the queued message handler for inbound messages. - // If nil, all responses will be ErrNotHandled. - Handler Handler - // OnInternalError, if non-nil, is called with any internal errors that occur - // while serving the connection, such as protocol errors or invariant - // violations. (If nil, internal errors result in panics.) - OnInternalError func(error) -} +// nomethodnotfoundcodeinerror is a compatibility parameter that restores the +// pre-fix behavior of [processResult], where wrapped [ErrNotHandled] or +// [ErrMethodNotFound] errors returned by request handlers were not +// recognized as "method not found" signals. The original switch statement +// compared sentinel errors with ==, which never matched errors returned via +// fmt.Errorf("%w: ...", ErrNotHandled, ...) — including the ones produced +// by checkRequest. As a result the wire error response carried code 0 +// instead of code -32601. The fix uses errors.Is to recognize wrapped +// sentinels and append the method name to the message. +// +// To restore the previous behavior, set MCPGODEBUG=nomethodnotfoundcodeinerror=1. +// This option will be removed in a future SDK version. +// See the documentation for the mcpgodebug package for instructions on how +// to use it. +var nomethodnotfoundcodeinerror = mcpgodebug.Value("nomethodnotfoundcodeinerror") // Connection manages the jsonrpc2 protocol, connecting responses back to their // calls. Connection is bidirectional; it does not have a designated server or @@ -197,9 +175,28 @@ type incomingRequest struct { cancel context.CancelFunc } -// Bind returns the options unmodified. -func (o ConnectionOptions) Bind(context.Context, *Connection) ConnectionOptions { - return o +// Reader abstracts the transport mechanics from the JSON RPC protocol. +// A Connection reads messages from the reader it was provided on construction, +// and assumes that each call to Read fully transfers a single message, +// or returns an error. +// +// A reader is not safe for concurrent use, it is expected it will be used by +// a single Connection in a safe manner. +type Reader interface { + // Read gets the next message from the stream. + Read(context.Context) (Message, error) +} + +// Writer abstracts the transport mechanics from the JSON RPC protocol. +// A Connection writes messages using the writer it was provided on construction, +// and assumes that each call to Write fully transfers a single message, +// or returns an error. +// +// A writer must be safe for concurrent use, as writes may occur concurrently +// in practice: libraries may make calls or respond to requests asynchronously. +type Writer interface { + // Write sends a message to the stream. + Write(context.Context, Message) error } // A ConnectionConfig configures a bidirectional jsonrpc2 connection. @@ -211,12 +208,30 @@ type ConnectionConfig struct { Bind func(*Connection) Handler // required OnDone func() // optional OnInternalError func(error) // optional + + // PropagateCancellation controls whether cancellation of the context + // passed to [NewConnection] is observable by request handlers. + // + // By default (false), the connection wraps that context (see [notDone]) + // so handlers' Done channels do not fire when the connection's root + // context is cancelled. Cancellation of an in-flight handler is then + // expected to flow only through the jsonrpc2 layer's explicit channels + // (the [Preempter] reacting to the peer's cancel notification, or a + // transport read/write failure cancelling every in-flight request). + // + // Set this true when cancellation of the connection's context is itself + // a meaningful signal that handlers should react to (for example, when + // the connection is tied to a carrier that owns it and whose end means + // the request is cancelled). + PropagateCancellation bool // optional } // NewConnection creates a new [Connection] object and starts processing // incoming messages. func NewConnection(ctx context.Context, cfg ConnectionConfig) *Connection { - ctx = notDone{ctx} + if !cfg.PropagateCancellation { + ctx = notDone{ctx} + } c := &Connection{ state: inFlightState{closer: cfg.Closer}, @@ -230,59 +245,16 @@ func NewConnection(ctx context.Context, cfg ConnectionConfig) *Connection { return c } -// bindConnection creates a new connection and runs it. -// -// This is used by the Dial and Serve functions to build the actual connection. -// -// The connection is closed automatically (and its resources cleaned up) when -// the last request has completed after the underlying ReadWriteCloser breaks, -// but it may be stopped earlier by calling Close (for a clean shutdown). -func bindConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) *Connection { - // TODO: Should we create a new event span here? - // This will propagate cancellation from ctx; should it? - ctx := notDone{bindCtx} - - c := &Connection{ - state: inFlightState{closer: rwc}, - done: make(chan struct{}), - onDone: onDone, - } - // It's tempting to set a finalizer on c to verify that the state has gone - // idle when the connection becomes unreachable. Unfortunately, the Binder - // interface makes that unsafe: it allows the Handler to close over the - // Connection, which could create a reference cycle that would cause the - // Connection to become uncollectable. - - options := binder.Bind(bindCtx, c) - framer := options.Framer - if framer == nil { - framer = HeaderFramer() - } - c.handler = options.Handler - if c.handler == nil { - c.handler = defaultHandler{} - } - c.onInternalError = options.OnInternalError - - c.writer = framer.Writer(rwc) - reader := framer.Reader(rwc) - c.start(ctx, reader, options.Preempter) - return c -} - func (c *Connection) start(ctx context.Context, reader Reader, preempter Preempter) { c.updateInFlight(func(s *inFlightState) { select { case <-c.done: - // Bind already closed the connection; don't start a goroutine to read it. + // The connection was already closed; don't start a goroutine to read it. return default: } // The goroutine started here will continue until the underlying stream is closed. - // - // (If the Binder closed the Connection already, this should error out and - // return almost immediately.) s.reading = true go c.readIncoming(ctx, reader, preempter) }) @@ -439,18 +411,6 @@ type AsyncCall struct { // This can be used to cancel the call if needed. func (ac *AsyncCall) ID() ID { return ac.id } -// IsReady can be used to check if the result is already prepared. -// This is guaranteed to return true on a result for which Await has already -// returned, or a call that failed to send in the first place. -func (ac *AsyncCall) IsReady() bool { - select { - case <-ac.ready: - return true - default: - return false - } -} - // retire processes the response to the call. // // It is an error to call retire more than once: retire is guarded by the @@ -588,6 +548,14 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter ac.retire(&Response{ID: id, Error: err}) } s.outgoingCalls = nil + + // Cancel any incoming requests still in flight: with the reader gone we + // cannot receive cancellation notifications, and likely cannot write a + // response either, so parked handlers have nothing useful left to do. + // Mirrors the equivalent cleanup on write failure. + for _, r := range s.incomingByID { + r.cancel() + } }) } @@ -723,9 +691,7 @@ func (c *Connection) handleAsync() { // processResult processes the result of a request and, if appropriate, sends a response. func (c *Connection) processResult(from any, req *incomingRequest, result any, err error) error { - switch err { - case ErrNotHandled, ErrMethodNotFound: - // Add detail describing the unhandled method. + if nomethodnotfoundcodeinerror != "1" && (errors.Is(err, ErrNotHandled) || errors.Is(err, ErrMethodNotFound)) { err = fmt.Errorf("%w: %q", ErrMethodNotFound, req.Method) } @@ -784,9 +750,12 @@ func (c *Connection) write(ctx context.Context, msg Message) error { var err error // Fail writes immediately if the connection is shutting down. // - // TODO(rfindley): should we allow cancellation notifications through? It - // could be the case that writes can still succeed. + // Allow outgoing "notifications" forwarded by the Notify method. + // This will allow to send the cancelled notification when the client is shutting down. c.updateInFlight(func(s *inFlightState) { + if req, ok := msg.(*Request); ok && !req.IsCall() && s.outgoingNotifications > 0 { + return + } err = s.shuttingDown(ErrServerClosing) }) if err == nil { @@ -831,6 +800,15 @@ func (c *Connection) internalErrorf(format string, args ...any) error { } // notDone is a context.Context wrapper that returns a nil Done channel. +// +// Request handlers' contexts are derived from the connection's root context, +// which by default is wrapped in notDone so a transport-level cancellation +// does not implicitly cancel every in-flight handler. Cancellation of an +// in-flight handler is instead expected to flow only through the jsonrpc2 +// layer's explicit channels: the [Preempter] calling [Connection.Cancel] in +// response to the peer's cancel notification, or the transport itself +// failing (the read loop exits on EOF or a write fails) — both of which +// cancel every in-flight incoming request in turn. type notDone struct{ ctx context.Context } func (ic notDone) Value(key any) any { diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/frame.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/frame.go deleted file mode 100644 index 72527cb9c..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/frame.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2018 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package jsonrpc2 - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "io" - "strconv" - "strings" - "sync" -) - -// Reader abstracts the transport mechanics from the JSON RPC protocol. -// A Conn reads messages from the reader it was provided on construction, -// and assumes that each call to Read fully transfers a single message, -// or returns an error. -// -// A reader is not safe for concurrent use, it is expected it will be used by -// a single Conn in a safe manner. -type Reader interface { - // Read gets the next message from the stream. - Read(context.Context) (Message, error) -} - -// Writer abstracts the transport mechanics from the JSON RPC protocol. -// A Conn writes messages using the writer it was provided on construction, -// and assumes that each call to Write fully transfers a single message, -// or returns an error. -// -// A writer must be safe for concurrent use, as writes may occur concurrently -// in practice: libraries may make calls or respond to requests asynchronously. -type Writer interface { - // Write sends a message to the stream. - Write(context.Context, Message) error -} - -// Framer wraps low level byte readers and writers into jsonrpc2 message -// readers and writers. -// It is responsible for the framing and encoding of messages into wire form. -// -// TODO(rfindley): rethink the framer interface, as with JSONRPC2 batching -// there is a need for Reader and Writer to be correlated, and while the -// implementation of framing here allows that, it is not made explicit by the -// interface. -// -// Perhaps a better interface would be -// -// Frame(io.ReadWriteCloser) (Reader, Writer). -type Framer interface { - // Reader wraps a byte reader into a message reader. - Reader(io.Reader) Reader - // Writer wraps a byte writer into a message writer. - Writer(io.Writer) Writer -} - -// RawFramer returns a new Framer. -// The messages are sent with no wrapping, and rely on json decode consistency -// to determine message boundaries. -func RawFramer() Framer { return rawFramer{} } - -type rawFramer struct{} -type rawReader struct{ in *json.Decoder } -type rawWriter struct { - mu sync.Mutex - out io.Writer -} - -func (rawFramer) Reader(rw io.Reader) Reader { - return &rawReader{in: json.NewDecoder(rw)} -} - -func (rawFramer) Writer(rw io.Writer) Writer { - return &rawWriter{out: rw} -} - -func (r *rawReader) Read(ctx context.Context) (Message, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - var raw json.RawMessage - if err := r.in.Decode(&raw); err != nil { - return nil, err - } - msg, err := DecodeMessage(raw) - return msg, err -} - -func (w *rawWriter) Write(ctx context.Context, msg Message) error { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - data, err := EncodeMessage(msg) - if err != nil { - return fmt.Errorf("marshaling message: %v", err) - } - - w.mu.Lock() - defer w.mu.Unlock() - _, err = w.out.Write(data) - return err -} - -// HeaderFramer returns a new Framer. -// The messages are sent with HTTP content length and MIME type headers. -// This is the format used by LSP and others. -func HeaderFramer() Framer { return headerFramer{} } - -type headerFramer struct{} -type headerReader struct{ in *bufio.Reader } -type headerWriter struct { - mu sync.Mutex - out io.Writer -} - -func (headerFramer) Reader(rw io.Reader) Reader { - return &headerReader{in: bufio.NewReader(rw)} -} - -func (headerFramer) Writer(rw io.Writer) Writer { - return &headerWriter{out: rw} -} - -func (r *headerReader) Read(ctx context.Context) (Message, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - firstRead := true // to detect a clean EOF below - var contentLength int64 - // read the header, stop on the first empty line - for { - line, err := r.in.ReadString('\n') - if err != nil { - if err == io.EOF { - if firstRead && line == "" { - return nil, io.EOF // clean EOF - } - err = io.ErrUnexpectedEOF - } - return nil, fmt.Errorf("failed reading header line: %w", err) - } - firstRead = false - - line = strings.TrimSpace(line) - // check we have a header line - if line == "" { - break - } - colon := strings.IndexRune(line, ':') - if colon < 0 { - return nil, fmt.Errorf("invalid header line %q", line) - } - name, value := line[:colon], strings.TrimSpace(line[colon+1:]) - switch { - case strings.EqualFold(name, "Content-Length"): - if contentLength, err = strconv.ParseInt(value, 10, 32); err != nil { - return nil, fmt.Errorf("failed parsing Content-Length: %v", value) - } - if contentLength <= 0 { - return nil, fmt.Errorf("invalid Content-Length: %v", contentLength) - } - default: - // ignoring unknown headers - } - } - if contentLength == 0 { - return nil, fmt.Errorf("missing Content-Length header") - } - data := make([]byte, contentLength) - _, err := io.ReadFull(r.in, data) - if err != nil { - return nil, err - } - msg, err := DecodeMessage(data) - return msg, err -} - -func (w *headerWriter) Write(ctx context.Context, msg Message) error { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - w.mu.Lock() - defer w.mu.Unlock() - - data, err := EncodeMessage(msg) - if err != nil { - return fmt.Errorf("marshaling message: %v", err) - } - _, err = fmt.Fprintf(w.out, "Content-Length: %v\r\n\r\n", len(data)) - if err == nil { - _, err = w.out.Write(data) - } - return err -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/jsonrpc2.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/jsonrpc2.go index 234e6ee3a..76a511613 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/jsonrpc2.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/jsonrpc2.go @@ -12,17 +12,12 @@ import ( "errors" ) -var ( - // ErrIdleTimeout is returned when serving timed out waiting for new connections. - ErrIdleTimeout = errors.New("timed out waiting for new connections") - - // ErrNotHandled is returned from a Handler or Preempter to indicate it did - // not handle the request. - // - // If a Handler returns ErrNotHandled, the server replies with - // ErrMethodNotFound. - ErrNotHandled = errors.New("JSON RPC not handled") -) +// ErrNotHandled is returned from a Handler or Preempter to indicate it did +// not handle the request. +// +// If a Handler returns ErrNotHandled, the server replies with +// ErrMethodNotFound. +var ErrNotHandled = errors.New("JSON RPC not handled") // Preempter handles messages on a connection before they are queued to the main // handler. @@ -40,15 +35,6 @@ type Preempter interface { Preempt(ctx context.Context, req *Request) (result any, err error) } -// A PreempterFunc implements the Preempter interface for a standalone Preempt function. -type PreempterFunc func(ctx context.Context, req *Request) (any, error) - -func (f PreempterFunc) Preempt(ctx context.Context, req *Request) (any, error) { - return f(ctx, req) -} - -var _ Preempter = PreempterFunc(nil) - // Handler handles messages on a connection. type Handler interface { // Handle is invoked sequentially for each incoming request that has not @@ -67,16 +53,6 @@ type Handler interface { Handle(ctx context.Context, req *Request) (result any, err error) } -type defaultHandler struct{} - -func (defaultHandler) Preempt(context.Context, *Request) (any, error) { - return nil, ErrNotHandled -} - -func (defaultHandler) Handle(context.Context, *Request) (any, error) { - return nil, ErrNotHandled -} - // A HandlerFunc implements the Handler interface for a standalone Handle function. type HandlerFunc func(ctx context.Context, req *Request) (any, error) @@ -85,37 +61,3 @@ func (f HandlerFunc) Handle(ctx context.Context, req *Request) (any, error) { } var _ Handler = HandlerFunc(nil) - -// async is a small helper for operations with an asynchronous result that you -// can wait for. -type async struct { - ready chan struct{} // closed when done - firstErr chan error // 1-buffered; contains either nil or the first non-nil error -} - -func newAsync() *async { - var a async - a.ready = make(chan struct{}) - a.firstErr = make(chan error, 1) - a.firstErr <- nil - return &a -} - -func (a *async) done() { - close(a.ready) -} - -func (a *async) wait() error { - <-a.ready - err := <-a.firstErr - a.firstErr <- err - return err -} - -func (a *async) setError(err error) { - storedErr := <-a.firstErr - if storedErr == nil { - storedErr = err - } - a.firstErr <- storedErr -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/messages.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/messages.go index b424780eb..8b9677061 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/messages.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/messages.go @@ -11,8 +11,6 @@ import ( "fmt" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" - - "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" ) // ID is a Request identifier, which is defined by the spec to be a string, integer, or null. @@ -173,8 +171,20 @@ func EncodeIndent(msg Message, prefix, indent string) ([]byte, error) { return bytes.TrimRight(buf.Bytes(), "\n"), nil } +// wireDecode is the decode form of [wireCombined]. Method is a [json.RawMessage] +// so we can tell whether the "method" key was present on the wire, including +// when its value is the empty string (see go-sdk#976). +type wireDecode struct { + VersionTag string `json:"jsonrpc"` + ID any `json:"id,omitempty"` + Method json.RawMessage `json:"method"` + Params json.RawMessage `json:"params,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + Error *WireError `json:"error,omitempty"` +} + func DecodeMessage(data []byte) (Message, error) { - msg := wireCombined{} + msg := wireDecode{} if err := internaljson.Unmarshal(data, &msg); err != nil { return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err) } @@ -185,15 +195,19 @@ func DecodeMessage(data []byte) (Message, error) { if err != nil { return nil, err } - if msg.Method != "" { - // has a method, must be a call + if len(msg.Method) > 0 { + // The "method" key was present. Decode its value (including ""). + var method string + if err := internaljson.Unmarshal(msg.Method, &method); err != nil { + return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err) + } return &Request{ - Method: msg.Method, + Method: method, ID: id, Params: msg.Params, }, nil } - // no method, should be a response + // no method key, should be a response if !id.IsValid() { return nil, ErrInvalidRequest } @@ -219,18 +233,8 @@ func marshalToRaw(obj any) (json.RawMessage, error) { return json.RawMessage(data), nil } -// jsonescaping is a compatibility parameter that allows to restore -// JSON escaping in the JSON marshaling, which stopped being the default -// in the 1.4.0 version of the SDK. See the documentation for the -// mcpgodebug package for instructions how to enable it. -// The option will be removed in the 1.6.0 version of the SDK. -var jsonescaping = mcpgodebug.Value("jsonescaping") - // jsonMarshal marshals obj to JSON like json.Marshal but without HTML escaping. func jsonMarshal(obj any) ([]byte, error) { - if jsonescaping == "1" { - return json.Marshal(obj) - } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/net.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/net.go deleted file mode 100644 index 05db06261..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/net.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2018 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package jsonrpc2 - -import ( - "context" - "io" - "net" - "os" -) - -// This file contains implementations of the transport primitives that use the standard network -// package. - -// NetListenOptions is the optional arguments to the NetListen function. -type NetListenOptions struct { - NetListenConfig net.ListenConfig - NetDialer net.Dialer -} - -// NetListener returns a new Listener that listens on a socket using the net package. -func NetListener(ctx context.Context, network, address string, options NetListenOptions) (Listener, error) { - ln, err := options.NetListenConfig.Listen(ctx, network, address) - if err != nil { - return nil, err - } - return &netListener{net: ln}, nil -} - -// netListener is the implementation of Listener for connections made using the net package. -type netListener struct { - net net.Listener -} - -// Accept blocks waiting for an incoming connection to the listener. -func (l *netListener) Accept(context.Context) (io.ReadWriteCloser, error) { - return l.net.Accept() -} - -// Close will cause the listener to stop listening. It will not close any connections that have -// already been accepted. -func (l *netListener) Close() error { - addr := l.net.Addr() - err := l.net.Close() - if addr.Network() == "unix" { - rerr := os.Remove(addr.String()) - if rerr != nil && err == nil { - err = rerr - } - } - return err -} - -// Dialer returns a dialer that can be used to connect to the listener. -func (l *netListener) Dialer() Dialer { - return NetDialer(l.net.Addr().Network(), l.net.Addr().String(), net.Dialer{}) -} - -// NetDialer returns a Dialer using the supplied standard network dialer. -func NetDialer(network, address string, nd net.Dialer) Dialer { - return &netDialer{ - network: network, - address: address, - dialer: nd, - } -} - -type netDialer struct { - network string - address string - dialer net.Dialer -} - -func (n *netDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) { - return n.dialer.DialContext(ctx, n.network, n.address) -} - -// NetPipeListener returns a new Listener that listens using net.Pipe. -// It is only possibly to connect to it using the Dialer returned by the -// Dialer method, each call to that method will generate a new pipe the other -// side of which will be returned from the Accept call. -func NetPipeListener(ctx context.Context) (Listener, error) { - return &netPiper{ - done: make(chan struct{}), - dialed: make(chan io.ReadWriteCloser), - }, nil -} - -// netPiper is the implementation of Listener build on top of net.Pipes. -type netPiper struct { - done chan struct{} - dialed chan io.ReadWriteCloser -} - -// Accept blocks waiting for an incoming connection to the listener. -func (l *netPiper) Accept(context.Context) (io.ReadWriteCloser, error) { - // Block until the pipe is dialed or the listener is closed, - // preferring the latter if already closed at the start of Accept. - select { - case <-l.done: - return nil, net.ErrClosed - default: - } - select { - case rwc := <-l.dialed: - return rwc, nil - case <-l.done: - return nil, net.ErrClosed - } -} - -// Close will cause the listener to stop listening. It will not close any connections that have -// already been accepted. -func (l *netPiper) Close() error { - // unblock any accept calls that are pending - close(l.done) - return nil -} - -func (l *netPiper) Dialer() Dialer { - return l -} - -func (l *netPiper) Dial(ctx context.Context) (io.ReadWriteCloser, error) { - client, server := net.Pipe() - - select { - case l.dialed <- server: - return client, nil - - case <-l.done: - client.Close() - server.Close() - return nil, net.ErrClosed - } -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/serve.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/serve.go deleted file mode 100644 index 424163aaf..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/serve.go +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright 2020 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package jsonrpc2 - -import ( - "context" - "fmt" - "io" - "runtime" - "sync" - "sync/atomic" - "time" -) - -// Listener is implemented by protocols to accept new inbound connections. -type Listener interface { - // Accept accepts an inbound connection to a server. - // It blocks until either an inbound connection is made, or the listener is closed. - Accept(context.Context) (io.ReadWriteCloser, error) - - // Close closes the listener. - // Any blocked Accept or Dial operations will unblock and return errors. - Close() error - - // Dialer returns a dialer that can be used to connect to this listener - // locally. - // If a listener does not implement this it will return nil. - Dialer() Dialer -} - -// Dialer is used by clients to dial a server. -type Dialer interface { - // Dial returns a new communication byte stream to a listening server. - Dial(ctx context.Context) (io.ReadWriteCloser, error) -} - -// Server is a running server that is accepting incoming connections. -type Server struct { - listener Listener - binder Binder - async *async - - shutdownOnce sync.Once - closing int32 // atomic: set to nonzero when Shutdown is called -} - -// Dial uses the dialer to make a new connection, wraps the returned -// reader and writer using the framer to make a stream, and then builds -// a connection on top of that stream using the binder. -// -// The returned Connection will operate independently using the Preempter and/or -// Handler provided by the Binder, and will release its own resources when the -// connection is broken, but the caller may Close it earlier to stop accepting -// (or sending) new requests. -// -// If non-nil, the onDone function is called when the connection is closed. -func Dial(ctx context.Context, dialer Dialer, binder Binder, onDone func()) (*Connection, error) { - // dial a server - rwc, err := dialer.Dial(ctx) - if err != nil { - return nil, err - } - return bindConnection(ctx, rwc, binder, onDone), nil -} - -// NewServer starts a new server listening for incoming connections and returns -// it. -// This returns a fully running and connected server, it does not block on -// the listener. -// You can call Wait to block on the server, or Shutdown to get the sever to -// terminate gracefully. -// To notice incoming connections, use an intercepting Binder. -func NewServer(ctx context.Context, listener Listener, binder Binder) *Server { - server := &Server{ - listener: listener, - binder: binder, - async: newAsync(), - } - go server.run(ctx) - return server -} - -// Wait returns only when the server has shut down. -func (s *Server) Wait() error { - return s.async.wait() -} - -// Shutdown informs the server to stop accepting new connections. -func (s *Server) Shutdown() { - s.shutdownOnce.Do(func() { - atomic.StoreInt32(&s.closing, 1) - s.listener.Close() - }) -} - -// run accepts incoming connections from the listener, -// If IdleTimeout is non-zero, run exits after there are no clients for this -// duration, otherwise it exits only on error. -func (s *Server) run(ctx context.Context) { - defer s.async.done() - - var activeConns sync.WaitGroup - for { - rwc, err := s.listener.Accept(ctx) - if err != nil { - // Only Shutdown closes the listener. If we get an error after Shutdown is - // called, assume that was the cause and don't report the error; - // otherwise, report the error in case it is unexpected. - if atomic.LoadInt32(&s.closing) == 0 { - s.async.setError(err) - } - // We are done generating new connections for good. - break - } - - // A new inbound connection. - activeConns.Add(1) - _ = bindConnection(ctx, rwc, s.binder, activeConns.Done) // unregisters itself when done - } - activeConns.Wait() -} - -// NewIdleListener wraps a listener with an idle timeout. -// -// When there are no active connections for at least the timeout duration, -// calls to Accept will fail with ErrIdleTimeout. -// -// A connection is considered inactive as soon as its Close method is called. -func NewIdleListener(timeout time.Duration, wrap Listener) Listener { - l := &idleListener{ - wrapped: wrap, - timeout: timeout, - active: make(chan int, 1), - timedOut: make(chan struct{}), - idleTimer: make(chan *time.Timer, 1), - } - l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) - return l -} - -type idleListener struct { - wrapped Listener - timeout time.Duration - - // Only one of these channels is receivable at any given time. - active chan int // count of active connections; closed when Close is called if not timed out - timedOut chan struct{} // closed when the idle timer expires - idleTimer chan *time.Timer // holds the timer only when idle -} - -// Accept accepts an incoming connection. -// -// If an incoming connection is accepted concurrent to the listener being closed -// due to idleness, the new connection is immediately closed. -func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { - rwc, err := l.wrapped.Accept(ctx) - - select { - case n, ok := <-l.active: - if err != nil { - if ok { - l.active <- n - } - return nil, err - } - if ok { - l.active <- n + 1 - } else { - // l.wrapped.Close Close has been called, but Accept returned a - // connection. This race can occur with concurrent Accept and Close calls - // with any net.Listener, and it is benign: since the listener was closed - // explicitly, it can't have also timed out. - } - return l.newConn(rwc), nil - - case <-l.timedOut: - if err == nil { - // Keeping the connection open would leave the listener simultaneously - // active and closed due to idleness, which would be contradictory and - // confusing. Close the connection and pretend that it never happened. - rwc.Close() - } else { - // In theory the timeout could have raced with an unrelated error return - // from Accept. However, ErrIdleTimeout is arguably still valid (since we - // would have closed due to the timeout independent of the error), and the - // harm from returning a spurious ErrIdleTimeout is negligible anyway. - } - return nil, ErrIdleTimeout - - case timer := <-l.idleTimer: - if err != nil { - // The idle timer doesn't run until it receives itself from the idleTimer - // channel, so it can't have called l.wrapped.Close yet and thus err can't - // be ErrIdleTimeout. Leave the idle timer as it was and return whatever - // error we got. - l.idleTimer <- timer - return nil, err - } - - if !timer.Stop() { - // Failed to stop the timer — the timer goroutine is in the process of - // firing. Send the timer back to the timer goroutine so that it can - // safely close the timedOut channel, and then wait for the listener to - // actually be closed before we return ErrIdleTimeout. - l.idleTimer <- timer - rwc.Close() - <-l.timedOut - return nil, ErrIdleTimeout - } - - l.active <- 1 - return l.newConn(rwc), nil - } -} - -func (l *idleListener) Close() error { - select { - case _, ok := <-l.active: - if ok { - close(l.active) - } - - case <-l.timedOut: - // Already closed by the timer; take care not to double-close if the caller - // only explicitly invokes this Close method once, since the io.Closer - // interface explicitly leaves doubled Close calls undefined. - return ErrIdleTimeout - - case timer := <-l.idleTimer: - if !timer.Stop() { - // Couldn't stop the timer. It shouldn't take long to run, so just wait - // (so that the Listener is guaranteed to be closed before we return) - // and pretend that this call happened afterward. - // That way we won't leak any timers or goroutines when Close returns. - l.idleTimer <- timer - <-l.timedOut - return ErrIdleTimeout - } - close(l.active) - } - - return l.wrapped.Close() -} - -func (l *idleListener) Dialer() Dialer { - return l.wrapped.Dialer() -} - -func (l *idleListener) timerExpired() { - select { - case n, ok := <-l.active: - if ok { - panic(fmt.Sprintf("jsonrpc2: idleListener idle timer fired with %d connections still active", n)) - } else { - panic("jsonrpc2: Close finished with idle timer still running") - } - - case <-l.timedOut: - panic("jsonrpc2: idleListener idle timer fired more than once") - - case <-l.idleTimer: - // The timer for this very call! - } - - // Close the Listener with all channels still blocked to ensure that this call - // to l.wrapped.Close doesn't race with the one in l.Close. - defer close(l.timedOut) - l.wrapped.Close() -} - -func (l *idleListener) connClosed() { - select { - case n, ok := <-l.active: - if !ok { - // l is already closed, so it can't close due to idleness, - // and we don't need to track the number of active connections any more. - return - } - n-- - if n == 0 { - l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) - } else { - l.active <- n - } - - case <-l.timedOut: - panic("jsonrpc2: idleListener idle timer fired before last active connection was closed") - - case <-l.idleTimer: - panic("jsonrpc2: idleListener idle timer active before last active connection was closed") - } -} - -type idleListenerConn struct { - wrapped io.ReadWriteCloser - l *idleListener - closeOnce sync.Once -} - -func (l *idleListener) newConn(rwc io.ReadWriteCloser) *idleListenerConn { - c := &idleListenerConn{ - wrapped: rwc, - l: l, - } - - // A caller that forgets to call Close may disrupt the idleListener's - // accounting, even though the file descriptor for the underlying connection - // may eventually be garbage-collected anyway. - // - // Set a (best-effort) finalizer to verify that a Close call always occurs. - // (We will clear the finalizer explicitly in Close.) - runtime.SetFinalizer(c, func(c *idleListenerConn) { - panic("jsonrpc2: IdleListener connection became unreachable without a call to Close") - }) - - return c -} - -func (c *idleListenerConn) Read(p []byte) (int, error) { return c.wrapped.Read(p) } -func (c *idleListenerConn) Write(p []byte) (int, error) { return c.wrapped.Write(p) } - -func (c *idleListenerConn) Close() error { - defer c.closeOnce.Do(func() { - c.l.connClosed() - runtime.SetFinalizer(c, nil) - }) - return c.wrapped.Close() -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/wire.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/wire.go index c0a41bffb..4d123f2c0 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/wire.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2/wire.go @@ -28,9 +28,6 @@ var ( // The following errors are not part of the json specification, but // compliant extensions specific to this implementation. - // ErrServerOverloaded is returned when a message was refused due to a - // server being temporarily unable to accept any new messages. - ErrServerOverloaded = NewError(-32000, "overloaded") // ErrUnknown should be used for all non coded errors. ErrUnknown = NewError(-32001, "unknown error") // ErrServerClosing is returned for calls that arrive while the server is closing. diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/errors.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/errors.go new file mode 100644 index 000000000..ebeb6149a --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/errors.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package util + +import ( + "fmt" +) + +// Wrapf wraps *errp with the given formatted message if *errp is not nil. +func Wrapf(errp *error, format string, args ...any) { + if *errp != nil { + *errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp) + } +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/util.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/util.go deleted file mode 100644 index 4b5c325fa..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/util/util.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2025 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package util - -import ( - "cmp" - "fmt" - "iter" - "slices" -) - -// Helpers below are copied from gopls' moremaps package. - -// Sorted returns an iterator over the entries of m in key order. -func Sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] { - // TODO(adonovan): use maps.Sorted if proposal #68598 is accepted. - return func(yield func(K, V) bool) { - keys := KeySlice(m) - slices.Sort(keys) - for _, k := range keys { - if !yield(k, m[k]) { - break - } - } - } -} - -// KeySlice returns the keys of the map M, like slices.Collect(maps.Keys(m)). -func KeySlice[M ~map[K]V, K comparable, V any](m M) []K { - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -} - -// Wrapf wraps *errp with the given formatted message if *errp is not nil. -func Wrapf(errp *error, format string, args ...any) { - if *errp != nil { - *errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp) - } -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/cache.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/cache.go new file mode 100644 index 000000000..98373373f --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/cache.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package mcp + +import ( + "sync" + "time" +) + +// methodCache is a per-method TTL cache for list and read results, as +// described in SEP-2549. Each entry is keyed by cursor (for paginated list +// methods) or URI (for resources/read). +type methodCache[R CacheableResult] struct { + mu sync.Mutex + cachedValues map[string]*cacheEntry[R] +} + +type cacheEntry[R CacheableResult] struct { + result R + receivedAt time.Time +} + +func (e *cacheEntry[R]) isValid() bool { + return time.Since(e.receivedAt) < time.Duration(e.result.GetTTLMs())*time.Millisecond +} + +func (mc *methodCache[R]) get(key string) (R, bool) { + mc.mu.Lock() + defer mc.mu.Unlock() + entry, ok := mc.cachedValues[key] + if !ok { + var zero R + return zero, false + } + if entry.result.GetTTLMs() <= 0 || !entry.isValid() { + delete(mc.cachedValues, key) + var zero R + return zero, false + } + return entry.result, true +} + +func (mc *methodCache[R]) put(key string, result R) { + mc.mu.Lock() + defer mc.mu.Unlock() + if mc.cachedValues == nil { + mc.cachedValues = make(map[string]*cacheEntry[R]) + } + mc.cachedValues[key] = &cacheEntry[R]{ + result: result, + receivedAt: time.Now(), + } +} + +func (mc *methodCache[R]) invalidate() { + mc.mu.Lock() + defer mc.mu.Unlock() + clear(mc.cachedValues) +} + +func (mc *methodCache[R]) invalidateKey(key string) { + mc.mu.Lock() + defer mc.mu.Unlock() + delete(mc.cachedValues, key) +} + +// cursorParams is the constraint for list-method params that carry a pagination +// cursor and can be checked for nil. Both methods are already implemented by +// every concrete list-params type. +type cursorParams interface { + Params + cursorPtr() *string +} + +// cachedListResult returns a cached list result keyed by the request cursor +// (SEP-2549). It returns the zero value and false on miss or when params is nil. +func cachedListResult[P cursorParams, R CacheableResult](cache *methodCache[R], params P) (R, bool) { + key := "" + if !params.isNil() { + if cp := params.cursorPtr(); cp != nil { + key = *cp + } + } + return cache.get(key) +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go index 74900b1c7..6f1073add 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go @@ -10,6 +10,8 @@ import ( "fmt" "iter" "log/slog" + "maps" + "reflect" "slices" "strings" "sync" @@ -32,6 +34,11 @@ type Client struct { sessions []*ClientSession sendingMethodHandler_ MethodHandler receivingMethodHandler_ MethodHandler + // sendMethods is the list of methods this client may send to a + // server: it always contains the standard server methods (from + // serverMethodInfos) plus any custom methods registered via + // [AddSendingCustomMethod]. + sendMethods map[string]methodInfo } // NewClient creates a new [Client]. @@ -58,13 +65,21 @@ func NewClient(impl *Implementation, options *ClientOptions) *Client { opts.Logger = ensureLogger(nil) } - return &Client{ + sendMethods := make(map[string]methodInfo, len(serverMethodInfos)) + maps.Copy(sendMethods, serverMethodInfos) + + c := &Client{ impl: impl, opts: opts, roots: newFeatureSet(func(r *Root) string { return r.URI }), sendingMethodHandler_: defaultSendingMethodHandler, receivingMethodHandler_: defaultReceivingMethodHandler[*ClientSession], + sendMethods: sendMethods, + } + if opts.MultiRoundTrip == nil || !opts.MultiRoundTrip.Disabled { + c.AddSendingMiddleware(clientMultiRoundTripMiddleware()) } + return c } // ClientOptions configures the behavior of the client. @@ -78,6 +93,12 @@ type ClientOptions struct { // &SamplingCapabilities{}. If [ClientOptions.Capabilities] is set and has a // non nil value for [ClientCapabilities.Sampling], that value overrides the // inferred capability. + // + // Deprecated: the sampling feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). Migrate to calling LLM provider APIs + // directly from your server. See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. CreateMessageHandler func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) // CreateMessageWithToolsHandler handles incoming sampling/createMessage // requests that may involve tool use. It returns @@ -91,6 +112,12 @@ type ClientOptions struct { // // It is a panic to set both CreateMessageHandler and // CreateMessageWithToolsHandler. + // + // Deprecated: the sampling feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). Migrate to calling LLM provider APIs + // directly from your server. See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. CreateMessageWithToolsHandler func(context.Context, *CreateMessageWithToolsRequest) (*CreateMessageWithToolsResult, error) // ElicitationHandler handles incoming requests for elicitation/create. // @@ -148,18 +175,43 @@ type ClientOptions struct { // ElicitationCompleteHandler handles incoming notifications for notifications/elicitation/complete. ElicitationCompleteHandler func(context.Context, *ElicitationCompleteNotificationRequest) // Handlers for notifications from the server. - ToolListChangedHandler func(context.Context, *ToolListChangedRequest) - PromptListChangedHandler func(context.Context, *PromptListChangedRequest) - ResourceListChangedHandler func(context.Context, *ResourceListChangedRequest) - ResourceUpdatedHandler func(context.Context, *ResourceUpdatedNotificationRequest) + ToolListChangedHandler func(context.Context, *ToolListChangedRequest) + PromptListChangedHandler func(context.Context, *PromptListChangedRequest) + ResourceListChangedHandler func(context.Context, *ResourceListChangedRequest) + ResourceUpdatedHandler func(context.Context, *ResourceUpdatedNotificationRequest) + // LoggingMessageHandler handles incoming notifications/message + // notifications from the server. + // + // Deprecated: the logging feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. LoggingMessageHandler func(context.Context, *LoggingMessageRequest) ProgressNotificationHandler func(context.Context, *ProgressNotificationClientRequest) + // MultiRoundTrip configures the automatic MultiRoundTrip (Multi Round-Trip Requests) middleware. + // By default (nil), the middleware is enabled with default settings. + // Set Disabled to true to opt out of automatic MultiRoundTrip handling. + MultiRoundTrip *MultiRoundTripOptions // If non-zero, defines an interval for regular "ping" requests. // If the peer fails to respond to pings originating from the keepalive check, // the session is automatically closed. + // NOTE: The keepalive feature is only available for protocol versions < 2026-07-28 KeepAlive time.Duration + // KeepAliveFailureThreshold is the number of consecutive keepalive ping + // failures tolerated before the session is closed. A value of 0 or 1 + // closes the session on the first failure (the default). Higher values + // align with the spec's "multiple failed pings MAY trigger a connection + // reset" guidance, letting a transient miss pass without tearing down an + // otherwise live session. Has no effect unless KeepAlive is non-zero. + KeepAliveFailureThreshold int } +// toolContextKeyType is the context key type for passing tool definitions +// from CallTool to the transport layer. +type toolContextKeyType struct{} + +var toolContextKey = toolContextKeyType{} + // bind implements the binder[*ClientSession] interface, so that Clients can // be connected using [connect]. func (c *Client) bind(mcpConn Connection, conn *jsonrpc2.Connection, state *clientSessionState, onClose func()) *ClientSession { @@ -253,7 +305,7 @@ func (c *Client) capabilities(protocolVersion string) *ClientCapabilities { // server, calls or notifications will return an error wrapping // [ErrConnectionClosed]. func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOptions) (cs *ClientSession, err error) { - cs, err = connect(ctx, t, c, (*clientSessionState)(nil), nil) + cs, err = connect(ctx, t, c, (*clientSessionState)(nil), nil, c.opts.Logger) if err != nil { return nil, err } @@ -262,6 +314,68 @@ func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOp if opts != nil && opts.protocolVersion != "" { protocolVersion = opts.protocolVersion } + + if protocolVersion >= protocolVersion20260728 { + // Per SEP-2575, try the stateless server/discover RPC first. If the server + // signals it doesn't support it, fall back to the legacy initialize + // handshake. + discoverCtx := context.WithValue(ctx, protocolVersionContextKey{}, protocolVersion) + // We try to discover the server's capabilities. If the server rejects the + // requested version but specifies which versions it supports, we negotiate + // a mutually supported version and try again. + for range 2 { + discRes, err := c.discover(discoverCtx, cs) + if err == nil { + cs.state.InitializeResult = discRes + if hc, ok := cs.mcpConn.(clientConnection); ok { + hc.sessionUpdated(cs.state) + } + subscribeParams := &SubscriptionsListenParams{ + Notifications: &NotificationSubscriptions{}, + } + if c.opts.ToolListChangedHandler != nil { + subscribeParams.Notifications.ToolsListChanged = true + } + if c.opts.PromptListChangedHandler != nil { + subscribeParams.Notifications.PromptsListChanged = true + } + if c.opts.ResourceListChangedHandler != nil { + subscribeParams.Notifications.ResourcesListChanged = true + } + if subscribeParams.Notifications.ToolsListChanged || + subscribeParams.Notifications.PromptsListChanged || + subscribeParams.Notifications.ResourcesListChanged { + // ClientSession.Close cancels the listenCtx context to send notifications/cancelled. + listenCtx, cancelListen := context.WithCancel(context.Background()) + cs.listenCancel = cancelListen + if err := cs.subscriptionsListen(listenCtx, subscribeParams); err != nil { + cancelListen() + return nil, fmt.Errorf("opening subscriptions/listen: %w", err) + } + } + return cs, nil + } + + // Try to negotiate a mutually supported version if the server + // reports an UnsupportedProtocolVersionError with a supported version. + var werr *jsonrpc.Error + if errors.As(err, &werr) && werr.Code == CodeUnsupportedProtocolVersion && len(werr.Data) > 0 { + var data UnsupportedProtocolVersionData + if err := json.Unmarshal(werr.Data, &data); err == nil { + if negotiatedVersion := negotiateMutuallySupportedVersion(data.Supported); negotiatedVersion != "" && negotiatedVersion >= protocolVersion20260728 { + discoverCtx = context.WithValue(ctx, protocolVersionContextKey{}, negotiatedVersion) + continue + } + } + } + // Per the spec, fall back to the legacy initialize handshake on any + // non-modern error from server/discover. + break + } + // Use the latest legacy protocol version for the fallback initialize. + protocolVersion = protocolVersion20251125 + } + params := &InitializeParams{ ProtocolVersion: protocolVersion, ClientInfo: c.impl, @@ -293,6 +407,54 @@ func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOp return cs, nil } +// discover sends a SEP-2575 server/discover request to probe the server for +// stateless protocol support. +func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeResult, error) { + protocolVersion := protocolVersionFromContext(ctx) + caps := c.capabilities(protocolVersion) + params := &DiscoverParams{ + Meta: Meta{ + MetaKeyProtocolVersion: protocolVersion, + MetaKeyClientInfo: c.impl, + MetaKeyClientCapabilities: caps.toV2(), + }, + } + req := &DiscoverRequest{Session: cs, Params: params} + res, err := handleSend[*DiscoverResult](ctx, methodDiscover, req) + if err != nil { + return nil, err + } + + // Pick the highest protocol version that both the server and this SDK support. + // Since supportedProtocolVersions is defined in descending order (newest to oldest), + // the first match we find is the highest supported version. + var negotiated string + if slices.Contains(res.SupportedVersions, protocolVersion) { + negotiated = protocolVersion + } else { + negotiated = negotiateMutuallySupportedVersion(res.SupportedVersions) + } + if negotiated == "" || negotiated < protocolVersion20260728 { + // If there is no overlap, fall back to initialize so version + // negotiation can happen via the legacy path. + return nil, &jsonrpc2.WireError{ + Code: CodeUnsupportedProtocolVersion, + Message: "unsupported protocol version", + } + } + + var serverInfo *Implementation + if v, ok := decodeMetaValue[*Implementation](res.GetMeta(), MetaKeyServerInfo); ok { + serverInfo = v + } + return &InitializeResult{ + Capabilities: res.Capabilities, + Instructions: res.Instructions, + ProtocolVersion: negotiated, + ServerInfo: serverInfo, + }, nil +} + // A ClientSession is a logical connection with an MCP server. Its // methods can be used to send requests or notifications to the server. Create // a session by calling [Client.Connect]. @@ -309,15 +471,32 @@ type ClientSession struct { conn *jsonrpc2.Connection client *Client keepaliveCancel context.CancelFunc + listenCancel context.CancelFunc mcpConn Connection // No mutex is (currently) required to guard the session state, because it is // only set synchronously during Client.Connect. state clientSessionState + // Per-method TTL caches for list results (SEP-2549). + toolsCache methodCache[*ListToolsResult] + promptsCache methodCache[*ListPromptsResult] + resourcesCache methodCache[*ListResourcesResult] + resourceTemplatesCache methodCache[*ListResourceTemplatesResult] + readResourceCache methodCache[*ReadResourceResult] + // Pending URL elicitations waiting for completion notifications. pendingElicitationsMu sync.Mutex pendingElicitations map[string]chan struct{} + + // resourceSubsMu guards resourceSubs. + resourceSubsMu sync.Mutex + // resourceSubs maps a subscribed resource URI to the cancel func of the + // goroutine running its dedicated subscriptions/listen stream. Populated + // only under SEP-2575; the legacy protocol routes Subscribe and + // Unsubscribe straight to the resources/subscribe and resources/unsubscribe + // RPCs and leaves this map untouched. + resourceSubs map[string]context.CancelFunc } type clientSessionState struct { @@ -326,6 +505,45 @@ type clientSessionState struct { func (cs *ClientSession) InitializeResult() *InitializeResult { return cs.state.InitializeResult } +// usesNewProtocol reports whether this session has negotiated a protocol +// version >= 2026-07-28, which requires the SEP-2575 per-request `_meta` +// triple on every outgoing request. +func (cs *ClientSession) usesNewProtocol() bool { + res := cs.state.InitializeResult + return res != nil && res.ProtocolVersion >= protocolVersion20260728 +} + +// injectRequestMeta populates the SEP-2575 per-request `_meta` fields +// (protocolVersion, optional clientInfo, clientCapabilities) on the given +// outgoing request params. Keys already present in params.Meta are not +// overwritten. Per PR modelcontextprotocol/modelcontextprotocol#3002 +// clientInfo is SHOULD (not MUST), and is omitted when the client has no +// [Implementation] configured. +func injectRequestMeta[T any, P interface { + *T + Params +}](cs *ClientSession, params P) P { + res := cs.state.InitializeResult + if params == nil { + params = new(T) + } + m := params.GetMeta() + if m == nil { + m = map[string]any{} + } + if _, ok := m[MetaKeyProtocolVersion]; !ok { + m[MetaKeyProtocolVersion] = res.ProtocolVersion + } + if _, ok := m[MetaKeyClientInfo]; !ok && cs.client.impl != nil { + m[MetaKeyClientInfo] = cs.client.impl + } + if _, ok := m[MetaKeyClientCapabilities]; !ok { + m[MetaKeyClientCapabilities] = cs.client.capabilities(res.ProtocolVersion).toV2() + } + params.SetMeta(m) + return params +} + func (cs *ClientSession) ID() string { if c, ok := cs.mcpConn.(hasSessionID); ok { return c.SessionID() @@ -340,13 +558,18 @@ func (cs *ClientSession) ID() string { // Close is idempotent and concurrency safe. func (cs *ClientSession) Close() error { // Note: keepaliveCancel access is safe without a mutex because: - // 1. keepaliveCancel is only written once during startKeepalive (happens-before all Close calls) + // 1. keepaliveCancel is only written once during Client.Connect (through startKeepalive), + // which happens before any code that may call Close from another goroutine // 2. context.CancelFunc is safe to call multiple times and from multiple goroutines // 3. The keepalive goroutine calls Close on ping failure, but this is safe since // Close is idempotent and conn.Close() handles concurrent calls correctly if cs.keepaliveCancel != nil { cs.keepaliveCancel() } + if cs.listenCancel != nil { + cs.listenCancel() + } + cs.cancelAllResourceSubscriptions() err := cs.conn.Close() if cs.onClose != nil && cs.calledOnClose.CompareAndSwap(false, true) { @@ -362,6 +585,24 @@ func (cs *ClientSession) Wait() error { return cs.conn.Wait() } +// lookupTool returns the most recently seen definition of the tool with the +// given name across all cached ListTools results, or nil if no such tool has +// been seen. It is used by CallTool to inject the tool definition into the +// outgoing request context for transport-layer features (e.g. x-mcp-header +// param annotations). +func (cs *ClientSession) lookupTool(name string) *Tool { + cs.toolsCache.mu.Lock() + defer cs.toolsCache.mu.Unlock() + for _, entry := range cs.toolsCache.cachedValues { + for _, t := range entry.result.Tools { + if t.Name == name { + return t + } + } + } + return nil +} + // registerElicitationWaiter registers a waiter for an elicitation complete // notification with the given elicitation ID. It returns two functions: an await // function that waits for the notification or context cancellation, and a cleanup @@ -404,12 +645,18 @@ func (cs *ClientSession) registerElicitationWaiter(elicitationID string) (await // startKeepalive starts the keepalive mechanism for this client session. func (cs *ClientSession) startKeepalive(interval time.Duration) { - startKeepalive(cs, interval, &cs.keepaliveCancel) + startKeepalive(cs, interval, cs.client.opts.KeepAliveFailureThreshold, &cs.keepaliveCancel, cs.client.opts.Logger) } // AddRoots adds the given roots to the client, // replacing any with the same URIs, // and notifies any connected servers. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to passing paths via tool parameters, +// resource URIs, or configuration. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (c *Client) AddRoots(roots ...*Root) { // Only notify if something could change. if len(roots) == 0 { @@ -422,6 +669,12 @@ func (c *Client) AddRoots(roots ...*Root) { // RemoveRoots removes the roots with the given URIs, // and notifies any connected servers if the list has changed. // It is not an error to remove a nonexistent root. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to passing paths via tool parameters, +// resource URIs, or configuration. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (c *Client) RemoveRoots(uris ...string) { changeAndNotify(c, notificationRootsListChanged, &RootsListChangedParams{}, func() bool { return c.roots.remove(uris...) }) @@ -641,7 +894,7 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult, } err = resolved.ApplyDefaults(&res.Content) if err != nil { - return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err)} + return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to apply schema defaults to elicitation result: %v", err)} } } return res, nil @@ -913,10 +1166,13 @@ var clientMethodInfos = map[string]methodInfo{ notificationLoggingMessage: newClientMethodInfo(clientMethod((*Client).callLoggingHandler), notification), notificationProgress: newClientMethodInfo(clientSessionMethod((*ClientSession).callProgressNotificationHandler), notification), notificationElicitationComplete: newClientMethodInfo(clientMethod((*Client).callElicitationCompleteHandler), notification|missingParamsOK), + notificationSubscriptionsAck: newClientMethodInfo(clientMethod((*Client).callSubscriptionsAckHandler), notification|missingParamsOK), } func (cs *ClientSession) sendingMethodInfos() map[string]methodInfo { - return serverMethodInfos + cs.client.mu.Lock() + defer cs.client.mu.Unlock() + return cs.client.sendMethods } func (cs *ClientSession) receivingMethodInfos() map[string]methodInfo { @@ -969,18 +1225,51 @@ func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { } // ListPrompts lists prompts that are currently available on the server. +// +// Results may be served from a client-side TTL cache populated by previous +// calls; see SEP-2549. func (cs *ClientSession) ListPrompts(ctx context.Context, params *ListPromptsParams) (*ListPromptsResult, error) { - return handleSend[*ListPromptsResult](ctx, methodListPrompts, newClientRequest(cs, orZero[Params](params))) + if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.promptsCache, params); ok { + return result, nil + } + params = injectRequestMeta(cs, params) + } + result, err := handleSend[*ListPromptsResult](ctx, methodListPrompts, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.promptsCache.put(params.Cursor, result) + } + return result, nil } // GetPrompt gets a prompt from the server. func (cs *ClientSession) GetPrompt(ctx context.Context, params *GetPromptParams) (*GetPromptResult, error) { + if cs.usesNewProtocol() { + params = injectRequestMeta(cs, params) + } return handleSend[*GetPromptResult](ctx, methodGetPrompt, newClientRequest(cs, orZero[Params](params))) } // ListTools lists tools that are currently available on the server. func (cs *ClientSession) ListTools(ctx context.Context, params *ListToolsParams) (*ListToolsResult, error) { - return handleSend[*ListToolsResult](ctx, methodListTools, newClientRequest(cs, orZero[Params](params))) + if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.toolsCache, params); ok { + return result, nil + } + params = injectRequestMeta(cs, params) + } + result, err := handleSend[*ListToolsResult](ctx, methodListTools, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + result.Tools = filterValidTools(cs.client.opts.Logger, result.Tools) + if cs.usesNewProtocol() { + cs.toolsCache.put(params.Cursor, result) + } + return result, nil } // CallTool calls the tool with the given parameters. @@ -994,9 +1283,23 @@ func (cs *ClientSession) CallTool(ctx context.Context, params *CallToolParams) ( // Avoid sending nil over the wire. params.Arguments = map[string]any{} } + if tool := cs.lookupTool(params.Name); tool != nil { + ctx = context.WithValue(ctx, toolContextKey, tool) + } + if cs.usesNewProtocol() { + params = injectRequestMeta(cs, params) + } return handleSend[*CallToolResult](ctx, methodCallTool, newClientRequest(cs, orZero[Params](params))) } +// SetLoggingLevel sets the minimum severity level for log messages sent by +// the server. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to consuming stderr output (for STDIO +// servers) or OpenTelemetry. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (cs *ClientSession) SetLoggingLevel(ctx context.Context, params *SetLoggingLevelParams) error { _, err := handleSend[*emptyResult](ctx, methodSetLevel, newClientRequest(cs, orZero[Params](params))) return err @@ -1004,38 +1307,161 @@ func (cs *ClientSession) SetLoggingLevel(ctx context.Context, params *SetLogging // ListResources lists the resources that are currently available on the server. func (cs *ClientSession) ListResources(ctx context.Context, params *ListResourcesParams) (*ListResourcesResult, error) { - return handleSend[*ListResourcesResult](ctx, methodListResources, newClientRequest(cs, orZero[Params](params))) + if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.resourcesCache, params); ok { + return result, nil + } + params = injectRequestMeta(cs, params) + } + result, err := handleSend[*ListResourcesResult](ctx, methodListResources, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.resourcesCache.put(params.Cursor, result) + } + return result, nil } // ListResourceTemplates lists the resource templates that are currently available on the server. func (cs *ClientSession) ListResourceTemplates(ctx context.Context, params *ListResourceTemplatesParams) (*ListResourceTemplatesResult, error) { - return handleSend[*ListResourceTemplatesResult](ctx, methodListResourceTemplates, newClientRequest(cs, orZero[Params](params))) + if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.resourceTemplatesCache, params); ok { + return result, nil + } + params = injectRequestMeta(cs, params) + } + result, err := handleSend[*ListResourceTemplatesResult](ctx, methodListResourceTemplates, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.resourceTemplatesCache.put(params.Cursor, result) + } + return result, nil } // ReadResource asks the server to read a resource and return its contents. func (cs *ClientSession) ReadResource(ctx context.Context, params *ReadResourceParams) (*ReadResourceResult, error) { - return handleSend[*ReadResourceResult](ctx, methodReadResource, newClientRequest(cs, orZero[Params](params))) + if cs.usesNewProtocol() { + var uri string + if params != nil { + uri = params.URI + } + if result, ok := cs.readResourceCache.get(uri); ok { + return result, nil + } + params = injectRequestMeta(cs, params) + } + result, err := handleSend[*ReadResourceResult](ctx, methodReadResource, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.readResourceCache.put(params.URI, result) + } + return result, nil } func (cs *ClientSession) Complete(ctx context.Context, params *CompleteParams) (*CompleteResult, error) { + if cs.usesNewProtocol() { + params = injectRequestMeta(cs, params) + } return handleSend[*CompleteResult](ctx, methodComplete, newClientRequest(cs, orZero[Params](params))) } // Subscribe sends a "resources/subscribe" request to the server, asking for // notifications when the specified resource changes. func (cs *ClientSession) Subscribe(ctx context.Context, params *SubscribeParams) error { - _, err := handleSend[*emptyResult](ctx, methodSubscribe, newClientRequest(cs, orZero[Params](params))) - return err + if !cs.usesNewProtocol() { + _, err := handleSend[*emptyResult](ctx, methodSubscribe, newClientRequest(cs, orZero[Params](params))) + return err + } + if params == nil || params.URI == "" { + return fmt.Errorf("Subscribe: missing URI") + } + uri := params.URI + + var listenCtx context.Context + cs.resourceSubsMu.Lock() + if _, exists := cs.resourceSubs[uri]; !exists { + var cancel context.CancelFunc + listenCtx, cancel = context.WithCancel(context.Background()) + if cs.resourceSubs == nil { + cs.resourceSubs = make(map[string]context.CancelFunc) + } + cs.resourceSubs[uri] = cancel + } + cs.resourceSubsMu.Unlock() + if listenCtx == nil { + // Already subscribed to this URI + return nil + } + + return cs.subscriptionsListen(listenCtx, &SubscriptionsListenParams{ + Notifications: &NotificationSubscriptions{ + ResourceSubscriptions: []string{uri}, + }, + }) } -// Unsubscribe sends a "resources/unsubscribe" request to the server, cancelling -// a previous subscription. +// Unsubscribe cancels a previous [ClientSession.Subscribe] for params.URI. +// +// Under the legacy protocol it sends a "resources/unsubscribe" request. +// +// Under SEP-2575 it cancels the background "subscriptions/listen" stream +// opened by Subscribe for the URI. Unsubscribe is idempotent: calling it for +// a URI that is not currently subscribed is a no-op. func (cs *ClientSession) Unsubscribe(ctx context.Context, params *UnsubscribeParams) error { - _, err := handleSend[*emptyResult](ctx, methodUnsubscribe, newClientRequest(cs, orZero[Params](params))) + if !cs.usesNewProtocol() { + _, err := handleSend[*emptyResult](ctx, methodUnsubscribe, newClientRequest(cs, orZero[Params](params))) + return err + } + if params == nil || params.URI == "" { + return fmt.Errorf("Unsubscribe: missing URI") + } + cs.resourceSubsMu.Lock() + cancel, ok := cs.resourceSubs[params.URI] + delete(cs.resourceSubs, params.URI) + cs.resourceSubsMu.Unlock() + if ok { + cancel() + } + return nil +} + +// cancelAllResourceSubscriptions cancels every active SEP-2575 resource +// subscription opened via Subscribe. The listen goroutines exit +// asynchronously as their contexts unwind. Called from Close. +func (cs *ClientSession) cancelAllResourceSubscriptions() { + cs.resourceSubsMu.Lock() + subs := cs.resourceSubs + cs.resourceSubs = nil + cs.resourceSubsMu.Unlock() + for _, cancel := range subs { + cancel() + } +} + +// SubscriptionsListen opens a SEP-2575 "subscriptions/listen" stream. +// +// The server's first message on the stream is "notifications/subscriptions/acknowledged"; +// subsequent opted-in notifications (e.g. tools/list_changed) are delivered through the +// usual handlers registered in [ClientOptions]. +func (cs *ClientSession) subscriptionsListen(ctx context.Context, params *SubscriptionsListenParams) error { + params = injectRequestMeta(cs, params) + _, err := handleSend[*SubscriptionsListenResult](ctx, methodSubscriptionsListen, newClientRequest(cs, orZero[Params](params))) return err } +func (c *Client) callSubscriptionsAckHandler(context.Context, *ClientRequest[*SubscriptionsAcknowledgedParams]) (Result, error) { + return nil, nil +} + func (c *Client) callToolChangedHandler(ctx context.Context, req *ToolListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.toolsCache.invalidate() + } if h := c.opts.ToolListChangedHandler; h != nil { h(ctx, req) } @@ -1043,6 +1469,9 @@ func (c *Client) callToolChangedHandler(ctx context.Context, req *ToolListChange } func (c *Client) callPromptChangedHandler(ctx context.Context, req *PromptListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.promptsCache.invalidate() + } if h := c.opts.PromptListChangedHandler; h != nil { h(ctx, req) } @@ -1050,6 +1479,10 @@ func (c *Client) callPromptChangedHandler(ctx context.Context, req *PromptListCh } func (c *Client) callResourceChangedHandler(ctx context.Context, req *ResourceListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.resourcesCache.invalidate() + cs.resourceTemplatesCache.invalidate() + } if h := c.opts.ResourceListChangedHandler; h != nil { h(ctx, req) } @@ -1057,6 +1490,9 @@ func (c *Client) callResourceChangedHandler(ctx context.Context, req *ResourceLi } func (c *Client) callResourceUpdatedHandler(ctx context.Context, req *ResourceUpdatedNotificationRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok && req.Params != nil { + cs.readResourceCache.invalidateKey(req.Params.URI) + } if h := c.opts.ResourceUpdatedHandler; h != nil { h(ctx, req) } @@ -1180,3 +1616,67 @@ func paginate[P listParams, R listResult[T], T any](ctx context.Context, params } } } + +// AddSendingCustomMethod registers a custom JSON-RPC method +// that the client may send to the server. +// +// Registration is decoupled from invocation: extensions typically call this +// during setup, while the actual call site uses [CallCustomMethod]. +// +// if err := mcp.AddSendingCustomMethod[*SearchParams, *SearchResult](c, "acme/search"); err != nil { +// return err +// } +// // ... later, anywhere a *ClientSession is available: +// result, err := mcp.CallCustomMethod[*SearchParams, *SearchResult]( +// ctx, cs, "acme/search", &SearchParams{Query: "hello"}) +// +// AddSendingCustomMethod returns an error if method is the name of a standard +// MCP method. Registering the same method twice replaces the previous +// registration. +func AddSendingCustomMethod[P paramsPtr[PT], R Result, PT any]( + c *Client, + method string, +) error { + if _, ok := serverMethodInfos[method]; ok { + return fmt.Errorf("mcp: AddSendingCustomMethod: %q shadows a standard MCP method", method) + } + + mi := methodInfo{ + newResult: func() Result { + return reflect.New(reflect.TypeFor[R]().Elem()).Interface().(R) + }, + } + + c.mu.Lock() + defer c.mu.Unlock() + c.sendMethods[method] = mi + return nil +} + +// CallCustomMethod sends a custom (non-standard) JSON-RPC method to the +// server and decodes the response into R. +// +// The method must have been registered on the session's client via +// [AddSendingCustomMethod]. +func CallCustomMethod[P paramsPtr[PT], R Result, PT any]( + ctx context.Context, + cs *ClientSession, + method string, + params P, +) (R, error) { + c := cs.client + c.mu.Lock() + _, ok := c.sendMethods[method] + c.mu.Unlock() + if !ok { + var zero R + return zero, fmt.Errorf("mcp: CallCustomMethod: %q is not registered; call AddSendingCustomMethod first", method) + } + if cs.usesNewProtocol() { + params = injectRequestMeta(cs, params) + } + return handleSend[R](ctx, method, &ClientRequest[P]{ + Session: cs, + Params: params, + }) +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/content.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/content.go index 95ea40d80..7af90fe7a 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/content.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/content.go @@ -189,6 +189,11 @@ func (c *EmbeddedResource) fromWire(wire *wireContent) { // ToolUseContent represents a request from the assistant to invoke a tool. // This content type is only valid in sampling messages. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ToolUseContent struct { // ID is a unique identifier for this tool use, used to match with ToolResultContent. ID string @@ -229,12 +234,19 @@ func (c *ToolUseContent) fromWire(wire *wireContent) { // ToolResultContent represents the result of a tool invocation. // This content type is only valid in sampling messages with role "user". +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ToolResultContent struct { // ToolUseID references the ID from the corresponding ToolUseContent. ToolUseID string // Content holds the unstructured result of the tool call. Content []Content - // StructuredContent holds an optional structured result as a JSON object. + // StructuredContent holds an optional structured result. Per SEP-2106, it + // may be any valid JSON value (object, array, or primitive) conforming to + // the tool's output schema. StructuredContent any // IsError indicates whether the tool call ended in an error. IsError bool diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go index 62dd2ad2b..bbb3e6c88 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go @@ -41,7 +41,7 @@ func (e Event) Empty() bool { } // writeEvent writes the event to w, and flushes. -func writeEvent(w io.Writer, evt Event) (int, error) { +func writeEvent(w http.ResponseWriter, evt Event) (int, error) { var b bytes.Buffer if evt.Name != "" { fmt.Fprintf(&b, "event: %s\n", evt.Name) @@ -52,11 +52,16 @@ func writeEvent(w io.Writer, evt Event) (int, error) { if evt.Retry != "" { fmt.Fprintf(&b, "retry: %s\n", evt.Retry) } - fmt.Fprintf(&b, "data: %s\n\n", string(evt.Data)) + // Write the payload directly into a pre-grown buffer to avoid the extra + // copy from string(evt.Data) and repeated buffer regrowths. + b.Grow(len("data: \n\n") + len(evt.Data)) + b.WriteString("data: ") + b.Write(evt.Data) + b.WriteString("\n\n") n, err := w.Write(b.Bytes()) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } + rc := http.NewResponseController(w) + // Ignore returned error as flushing is best-effort. + _ = rc.Flush() return n, err } @@ -376,10 +381,11 @@ func (s *MemoryEventStore) purge() { for _, dl := range sm { if dl.size > 0 { r := dl.removeFirst() - if r > 0 { - changed = true - s.nBytes -= r - } + // Even if we remove an empty chunk, that's + // still progress. There may be non-empty + // chunks after it. + changed = true + s.nBytes -= r } } } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/logging.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/logging.go index b1bd82b15..402e24320 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/logging.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/logging.go @@ -13,6 +13,8 @@ import ( "slices" "sync" "time" + + "golang.org/x/time/rate" ) // Logging levels. @@ -67,6 +69,11 @@ func compareLevels(l1, l2 LoggingLevel) int { } // LoggingHandlerOptions are options for a LoggingHandler. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type LoggingHandlerOptions struct { // The value for the "logger" field of logging notifications. LoggerName string @@ -77,16 +84,21 @@ type LoggingHandlerOptions struct { } // A LoggingHandler is a [slog.Handler] for MCP. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type LoggingHandler struct { opts LoggingHandlerOptions ss *ServerSession // Ensures that the buffer reset is atomic with the write (see Handle). // A pointer so that clones share the mutex. See // https://github.com/golang/example/blob/master/slog-handler-guide/README.md#getting-the-mutex-right. - mu *sync.Mutex - lastMessageSent time.Time // for rate-limiting - buf *bytes.Buffer - handler slog.Handler + mu *sync.Mutex + limiter *rate.Limiter // for rate-limiting + buf *bytes.Buffer + handler slog.Handler } // ensureLogger returns l if non-nil, otherwise a discard logger. @@ -99,6 +111,11 @@ func ensureLogger(l *slog.Logger) *slog.Logger { // NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a // [slog.JSONHandler]. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler { var buf bytes.Buffer jsonHandler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{ @@ -118,6 +135,9 @@ func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingH } if opts != nil { lh.opts = *opts + if opts.MinInterval > 0 { + lh.limiter = rate.NewLimiter(rate.Every(opts.MinInterval), 1) + } } return lh } @@ -157,11 +177,7 @@ func (h *LoggingHandler) Handle(ctx context.Context, r slog.Record) error { func (h *LoggingHandler) handle(ctx context.Context, r slog.Record) error { // Observe the rate limit. - // TODO(jba): use golang.org/x/time/rate. - h.mu.Lock() - skip := time.Since(h.lastMessageSent) < h.opts.MinInterval - h.mu.Unlock() - if skip { + if h.limiter != nil && !h.limiter.Allow() { return nil } @@ -184,10 +200,6 @@ func (h *LoggingHandler) handle(ctx context.Context, r slog.Record) error { return err } - h.mu.Lock() - h.lastMessageSent = time.Now() - h.mu.Unlock() - params := &LoggingMessageParams{ Logger: h.opts.LoggerName, Level: slogLevelToMCP(r.Level), diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/mrtr.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/mrtr.go new file mode 100644 index 000000000..5f65f5691 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/mrtr.go @@ -0,0 +1,268 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package mcp + +import ( + "context" + "fmt" + "log/slog" + "sync" + + "github.com/modelcontextprotocol/go-sdk/jsonrpc" + "golang.org/x/sync/errgroup" +) + +const maxMultiRoundTripRetries = 10 +const maxLoadSheddingMultiRoundTripRetries = 3 + +// MultiRoundTripOptions configures the client-side multi round-trip request (SEP-2322) +// middleware. The middleware is enabled by default and automatically fulfills input +// requests from the server by invoking the appropriate client handlers and +// retrying the original call. +type MultiRoundTripOptions struct { + // Disabled prevents the automatic multi-round-tirp middleware from being installed. + // When true, the client returns input-required results directly and callers must + // handle the retry loop themselves using [CallToolResult.NeedsInput], + // [GetPromptResult.NeedsInput], or [ReadResourceResult.NeedsInput]. + Disabled bool +} + +type multiRoundTripResponse interface { + setResultType(resultType) + inputRequests() map[string]InputRequest + requestState() string + hasContent() bool +} + +func handleMultiRoundTripResult(ss *ServerSession, logger *slog.Logger, res multiRoundTripResponse) error { + if res == nil { + return nil + } + hasInputRequests := res.inputRequests() != nil + + if hasInputRequests && res.hasContent() { + logger.Warn("handler returned both content and inputRequests") + return &jsonrpc.Error{ + Code: jsonrpc.CodeInternalError, + Message: "server bug: result has both content and inputRequests", + } + } + + if clientSupportsMultiRoundTrip(ss) { + // For older clients the resultType is left unset. Input requests will be handled + // by serverMultiRoundTripMiddleware client calls and handler reinvocation. + if hasInputRequests { + res.setResultType(resultTypeInputRequired) + } else { + res.setResultType(resultTypeComplete) + } + } + return nil +} + +func clientSupportsMultiRoundTrip(ss *ServerSession) bool { + protocolVersion := latestProtocolVersion + if iparams := ss.InitializeParams(); iparams != nil { + protocolVersion = iparams.ProtocolVersion + } + return protocolVersion >= protocolVersion20260728 +} + +func clientMultiRoundTripMiddleware() Middleware { + return func(next MethodHandler) MethodHandler { + return func(ctx context.Context, method string, req Request) (Result, error) { + if method != methodCallTool && method != methodGetPrompt && method != methodReadResource { + return next(ctx, method, req) + } + + loadSheddingFailures := 0 + for retries := 1; ; retries++ { + res, err := next(ctx, method, req) + if err != nil { + return res, err + } + mrtrResult, ok := res.(multiRoundTripResponse) + if !ok { + return res, nil + } + reqMap := mrtrResult.inputRequests() + if reqMap == nil { + return res, nil + } + if len(reqMap) == 0 { + loadSheddingFailures++ + } + if loadSheddingFailures >= maxLoadSheddingMultiRoundTripRetries { + return nil, fmt.Errorf("multi-round-trip: exceeded maximum load-shedding retries (%d)", maxLoadSheddingMultiRoundTripRetries) + } + if retries >= maxMultiRoundTripRetries { + return nil, fmt.Errorf("multi-round-trip: exceeded maximum retries (%d)", maxMultiRoundTripRetries) + } + cs, ok := req.GetSession().(*ClientSession) + if !ok { + return res, nil + } + responses, err := fulfillInputRequests(ctx, cs, reqMap) + if err != nil { + return nil, err + } + setMultiRoundTripRetryParams(req, responses, mrtrResult.requestState()) + } + } + } +} + +// serverMultiRoundTripMiddleware is a receiving middleware for servers that transparently +// handles multi-round-trip for clients on older protocol versions. When a handler returns +// InputRequests and the client does not support multi-round-trip, the middleware fulfills +// the requests by calling the client directly and reinvokes the handler once with the responses. +func serverMultiRoundTripMiddleware() Middleware { + return func(next MethodHandler) MethodHandler { + return func(ctx context.Context, method string, req Request) (Result, error) { + if method != methodCallTool && method != methodGetPrompt && method != methodReadResource { + return next(ctx, method, req) + } + + ss, ok := req.GetSession().(*ServerSession) + if !ok { + return next(ctx, method, req) + } + if clientSupportsMultiRoundTrip(ss) { + return next(ctx, method, req) + } + + res, err := next(ctx, method, req) + if err != nil { + return res, err + } + mrtrResult, ok := res.(multiRoundTripResponse) + if !ok { + return res, nil + } + reqMap := mrtrResult.inputRequests() + if reqMap == nil { + return res, nil + } + if len(reqMap) == 0 { + return nil, fmt.Errorf("the server is busy, retry later") + } + responses, err := fulfillServerInputRequests(ctx, ss, reqMap) + if err != nil { + return nil, err + } + setMultiRoundTripRetryParams(req, responses, mrtrResult.requestState()) + return next(ctx, method, req) + } + } +} + +func fulfillServerInputRequests(ctx context.Context, ss *ServerSession, requests InputRequestMap) (InputResponseMap, error) { + g, ctx := errgroup.WithContext(ctx) + var mu sync.Mutex + responses := make(InputResponseMap, len(requests)) + for id, ir := range requests { + g.Go(func() error { + resp, err := fulfillServerInputRequest(ctx, ss, ir) + if err != nil { + return fmt.Errorf("fulfilling input request %q: %w", id, err) + } + mu.Lock() + responses[id] = resp + mu.Unlock() + return nil + }) + } + if err := g.Wait(); err != nil { + return nil, fmt.Errorf("multi-round-trip: %w", err) + } + return responses, nil +} + +func fulfillServerInputRequest(ctx context.Context, ss *ServerSession, ir InputRequest) (InputResponse, error) { + switch p := ir.(type) { + case *ElicitParams: + return ss.Elicit(ctx, p) + case *CreateMessageParams: + return ss.CreateMessageWithTools(ctx, createMessageParamsToWithTools(p)) + case *CreateMessageWithToolsParams: + return ss.CreateMessageWithTools(ctx, p) + case *ListRootsParams: + return ss.ListRoots(ctx, p) + default: + return nil, fmt.Errorf("unknown input request type: %T", ir) + } +} + +func createMessageParamsToWithTools(p *CreateMessageParams) *CreateMessageWithToolsParams { + var msgs []*SamplingMessageV2 + for _, m := range p.Messages { + msgs = append(msgs, &SamplingMessageV2{Content: []Content{m.Content}, Role: m.Role}) + } + return &CreateMessageWithToolsParams{ + Meta: p.Meta, + IncludeContext: p.IncludeContext, + MaxTokens: p.MaxTokens, + Messages: msgs, + Metadata: p.Metadata, + ModelPreferences: p.ModelPreferences, + StopSequences: p.StopSequences, + SystemPrompt: p.SystemPrompt, + Temperature: p.Temperature, + } +} + +func setMultiRoundTripRetryParams(req Request, responses InputResponseMap, state string) { + switch p := req.GetParams().(type) { + case *CallToolParams: + p.InputResponses = responses + p.RequestState = state + case *CallToolParamsRaw: + p.InputResponses = responses + p.RequestState = state + case *GetPromptParams: + p.InputResponses = responses + p.RequestState = state + case *ReadResourceParams: + p.InputResponses = responses + p.RequestState = state + } +} + +func fulfillInputRequests(ctx context.Context, cs *ClientSession, requests InputRequestMap) (InputResponseMap, error) { + g, ctx := errgroup.WithContext(ctx) + var mu sync.Mutex + responses := make(InputResponseMap, len(requests)) + for id, ir := range requests { + g.Go(func() error { + resp, err := fulfillInputRequest(ctx, cs, ir) + if err != nil { + return fmt.Errorf("fulfilling input request %q: %w", id, err) + } + mu.Lock() + responses[id] = resp + mu.Unlock() + return nil + }) + } + if err := g.Wait(); err != nil { + return nil, fmt.Errorf("multi round-trip: %w", err) + } + return responses, nil +} + +func fulfillInputRequest(ctx context.Context, cs *ClientSession, ir InputRequest) (InputResponse, error) { + switch p := ir.(type) { + case *ElicitParams: + return cs.client.elicit(ctx, newClientRequest(cs, p)) + case *CreateMessageParams: + return cs.client.createMessage(ctx, &CreateMessageWithToolsRequest{Session: cs, Params: createMessageParamsToWithTools(p)}) + case *CreateMessageWithToolsParams: + return cs.client.createMessage(ctx, &CreateMessageWithToolsRequest{Session: cs, Params: p}) + case *ListRootsParams: + return cs.client.listRoots(ctx, newClientRequest(cs, p)) + default: + return nil, fmt.Errorf("unknown input request type: %T", ir) + } +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go index 837ce7843..47cb9cbd4 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go @@ -4,20 +4,194 @@ package mcp -// Protocol types for version 2025-06-18. -// To see the schema changes from the previous version, run: -// -// prefix=https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema -// sdiff -l <(curl $prefix/2025-03-26/schema.ts) <(curl $prefix/2025/06-18/schema.ts) - import ( "encoding/json" "fmt" "maps" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" ) +// resultType indicates whether a result is complete or requires further input +// from the client via the multi round-trip request protocol. +type resultType string + +const ( + // resultTypeComplete indicates the result is final. + // This is the default when ResultType is empty. + resultTypeComplete resultType = "complete" + + // resultTypeInputRequired indicates the server needs additional client + // input before it can complete the request. The client should fulfill the + // InputRequests and retry the call with the responses. + resultTypeInputRequired resultType = "input_required" +) + +type completeResultWithType struct { + ResultType resultType `json:"resultType,omitempty"` +} + +func (r *completeResultWithType) setResultType(rt resultType) { r.ResultType = rt } +func (*completeResultWithType) isCompleteResult() {} + +type completeResultResponse interface { + setResultType(resultType) + isCompleteResult() +} + +func setCompleteResultType(res Result) { + if r, ok := res.(completeResultResponse); ok { + r.setResultType(resultTypeComplete) + } +} + +// InputRequest is a type for parameters that a server can include in the response +// to request input from client (SEP-2322). Implementations are [*ElicitParams], +// [*CreateMessageParams], and [*ListRootsParams]. +type InputRequest interface{ isInputRequest() } + +// InputRequestMap maps server-assigned request IDs to [InputRequest] values. +// It is used in result types to tell the client what input the server needs. +type InputRequestMap map[string]InputRequest + +func (m InputRequestMap) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(map[string]any(nil)) + } + type wire struct { + Method string `json:"method"` + Params InputRequest `json:"params,omitempty"` + } + typeToMethod := func(v InputRequest) (string, error) { + switch v.(type) { + case *ElicitParams: + return methodElicit, nil + case *CreateMessageParams, *CreateMessageWithToolsParams: + return methodCreateMessage, nil + case *ListRootsParams: + return methodListRoots, nil + default: + return "", fmt.Errorf("unsupported type: %T", v) + } + } + converted := map[string]*wire{} + for k, v := range m { + method, err := typeToMethod(v) + if err != nil { + return nil, err + } + if ep, ok := v.(*ElicitParams); ok { + v = ep.inferElicitMode() + } + converted[k] = &wire{Method: method, Params: v} + } + return json.Marshal(converted) +} + +func (m *InputRequestMap) UnmarshalJSON(data []byte) error { + type raw struct { + Method string `json:"method"` + Params json.RawMessage `json:"params"` + } + var rawMap map[string]*raw + if err := json.Unmarshal(data, &rawMap); err != nil { + return err + } + if rawMap == nil { + return nil + } + result := make(InputRequestMap, len(rawMap)) + for k, raw := range rawMap { + switch raw.Method { + case methodElicit: + var p ElicitParams + if err := json.Unmarshal(raw.Params, &p); err != nil { + return err + } + result[k] = &p + case methodCreateMessage: + var p CreateMessageWithToolsParams + if err := json.Unmarshal(raw.Params, &p); err != nil { + return err + } + result[k] = &p + case methodListRoots: + var p ListRootsParams + if err := json.Unmarshal(raw.Params, &p); err != nil { + return err + } + result[k] = &p + default: + return fmt.Errorf("unsupported InputRequest method: %q", raw.Method) + } + } + *m = result + return nil +} + +// InputResponse is a type for results that a client sends back when fulfilling +// a server input request (SEP-2322). Implementations are [*ElicitResult], +// [*CreateMessageResult], and [*ListRootsResult]. +type InputResponse interface{ isInputResponse() } + +// InputResponseMap maps request IDs (from [InputRequestMap]) to [InputResponse] +// values. It is used in params types when retrying a call after an +// input-required result. +type InputResponseMap map[string]InputResponse + +func (m *InputResponseMap) UnmarshalJSON(data []byte) error { + var rawMap map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMap); err != nil { + return err + } + result := make(InputResponseMap, len(rawMap)) + for k, raw := range rawMap { + v, err := unmarshalInputResponse(raw) + if err != nil { + return fmt.Errorf("inputResponses[%q]: %w", k, err) + } + result[k] = v + } + *m = result + return nil +} + +// unmarshalInputResponse determines the concrete InputResponse type from the +// JSON structure by searching for a discriminating key in a raw message. +func unmarshalInputResponse(data json.RawMessage) (InputResponse, error) { + var probe struct { + Action json.RawMessage `json:"action"` + Role json.RawMessage `json:"role"` + Roots json.RawMessage `json:"roots"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return nil, err + } + switch { + case probe.Roots != nil: + var p ListRootsResult + if err := json.Unmarshal(data, &p); err != nil { + return nil, err + } + return &p, nil + case probe.Action != nil: + var p ElicitResult + if err := json.Unmarshal(data, &p); err != nil { + return nil, err + } + return &p, nil + case probe.Role != nil: + var p CreateMessageWithToolsResult + if err := json.Unmarshal(data, &p); err != nil { + return nil, err + } + return &p, nil + default: + return nil, fmt.Errorf(`cannot determine InputResponse type: expected "action", "role", or "roots" key`) + } +} + // Optional annotations for the client. The client can use annotations to inform // how objects are used or displayed. type Annotations struct { @@ -51,6 +225,14 @@ type CallToolParams struct { // Arguments holds the tool arguments. It can hold any value that can be // marshaled to JSON. Arguments any `json:"arguments,omitempty"` + + // InputResponses maps input request IDs to responses, provided when + // retrying a call after receiving a result with ResultType + // ResultTypeInputRequired. + InputResponses InputResponseMap `json:"inputResponses,omitempty"` + // RequestState is the opaque state from the previous input-required result. + // The client must echo this back when retrying. + RequestState string `json:"requestState,omitempty"` } // CallToolParamsRaw is passed to tool handlers on the server. Its arguments @@ -66,6 +248,14 @@ type CallToolParamsRaw struct { // is the responsibility of the tool handler to unmarshal and validate the // Arguments (see [AddTool]). Arguments json.RawMessage `json:"arguments,omitempty"` + + // InputResponses maps input request IDs to responses, provided when + // retrying a call after receiving a result with ResultType + // ResultTypeInputRequired. + InputResponses InputResponseMap `json:"inputResponses,omitempty"` + // RequestState is the opaque state from the previous input-required result. + // The client must echo this back when retrying. + RequestState string `json:"requestState,omitempty"` } // A CallToolResult is the server's response to a tool call. @@ -87,7 +277,9 @@ type CallToolResult struct { Content []Content `json:"content"` // StructuredContent is an optional value that represents the structured - // result of the tool call. It must marshal to a JSON object. + // result of the tool call. Per SEP-2106, it may marshal to any valid JSON + // value (object, array, or primitive) conforming to the tool's + // [Tool.OutputSchema]. // // When using a [ToolHandlerFor] with structured output, you should not // populate this field. It will be automatically populated with the typed Out @@ -112,6 +304,24 @@ type CallToolResult struct { // the Content field. IsError bool `json:"isError,omitempty"` + // InputRequests is a map of server-assigned IDs to input requests. + // Populated only when ResultType is ResultTypeInputRequired. + // The client must fulfill these and echo the IDs back in InputResponses + // when retrying the call. + InputRequests InputRequestMap `json:"inputRequests,omitempty"` + + // RequestState is an opaque string the client must echo back when + // retrying after an input-required result. Servers use this to carry + // context between independent requests. + // + // Unauthenticated servers must encrypt, sign and verify this value. + RequestState string `json:"requestState,omitempty"` + + // ResultType indicates whether this result is complete or requires further + // client input. Empty or ResultTypeComplete means the call succeeded + // normally. ResultTypeInputRequired means the client should fulfill the + // InputRequests and retry the call. + resultType resultType // The error passed to setError, if any. // It is not marshaled, and therefore it is only visible on the server. // Its only use is in server sending middleware, where it can be accessed @@ -119,10 +329,25 @@ type CallToolResult struct { err error } -// SetError sets the error for the tool result and populates the Content field -// with the error text. It also sets IsError to true. +// seterroroverwrite is a compatibility parameter that restores the pre-1.6.0 +// behavior of [CallToolResult.SetError], where Content was always overwritten +// with the error text. See the documentation for the mcpgodebug package for +// instructions on how to enable it. +// The option will be removed in the 1.8.0 version of the SDK. +var seterroroverwrite = mcpgodebug.Value("seterroroverwrite") + +// SetError sets the error for the tool result and sets IsError to true. +// If Content has not already been populated, it is set to the error text. +// If Content has already been populated, it is left unchanged, allowing callers +// to provide a user-friendly message while still recording the underlying error +// for inspection via [GetError] in server middleware. +// +// To restore the previous behavior where Content was always overwritten, +// set MCPGODEBUG=seterroroverwrite=1. func (r *CallToolResult) SetError(err error) { - r.Content = []Content{&TextContent{Text: err.Error()}} + if len(r.Content) == 0 || seterroroverwrite == "1" { + r.Content = []Content{&TextContent{Text: err.Error()}} + } r.IsError = true r.err = err } @@ -135,13 +360,49 @@ func (r *CallToolResult) GetError() error { func (*CallToolResult) isResult() {} -// UnmarshalJSON handles the unmarshalling of content into the Content -// interface. +func (r *CallToolResult) setResultType(rt resultType) { r.resultType = rt } +func (r *CallToolResult) requestState() string { return r.RequestState } +func (r *CallToolResult) inputRequests() map[string]InputRequest { + if r == nil { + return nil + } + return r.InputRequests +} +func (r *CallToolResult) hasContent() bool { + return len(r.Content) > 0 || r.StructuredContent != nil +} + +// NeedsInput reports whether this result requires further client input. +// This is true when the server returned ResultType "input_required". +// When NeedsInput returns true, check InputRequests for the set of +// requests the server needs fulfilled before retrying the call. +// An empty InputRequests with NeedsInput true indicates load-shedding. +func (r *CallToolResult) NeedsInput() bool { return r.resultType == resultTypeInputRequired } + +func (x *CallToolResult) MarshalJSON() ([]byte, error) { + type res CallToolResult // avoid recursion + type wire struct { + res + ResultType resultType `json:"resultType,omitempty"` + InputRequests json.RawMessage `json:"inputRequests,omitempty"` // shadows res.InputRequests + } + w := wire{res: res(*x), ResultType: x.resultType} + if x.InputRequests != nil { + ir, err := json.Marshal(x.InputRequests) + if err != nil { + return nil, err + } + w.InputRequests = ir + } + return json.Marshal(w) +} + func (x *CallToolResult) UnmarshalJSON(data []byte) error { type res CallToolResult // avoid recursion var wire struct { res - Content []*wireContent `json:"content"` + Content []*wireContent `json:"content"` + ResultType resultType `json:"resultType"` } if err := internaljson.Unmarshal(data, &wire); err != nil { return err @@ -150,15 +411,18 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error { if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil { return err } + wire.res.resultType = wire.ResultType *x = CallToolResult(wire.res) return nil } func (x *CallToolParams) isParams() {} +func (x *CallToolParams) isNil() bool { return x == nil } func (x *CallToolParams) GetProgressToken() any { return getProgressToken(x) } func (x *CallToolParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *CallToolParamsRaw) isParams() {} +func (x *CallToolParamsRaw) isNil() bool { return x == nil } func (x *CallToolParamsRaw) GetProgressToken() any { return getProgressToken(x) } func (x *CallToolParamsRaw) SetProgressToken(t any) { setProgressToken(x, t) } @@ -177,10 +441,16 @@ type CancelledParams struct { } func (x *CancelledParams) isParams() {} +func (x *CancelledParams) isNil() bool { return x == nil } func (x *CancelledParams) GetProgressToken() any { return getProgressToken(x) } func (x *CancelledParams) SetProgressToken(t any) { setProgressToken(x, t) } // RootCapabilities describes a client's support for roots. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type RootCapabilities struct { // ListChanged reports whether the client supports notifications for // changes to the roots list. @@ -208,14 +478,27 @@ type ClientCapabilities struct { // Deprecated: use RootsV2. As described in #607, Roots should have been a // pointer to a RootCapabilities value. Roots will be continue to be // populated, but any new fields will only be added in the RootsV2 field. + // + // The roots feature itself is also deprecated by SEP-2577; see RootsV2. Roots struct { // ListChanged reports whether the client supports notifications for // changes to the roots list. ListChanged bool `json:"listChanged,omitempty"` } `json:"roots,omitempty"` - // RootsV2 is present if the client supports roots. When capabilities are explicitly configured via [ClientOptions.Capabilities] + // RootsV2 is present if the client supports roots. When capabilities are + // explicitly configured via [ClientOptions.Capabilities]. + // + // Deprecated: the roots feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. RootsV2 *RootCapabilities `json:"-"` // Sampling is present if the client supports sampling from an LLM. + // + // Deprecated: the sampling feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. Sampling *SamplingCapabilities `json:"sampling,omitempty"` // Elicitation is present if the client supports elicitation from the server. Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"` @@ -364,7 +647,8 @@ type CompleteParams struct { Ref *CompleteReference `json:"ref"` } -func (*CompleteParams) isParams() {} +func (x *CompleteParams) isParams() {} +func (x *CompleteParams) isNil() bool { return x == nil } type CompletionResultDetails struct { HasMore bool `json:"hasMore,omitempty"` @@ -374,6 +658,7 @@ type CompletionResultDetails struct { // The server's response to a completion/complete request type CompleteResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -382,6 +667,12 @@ type CompleteResult struct { func (*CompleteResult) isResult() {} +// CreateMessageParams holds parameters for a sampling/createMessage request. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type CreateMessageParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -389,10 +680,11 @@ type CreateMessageParams struct { // A request to include context from one or more MCP servers (including the // caller), to be attached to the prompt. The client may ignore this request. // - // The default is "none". Values "thisServer" and - // "allServers" are soft-deprecated. Servers SHOULD only use these values if - // the client declares ClientCapabilities.sampling.context. These values may - // be removed in future spec releases. + // The default is "none". The values "thisServer" and "allServers" are + // deprecated as of protocol version 2025-11-25 (SEP-2596) and will be + // removed no later than the sampling feature itself (SEP-2577). Servers + // SHOULD omit this field or use "none". See + // https://modelcontextprotocol.io/seps/2596-feature-lifecycle-and-deprecation-policy. IncludeContext string `json:"includeContext,omitempty"` // The maximum number of tokens to sample, as requested by the server. The // client may choose to sample fewer tokens than requested. @@ -412,6 +704,8 @@ type CreateMessageParams struct { } func (x *CreateMessageParams) isParams() {} +func (x *CreateMessageParams) isInputRequest() {} +func (x *CreateMessageParams) isNil() bool { return x == nil } func (x *CreateMessageParams) GetProgressToken() any { return getProgressToken(x) } func (x *CreateMessageParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -420,6 +714,11 @@ func (x *CreateMessageParams) SetProgressToken(t any) { setProgressToken(x, t) } // and messages that support array content (for parallel tool calls). // // Use with [ServerSession.CreateMessageWithTools]. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type CreateMessageWithToolsParams struct { Meta `json:"_meta,omitempty"` IncludeContext string `json:"includeContext,omitempty"` @@ -438,6 +737,8 @@ type CreateMessageWithToolsParams struct { } func (x *CreateMessageWithToolsParams) isParams() {} +func (x *CreateMessageWithToolsParams) isInputRequest() {} +func (x *CreateMessageWithToolsParams) isNil() bool { return x == nil } func (x *CreateMessageWithToolsParams) GetProgressToken() any { return getProgressToken(x) } func (x *CreateMessageWithToolsParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -478,6 +779,11 @@ func (p *CreateMessageWithToolsParams) toBase() (*CreateMessageParams, error) { // object for compatibility with pre-2025-11-25 implementations. When // unmarshaling, a single JSON content object is accepted and wrapped in a // one-element slice. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SamplingMessageV2 struct { Content []Content `json:"content"` Role Role `json:"role"` @@ -519,6 +825,11 @@ func (m *SamplingMessageV2) UnmarshalJSON(data []byte) error { // The client should inform the user before returning the sampled message, to // allow them to inspect the response (human in the loop) and decide whether to // allow the server to see it. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type CreateMessageResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -537,7 +848,8 @@ type CreateMessageResult struct { StopReason string `json:"stopReason,omitempty"` } -func (*CreateMessageResult) isResult() {} +func (*CreateMessageResult) isResult() {} +func (*CreateMessageResult) isInputResponse() {} func (r *CreateMessageResult) UnmarshalJSON(data []byte) error { type result CreateMessageResult // avoid recursion var wire struct { @@ -564,6 +876,11 @@ func (r *CreateMessageResult) UnmarshalJSON(data []byte) error { // // When unmarshaling, a single JSON content object is accepted and wrapped in a // one-element slice, for compatibility with clients that return a single block. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type CreateMessageWithToolsResult struct { Meta `json:"_meta,omitempty"` Content []Content `json:"content"` @@ -582,7 +899,8 @@ var createMessageWithToolsResultAllow = map[string]bool{ "tool_use": true, } -func (*CreateMessageWithToolsResult) isResult() {} +func (*CreateMessageWithToolsResult) isResult() {} +func (*CreateMessageWithToolsResult) isInputResponse() {} // MarshalJSON marshals the result. When Content has a single element, it is // marshaled as a single object for compatibility with pre-2025-11-25 @@ -641,9 +959,17 @@ type GetPromptParams struct { Arguments map[string]string `json:"arguments,omitempty"` // The name of the prompt or prompt template. Name string `json:"name"` + + // InputResponses maps input request IDs to responses, provided when + // retrying a call after receiving a result with ResultType + // ResultTypeInputRequired. + InputResponses InputResponseMap `json:"inputResponses,omitempty"` + // RequestState is the opaque state from the previous input-required result. + RequestState string `json:"requestState,omitempty"` } func (x *GetPromptParams) isParams() {} +func (x *GetPromptParams) isNil() bool { return x == nil } func (x *GetPromptParams) GetProgressToken() any { return getProgressToken(x) } func (x *GetPromptParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -655,10 +981,67 @@ type GetPromptResult struct { // An optional description for the prompt. Description string `json:"description,omitempty"` Messages []*PromptMessage `json:"messages"` + + // InputRequests is populated when ResultType is ResultTypeInputRequired. + // See [CallToolResult.InputRequests]. + InputRequests InputRequestMap `json:"inputRequests,omitempty"` + // RequestState is the opaque state for multi-round-trip retries. + // See [CallToolResult.RequestState]. + RequestState string `json:"requestState,omitempty"` + + // ResultType indicates whether this result is complete or requires further + // client input. See [CallToolResult.ResultType] for details. + resultType resultType } func (*GetPromptResult) isResult() {} +func (r *GetPromptResult) setResultType(rt resultType) { r.resultType = rt } +func (r *GetPromptResult) requestState() string { return r.RequestState } +func (r *GetPromptResult) inputRequests() map[string]InputRequest { + if r == nil { + return nil + } + return r.InputRequests +} +func (r *GetPromptResult) hasContent() bool { return len(r.Messages) > 0 } + +// NeedsInput reports whether this result requires further client input. +// See [CallToolResult.NeedsInput] for details. +func (r *GetPromptResult) NeedsInput() bool { return r.resultType == resultTypeInputRequired } + +func (x *GetPromptResult) MarshalJSON() ([]byte, error) { + type res GetPromptResult + type wire struct { + res + ResultType resultType `json:"resultType,omitempty"` + InputRequests json.RawMessage `json:"inputRequests,omitempty"` // shadows res.InputRequests + } + w := wire{res: res(*x), ResultType: x.resultType} + if x.InputRequests != nil { + ir, err := json.Marshal(x.InputRequests) + if err != nil { + return nil, err + } + w.InputRequests = ir + } + return json.Marshal(w) +} + +func (x *GetPromptResult) UnmarshalJSON(data []byte) error { + type res GetPromptResult + var wire struct { + res + ResultType resultType `json:"resultType"` + } + if err := internaljson.Unmarshal(data, &wire); err != nil { + return err + } + wire.res.resultType = wire.ResultType + *x = GetPromptResult(wire.res) + return nil +} + // InitializeParams is sent by the client to initialize the session. type InitializeParams struct { // This property is reserved by the protocol to allow clients and servers to @@ -696,6 +1079,7 @@ func (p *initializeParamsV2) toV1() *InitializeParams { } func (x *InitializeParams) isParams() {} +func (x *InitializeParams) isNil() bool { return x == nil } func (x *InitializeParams) GetProgressToken() any { return getProgressToken(x) } func (x *InitializeParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -729,6 +1113,7 @@ type InitializedParams struct { } func (x *InitializedParams) isParams() {} +func (x *InitializedParams) isNil() bool { return x == nil } func (x *InitializedParams) GetProgressToken() any { return getProgressToken(x) } func (x *InitializedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -741,16 +1126,83 @@ type ListPromptsParams struct { Cursor string `json:"cursor,omitempty"` } +type DiscoverParams struct { + Meta `json:"_meta,omitempty"` +} + +func (x *DiscoverParams) isParams() {} +func (x *DiscoverParams) isNil() bool { return x == nil } +func (x *DiscoverParams) GetProgressToken() any { return getProgressToken(x) } +func (x *DiscoverParams) SetProgressToken(t any) { setProgressToken(x, t) } + +type DiscoverResult struct { + completeResultWithType + Meta `json:"_meta,omitempty"` + Cacheable + // The versions of the Model Context Protocol that the server supports. + SupportedVersions []string `json:"supportedVersions"` + // The server's capabilities. + Capabilities *ServerCapabilities `json:"capabilities"` + // Instructions describing how to use the server and its features. + Instructions string `json:"instructions,omitempty"` +} + +func (*DiscoverResult) isResult() {} + func (x *ListPromptsParams) isParams() {} +func (x *ListPromptsParams) isNil() bool { return x == nil } func (x *ListPromptsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListPromptsParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListPromptsParams) cursorPtr() *string { return &x.Cursor } +// CacheableResult is a result that supports a time-to-live (TTL) hint for +// client-side caching. +type CacheableResult interface { + Result + GetTTLMs() int + GetCacheScope() string +} + +// Cacheable describes a result that supports a time-to-live (TTL) hint for +// client-side caching. +type Cacheable struct { + // A hint from the server indicating how long (in milliseconds) the + // client MAY cache this response before re-fetching. Semantics are + // analogous to HTTP Cache-Control max-age. + // + // If 0, the response SHOULD be considered immediately stale. + // If positive, the client SHOULD consider the result fresh for this + // many milliseconds after receiving the response. + TTLMs int `json:"ttlMs"` + + // Indicates the intended scope of the cached response, analogous to + // HTTP Cache-Control: public vs Cache-Control: private. + // + // "public": Any client or intermediary MAY cache and serve the response. + // "private": Only the requesting user's client MAY cache the response. + // + // Defaults to "public" if absent. + CacheScope string `json:"cacheScope"` +} + +// GetTTLMs returns the TTL hint in milliseconds. +func (c Cacheable) GetTTLMs() int { return c.TTLMs } + +// GetCacheScope returns the cache scope. +func (c Cacheable) GetCacheScope() string { return c.CacheScope } + +// setDefaultCacheableValues sets the default values for the cacheable fields. +func (c *Cacheable) setDefaultCacheableValues() { + c.CacheScope = "public" +} + // The server's response to a prompts/list request from the client. type ListPromptsResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -770,15 +1222,18 @@ type ListResourceTemplatesParams struct { } func (x *ListResourceTemplatesParams) isParams() {} +func (x *ListResourceTemplatesParams) isNil() bool { return x == nil } func (x *ListResourceTemplatesParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListResourceTemplatesParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListResourceTemplatesParams) cursorPtr() *string { return &x.Cursor } // The server's response to a resources/templates/list request from the client. type ListResourceTemplatesResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -798,15 +1253,18 @@ type ListResourcesParams struct { } func (x *ListResourcesParams) isParams() {} +func (x *ListResourcesParams) isNil() bool { return x == nil } func (x *ListResourcesParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListResourcesParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListResourcesParams) cursorPtr() *string { return &x.Cursor } // The server's response to a resources/list request from the client. type ListResourcesResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -816,6 +1274,12 @@ type ListResourcesResult struct { func (x *ListResourcesResult) isResult() {} func (x *ListResourcesResult) nextCursorPtr() *string { return &x.NextCursor } +// ListRootsParams holds parameters for a roots/list request. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ListRootsParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -823,12 +1287,19 @@ type ListRootsParams struct { } func (x *ListRootsParams) isParams() {} +func (x *ListRootsParams) isInputRequest() {} +func (x *ListRootsParams) isNil() bool { return x == nil } func (x *ListRootsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListRootsParams) SetProgressToken(t any) { setProgressToken(x, t) } // The client's response to a roots/list request from the server. This result // contains an array of Root objects, each representing a root directory or file // that the server can operate on. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ListRootsResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -836,7 +1307,8 @@ type ListRootsResult struct { Roots []*Root `json:"roots"` } -func (*ListRootsResult) isResult() {} +func (*ListRootsResult) isResult() {} +func (*ListRootsResult) isInputResponse() {} type ListToolsParams struct { // This property is reserved by the protocol to allow clients and servers to @@ -848,15 +1320,18 @@ type ListToolsParams struct { } func (x *ListToolsParams) isParams() {} +func (x *ListToolsParams) isNil() bool { return x == nil } func (x *ListToolsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListToolsParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListToolsParams) cursorPtr() *string { return &x.Cursor } // The server's response to a tools/list request from the client. type ListToolsResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -870,8 +1345,20 @@ func (x *ListToolsResult) nextCursorPtr() *string { return &x.NextCursor } // // These map to syslog message severities, as specified in RFC-5424: // https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type LoggingLevel string +// LoggingMessageParams holds the parameters for a notifications/message +// notification. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type LoggingMessageParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -886,6 +1373,7 @@ type LoggingMessageParams struct { } func (x *LoggingMessageParams) isParams() {} +func (x *LoggingMessageParams) isNil() bool { return x == nil } func (x *LoggingMessageParams) GetProgressToken() any { return getProgressToken(x) } func (x *LoggingMessageParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -893,6 +1381,11 @@ func (x *LoggingMessageParams) SetProgressToken(t any) { setProgressToken(x, t) // // Keys not declared here are currently left unspecified by the spec and are up // to the client to interpret. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ModelHint struct { // A hint for a model name. // @@ -919,6 +1412,11 @@ type ModelHint struct { // These preferences are always advisory. The client may ignore them. It is also // up to the client to decide how to interpret these preferences and how to // balance them against other considerations. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ModelPreferences struct { // How much to prioritize cost when selecting a model. A value of 0 means cost // is not important, while a value of 1 means cost is the most important factor. @@ -948,6 +1446,7 @@ type PingParams struct { } func (x *PingParams) isParams() {} +func (x *PingParams) isNil() bool { return x == nil } func (x *PingParams) GetProgressToken() any { return getProgressToken(x) } func (x *PingParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -968,7 +1467,8 @@ type ProgressNotificationParams struct { Total float64 `json:"total,omitempty"` } -func (*ProgressNotificationParams) isParams() {} +func (x *ProgressNotificationParams) isParams() {} +func (x *ProgressNotificationParams) isNil() bool { return x == nil } // IconTheme specifies the theme an icon is designed for. type IconTheme string @@ -1038,6 +1538,7 @@ type PromptListChangedParams struct { } func (x *PromptListChangedParams) isParams() {} +func (x *PromptListChangedParams) isNil() bool { return x == nil } func (x *PromptListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *PromptListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1076,9 +1577,17 @@ type ReadResourceParams struct { // The URI of the resource to read. The URI can use any protocol; it is up to // the server how to interpret it. URI string `json:"uri"` + + // InputResponses maps input request IDs to responses, provided when + // retrying a call after receiving a result with ResultType + // ResultTypeInputRequired. + InputResponses InputResponseMap `json:"inputResponses,omitempty"` + // RequestState is the opaque state from the previous input-required result. + RequestState string `json:"requestState,omitempty"` } func (x *ReadResourceParams) isParams() {} +func (x *ReadResourceParams) isNil() bool { return x == nil } func (x *ReadResourceParams) GetProgressToken() any { return getProgressToken(x) } func (x *ReadResourceParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1086,12 +1595,70 @@ func (x *ReadResourceParams) SetProgressToken(t any) { setProgressToken(x, t) } type ReadResourceResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. - Meta `json:"_meta,omitempty"` + Meta `json:"_meta,omitempty"` + Cacheable Contents []*ResourceContents `json:"contents"` + + // InputRequests is populated when ResultType is ResultTypeInputRequired. + // See [CallToolResult.InputRequests]. + InputRequests InputRequestMap `json:"inputRequests,omitempty"` + // RequestState is the opaque state for multi-round-trip retries. + // See [CallToolResult.RequestState]. + RequestState string `json:"requestState,omitempty"` + + // ResultType indicates whether this result is complete or requires further + // client input. See [CallToolResult.ResultType] for details. + resultType resultType } func (*ReadResourceResult) isResult() {} +func (r *ReadResourceResult) setResultType(rt resultType) { r.resultType = rt } +func (r *ReadResourceResult) requestState() string { return r.RequestState } +func (r *ReadResourceResult) inputRequests() map[string]InputRequest { + if r == nil { + return nil + } + return r.InputRequests +} +func (r *ReadResourceResult) hasContent() bool { return len(r.Contents) > 0 } + +// NeedsInput reports whether this result requires further client input. +// See [CallToolResult.NeedsInput] for details. +func (r *ReadResourceResult) NeedsInput() bool { return r.resultType == resultTypeInputRequired } + +func (x *ReadResourceResult) MarshalJSON() ([]byte, error) { + type res ReadResourceResult + type wire struct { + res + ResultType resultType `json:"resultType,omitempty"` + InputRequests json.RawMessage `json:"inputRequests,omitempty"` // shadows res.InputRequests + } + w := wire{res: res(*x), ResultType: x.resultType} + if x.InputRequests != nil { + ir, err := json.Marshal(x.InputRequests) + if err != nil { + return nil, err + } + w.InputRequests = ir + } + return json.Marshal(w) +} + +func (x *ReadResourceResult) UnmarshalJSON(data []byte) error { + type res ReadResourceResult + var wire struct { + res + ResultType resultType `json:"resultType"` + } + if err := internaljson.Unmarshal(data, &wire); err != nil { + return err + } + wire.res.resultType = wire.ResultType + *x = ReadResourceResult(wire.res) + return nil +} + // A known resource that the server is capable of reading. type Resource struct { // See [specification/2025-06-18/basic/index#general-fields] for notes on _meta @@ -1135,6 +1702,7 @@ type ResourceListChangedParams struct { } func (x *ResourceListChangedParams) isParams() {} +func (x *ResourceListChangedParams) isNil() bool { return x == nil } func (x *ResourceListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *ResourceListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1174,6 +1742,11 @@ type ResourceTemplate struct { type Role string // Represents a root directory or file that the server can operate on. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type Root struct { // See [specification/2025-06-18/basic/index#general-fields] for notes on _meta // usage. @@ -1188,6 +1761,13 @@ type Root struct { URI string `json:"uri"` } +// RootsListChangedParams holds parameters for a notifications/roots/list_changed +// notification. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type RootsListChangedParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -1195,6 +1775,7 @@ type RootsListChangedParams struct { } func (x *RootsListChangedParams) isParams() {} +func (x *RootsListChangedParams) isNil() bool { return x == nil } func (x *RootsListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1202,6 +1783,11 @@ func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t // below directly above ClientCapabilities. // SamplingCapabilities describes the client's support for sampling. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SamplingCapabilities struct { // Context indicates the client supports includeContext values other than "none". Context *SamplingContextCapabilities `json:"context,omitempty"` @@ -1210,12 +1796,27 @@ type SamplingCapabilities struct { } // SamplingContextCapabilities indicates the client supports context inclusion. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SamplingContextCapabilities struct{} // SamplingToolsCapabilities indicates the client supports tool use in sampling. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SamplingToolsCapabilities struct{} // ToolChoice controls how the model uses tools during sampling. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type ToolChoice struct { // Mode controls tool invocation behavior: // - "auto": Model decides whether to use tools (default) @@ -1226,7 +1827,7 @@ type ToolChoice struct { // ElicitationCapabilities describes the capabilities for elicitation. // -// If neither Form nor URL is set, the 'Form' capabilitiy is assumed. +// If neither Form nor URL is set, the 'Form' capability is assumed. type ElicitationCapabilities struct { Form *FormElicitationCapabilities `json:"form,omitempty"` URL *URLElicitationCapabilities `json:"url,omitempty"` @@ -1242,6 +1843,11 @@ type URLElicitationCapabilities struct{} // // For assistant messages, Content may be text, image, audio, or tool_use. // For user messages, Content may be text, image, audio, or tool_result. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SamplingMessage struct { Content Content `json:"content"` Role Role `json:"role"` @@ -1267,6 +1873,12 @@ func (m *SamplingMessage) UnmarshalJSON(data []byte) error { return nil } +// SetLoggingLevelParams holds parameters for a logging/setLevel request. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type SetLoggingLevelParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -1278,6 +1890,7 @@ type SetLoggingLevelParams struct { } func (x *SetLoggingLevelParams) isParams() {} +func (x *SetLoggingLevelParams) isNil() bool { return x == nil } func (x *SetLoggingLevelParams) GetProgressToken() any { return getProgressToken(x) } func (x *SetLoggingLevelParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1336,6 +1949,13 @@ type Tool struct { Icons []Icon `json:"icons,omitempty"` } +// hintomitempty is a compatibility parameter that restores the pre-1.7.0 +// behavior of [ToolAnnotations] JSON marshaling, where false-valued bare bool +// fields (ReadOnlyHint, IdempotentHint) were omitted from the output. +// See the documentation for the mcpgodebug package for instructions on how to +// enable it. +var hintomitempty = mcpgodebug.Value("hintomitempty") + // Additional properties describing a Tool to clients. // // NOTE: all properties in ToolAnnotations are hints. They are not @@ -1358,7 +1978,7 @@ type ToolAnnotations struct { // (This property is meaningful only when ReadOnlyHint == false.) // // Default: false - IdempotentHint bool `json:"idempotentHint,omitempty"` + IdempotentHint bool `json:"idempotentHint"` // If true, this tool may interact with an "open world" of external entities. If // false, the tool's domain of interaction is closed. For example, the world of // a web search tool is open, whereas that of a memory tool is not. @@ -1368,11 +1988,30 @@ type ToolAnnotations struct { // If true, the tool does not modify its environment. // // Default: false - ReadOnlyHint bool `json:"readOnlyHint,omitempty"` + ReadOnlyHint bool `json:"readOnlyHint"` // A human-readable title for the tool. Title string `json:"title,omitempty"` } +// MarshalJSON implements [json.Marshaler] for ToolAnnotations. +// +// To restore the previous behavior where false-valued ReadOnlyHint and +// IdempotentHint were omitted, set MCPGODEBUG=hintomitempty=1. +func (t ToolAnnotations) MarshalJSON() ([]byte, error) { + if hintomitempty == "1" { + type compat struct { + DestructiveHint *bool `json:"destructiveHint,omitempty"` + IdempotentHint bool `json:"idempotentHint,omitempty"` + OpenWorldHint *bool `json:"openWorldHint,omitempty"` + ReadOnlyHint bool `json:"readOnlyHint,omitempty"` + Title string `json:"title,omitempty"` + } + return json.Marshal(compat(t)) + } + type nomethod ToolAnnotations + return json.Marshal(nomethod(t)) +} + type ToolListChangedParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -1380,6 +2019,7 @@ type ToolListChangedParams struct { } func (x *ToolListChangedParams) isParams() {} +func (x *ToolListChangedParams) isNil() bool { return x == nil } func (x *ToolListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *ToolListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -1393,7 +2033,8 @@ type SubscribeParams struct { URI string `json:"uri"` } -func (*SubscribeParams) isParams() {} +func (x *SubscribeParams) isParams() {} +func (x *SubscribeParams) isNil() bool { return x == nil } // Sent from the client to request cancellation of resources/updated // notifications from the server. This should follow a previous @@ -1406,7 +2047,8 @@ type UnsubscribeParams struct { URI string `json:"uri"` } -func (*UnsubscribeParams) isParams() {} +func (x *UnsubscribeParams) isParams() {} +func (x *UnsubscribeParams) isNil() bool { return x == nil } // A notification from the server to the client, informing it that a resource // has changed and may need to be read again. This should only be sent if the @@ -1419,7 +2061,63 @@ type ResourceUpdatedNotificationParams struct { URI string `json:"uri"` } -func (*ResourceUpdatedNotificationParams) isParams() {} +func (x *ResourceUpdatedNotificationParams) isParams() {} +func (x *ResourceUpdatedNotificationParams) isNil() bool { return x == nil } + +// NotificationSubscriptions describes the set of server-to-client +// notifications a client wishes to receive on a [SubscriptionsListenParams] +// stream. Each field is an explicit opt-in: a server MUST NOT push +// notifications of a type the client did not request. +type NotificationSubscriptions struct { + // ToolsListChanged opts in to "notifications/tools/list_changed". + ToolsListChanged bool `json:"toolsListChanged,omitempty"` + // PromptsListChanged opts in to "notifications/prompts/list_changed". + PromptsListChanged bool `json:"promptsListChanged,omitempty"` + // ResourcesListChanged opts in to "notifications/resources/list_changed". + ResourcesListChanged bool `json:"resourcesListChanged,omitempty"` + // ResourceSubscriptions enumerates the resource URIs for which the client + // wants "notifications/resources/updated". Replaces the legacy + // resources/subscribe RPC. + ResourceSubscriptions []string `json:"resourceSubscriptions,omitempty"` +} + +// SubscriptionsListenParams are the parameters for the +// "subscriptions/listen" RPC. +type SubscriptionsListenParams struct { + // Meta carries the per-request `_meta` triple. + Meta `json:"_meta,omitempty"` + // Notifications declares which notification types the client wants to + // receive on this stream. + Notifications *NotificationSubscriptions `json:"notifications"` +} + +func (x *SubscriptionsListenParams) isParams() {} +func (x *SubscriptionsListenParams) isNil() bool { return x == nil } + +// SubscriptionsAcknowledgedParams are the parameters for the +// "notifications/subscriptions/acknowledged" notification, which the server +// MUST send as the first message on a subscriptions/listen stream. It carries +// the subset of the requested [NotificationSubscriptions] that the server has +// agreed to honor. +type SubscriptionsAcknowledgedParams struct { + Meta `json:"_meta,omitempty"` + Notifications NotificationSubscriptions `json:"notifications"` +} + +func (x *SubscriptionsAcknowledgedParams) isParams() {} +func (x *SubscriptionsAcknowledgedParams) isNil() bool { return x == nil } + +// SubscriptionsListenResult is the response to a "subscriptions/listen" +// request, signalling that the subscription has ended gracefully (for example, +// during server shutdown). Because the listen stream is long-lived, this +// result is sent only when the server tears the subscription down; an abrupt +// transport close carries no response. +type SubscriptionsListenResult struct { + completeResultWithType + Meta `json:"_meta"` +} + +func (*SubscriptionsListenResult) isResult() {} // TODO(jba): add CompleteRequest and related types. @@ -1458,11 +2156,28 @@ type ElicitParams struct { ElicitationID string `json:"elicitationId,omitempty"` } -func (x *ElicitParams) isParams() {} +func (x *ElicitParams) isParams() {} +func (x *ElicitParams) isInputRequest() {} +func (x *ElicitParams) isNil() bool { return x == nil } func (x *ElicitParams) GetProgressToken() any { return getProgressToken(x) } func (x *ElicitParams) SetProgressToken(t any) { setProgressToken(x, t) } +// inferElicitMode returns x with Mode populated by inference if it was empty. +// Mode is inferred as "url" when URL or ElicitationID is set, otherwise "form". +func (x *ElicitParams) inferElicitMode() *ElicitParams { + if x == nil || x.Mode != "" { + return x + } + x2 := *x + if x.URL != "" || x.ElicitationID != "" { + x2.Mode = "url" + } else { + x2.Mode = "form" + } + return &x2 +} + // The client's response to an elicitation/create request from the server. type ElicitResult struct { // This property is reserved by the protocol to allow clients and servers to @@ -1478,7 +2193,8 @@ type ElicitResult struct { Content map[string]any `json:"content,omitempty"` } -func (*ElicitResult) isResult() {} +func (*ElicitResult) isResult() {} +func (*ElicitResult) isInputResponse() {} // ElicitationCompleteParams is sent from the server to the client, informing it that an out-of-band elicitation interaction has completed. type ElicitationCompleteParams struct { @@ -1490,18 +2206,21 @@ type ElicitationCompleteParams struct { ElicitationID string `json:"elicitationId"` } -func (*ElicitationCompleteParams) isParams() {} +func (x *ElicitationCompleteParams) isParams() {} +func (x *ElicitationCompleteParams) isNil() bool { return x == nil } -// An Implementation describes the name and version of an MCP implementation, with an optional -// title for UI representation. +// An Implementation describes the name and version of an MCP implementation, with +// optional display metadata. type Implementation struct { // Intended for programmatic or logical use, but used as a display name in past // specs or fallback (if title isn't present). Name string `json:"name"` // Intended for UI and end-user contexts — optimized to be human-readable and // easily understood, even by those unfamiliar with domain-specific terminology. - Title string `json:"title,omitempty"` - Version string `json:"version"` + Title string `json:"title,omitempty"` + // A human-readable description of the implementation. + Description string `json:"description,omitempty"` + Version string `json:"version"` // WebsiteURL for the server, if any. WebsiteURL string `json:"websiteUrl,omitempty"` // Icons for the Server, if any. @@ -1512,6 +2231,11 @@ type Implementation struct { type CompletionCapabilities struct{} // LoggingCapabilities describes the server's support for sending log messages to the client. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. type LoggingCapabilities struct{} // PromptCapabilities describes the server's support for prompts. @@ -1555,10 +2279,15 @@ type ServerCapabilities struct { // suggestions. Completions *CompletionCapabilities `json:"completions,omitempty"` // Logging is present if the server supports log messages. + // + // Deprecated: the logging feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. Logging *LoggingCapabilities `json:"logging,omitempty"` // Prompts is present if the server supports prompts. Prompts *PromptCapabilities `json:"prompts,omitempty"` - // Resources is present if the server supports resourcs. + // Resources is present if the server supports resources. Resources *ResourceCapabilities `json:"resources,omitempty"` // Tools is present if the supports tools. Tools *ToolCapabilities `json:"tools,omitempty"` @@ -1596,6 +2325,7 @@ const ( methodCallTool = "tools/call" notificationCancelled = "notifications/cancelled" methodComplete = "completion/complete" + methodDiscover = "server/discover" methodCreateMessage = "sampling/createMessage" methodElicit = "elicitation/create" notificationElicitationComplete = "notifications/elicitation/complete" @@ -1617,6 +2347,60 @@ const ( notificationRootsListChanged = "notifications/roots/list_changed" methodSetLevel = "logging/setLevel" methodSubscribe = "resources/subscribe" + methodSubscriptionsListen = "subscriptions/listen" notificationToolListChanged = "notifications/tools/list_changed" methodUnsubscribe = "resources/unsubscribe" + notificationSubscriptionsAck = "notifications/subscriptions/acknowledged" +) + +// Per-request _meta field names for the >= 2026-07-28 protocol version. +// +// These keys appear inside a Params._meta map and carry information that +// previously came from the initialization handshake (SEP-2575). +const ( + // MetaKeyProtocolVersion identifies the MCP protocol version that the + // request follows. + MetaKeyProtocolVersion = "io.modelcontextprotocol/protocolVersion" + // MetaKeyClientInfo carries the client's [Implementation]. + MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo" + // MetaKeyServerInfo carries the server's [Implementation] on responses. + MetaKeyServerInfo = "io.modelcontextprotocol/serverInfo" + // MetaKeyClientCapabilities carries the client's [ClientCapabilities]. + MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities" + // MetaKeyLogLevel identifies the desired log level for the request. + // + // Deprecated: the logging feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. + MetaKeyLogLevel = "io.modelcontextprotocol/logLevel" + // MetaKeySubscriptionID identifies the subscriptions/listen request that an + // out-of-band notification belongs to. + MetaKeySubscriptionID = "io.modelcontextprotocol/subscriptionId" ) + +// UnsupportedProtocolVersionData is the SEP-2575 payload carried in the +// `data` field of a JSON-RPC error response with code +// [CodeUnsupportedProtocolVersion]. The server uses it to advertise which +// versions it supports so the client can pick a mutually supported one. +type UnsupportedProtocolVersionData struct { + // Supported is the list of protocol versions the server supports. + Supported []string `json:"supported"` + // Requested is the protocol version the client asked for. + Requested string `json:"requested"` +} + +// MissingRequiredClientCapabilityData is the SEP-2575 payload carried in the +// `data` field of a JSON-RPC error response with code +// [CodeMissingRequiredClientCapabilities]. The server uses it to indicate +// which client capabilities are required to process the request but were not +// declared by the client in its per-request `_meta` field. +// +// Handlers that require a specific client capability should inspect the +// per-request [ServerRequest.ClientCapabilities] and return a JSON-RPC error +// populated with this structure when the required capability is missing. +type MissingRequiredClientCapabilityData struct { + // RequiredCapabilities is the set of capabilities the server requires + // from the client to process the request. + RequiredCapabilities *ClientCapabilities `json:"requiredCapabilities"` +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/requests.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/requests.go index 428094136..64414caa9 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/requests.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/requests.go @@ -19,12 +19,14 @@ type ( ReadResourceRequest = ServerRequest[*ReadResourceParams] RootsListChangedRequest = ServerRequest[*RootsListChangedParams] SubscribeRequest = ServerRequest[*SubscribeParams] + SubscriptionsListenRequest = ServerRequest[*SubscriptionsListenParams] UnsubscribeRequest = ServerRequest[*UnsubscribeParams] ) type ( CreateMessageRequest = ClientRequest[*CreateMessageParams] CreateMessageWithToolsRequest = ClientRequest[*CreateMessageWithToolsParams] + DiscoverRequest = ClientRequest[*DiscoverParams] ElicitRequest = ClientRequest[*ElicitParams] initializedClientRequest = ClientRequest[*InitializedParams] InitializeRequest = ClientRequest[*InitializeParams] diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/resource.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/resource.go index bc4b3cb1f..fff38a224 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/resource.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/resource.go @@ -15,6 +15,7 @@ import ( "path/filepath" "strings" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/yosida95/uritemplate/v3" @@ -37,8 +38,25 @@ type serverResourceTemplate struct { // If it cannot find the resource, it should return the result of calling [ResourceNotFoundError]. type ResourceHandler func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) +// customresnotfounderrcode is a compatibility parameter that restores the +// pre-1.7.0 behavior of [ResourceNotFoundError] and [CodeResourceNotFound], +// where the error code was a custom -32002. See the documentation for the mcpgodebug +// package for instructions on how to enable it. +// The option will be removed in the future version of the SDK. +var customresnotfounderrcode = mcpgodebug.Value("customresnotfounderrcode") + +func init() { + if customresnotfounderrcode == "1" { + CodeResourceNotFound = -32002 + } +} + // ResourceNotFoundError returns an error indicating that a resource being read could // not be found. +// +// By default, the error code is -32602 (Invalid Params), as specified in the +// MCP specification (SEP-2164). To restore the pre-1.7.0 release behavior where the +// error code was -32002, set MCPGODEBUG=customresnotfounderrcode=1. func ResourceNotFoundError(uri string) error { return &jsonrpc.Error{ Code: CodeResourceNotFound, diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go index e3c03e278..b5805488d 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go @@ -27,6 +27,7 @@ import ( "github.com/google/jsonschema-go/jsonschema" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/yosida95/uritemplate/v3" @@ -44,16 +45,24 @@ type Server struct { impl *Implementation opts ServerOptions - mu sync.Mutex - prompts *featureSet[*serverPrompt] - tools *featureSet[*serverTool] - resources *featureSet[*serverResource] - resourceTemplates *featureSet[*serverResourceTemplate] - sessions []*ServerSession - sendingMethodHandler_ MethodHandler - receivingMethodHandler_ MethodHandler - resourceSubscriptions map[string]map[*ServerSession]bool // uri -> session -> bool - pendingNotifications map[string]*time.Timer // notification name -> timer for pending notification send + mu sync.Mutex + prompts *featureSet[*serverPrompt] + tools *featureSet[*serverTool] + resources *featureSet[*serverResource] + resourceTemplates *featureSet[*serverResourceTemplate] + sessions []*ServerSession + sendingMethodHandler_ MethodHandler + receivingMethodHandler_ MethodHandler + toolChangeSubscriptions map[*ServerSession]jsonrpc.ID // session -> requestID for "tools/changed" + promptChangeSubscriptions map[*ServerSession]jsonrpc.ID // session -> requestID for "prompts/changed" + resourceChangeSubscriptions map[*ServerSession]jsonrpc.ID // session -> requestID for "resources/changed" + resourceSubscriptions map[string]map[*ServerSession]jsonrpc.ID // uri -> session -> requestID + pendingNotifications map[string]*time.Timer // notification name -> timer for pending notification send + // receiveMethods is the merged map of methods this server may receive + // from a client: it always contains the standard server methods (from + // serverMethodInfos) plus any custom methods registered via + // [AddReceivingCustomMethod]. + receiveMethods map[string]methodInfo } // ServerOptions is used to configure behavior of the server. @@ -70,6 +79,12 @@ type ServerOptions struct { // If zero, defaults to [DefaultPageSize]. PageSize int // If non-nil, called when "notifications/roots/list_changed" is received. + // + // Deprecated: the roots feature is deprecated as of protocol version + // 2026-07-28 (SEP-2577). It remains functional during the deprecation + // window (at least twelve months). Migrate to passing paths via tool + // parameters, resource URIs, or configuration. See + // https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. RootsListChangedHandler func(context.Context, *RootsListChangedRequest) // If non-nil, called when "notifications/progress" is received. ProgressNotificationHandler func(context.Context, *ProgressNotificationServerRequest) @@ -79,6 +94,13 @@ type ServerOptions struct { // If the peer fails to respond to pings originating from the keepalive check, // the session is automatically closed. KeepAlive time.Duration + // KeepAliveFailureThreshold is the number of consecutive keepalive ping + // failures tolerated before the session is closed. A value of 0 or 1 + // closes the session on the first failure (the default). Higher values + // align with the spec's "multiple failed pings MAY trigger a connection + // reset" guidance, letting a transient miss pass without tearing down an + // otherwise live session. Has no effect unless KeepAlive is non-zero. + KeepAliveFailureThreshold int // Function called when a client session subscribes to a resource. SubscribeHandler func(context.Context, *SubscribeRequest) error // Function called when a client session unsubscribes from a resource. @@ -143,6 +165,9 @@ type ServerOptions struct { // // As a special case, if GetSessionID returns the empty string, the // Mcp-Session-Id header will not be set. + // + // GetSessionID is not consulted when [StreamableHTTPOptions.Stateless] is + // true, since stateless servers do not maintain sessions. GetSessionID func() string } @@ -184,18 +209,27 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server { opts.Logger = ensureLogger(nil) } - return &Server{ - impl: impl, - opts: opts, - prompts: newFeatureSet(func(p *serverPrompt) string { return p.prompt.Name }), - tools: newFeatureSet(func(t *serverTool) string { return t.tool.Name }), - resources: newFeatureSet(func(r *serverResource) string { return r.resource.URI }), - resourceTemplates: newFeatureSet(func(t *serverResourceTemplate) string { return t.resourceTemplate.URITemplate }), - sendingMethodHandler_: defaultSendingMethodHandler, - receivingMethodHandler_: defaultReceivingMethodHandler[*ServerSession], - resourceSubscriptions: make(map[string]map[*ServerSession]bool), - pendingNotifications: make(map[string]*time.Timer), + receiveMethods := make(map[string]methodInfo, len(serverMethodInfos)) + maps.Copy(receiveMethods, serverMethodInfos) + + s := &Server{ + impl: impl, + opts: opts, + prompts: newFeatureSet(func(p *serverPrompt) string { return p.prompt.Name }), + tools: newFeatureSet(func(t *serverTool) string { return t.tool.Name }), + resources: newFeatureSet(func(r *serverResource) string { return r.resource.URI }), + resourceTemplates: newFeatureSet(func(t *serverResourceTemplate) string { return t.resourceTemplate.URITemplate }), + sendingMethodHandler_: defaultSendingMethodHandler, + receivingMethodHandler_: defaultReceivingMethodHandler[*ServerSession], + toolChangeSubscriptions: make(map[*ServerSession]jsonrpc.ID), + promptChangeSubscriptions: make(map[*ServerSession]jsonrpc.ID), + resourceChangeSubscriptions: make(map[*ServerSession]jsonrpc.ID), + resourceSubscriptions: make(map[string]map[*ServerSession]jsonrpc.ID), + pendingNotifications: make(map[string]*time.Timer), + receiveMethods: receiveMethods, } + s.AddReceivingMiddleware(serverMultiRoundTripMiddleware()) + return s } // AddPrompt adds a [Prompt] to the server, or replaces one with the same name. @@ -220,7 +254,8 @@ func (s *Server) RemovePrompts(names ...string) { // that takes no input, or one where any input is valid, set [Tool.InputSchema] to // `{"type": "object"}`, using your preferred library or `json.RawMessage`. // -// If present, [Tool.OutputSchema] must also have type "object". +// If present, [Tool.OutputSchema] may be any valid JSON Schema (object, array, +// primitive, or composition). // // When the handler is invoked as part of a CallTool request, req.Params.Arguments // will be a json.RawMessage. @@ -247,6 +282,9 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) { panic(fmt.Errorf("AddTool %q: missing input schema", t.Name)) } if s, ok := t.InputSchema.(*jsonschema.Schema); ok { + if s == nil { + panic(fmt.Errorf("AddTool %q: input schema is nil", t.Name)) + } if s.Type != "object" { panic(fmt.Errorf(`AddTool %q: input schema must have type "object"`, t.Name)) } @@ -261,19 +299,19 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) { } if t.OutputSchema != nil { if s, ok := t.OutputSchema.(*jsonschema.Schema); ok { - if s.Type != "object" { - panic(fmt.Errorf(`AddTool %q: output schema must have type "object"`, t.Name)) + if s == nil { + panic(fmt.Errorf("AddTool %q: output schema is nil", t.Name)) } } else { - var m map[string]any + var m any if err := remarshal(t.OutputSchema, &m); err != nil { - panic(fmt.Errorf("AddTool %q: can't marshal output schema to a JSON object: %v", t.Name, err)) - } - if typ := m["type"]; typ != "object" { - panic(fmt.Errorf(`AddTool %q: output schema must have type "object" (got %v)`, t.Name, typ)) + panic(fmt.Errorf("AddTool %q: can't marshal output schema to JSON: %v", t.Name, err)) } } } + if err := validateParamHeaderAnnotations(t); err != nil { + panic(fmt.Errorf("AddTool %q: invalid parameter header annotations: %v", t.Name, err)) + } st := &serverTool{tool: t, handler: h} // Assume there was a change, since add replaces existing tools. // (It's possible a tool was replaced with an identical one, but not worth checking.) @@ -319,17 +357,20 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa } // Validate input and apply defaults. var err error - input, err = applySchema(input, inputResolved) + input, err = applySchema(input, inputResolved, false) if err != nil { - // TODO(#450): should this be considered a tool error? (and similar below) - return nil, fmt.Errorf("%w: validating \"arguments\": %v", jsonrpc2.ErrInvalidParams, err) + var errRes CallToolResult + errRes.SetError(fmt.Errorf("validating \"arguments\": %v", err)) + return &errRes, nil } // Unmarshal and validate args. var in In if input != nil { if err := internaljson.Unmarshal(input, &in); err != nil { - return nil, fmt.Errorf("%w: %v", jsonrpc2.ErrInvalidParams, err) + var errRes CallToolResult + errRes.SetError(err) + return &errRes, nil } } @@ -355,8 +396,12 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa } // Marshal the output and put the RawMessage in the StructuredContent field. + // Skip when the handler returned input requests (multi round-trip): content and + // inputRequests are mutually exclusive on the wire. var outval any = out - if elemZero != nil { + if res.InputRequests != nil { + outval = nil + } else if elemZero != nil { // Avoid typed nil, which will serialize as JSON null. // Instead, use the zero value of the unpointered type. var z Out @@ -374,7 +419,7 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa // // We validate against the JSON, rather than the output value, as // some types may have custom JSON marshalling (issue #447). - outJSON, err = applySchema(outJSON, outputResolved) + outJSON, err = applySchema(outJSON, outputResolved, true) if err != nil { return nil, fmt.Errorf("validating tool output: %w", err) } @@ -383,10 +428,18 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa // If the Content field isn't being used, return the serialized JSON in a // TextContent block, as the spec suggests: // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content. + // + // Ensure a serialized-JSON TextContent fallback is present in case of servers using array + // or primitive structuredContent, so that pre-SEP-2106 clients can recover the structured + // payload from unstructured content. if res.Content == nil { res.Content = []Content{&TextContent{ Text: string(outJSON), }} + } else if !isObjectJSON(outJSON) { + res.Content = append(res.Content, &TextContent{ + Text: string(outJSON), + }) } } return res, nil @@ -434,6 +487,9 @@ func setSchema[T any](sfield *any, rfield **jsonschema.Resolved, cache *SchemaCa if err != nil { return zero, err } + if internalSchema == nil { + return zero, fmt.Errorf("schema is nil for type %v", rt) + } *sfield = internalSchema resolved, err := internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true}) @@ -463,6 +519,10 @@ func setSchema[T any](sfield *any, rfield **jsonschema.Resolved, cache *SchemaCa } } + if internalSchema == nil { + return zero, fmt.Errorf("schema is nil for type %v", rt) + } + resolved, err := internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true}) if err != nil { return zero, err @@ -490,9 +550,10 @@ func setSchema[T any](sfield *any, rfield **jsonschema.Resolved, cache *SchemaCa // empty object schema value. // // If the tool's output schema is nil, and the Out type is not 'any', the -// output schema is set to the schema inferred from the Out type argument, -// which must also be a map or struct. If the Out type is 'any', the output -// schema is omitted. +// output schema is set to the schema inferred from the Out type argument. +// Per SEP-2106, the Out type may be any Go type whose inferred schema is a +// valid JSON Schema (struct, map, slice, primitive, etc.). If the Out type is +// 'any', the output schema is omitted. // // Unlike [Server.AddTool], AddTool does a lot automatically, and forces // tools to conform to the MCP spec. See [ToolHandlerFor] for a detailed @@ -601,19 +662,36 @@ func (s *Server) capabilities() *ServerCapabilities { return caps } +// disablecompleteparamsvalidation is a compatibility parameter that restores +// the previous behavior of [Server.complete], where required fields on +// [CompleteParams] were not validated before dispatching to the completion +// handler. See the documentation for the mcpgodebug package for instructions +// how to enable it. +// The option will be removed in a future version of the SDK. +var disablecompleteparamsvalidation = mcpgodebug.Value("disablecompleteparamsvalidation") + func (s *Server) complete(ctx context.Context, req *CompleteRequest) (*CompleteResult, error) { + if disablecompleteparamsvalidation != "1" { + if req.Params.Ref == nil { + return nil, fmt.Errorf("%w: missing required 'ref' field", jsonrpc2.ErrInvalidParams) + } + if req.Params.Argument.Name == "" { + return nil, fmt.Errorf("%w: missing required 'argument.name' field", jsonrpc2.ErrInvalidParams) + } + } if s.opts.CompletionHandler == nil { return nil, jsonrpc2.ErrMethodNotFound } return s.opts.CompletionHandler(ctx, req) } -// Map from notification name to its corresponding params. The params have no fields, -// so a single struct can be reused. -var changeNotificationParams = map[string]Params{ - notificationToolListChanged: &ToolListChangedParams{}, - notificationPromptListChanged: &PromptListChangedParams{}, - notificationResourceListChanged: &ResourceListChangedParams{}, +// Map from notification name to a function creating its corresponding Params. +// We need to create a fresh one each time to add the jsonrpc ID. See +// [injectMetaSubscriptionID]. +var changeNotificationParams = map[string]func() Params{ + notificationToolListChanged: func() Params { return &ToolListChangedParams{} }, + notificationPromptListChanged: func() Params { return &PromptListChangedParams{} }, + notificationResourceListChanged: func() Params { return &ResourceListChangedParams{} }, } // How long to wait before sending a change notification. @@ -628,6 +706,14 @@ func (s *Server) changeAndNotify(notification string, change func() bool) { s.mu.Lock() defer s.mu.Unlock() if change() && s.shouldSendListChangedNotification(notification) { + if len(s.sessions) == 0 { + if t := s.pendingNotifications[notification]; t != nil { + t.Stop() + s.pendingNotifications[notification] = nil + } + return + } + // Reset the outstanding delayed call, if any. if t := s.pendingNotifications[notification]; t == nil { s.pendingNotifications[notification] = time.AfterFunc(notificationDelay, func() { s.notifySessions(notification) }) @@ -639,12 +725,75 @@ func (s *Server) changeAndNotify(notification string, change func() bool) { // notifySessions sends the notification n to all existing sessions. // It is called asynchronously by changeAndNotify. +// +// Legacy (pre-SEP-2575) sessions receive the notification on the shared +// session channel. Sessions speaking the new protocol receive it only if they +// have an active subscriptions/listen stream that opted in to this +// notification type. func (s *Server) notifySessions(n string) { s.mu.Lock() - sessions := slices.Clone(s.sessions) s.pendingNotifications[n] = nil + // Legacy (pre-SEP-2575) sessions receive list-changed notifications on the + // shared session channel without opt-in; collect them while we hold the lock. + var legacySessions []*ServerSession + for _, sess := range s.sessions { + if sess.InitializeParams().isNil() || sess.InitializeParams().ProtocolVersion < protocolVersion20260728 { + legacySessions = append(legacySessions, sess) + } + } + var subscribers map[*ServerSession]jsonrpc.ID + switch n { + case notificationToolListChanged: + subscribers = maps.Clone(s.toolChangeSubscriptions) + case notificationPromptListChanged: + subscribers = maps.Clone(s.promptChangeSubscriptions) + case notificationResourceListChanged: + subscribers = maps.Clone(s.resourceChangeSubscriptions) + } s.mu.Unlock() // Don't hold the lock during notification: it causes deadlock. - notifySessions(sessions, n, changeNotificationParams[n], s.opts.Logger) + + // Notify legacy sessions on the shared session channel regardless of + // their subscription status. + notifySessions(legacySessions, n, changeNotificationParams[n](), s.opts.Logger) + + // Notify modern sessions only if they have an active subscription for + // this notification type. + s.notifySubscribedSessions(subscribers, n, changeNotificationParams[n]) +} + +// notifySubscribedSessions delivers a list-changed or resource-updated +// notification to each session in subscribers, stamping the session's +// listen-request ID into the params' _meta so the receiving client can demultiplex +// notifications belonging to different concurrent listens. +func (s *Server) notifySubscribedSessions(subscribers map[*ServerSession]jsonrpc.ID, method string, makeParams func() Params) { + if len(subscribers) == 0 { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + for sess, reqID := range subscribers { + params := makeParams() + injectMetaSubscriptionID(params, reqID) + req := newRequest(sess, params) + if err := handleNotify(ctx, method, req); err != nil { + s.opts.Logger.Warn(fmt.Sprintf("calling %s: %v", method, err)) + } + } +} + +// injectMetaSubscriptionID stamps the listen request's JSON-RPC ID into the +// params' _meta under [MetaKeySubscriptionID], so the receiving client can +// correlate the notification with the [subscriptions/listen] stream it +// originated. +// +// [subscriptions/listen]: https://modelcontextprotocol.io/seps/2575-stateless-mcp#multiple-concurrent-subscriptions +func injectMetaSubscriptionID(params Params, reqID jsonrpc.ID) { + m := params.GetMeta() + if m == nil { + m = map[string]any{} + } + m[MetaKeySubscriptionID] = reqID.Raw() + params.SetMeta(m) } // shouldSendListChangedNotification checks if the server's capabilities allow @@ -693,12 +842,17 @@ func (s *Server) listPrompts(_ context.Context, req *ListPromptsRequest) (*ListP if req.Params == nil { req.Params = &ListPromptsParams{} } - return paginateList(s.prompts, s.opts.PageSize, req.Params, &ListPromptsResult{}, func(res *ListPromptsResult, prompts []*serverPrompt) { + res, err := paginateList(s.prompts, s.opts.PageSize, req.Params, &ListPromptsResult{}, func(res *ListPromptsResult, prompts []*serverPrompt) { res.Prompts = []*Prompt{} // avoid JSON null for _, p := range prompts { res.Prompts = append(res.Prompts, p.prompt) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetPromptResult, error) { @@ -712,7 +866,64 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm Message: fmt.Sprintf("unknown prompt %q", req.Params.Name), } } - return prompt.handler(ctx, req) + res, err := prompt.handler(ctx, req) + if err == nil && res != nil { + if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil { + return nil, err + } + } + return res, err +} + +// discover is the server-side handler for the SEP-2575 "server/discover" RPC. +// +// It returns the protocol versions supported by the underlying transport, +// the server's capabilities, the server's identity, and the server's +// instructions, allowing clients to negotiate without performing the legacy +// initialize handshake. +func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams]) (*DiscoverResult, error) { + req.Session.mu.Lock() + versions := req.Session.supportedVersions + req.Session.mu.Unlock() + if versions == nil { + versions = slices.Clone(supportedProtocolVersions) + } + // Read the request-scoped identity/capabilities before acquiring the + // session lock: these accessors may fall back to Session.InitializeParams + // (which also locks Session.mu), so calling them from inside updateState + // would self-deadlock. + init := &InitializeParams{ + ProtocolVersion: req.ProtocolVersion(), + Capabilities: req.ClientCapabilities(), + ClientInfo: req.ClientInfo(), + } + req.Session.updateState(func(state *ServerSessionState) { + state.InitializeParams = init + }) + res := &DiscoverResult{ + SupportedVersions: versions, + Capabilities: s.capabilities(), + Instructions: s.opts.Instructions, + } + res.setDefaultCacheableValues() + return res, nil +} + +// filterSupportedVersions returns the subset of [supportedProtocolVersions] +// that the Transport can serve. If t does not implement [ProtocolVersionSupporter], every +// SDK-supported version is included. +func filterSupportedVersions(t Transport) []string { + pvs, ok := t.(ProtocolVersionSupporter) + if !ok { + return slices.Clone(supportedProtocolVersions) + } + out := make([]string, 0, len(supportedProtocolVersions)) + for _, v := range supportedProtocolVersions { + if pvs.SupportsProtocolVersion(v) { + out = append(out, v) + } + } + return out } func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListToolsResult, error) { @@ -721,18 +932,28 @@ func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListTools if req.Params == nil { req.Params = &ListToolsParams{} } - return paginateList(s.tools, s.opts.PageSize, req.Params, &ListToolsResult{}, func(res *ListToolsResult, tools []*serverTool) { + res, err := paginateList(s.tools, s.opts.PageSize, req.Params, &ListToolsResult{}, func(res *ListToolsResult, tools []*serverTool) { res.Tools = []*Tool{} // avoid JSON null for _, t := range tools { res.Tools = append(res.Tools, t.tool) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } -func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) { +// getServerTool looks up a server tool by name. +func (s *Server) getServerTool(name string) (*serverTool, bool) { s.mu.Lock() - st, ok := s.tools.get(req.Params.Name) - s.mu.Unlock() + defer s.mu.Unlock() + return s.tools.get(name) +} + +func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) { + st, ok := s.getServerTool(req.Params.Name) if !ok { return nil, &jsonrpc.Error{ Code: jsonrpc.CodeInvalidParams, @@ -740,10 +961,15 @@ func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolR } } res, err := st.handler(ctx, req) - if err == nil && res != nil && res.Content == nil { - res2 := *res - res2.Content = []Content{} // avoid "null" - res = &res2 + if err == nil && res != nil { + if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil { + return nil, err + } + if res.Content == nil && res.resultType != resultTypeInputRequired { + res2 := *res + res2.Content = []Content{} // avoid "null" + res = &res2 + } } return res, err } @@ -754,12 +980,17 @@ func (s *Server) listResources(_ context.Context, req *ListResourcesRequest) (*L if req.Params == nil { req.Params = &ListResourcesParams{} } - return paginateList(s.resources, s.opts.PageSize, req.Params, &ListResourcesResult{}, func(res *ListResourcesResult, resources []*serverResource) { + res, err := paginateList(s.resources, s.opts.PageSize, req.Params, &ListResourcesResult{}, func(res *ListResourcesResult, resources []*serverResource) { res.Resources = []*Resource{} // avoid JSON null for _, r := range resources { res.Resources = append(res.Resources, r.resource) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) listResourceTemplates(_ context.Context, req *ListResourceTemplatesRequest) (*ListResourceTemplatesResult, error) { @@ -768,13 +999,18 @@ func (s *Server) listResourceTemplates(_ context.Context, req *ListResourceTempl if req.Params == nil { req.Params = &ListResourceTemplatesParams{} } - return paginateList(s.resourceTemplates, s.opts.PageSize, req.Params, &ListResourceTemplatesResult{}, + res, err := paginateList(s.resourceTemplates, s.opts.PageSize, req.Params, &ListResourceTemplatesResult{}, func(res *ListResourceTemplatesResult, rts []*serverResourceTemplate) { res.ResourceTemplates = []*ResourceTemplate{} // avoid JSON null for _, rt := range rts { res.ResourceTemplates = append(res.ResourceTemplates, rt.resourceTemplate) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*ReadResourceResult, error) { @@ -791,6 +1027,13 @@ func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*R if err != nil { return nil, err } + if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil { + return nil, err + } + res.setDefaultCacheableValues() + if res.resultType == resultTypeInputRequired { + return res, nil + } if res == nil || res.Contents == nil { return nil, fmt.Errorf("reading resource %s: read handler returned nil information", uri) } @@ -834,7 +1077,7 @@ func (s *Server) lookupResourceHandler(uri string) (ResourceHandler, string, boo // and the current working directory is unavailable, fileResourceHandler panics. // // Lexical path traversal attacks, where the path has ".." elements that escape dir, -// are always caught. Go 1.24 and above also protects against symlink-based attacks, +// are always caught. The SDK also protects against symlink-based attacks, // where symlinks under dir lead out of the tree. func fileResourceHandler(dir string) ResourceHandler { // Convert dir to an absolute path. @@ -871,14 +1114,36 @@ func fileResourceHandler(dir string) ResourceHandler { func (s *Server) ResourceUpdated(ctx context.Context, params *ResourceUpdatedNotificationParams) error { s.mu.Lock() subscribedSessions := s.resourceSubscriptions[params.URI] - sessions := slices.Collect(maps.Keys(subscribedSessions)) + // Only add legacy sessions for the notification, new ones use the new notification mechanism. + var legacySessions []*ServerSession + newSessions := make(map[*ServerSession]jsonrpc.ID) + for sess, reqID := range subscribedSessions { + if sess.InitializeParams().isNil() || sess.InitializeParams().ProtocolVersion < protocolVersion20260728 { + legacySessions = append(legacySessions, sess) + } else { + newSessions[sess] = reqID + } + } s.mu.Unlock() - notifySessions(sessions, notificationResourceUpdated, params, s.opts.Logger) - s.opts.Logger.Info("resource updated notification sent", "uri", params.URI, "subscriber_count", len(sessions)) + + notifySessions(legacySessions, notificationResourceUpdated, params, s.opts.Logger) + + // Notify modern sessions, injecting the per-session subscription ID into the notification's metadata. + s.notifySubscribedSessions(newSessions, notificationResourceUpdated, func() Params { + p := *params + return &p + }) + s.opts.Logger.Info("resource updated notification sent", + "uri", params.URI, + "subscriber_count", len(legacySessions)+len(newSessions)) return nil } func (s *Server) subscribe(ctx context.Context, req *SubscribeRequest) (*emptyResult, error) { + requestID, ok := ctx.Value(idContextKey{}).(jsonrpc.ID) + if !ok || !requestID.IsValid() { + return nil, fmt.Errorf("%w: subscribe requires a request ID", jsonrpc2.ErrInvalidRequest) + } if s.opts.SubscribeHandler == nil { return nil, fmt.Errorf("%w: server does not support resource subscriptions", jsonrpc2.ErrMethodNotFound) } @@ -889,10 +1154,10 @@ func (s *Server) subscribe(ctx context.Context, req *SubscribeRequest) (*emptyRe s.mu.Lock() defer s.mu.Unlock() if s.resourceSubscriptions[req.Params.URI] == nil { - s.resourceSubscriptions[req.Params.URI] = make(map[*ServerSession]bool) + s.resourceSubscriptions[req.Params.URI] = make(map[*ServerSession]jsonrpc.ID) } - s.resourceSubscriptions[req.Params.URI][req.Session] = true - s.opts.Logger.Info("resource subscribed", "uri", req.Params.URI, "session_id", req.Session.ID()) + s.resourceSubscriptions[req.Params.URI][req.Session] = requestID + s.opts.Logger.Info("resource subscribed", "uri", req.Params.URI, "session_id", req.Session.ID(), "request_id", requestID) return &emptyResult{}, nil } @@ -919,6 +1184,91 @@ func (s *Server) unsubscribe(ctx context.Context, req *UnsubscribeRequest) (*emp return &emptyResult{}, nil } +func (s *Server) subscriptionsListen(ctx context.Context, req *SubscriptionsListenRequest) (*SubscriptionsListenResult, error) { + requestID, ok := ctx.Value(idContextKey{}).(jsonrpc.ID) + if !ok || !requestID.IsValid() { + return nil, fmt.Errorf("%w: subscriptions/listen requires a request ID", jsonrpc2.ErrInvalidRequest) + } + + if req.Params.Notifications == nil { + return nil, fmt.Errorf("%w: missing required 'notifications' field", jsonrpc2.ErrInvalidParams) + } + + allowed := s.allowedSubscriptions(req.Params.Notifications) + s.mu.Lock() + if allowed.ToolsListChanged { + s.toolChangeSubscriptions[req.Session] = requestID + } + if allowed.PromptsListChanged { + s.promptChangeSubscriptions[req.Session] = requestID + } + if allowed.ResourcesListChanged { + s.resourceChangeSubscriptions[req.Session] = requestID + } + s.mu.Unlock() + defer func() { + s.mu.Lock() + delete(s.toolChangeSubscriptions, req.Session) + delete(s.promptChangeSubscriptions, req.Session) + delete(s.resourceChangeSubscriptions, req.Session) + s.mu.Unlock() + }() + + for _, uri := range allowed.ResourceSubscriptions { + _, err := s.subscribe(ctx, &SubscribeRequest{ + Session: req.Session, + Params: &SubscribeParams{ + URI: uri, + Meta: req.Params.GetMeta(), + }, + }) + if err != nil { + return nil, err + } + defer s.unsubscribe(ctx, &UnsubscribeRequest{ + Session: req.Session, + Params: &UnsubscribeParams{ + URI: uri, + Meta: req.Params.GetMeta(), + }, + }) + } + + ackParams := &SubscriptionsAcknowledgedParams{ + Notifications: allowed, + Meta: Meta{MetaKeySubscriptionID: requestID.Raw()}, + } + if err := req.Session.notifySubscriptionAcked(ctx, ackParams); err != nil { + return nil, fmt.Errorf("sending subscriptions/acknowledged: %w", err) + } + + // If there are any active subscriptions, we block until the context is cancelled. Otherwise, we return immediately. + if len(allowed.ResourceSubscriptions) > 0 || allowed.ToolsListChanged || allowed.PromptsListChanged || allowed.ResourcesListChanged { + <-ctx.Done() + } + return &SubscriptionsListenResult{ + Meta: Meta{MetaKeySubscriptionID: requestID.Raw()}, + }, nil +} + +func (s *Server) allowedSubscriptions(want *NotificationSubscriptions) NotificationSubscriptions { + caps := s.capabilities() + agreed := NotificationSubscriptions{} + if want.ToolsListChanged && caps.Tools != nil && caps.Tools.ListChanged { + agreed.ToolsListChanged = true + } + if want.PromptsListChanged && caps.Prompts != nil && caps.Prompts.ListChanged { + agreed.PromptsListChanged = true + } + if want.ResourcesListChanged && caps.Resources != nil && caps.Resources.ListChanged { + agreed.ResourcesListChanged = true + } + if len(want.ResourceSubscriptions) > 0 && caps.Resources != nil && caps.Resources.Subscribe { + agreed.ResourceSubscriptions = slices.Clone(want.ResourceSubscriptions) + } + return agreed +} + // Run runs the server over the given transport, which must be persistent. // // Run blocks until the client terminates the connection or the provided @@ -988,6 +1338,10 @@ func (s *Server) disconnect(cc *ServerSession) { for _, subscribedSessions := range s.resourceSubscriptions { delete(subscribedSessions, cc) } + delete(s.toolChangeSubscriptions, cc) + delete(s.promptChangeSubscriptions, cc) + delete(s.resourceChangeSubscriptions, cc) + s.opts.Logger.Info("server session disconnected", "session_id", cc.ID()) } @@ -1015,11 +1369,33 @@ func (s *Server) Connect(ctx context.Context, t Transport, opts *ServerSessionOp } s.opts.Logger.Info("server connecting") - ss, err := connect(ctx, t, s, state, onClose) + ss, err := connect(ctx, t, s, state, onClose, s.opts.Logger) if err != nil { s.opts.Logger.Error("server connect error", "error", err) return nil, err } + + // Compute the protocol versions this session can serve, filtered by the + // transport's capabilities (if it implements [ProtocolVersionSupporter]). + // The list is consumed by the SEP-2575 server/discover handler. + // + // The write is guarded by ss.mu to establish a happens-before edge with + // the matching read in Server.discover, which runs on the jsonrpc2 read + // goroutine spawned inside connect(). The two are not concurrent in + // wall-clock terms (no incoming message is dispatched until the caller + // has fed the transport, which happens after Server.Connect returns), + // but without the lock the Go memory model gives the read goroutine no + // guarantee of seeing this write, and -race flags it. + ss.mu.Lock() + ss.supportedVersions = filterSupportedVersions(t) + ss.mu.Unlock() + + // Start keepalive before returning the session to avoid race conditions with Close. + // This is safe because the spec allows sending pings before initialization (see ServerSession.handle for details). + if s.opts.KeepAlive > 0 { + ss.startKeepalive(ss.server.opts.KeepAlive) + } + return ss, nil } @@ -1047,9 +1423,6 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar ss.server.opts.Logger.Error("duplicate initialized notification") return nil, fmt.Errorf("duplicate %q received", notificationInitialized) } - if ss.server.opts.KeepAlive > 0 { - ss.startKeepalive(ss.server.opts.KeepAlive) - } if h := ss.server.opts.InitializedHandler; h != nil { h(ctx, serverRequestFor(ss, params)) } @@ -1079,6 +1452,13 @@ func (ss *ServerSession) NotifyProgress(ctx context.Context, params *ProgressNot return handleNotify(ctx, notificationProgress, newServerRequest(ss, orZero[Params](params))) } +// notifySubscriptionAcked sends a "notifications/subscriptions/acknowledged" +// notification on the listen stream represented by this session, indicating +// the subscription filter the server accepted (SEP-2575). +func (ss *ServerSession) notifySubscriptionAcked(ctx context.Context, params *SubscriptionsAcknowledgedParams) error { + return handleNotify(ctx, notificationSubscriptionsAck, newServerRequest(ss, orZero[Params](params))) +} + func newServerRequest[P Params](ss *ServerSession, params P) *ServerRequest[P] { return &ServerRequest[P]{Session: ss, Params: params} } @@ -1099,7 +1479,13 @@ type ServerSession struct { server *Server conn *jsonrpc2.Connection mcpConn Connection - keepaliveCancel context.CancelFunc // TODO: theory around why keepaliveCancel need not be guarded + keepaliveCancel context.CancelFunc + + // supportedVersions is the subset of [supportedProtocolVersions] that the + // transport can actually serve, computed once at connection time from + // [ProtocolVersionSupporter] (if implemented by the transport) and used by + // the SEP-2575 server/discover handler. + supportedVersions []string mu sync.Mutex state ServerSessionState @@ -1149,6 +1535,23 @@ func (ss *ServerSession) ID() string { return "" } +// assertServerInitiatedRequestAllowed returns an error when the session is +// negotiated at protocol version >= 2026-07-28, where the spec (SEP-2322 / +// SEP-2575) forbids server-initiated JSON-RPC requests for elicitation, +// sampling, and roots: those interactions MUST be embedded as [InputRequests] +// in an [InputRequiredResult] returned from a handler for one of the multi +// round-trip methods (`tools/call`, `prompts/get`, `resources/read`). +func (ss *ServerSession) assertServerInitiatedRequestAllowed(method string) error { + if iparams := ss.InitializeParams(); iparams != nil && + iparams.ProtocolVersion >= protocolVersion20260728 { + return fmt.Errorf( + "%q cannot be sent while serving a request on protocol version %s: "+ + "return an InputRequests map instead (multi round-trip requests, SEP-2322)", + method, iparams.ProtocolVersion) + } + return nil +} + // Ping pings the client. func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error { _, err := handleSend[*emptyResult](ctx, methodPing, newServerRequest(ss, orZero[Params](params))) @@ -1156,10 +1559,19 @@ func (ss *ServerSession) Ping(ctx context.Context, params *PingParams) error { } // ListRoots lists the client roots. +// +// Deprecated: the roots feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to passing paths via tool parameters, +// resource URIs, or configuration. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams) (*ListRootsResult, error) { if err := ss.checkInitialized(methodListRoots); err != nil { return nil, err } + if err := ss.assertServerInitiatedRequestAllowed(methodListRoots); err != nil { + return nil, err + } return handleSend[*ListRootsResult](ctx, methodListRoots, newServerRequest(ss, orZero[Params](params))) } @@ -1168,10 +1580,19 @@ func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams) // If the client returns multiple content blocks (e.g. parallel tool calls), // CreateMessage returns an error. Use [ServerSession.CreateMessageWithTools] // for tool-enabled sampling. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to calling LLM provider APIs directly +// from your server. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessageParams) (*CreateMessageResult, error) { if err := ss.checkInitialized(methodCreateMessage); err != nil { return nil, err } + if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil { + return nil, err + } if params == nil { params = &CreateMessageParams{Messages: []*SamplingMessage{}} } @@ -1205,10 +1626,19 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag // returning a [CreateMessageWithToolsResult] that supports array content // (for parallel tool calls). Use this instead of [ServerSession.CreateMessage] // when the request includes tools. +// +// Deprecated: the sampling feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). Migrate to calling LLM provider APIs directly +// from your server. See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (ss *ServerSession) CreateMessageWithTools(ctx context.Context, params *CreateMessageWithToolsParams) (*CreateMessageWithToolsResult, error) { if err := ss.checkInitialized(methodCreateMessage); err != nil { return nil, err } + if err := ss.assertServerInitiatedRequestAllowed(methodCreateMessage); err != nil { + return nil, err + } if params == nil { params = &CreateMessageWithToolsParams{Messages: []*SamplingMessageV2{}} } @@ -1225,19 +1655,14 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli if err := ss.checkInitialized(methodElicit); err != nil { return nil, err } + if err := ss.assertServerInitiatedRequestAllowed(methodElicit); err != nil { + return nil, err + } if params == nil { return nil, fmt.Errorf("%w: params cannot be nil", jsonrpc2.ErrInvalidParams) } - if params.Mode == "" { - params2 := *params - if params.URL != "" || params.ElicitationID != "" { - params2.Mode = "url" - } else { - params2.Mode = "form" - } - params = ¶ms2 - } + params = params.inferElicitMode() if iparams := ss.InitializeParams(); iparams == nil || iparams.Capabilities == nil || iparams.Capabilities.Elicitation == nil { return nil, fmt.Errorf("client does not support elicitation") @@ -1261,7 +1686,7 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli return nil, err } - if res.Action != "accept" { + if res.Action != "accept" || res.Content == nil { return res, nil } @@ -1285,15 +1710,23 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli } err = resolved.ApplyDefaults(&res.Content) if err != nil { - return nil, fmt.Errorf("failed to apply schema defalts to elicitation result: %v", err) + return nil, fmt.Errorf("failed to apply schema defaults to elicitation result: %v", err) } return res, nil } // Log sends a log message to the client. -// The message is not sent if the client has not called SetLevel, or if its level -// is below that of the last SetLevel. +// +// For new-protocol (>= 2026-07-28) requests, the level is taken from the +// originating request's `_meta` field (SEP-2575); an absent or empty value +// suppresses the message per spec. For old-protocol requests, the level is +// taken from the session state set via `logging/setLevel`. +// +// Deprecated: the logging feature is deprecated as of protocol version +// 2026-07-28 (SEP-2577). It remains functional during the deprecation window +// (at least twelve months). See +// https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging. func (ss *ServerSession) Log(ctx context.Context, params *LoggingMessageParams) error { ss.mu.Lock() logLevel := ss.state.LogLevel @@ -1347,6 +1780,7 @@ func (s *Server) AddReceivingMiddleware(middleware ...Middleware) { // curating these method flags. var serverMethodInfos = map[string]methodInfo{ methodComplete: newServerMethodInfo(serverMethod((*Server).complete), 0), + methodDiscover: newServerMethodInfo(serverMethod((*Server).discover), missingParamsOK), methodInitialize: initializeMethodInfo(), methodPing: newServerMethodInfo(serverSessionMethod((*ServerSession).ping), missingParamsOK), methodListPrompts: newServerMethodInfo(serverMethod((*Server).listPrompts), missingParamsOK), @@ -1358,6 +1792,7 @@ var serverMethodInfos = map[string]methodInfo{ methodReadResource: newServerMethodInfo(serverMethod((*Server).readResource), 0), methodSetLevel: newServerMethodInfo(serverSessionMethod((*ServerSession).setLevel), 0), methodSubscribe: newServerMethodInfo(serverMethod((*Server).subscribe), 0), + methodSubscriptionsListen: newServerMethodInfo(serverMethod((*Server).subscriptionsListen), 0), methodUnsubscribe: newServerMethodInfo(serverMethod((*Server).unsubscribe), 0), notificationCancelled: newServerMethodInfo(serverSessionMethod((*ServerSession).cancel), notification|missingParamsOK), notificationInitialized: newServerMethodInfo(serverSessionMethod((*ServerSession).initialized), notification|missingParamsOK), @@ -1386,7 +1821,15 @@ func initializeMethodInfo() methodInfo { func (ss *ServerSession) sendingMethodInfos() map[string]methodInfo { return clientMethodInfos } -func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo { return serverMethodInfos } +func (s *Server) receivingMethodInfos() map[string]methodInfo { + s.mu.Lock() + defer s.mu.Unlock() + return s.receiveMethods +} + +func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo { + return ss.server.receivingMethodInfos() +} func (ss *ServerSession) sendingMethodHandler() MethodHandler { s := ss.server @@ -1411,16 +1854,57 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, initialized := ss.state.InitializeParams != nil ss.mu.Unlock() - // From the spec: - // "The client SHOULD NOT send requests other than pings before the server - // has responded to the initialize request." + // Per-request protocol detection (SEP-2575): if the request carries + // `io.modelcontextprotocol/protocolVersion` in its `_meta` field, it + // follows the new sessionless protocol. The initialization gate is + // skipped for such requests. + validatedMeta, perRequestErr := validateRequestMeta(req) + if perRequestErr != nil { + return nil, perRequestErr + } + + if validatedMeta.usesNewProtocol && + !slices.Contains(supportedProtocolVersions, validatedMeta.initializeParams.ProtocolVersion) { + data, _ := json.Marshal(UnsupportedProtocolVersionData{ + Supported: supportedProtocolVersions, + Requested: validatedMeta.initializeParams.ProtocolVersion, + }) + return nil, &jsonrpc.Error{ + Code: CodeUnsupportedProtocolVersion, + Message: "unsupported protocol version", + Data: data, + } + } + switch req.Method { - case methodInitialize, methodPing, notificationInitialized: + case methodInitialize, methodPing, notificationInitialized, notificationRootsListChanged, methodSetLevel, methodSubscribe, methodUnsubscribe: + if validatedMeta.usesNewProtocol { + ss.server.opts.Logger.Error("method removed in the new protocol", "method", req.Method) + return nil, &jsonrpc.Error{ + Code: jsonrpc.CodeMethodNotFound, + Message: fmt.Sprintf("%q is not supported in the new protocol", req.Method), + } + } + case methodDiscover: + // In case of methodDiscover call the state.initializeParams is populated + // within the discover handle function to make sure the method is supported + // when the user is probing a pre-2026-07-28 server. + if !validatedMeta.usesNewProtocol { + return nil, &jsonrpc.Error{ + Code: jsonrpc.CodeMethodNotFound, + Message: fmt.Sprintf("%q is only supported in protocol version >= %s", req.Method, protocolVersion20260728), + } + } default: - if !initialized { + if !initialized && !validatedMeta.usesNewProtocol { ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method) return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method) } + if !initialized && validatedMeta.usesNewProtocol && validatedMeta.initializeParams != nil { + ss.updateState(func(state *ServerSessionState) { + state.InitializeParams = validatedMeta.initializeParams + }) + } } // modelcontextprotocol/go-sdk#26: handle calls asynchronously, and @@ -1434,7 +1918,40 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, // server->client calls and notifications to the incoming request from which // they originated. See [idContextKey] for details. ctx = context.WithValue(ctx, idContextKey{}, req.ID) - return handleReceive(ctx, ss, req) + // For new-protocol requests, propagate the per-request log level. + if validatedMeta.usesNewProtocol { + ss.setLevel(ctx, &SetLoggingLevelParams{Level: validatedMeta.logLevel}) + } + res, err := handleReceive(ctx, ss, req) + if err != nil { + return nil, err + } + if validatedMeta.usesNewProtocol { + setCompleteResultType(res) + annotateServerInfo(res, ss.server.impl) + } + return res, nil +} + +// annotateServerInfo sets [MetaKeyServerInfo] on the result's `_meta` unless +// it is already present. Per SEP-2575, servers should identify themselves in +// each result's `_meta`. +func annotateServerInfo(res Result, impl *Implementation) { + if res == nil || impl == nil { + return + } + if _, isEmpty := res.(*emptyResult); isEmpty { + return + } + m := res.GetMeta() + if m == nil { + m = map[string]any{} + } + if _, ok := m[MetaKeyServerInfo]; ok { + return + } + m[MetaKeyServerInfo] = impl + res.SetMeta(m) } // InitializeParams returns the InitializeParams provided during the client's @@ -1449,9 +1966,17 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam if params == nil { return nil, fmt.Errorf("%w: \"params\" must be be provided", jsonrpc2.ErrInvalidParams) } + var wasInit bool ss.updateState(func(state *ServerSessionState) { - state.InitializeParams = params + wasInit = state.InitializeParams != nil + if !wasInit { + state.InitializeParams = params + } }) + if wasInit { + ss.server.opts.Logger.Error("duplicate initialize request") + return nil, fmt.Errorf("duplicate %q received", methodInitialize) + } s := ss.server return &InitializeResult{ @@ -1493,7 +2018,8 @@ func (ss *ServerSession) setLevel(_ context.Context, params *SetLoggingLevelPara func (ss *ServerSession) Close() error { if ss.keepaliveCancel != nil { // Note: keepaliveCancel access is safe without a mutex because: - // 1. keepaliveCancel is only written once during startKeepalive (happens-before all Close calls) + // 1. keepaliveCancel is only written once during Server.Connect (through startKeepalive), + // which happens before any code that may call Close from another goroutine // 2. context.CancelFunc is safe to call multiple times and from multiple goroutines // 3. The keepalive goroutine calls Close on ping failure, but this is safe since // Close is idempotent and conn.Close() handles concurrent calls correctly @@ -1515,7 +2041,7 @@ func (ss *ServerSession) Wait() error { // startKeepalive starts the keepalive mechanism for this server session. func (ss *ServerSession) startKeepalive(interval time.Duration) { - startKeepalive(ss, interval, &ss.keepaliveCancel) + startKeepalive(ss, interval, ss.server.opts.KeepAliveFailureThreshold, &ss.keepaliveCancel, ss.server.opts.Logger) } // pageToken is the internal structure for the opaque pagination cursor. @@ -1593,3 +2119,55 @@ func paginateList[P listParams, R listResult[T], T any](fs *featureSet[T], pageS *res.nextCursorPtr() = nextCursor return res, nil } + +// AddReceivingCustomMethod registers a handler for a custom (non-standard) +// JSON-RPC method on the server. +// +// When a client sends a request with the given method name, the params will be +// unmarshaled into P, the handler will be called, and the returned R will be +// marshaled as the JSON-RPC result. +// +// Custom methods go through the server's middleware chain just like standard +// MCP methods (tools/call, prompts/list, etc.). +// +// P and R must implement [Params] and [Result] respectively, which is most +// easily done by embedding [ParamsBase] and [ResultBase]: +// +// type SearchParams struct { +// mcp.ParamsBase +// Query string `json:"query"` +// } +// +// type SearchResult struct { +// mcp.ResultBase +// Hits []string `json:"hits"` +// } +// +// if err := mcp.AddReceivingCustomMethod(server, "acme/search", +// func(ctx context.Context, ss *mcp.ServerSession, params *SearchParams) (*SearchResult, error) { +// return &SearchResult{Hits: []string{"result"}}, nil +// }); err != nil { +// return err +// } +// +// AddReceivingCustomMethod returns an error if method is the name of a +// standard MCP method. Registering the same custom method twice replaces the +// previous handler. +func AddReceivingCustomMethod[P paramsPtr[T], R Result, T any]( + s *Server, + method string, + handler func(ctx context.Context, ss *ServerSession, params P) (R, error), +) error { + if _, ok := serverMethodInfos[method]; ok { + return fmt.Errorf("mcp: AddReceivingCustomMethod: %q shadows a standard MCP method", method) + } + + typed := typedServerMethodHandler[P, R](func(ctx context.Context, req *ServerRequest[P]) (R, error) { + return handler(ctx, req.Session, req.Params) + }) + + s.mu.Lock() + defer s.mu.Unlock() + s.receiveMethods[method] = newServerMethodInfo(typed, missingParamsOK) + return nil +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go index bda00c203..5069a470e 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go @@ -14,6 +14,7 @@ package mcp import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "net/http" @@ -25,23 +26,37 @@ import ( "github.com/modelcontextprotocol/go-sdk/auth" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" + "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" "github.com/modelcontextprotocol/go-sdk/jsonrpc" ) +// nowrapinvalidparams is a compatibility parameter that restores the previous +// behavior of [methodInfo.unmarshalParams]. When unset (the default), a +// params-decoding failure is wrapped with [jsonrpc2.ErrInvalidParams] so the +// wire response carries error code -32602 ("invalid params") rather than the +// zero-value code 0. See: +// https://github.com/modelcontextprotocol/go-sdk/issues/976#issuecomment-4829124838. +// +// See the documentation for the mcpgodebug package for instructions how to enable it. +// The option will be removed in a future version of the SDK. +var nowrapinvalidparams = mcpgodebug.Value("nowrapinvalidparams") + const ( // latestProtocolVersion is the latest protocol version that this version of // the SDK supports. // // It is the version that the client sends in the initialization request, and // the default version used by the server. - latestProtocolVersion = protocolVersion20250618 - protocolVersion20251125 = "2025-11-25" // not yet released + latestProtocolVersion = protocolVersion20260728 + protocolVersion20260728 = "2026-07-28" + protocolVersion20251125 = "2025-11-25" protocolVersion20250618 = "2025-06-18" protocolVersion20250326 = "2025-03-26" protocolVersion20241105 = "2024-11-05" ) var supportedProtocolVersions = []string{ + protocolVersion20260728, protocolVersion20251125, protocolVersion20250618, protocolVersion20250326, @@ -54,12 +69,24 @@ func negotiatedVersion(clientVersion string) string { // In general, prefer to use the clientVersion, but if we don't support the // client's version, use the latest version. // - // This handles the case where a new spec version is released, and the SDK - // does not support it yet. - if !slices.Contains(supportedProtocolVersions, clientVersion) { - return latestProtocolVersion + // Cap the supported versions at the legacy protocolVersion20251125, as this + // method is used by the initialize method which is deprecated in + // version protocolVersion20260728. + if slices.Contains(supportedProtocolVersions, clientVersion) && clientVersion < protocolVersion20260728 { + return clientVersion } - return clientVersion + return protocolVersion20251125 +} + +// negotiateMutuallySupportedVersion returns a protocol version that is supported +// by both the client and the server. +func negotiateMutuallySupportedVersion(supported []string) string { + for _, ver := range supportedProtocolVersions { + if slices.Contains(supported, ver) { + return ver + } + } + return "" } // A MethodHandler handles MCP messages. @@ -110,8 +137,12 @@ func defaultSendingMethodHandler(ctx context.Context, method string, req Request // Create the result to unmarshal into. // The concrete type of the result is the return type of the receiving function. res := info.newResult() - if err := call(ctx, req.GetSession().getConn(), method, params, res); err != nil { - return nil, err + if method == methodSubscriptionsListen { + callSubscriptionsListen(ctx, req.GetSession().getConn(), method, params) + } else { + if err := call(ctx, req.GetSession().getConn(), method, params, res); err != nil { + return nil, err + } } return res, nil } @@ -285,7 +316,17 @@ func newMethodInfo[P paramsPtr[T], R Result, T any](flags methodFlags) methodInf var p P if m != nil { if err := internaljson.Unmarshal(m, &p); err != nil { - return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, p, err) + // Legacy behavior: pre-fix versions surfaced this as a + // plain wrapped error, which caused the wire response to + // carry code 0 instead of -32602. Restore via + // MCPGODEBUG=nowrapinvalidparams=1. + if nowrapinvalidparams == "1" { + return nil, fmt.Errorf("unmarshaling %q into a %T: %w", m, p, err) + } + // Wrap jsonrpc2.ErrInvalidParams so toWireError surfaces + // code -32602 ("invalid params") while preserving the + // descriptive message. + return nil, fmt.Errorf("%w: unmarshaling %q into a %T: %w", jsonrpc2.ErrInvalidParams, m, p, err) } } // We must check missingParamsOK here, in addition to checkRequest, to @@ -342,14 +383,31 @@ func clientSessionMethod[P Params, R Result](f func(*ClientSession, context.Cont // MCP-specific error codes. const ( - // CodeResourceNotFound indicates that a requested resource could not be found. - CodeResourceNotFound = -32002 + // CodeHeaderMismatch indicates that HTTP headers do not match the corresponding values + // in the request body, or that required headers are missing or malformed. + CodeHeaderMismatch = -32020 + // CodeMissingRequiredClientCapabilities is the JSON-RPC error code defined by + // SEP-2575 for MissingRequiredClientCapabilitiesError. + CodeMissingRequiredClientCapabilities = -32021 + // CodeUnsupportedProtocolVersion is the JSON-RPC error code defined by + // SEP-2575 for UnsupportedProtocolVersionError. + CodeUnsupportedProtocolVersion = -32022 // CodeURLElicitationRequired indicates that the server requires URL elicitation // before processing the request. The client should execute the elicitation handler // with the elicitations provided in the error data. CodeURLElicitationRequired = -32042 ) +// CodeResourceNotFound indicates that a requested resource could not be found. +// +// By default, the value is -32602 (Invalid Params), as specified in the +// MCP specification (SEP-2164). To restore the pre-1.7.0 release behavior where the +// error code was -32002, set MCPGODEBUG=customresnotfounderrcode=1. +// +// Deprecated: Use [jsonrpc.CodeInvalidParams] directly. This variable will be +// removed in a future version. +var CodeResourceNotFound int64 = jsonrpc.CodeInvalidParams + // URLElicitationRequiredError returns an error indicating that URL elicitation is required // before the request can be processed. The elicitations parameter should contain the // elicitation requests that must be completed. @@ -447,10 +505,76 @@ func setProgressToken(p Params, pt any) { m := p.GetMeta() if m == nil { m = map[string]any{} + p.SetMeta(m) } m[progressTokenKey] = pt } +// extractRequestMeta performs a lightweight partial unmarshal of the `_meta` +// field from a JSON-RPC request's raw params. +func extractRequestMeta(rawParams json.RawMessage) Meta { + if len(rawParams) == 0 { + return nil + } + var meta struct { + Meta Meta `json:"_meta"` + } + if err := internaljson.Unmarshal(rawParams, &meta); err != nil { + return nil + } + return meta.Meta +} + +type validatedMeta struct { + usesNewProtocol bool + initializeParams *InitializeParams + logLevel LoggingLevel +} + +// validateRequestMeta inspects a JSON-RPC request to detect whether it follows +// the >= 2026-07-28 protocol via the `_meta` field. +// If the request has no _meta, or no protocolVersion in _meta, it returns a non-nil +// validatedMeta with usesNewProtocol set to false, and a nil error. +// If the request has a protocolVersion in _meta it validates the presence of +// clientCapabilities in _meta. If it is missing or invalid, it returns nil and +// a non-nil error. clientInfo is optional; if present but invalid, an error is +// returned. Otherwise, it returns usesNewProtocol set to true and the populated +// initializeParams. +func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { + meta := extractRequestMeta(req.Params) + if meta == nil { + return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil + } + protocolVersion, ok := meta[MetaKeyProtocolVersion].(string) + if !ok || protocolVersion < protocolVersion20260728 { + return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil + } + var clientInfo *Implementation + if _, present := meta[MetaKeyClientInfo]; present { + var ok bool + clientInfo, ok = decodeMetaValue[*Implementation](meta, MetaKeyClientInfo) + if !ok { + return nil, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidParams, + Message: fmt.Sprintf("invalid _meta field %q", MetaKeyClientInfo), + } + } + } + capabilities, ok := decodeMetaValue[*clientCapabilitiesV2](meta, MetaKeyClientCapabilities) + if !ok { + return nil, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidParams, + Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientCapabilities), + } + } + logLevel, _ := decodeMetaValue[LoggingLevel](meta, MetaKeyLogLevel) + return &validatedMeta{usesNewProtocol: true, initializeParams: &InitializeParams{ + ProtocolVersion: protocolVersion, + Capabilities: capabilities.toV1(), + ClientInfo: clientInfo, + }, logLevel: logLevel}, nil +} + // A Request is a method request with parameters and additional information, such as the session. // Request is implemented by [*ClientRequest] and [*ServerRequest]. type Request interface { @@ -489,6 +613,8 @@ type RequestExtra struct { // to configure the reconnection delay. // // [SEP-1699]: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699 + // This mechanism is deprecated in protocol version 2026-07-28 as the resumability + // feature is removed. CloseSSEStream func(CloseSSEStreamArgs) } @@ -511,6 +637,94 @@ func (r *ServerRequest[P]) GetParams() Params { return r.Params } func (r *ClientRequest[P]) GetExtra() *RequestExtra { return nil } func (r *ServerRequest[P]) GetExtra() *RequestExtra { return r.Extra } +// ProtocolVersion returns the protocol version negotiated for this request. +// +// For requests following the >= 2026-07-28 protocol, the value is read from +// the per-request `_meta` field. For older protocol requests, the value falls +// back to the session-level [InitializeParams] established during the +// initialize handshake. +func (r *ServerRequest[P]) ProtocolVersion() string { + if m := getRequestMeta(r); m != nil { + if v, ok := m[MetaKeyProtocolVersion].(string); ok { + return v + } + } + if r.Session != nil { + if p := r.Session.InitializeParams(); p != nil { + return p.ProtocolVersion + } + } + return "" +} + +// ClientInfo returns the [Implementation] identifying the calling client. +// +// For requests following the >= 2026-07-28 protocol, the value is read from +// the per-request `_meta` field. For older protocol requests, the value falls +// back to the session-level [InitializeParams]. +func (r *ServerRequest[P]) ClientInfo() *Implementation { + if m := getRequestMeta(r); m != nil { + if v, ok := decodeMetaValue[*Implementation](m, MetaKeyClientInfo); ok { + return v + } + } + if r.Session != nil { + if p := r.Session.InitializeParams(); p != nil { + return p.ClientInfo + } + } + return nil +} + +// ClientCapabilities returns the [ClientCapabilities] of the calling client. +// +// For requests following the >= 2026-07-28 protocol, the value is read from +// the per-request `_meta` field. For older protocol requests, the value falls +// back to the session-level [InitializeParams]. +func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities { + if m := getRequestMeta(r); m != nil { + if v, ok := decodeMetaValue[*clientCapabilitiesV2](m, MetaKeyClientCapabilities); ok { + return v.toV1() + } + } + if r.Session != nil { + if p := r.Session.InitializeParams(); p != nil { + return p.Capabilities + } + } + return nil +} + +// getRequestMeta returns the raw `_meta` map from the request's params, or +// nil if the params are absent. +func getRequestMeta[P Params](r *ServerRequest[P]) map[string]any { + // In practice P is a pointer type implementing Params. + if any(r.Params) == nil || r.Params.isNil() { + return nil + } + return r.Params.GetMeta() +} + +// decodeMetaValue decodes a typed value out of a `_meta` map. Values may +// arrive either as the typed Go value (when constructed in-process) or as +// the generic JSON map produced by encoding/json after wire transit. In the +// latter case, the value is re-encoded and decoded into the target type. +func decodeMetaValue[T any](m map[string]any, key string) (T, bool) { + var zero T + raw, ok := m[key] + if !ok || raw == nil { + return zero, false + } + if v, ok := raw.(T); ok { + return v, true + } + var v T + if err := remarshal(raw, &v); err != nil { + return zero, false + } + return v, true +} + func serverRequestFor[P Params](s *ServerSession, p P) *ServerRequest[P] { return &ServerRequest[P]{Session: s, Params: p} } @@ -528,8 +742,27 @@ type Params interface { // isParams discourages implementation of Params outside of this package. isParams() + + // isNil returns true if the underlying value is nil. + isNil() bool } +// ParamsBase can be embedded in custom parameter structs to satisfy the +// [Params] interface. It provides the required [Meta] field and the unexported +// isParams marker method. +// +// type SearchParams struct { +// mcp.ParamsBase +// Query string `json:"query"` +// } +type ParamsBase struct { + Meta `json:"_meta,omitempty"` +} + +func (*ParamsBase) isParams() {} + +func (p *ParamsBase) isNil() bool { return p == nil } + // RequestParams is a parameter (input) type for an MCP request. type RequestParams interface { Params @@ -554,6 +787,20 @@ type Result interface { SetMeta(map[string]any) } +// ResultBase can be embedded in custom result structs to satisfy the +// [Result] interface. It provides the required [Meta] field and the unexported +// isResult marker method. +// +// type SearchResult struct { +// mcp.ResultBase +// Hits []string `json:"hits"` +// } +type ResultBase struct { + Meta `json:"_meta,omitempty"` +} + +func (*ResultBase) isResult() {} + // emptyResult is returned by methods that have no result, like ping. // Those methods cannot return nil, because jsonrpc2 cannot handle nils. type emptyResult struct{} @@ -581,7 +828,21 @@ type keepaliveSession interface { // startKeepalive starts the keepalive mechanism for a session. // It assigns the cancel function to the provided cancelPtr and starts a goroutine // that sends ping messages at the specified interval. -func startKeepalive(session keepaliveSession, interval time.Duration, cancelPtr *context.CancelFunc) { +// +// failureThreshold is the number of consecutive ping failures tolerated before +// the session is closed; a value below 1 is treated as 1 (close on the first +// failure). A successful ping resets the counter. This mirrors the spec's +// "multiple failed pings MAY trigger a connection reset" language, letting a +// transient miss pass without tearing down an otherwise live session. +// +// logger must be non-nil; ping failures (both the tolerated ones and the final +// one that closes the session) are reported via logger so they are not silently +// dropped. +func startKeepalive(session keepaliveSession, interval time.Duration, failureThreshold int, cancelPtr *context.CancelFunc, logger *slog.Logger) { + if failureThreshold < 1 { + failureThreshold = 1 + } + ctx, cancel := context.WithCancel(context.Background()) // Assign cancel function before starting goroutine to avoid race condition. // We cannot return it because the caller may need to cancel during the @@ -592,6 +853,7 @@ func startKeepalive(session keepaliveSession, interval time.Duration, cancelPtr ticker := time.NewTicker(interval) defer ticker.Stop() + consecutiveFailures := 0 for { select { case <-ctx.Done(): @@ -600,11 +862,32 @@ func startKeepalive(session keepaliveSession, interval time.Duration, cancelPtr pingCtx, pingCancel := context.WithTimeout(context.Background(), interval/2) err := session.Ping(pingCtx, nil) pingCancel() - if err != nil { - // Ping failed, close the session - _ = session.Close() + if err == nil { + consecutiveFailures = 0 + continue + } + if errors.Is(err, jsonrpc2.ErrMethodNotFound) { + // Peer doesn't support ping, stop the keepalive process. return } + consecutiveFailures++ + if consecutiveFailures < failureThreshold { + // Tolerate transient failures below the threshold; log so + // the misses are still observable to operators. See #218. + logger.Warn("keepalive ping failed; tolerating below threshold", + "error", err, + "consecutiveFailures", consecutiveFailures, + "failureThreshold", failureThreshold) + continue + } + // Threshold reached; log before closing the session so the + // failure is observable to operators. See #218. + logger.Error("keepalive ping failed; closing session", + "error", err, + "consecutiveFailures", consecutiveFailures, + "failureThreshold", failureThreshold) + _ = session.Close() + return } } }() diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/sse.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/sse.go index e57dad102..7efaa95f5 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/sse.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/sse.go @@ -10,11 +10,14 @@ import ( "crypto/rand" "fmt" "io" + "mime" + "net" "net/http" "net/url" "sync" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" + "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/jsonrpc" ) @@ -52,9 +55,17 @@ type SSEHandler struct { } // SSEOptions specifies options for an [SSEHandler]. -// for now, it is empty, but may be extended in future. -// https://github.com/modelcontextprotocol/go-sdk/issues/507 -type SSEOptions struct{} +type SSEOptions struct { + // DisableLocalhostProtection disables automatic DNS rebinding protection. + // By default, requests arriving via a localhost address (127.0.0.1, [::1]) + // that have a non-localhost Host header are rejected with 403 Forbidden. + // This protects against DNS rebinding attacks regardless of whether the + // server is listening on localhost specifically or on 0.0.0.0. + // + // Only disable this if you understand the security implications. + // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise + DisableLocalhostProtection bool +} // NewSSEHandler returns a new [SSEHandler] that creates and manages MCP // sessions created via incoming HTTP requests. @@ -179,9 +190,27 @@ func (t *SSEServerTransport) Connect(context.Context) (Connection, error) { } func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - sessionID := req.URL.Query().Get("sessionid") + // DNS rebinding protection: auto-enabled for localhost servers. + // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise + if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" { + if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil { + if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { + http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden) + return + } + } + } - // TODO: consider checking Content-Type here. For now, we are lax. + // Validate 'Content-Type' header. + if disablecontenttypecheck != "1" && req.Method == http.MethodPost { + mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) + if err != nil || mediaType != "application/json" { + http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) + return + } + } + + sessionID := req.URL.Query().Get("sessionid") // For POST requests, the message body is a message to send to a session. if req.Method == http.MethodPost { diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go index 16bca0702..f642db1db 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go @@ -20,12 +20,14 @@ import ( "maps" "math" "math/rand/v2" + "mime" "net" "net/http" "slices" "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/modelcontextprotocol/go-sdk/auth" @@ -35,12 +37,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/internal/xcontext" "github.com/modelcontextprotocol/go-sdk/jsonrpc" -) - -const ( - protocolVersionHeader = "Mcp-Protocol-Version" - sessionIDHeader = "Mcp-Session-Id" - lastEventIDHeader = "Last-Event-ID" + "golang.org/x/oauth2" ) // A StreamableHTTPHandler is an http.Handler that serves streamable MCP @@ -96,7 +93,7 @@ func (i *sessionInfo) startPOST() { i.refs++ } -// endPOST sigals that a request for this session is ending, starting the +// endPOST signals that a request for this session is ending, starting the // timeout if there are no other requests running. func (i *sessionInfo) endPOST() { if i.timeout <= 0 { @@ -131,12 +128,21 @@ func (i *sessionInfo) stopTimer() { type StreamableHTTPOptions struct { // Stateless controls whether the session is 'stateless'. // - // A stateless server does not validate the Mcp-Session-Id header, and uses a - // temporary session with default initialization parameters. Any + // A stateless server does not read or set the Mcp-Session-Id header, and + // uses a temporary session with default initialization parameters for each + // request. [ServerOptions.GetSessionID] is not consulted. Any // server->client request is rejected immediately as there's no way for the // client to respond. Server->Client notifications may reach the client if // they are made in the context of an incoming request, as described in the // documentation for [StreamableServerTransport]. + // In Stateless mode, GET and DELETE requests return 405 Method Not Allowed. + // + // This mode aligns with the sessionless direction of the MCP spec; see + // [SEP-2567]. The previous behavior, in which stateless servers still + // honored Mcp-Session-Id, can be restored temporarily via the + // MCPGODEBUG compatibility parameter "allowsessionsinstateless=1". + // + // [SEP-2567]: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567 Stateless bool // TODO(#148): support session retention (?) @@ -178,12 +184,46 @@ type StreamableHTTPOptions struct { // CrossOriginProtection allows to customize cross-origin protection. // The deny handler set in the CrossOriginProtection through SetDenyHandler // is ignored. - // If nil, default (zero-value) cross-origin protection will be used. - // Use `disablecrossoriginprotection` MCPGODEBUG compatibility parameter - // to disable the default protection until v1.6.0. + // If nil, no cross-origin protection is applied. Use the `enableoriginverification` + // MCPGODEBUG compatibility parameter to enable the default protection until v1.8.0. + // + // Deprecated: wrap the handler with cross-origin protection middleware + // instead. For example: + // + // handler := mcp.NewStreamableHTTPHandler(...) + // protection := http.NewCrossOriginProtection() + // protectedHandler := protection.Handler(handler) CrossOriginProtection *http.CrossOriginProtection + + // MaxRequestBodyBytes limits the number of bytes read from any incoming + // HTTP request body. Requests that exceed this limit are rejected with + // 413 Request Entity Too Large. + // + // The limit is enforced during the read, so it applies uniformly to + // requests using Content-Length, Transfer-Encoding: chunked, or HTTP/2 + // (which has no Content-Length). + // + // If zero, [DefaultMaxRequestBodyBytes] is used. + // A negative value disables the limit entirely; do not use this on + // servers exposed to untrusted clients. + MaxRequestBodyBytes int64 + + // PropagateRequestCancellation, when true, ties the in-flight handler + // context to the originating HTTP request's context. Only applies to + // requests using the >= 2026-07-28 protocol, where the POST is the whole + // request lifecycle. + // The handler context cancels whenever the HTTP request context does as the + // response can no longer be delivered, so cancelling the handler is safe. + // + // Requests using older protocol versions (including those routed through + // the allowsessionsinstateless compatibility path) are unaffected. + PropagateRequestCancellation bool } +// DefaultMaxRequestBodyBytes is the default value used for +// [StreamableHTTPOptions.MaxRequestBodyBytes] when it is left at zero. +const DefaultMaxRequestBodyBytes = 4 << 20 // 4 MiB + // NewStreamableHTTPHandler returns a new [StreamableHTTPHandler]. // // The getServer function is used to create or look up servers for new @@ -200,10 +240,14 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea h.opts.Logger = ensureLogger(h.opts.Logger) - if h.opts.CrossOriginProtection == nil { + if h.opts.CrossOriginProtection == nil && enableoriginverification == "1" { h.opts.CrossOriginProtection = &http.CrossOriginProtection{} } + if h.opts.MaxRequestBodyBytes == 0 { + h.opts.MaxRequestBodyBytes = DefaultMaxRequestBodyBytes + } + return h } @@ -233,15 +277,49 @@ func (h *StreamableHTTPHandler) closeAll() { // disablelocalhostprotection is a compatibility parameter that allows to disable // DNS rebinding protection, which was added in the 1.4.0 version of the SDK. // See the documentation for the mcpgodebug package for instructions how to enable it. -// The option will be removed in the 1.6.0 version of the SDK. +// The option will be removed in the 1.8.0 version of the SDK. var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection") -// disablecrossoriginprotection is a compatibility parameter that allows to disable -// the verification of the 'Origin' and 'Content-Type' headers, which was added in -// the 1.4.1 version of the SDK. See the documentation for the mcpgodebug package -// for instructions how to enable it. -// The option will be removed in the 1.6.0 version of the SDK. -var disablecrossoriginprotection = mcpgodebug.Value("disablecrossoriginprotection") +// enableoriginverification is a compatibility parameter that restores the +// default cross-origin protection behavior from v1.4.1-v1.5.0. When set to +// "1", a zero-value CrossOriginProtection will be applied if none is +// explicitly provided in StreamableHTTPOptions. +// See the documentation for the mcpgodebug package for instructions how to enable it. +// The option will be removed in the 1.8.0 version of the SDK. +var enableoriginverification = mcpgodebug.Value("enableoriginverification") + +// allowsessionsinstateless is a compatibility parameter that restores the old +// behavior of reading and using Mcp-Session-Id headers in stateless mode. When +// set to "1", stateless servers will read the session ID from the request +// header (or generate one via GetSessionID), set it on response headers, and +// accept DELETE requests. When unset (the default), stateless servers ignore +// session IDs entirely and reject DELETE with 405. +// See the documentation for the mcpgodebug package for instructions how to enable it. +// The option will be removed in the 1.9.0 version of the SDK. +var allowsessionsinstateless = mcpgodebug.Value("allowsessionsinstateless") + +// noprotocolerrorbody is a compatibility parameter that restores the previous +// behavior of [streamableClientConn.checkResponse]. When unset (the default), +// the client always attempts to surface the underlying JSON-RPC error. +var noprotocolerrorbody = mcpgodebug.Value("noprotocolerrorbody") + +// disablecontenttypecheck is a compatibility parameter that allows to disable +// Content-Type validation on POST requests. +// See the documentation for the mcpgodebug package for instructions how to enable it. +// The option will be removed in the 1.8.0 version of the SDK. +var disablecontenttypecheck = mcpgodebug.Value("disablecontenttypecheck") + +// writeJSONRPCError writes a JSON-RPC error response with the given HTTP +// status code, request ID (may be a zero ID for errors that occur before the +// request body has been parsed), and JSON-RPC error. +func writeJSONRPCError(w http.ResponseWriter, status int, id jsonrpc.ID, jerr *jsonrpc.Error) { + resp := &jsonrpc.Response{ID: id, Error: jerr} + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if data, err := jsonrpc2.EncodeMessage(resp); err == nil { + w.Write(data) + } +} func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // DNS rebinding protection: auto-enabled for localhost servers. @@ -255,301 +333,440 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque } } - if disablecrossoriginprotection != "1" { - // Verify the 'Origin' header to protect against CSRF attacks. + if h.opts.CrossOriginProtection != nil { if err := h.opts.CrossOriginProtection.Check(req); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } - // Validate 'Content-Type' header. - if req.Method == http.MethodPost { - contentType := req.Header.Get("Content-Type") - if contentType != "application/json" { - http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) - return - } - } } - // Allow multiple 'Accept' headers. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax - accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",") - var jsonOK, streamOK bool - for _, c := range accept { - switch strings.TrimSpace(c) { - case "application/json", "application/*": - jsonOK = true - case "text/event-stream", "text/*": - streamOK = true - case "*/*": - jsonOK = true - streamOK = true - } + // Bound the request body to protect against OOM attacks. + if req.Body != nil && h.opts.MaxRequestBodyBytes > 0 { + req.Body = http.MaxBytesReader(w, req.Body, h.opts.MaxRequestBodyBytes) } - if req.Method == http.MethodGet { - if !streamOK { - http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest) - return - } - } else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below. + // [§2.7] of the spec (2025-06-18): validate the MCP-Protocol-Version + // header. If provided, it must be a supported version. If absent, the + // version is unknown (the request may be an initialize for any version). + // + // [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header + protocolVersion := req.Header.Get(protocolVersionHeader) + if protocolVersion != "" && !slices.Contains(supportedProtocolVersions, protocolVersion) && protocolVersion < protocolVersion20260728 { + http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest) + return + } + req = req.WithContext(context.WithValue(req.Context(), protocolVersionContextKey{}, protocolVersion)) + + if h.opts.Stateless { + h.serveStateless(w, req) + } else { + h.serveStateful(w, req) + } +} + +// serveStateless handles requests for stateless servers. +// Stateless servers only support POST. Each request creates a temporary +// session that is closed when the request completes. +// +// When the allowsessionsinstateless compatibility flag is set, DELETE is also +// accepted (as a no-op) and session IDs are read from the request header. +func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http.Request) { + legacySessions := allowsessionsinstateless == "1" + + if req.Method == http.MethodDelete && legacySessions { + h.serveStatelessLegacyDELETE(w, req) + return + } + + if req.Method != http.MethodPost { + // RFC 9110 §15.5.6: 405 responses MUST include Allow header. + w.Header().Set("Allow", "POST") + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + if disablecontenttypecheck != "1" && baseMediaType(req.Header.Get("Content-Type")) != "application/json" { + http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) + return + } + + // Accept must contain both 'application/json' and 'text/event-stream'. + jsonOK, streamOK := streamableAccepts(req.Header.Values("Accept")) + if !jsonOK || !streamOK { http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest) return } - sessionID := req.Header.Get(sessionIDHeader) - var sessInfo *sessionInfo - if sessionID != "" { - h.mu.Lock() - sessInfo = h.sessions[sessionID] - h.mu.Unlock() - if sessInfo == nil && !h.opts.Stateless { - // Unless we're in 'stateless' mode, which doesn't perform any Session-ID - // validation, we require that the session ID matches a known session. - // - // In stateless mode, a temporary transport is be created below. - http.Error(w, "session not found", http.StatusNotFound) + server := h.getServer(req) + if server == nil { + http.Error(w, "no server available", http.StatusBadRequest) + return + } + + info, err := h.ephemeralConnectOpts(req) + if err != nil { + var mbe *http.MaxBytesError + if errors.As(err, &mbe) { + http.Error(w, fmt.Sprintf("request body exceeds %d bytes", mbe.Limit), http.StatusRequestEntityTooLarge) return } - // Prevent session hijacking: if the session was created with a user ID, - // verify that subsequent requests come from the same user. - if sessInfo != nil && sessInfo.userID != "" { - tokenInfo := auth.TokenInfoFromContext(req.Context()) - if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID { - http.Error(w, "session user mismatch", http.StatusForbidden) - return - } - } + http.Error(w, err.Error(), http.StatusBadRequest) + return } - if req.Method == http.MethodDelete { + var sessionID string + if legacySessions && !info.usesNewProtocol { + sessionID = req.Header.Get(sessionIDHeader) if sessionID == "" { - http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest) - return - } - if sessInfo != nil { // sessInfo may be nil in stateless mode - // Closing the session also removes it from h.sessions, due to the - // onClose callback. - sessInfo.session.Close() + sessionID = server.opts.GetSessionID() } - w.WriteHeader(http.StatusNoContent) + } + + transport := &StreamableServerTransport{ + SessionID: sessionID, + Stateless: true, + EventStore: h.opts.EventStore, + jsonResponse: h.opts.JSONResponse, + logger: h.opts.Logger, + shouldPropagateCancellation: info.usesNewProtocol && (info.isSubscriptionsListen || h.opts.PropagateRequestCancellation), + } + + session, err := connectStreamable(req.Context(), server, transport, info.opts) + if err != nil { + h.opts.Logger.Error(fmt.Sprintf("failed to connect: %v", err)) + http.Error(w, "failed connection", http.StatusInternalServerError) return } + defer session.Close() - switch req.Method { - case http.MethodPost, http.MethodGet: - if req.Method == http.MethodGet && (h.opts.Stateless || sessionID == "") { - if h.opts.Stateless { - // Per MCP spec: server MUST return 405 if it doesn't offer SSE stream. - // In stateless mode, GET (SSE streaming) is not supported. - // RFC 9110 §15.5.6: 405 responses MUST include Allow header. - w.Header().Set("Allow", "POST") - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - } else { - // In stateful mode, GET is supported but requires a session ID. - // This is a precondition error, similar to DELETE without session. - http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest) + transport.ServeHTTP(w, req) +} + +// serveStatelessLegacyDELETE handles DELETE requests in stateless mode when the +// allowsessionsinstateless compatibility flag is set. DELETE requires a +// Mcp-Session-Id header but is otherwise a no-op since stateless servers don't +// persist sessions. +func (h *StreamableHTTPHandler) serveStatelessLegacyDELETE(w http.ResponseWriter, req *http.Request) { + sessionID := req.Header.Get(sessionIDHeader) + if sessionID == "" { + http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusNoContent) +} + +type ephemeralConnectInfo struct { + opts *ServerSessionOptions + usesNewProtocol bool + isSubscriptionsListen bool +} + +// ephemeralConnectOpts peeks at the request body to determine connection +// parameters and whether protocol version >= 2026-06-30 (SEP-2575). +// +// For old-protocol requests, default session state is synthesized so that +// the session's init gate doesn't reject the request. +// +// It is used for both stateless servers and stateful servers with no session ID. +func (h *StreamableHTTPHandler) ephemeralConnectOpts(req *http.Request) (*ephemeralConnectInfo, error) { + protocolVersion := protocolVersionFromContext(req.Context()) + if protocolVersion == "" { + protocolVersion = protocolVersion20250326 + } + + var hasInitialize, hasInitialized, usesNewProtocol, isSubscriptionsListen bool + body, err := io.ReadAll(req.Body) + if err != nil { + // Preserve *http.MaxBytesError so serveStateless can respond with 413. + return nil, fmt.Errorf("failed to read body: %w", err) + } + req.Body.Close() + req.Body = io.NopCloser(bytes.NewBuffer(body)) + msgs, _, err := readBatch(body) + if err == nil { + for _, msg := range msgs { + if r, ok := msg.(*jsonrpc.Request); ok { + switch r.Method { + case methodInitialize: + hasInitialize = true + case notificationInitialized: + hasInitialized = true + case methodSubscriptionsListen: + isSubscriptionsListen = true + } + if protocolVersion >= protocolVersion20260728 { + usesNewProtocol = true + } } - return } + } + + state := new(ServerSessionState) + // Only synthesize fake InitializeParams/InitializedParams for old-protocol + // requests. + if !hasInitialize && !usesNewProtocol { + state.InitializeParams = &InitializeParams{ + ProtocolVersion: protocolVersion, + } + } + if !hasInitialized && !usesNewProtocol { + state.InitializedParams = new(InitializedParams) + } + if !usesNewProtocol { + state.LogLevel = "info" + } + return &ephemeralConnectInfo{ + opts: &ServerSessionOptions{ + State: state, + }, + usesNewProtocol: usesNewProtocol, + isSubscriptionsListen: isSubscriptionsListen, + }, nil +} + +func connectStreamable(ctx context.Context, server *Server, transport *StreamableServerTransport, opts *ServerSessionOptions) (*ServerSession, error) { + s, err := server.Connect(ctx, transport, opts) + if err != nil { + return nil, err + } + transport.connection.server = server + transport.connection.toolLookup = server.getServerTool + return s, nil +} + +// serveStateful handles requests for stateful servers. +// Stateful servers support GET, POST, and DELETE, and maintain persistent +// sessions keyed by session ID. +func (h *StreamableHTTPHandler) serveStateful(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + h.serveStatefulGET(w, req) + case http.MethodPost: + h.serveStatefulPOST(w, req) + case http.MethodDelete: + h.serveStatefulDELETE(w, req) default: // RFC 9110 §15.5.6: 405 responses MUST include Allow header. - if h.opts.Stateless { - w.Header().Set("Allow", "POST") - } else { - w.Header().Set("Allow", "GET, POST, DELETE") - } + w.Header().Set("Allow", "GET, POST, DELETE") http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + } +} + +// lookupSession looks up a session by the Mcp-Session-Id header value. +// It returns the session info and whether the caller should proceed. If ok is +// false, an error response has been written. The sessionID must be non-empty; +// callers are responsible for checking this before calling lookupSession. +func (h *StreamableHTTPHandler) lookupSession(w http.ResponseWriter, req *http.Request, sessionID string) (info *sessionInfo, ok bool) { + h.mu.Lock() + info = h.sessions[sessionID] + h.mu.Unlock() + if info == nil { + http.Error(w, "session not found", http.StatusNotFound) + return nil, false + } + if info.userID != "" { + tokenInfo := auth.TokenInfoFromContext(req.Context()) + if tokenInfo == nil || tokenInfo.UserID != info.userID { + http.Error(w, "session user mismatch", http.StatusForbidden) + return nil, false + } + } + return info, true +} + +// serveStatefulGET handles GET requests for standalone SSE streams. +// GET requires a valid Mcp-Session-Id header. +func (h *StreamableHTTPHandler) serveStatefulGET(w http.ResponseWriter, req *http.Request) { + if _, streamOK := streamableAccepts(req.Header.Values("Accept")); !streamOK { + http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest) return } - // [§2.7] of the spec (2025-06-18) states: - // - // "If using HTTP, the client MUST include the MCP-Protocol-Version: - // HTTP header on all subsequent requests to the MCP - // server, allowing the MCP server to respond based on the MCP protocol - // version. - // - // For example: MCP-Protocol-Version: 2025-06-18 - // The protocol version sent by the client SHOULD be the one negotiated during - // initialization. - // - // For backwards compatibility, if the server does not receive an - // MCP-Protocol-Version header, and has no other way to identify the version - - // for example, by relying on the protocol version negotiated during - // initialization - the server SHOULD assume protocol version 2025-03-26. - // - // If the server receives a request with an invalid or unsupported - // MCP-Protocol-Version, it MUST respond with 400 Bad Request." - // - // Since this wasn't present in the 2025-03-26 version of the spec, this - // effectively means: - // 1. IF the client provides a version header, it must be a supported - // version. - // 2. In stateless mode, where we've lost the state of the initialize - // request, we assume that whatever the client tells us is the truth (or - // assume 2025-03-26 if the client doesn't say anything). - // - // This logic matches the typescript SDK. - // - // [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header - protocolVersion := req.Header.Get(protocolVersionHeader) - if protocolVersion == "" { - protocolVersion = protocolVersion20250326 + sessionID := req.Header.Get(sessionIDHeader) + if sessionID == "" { + http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest) + return } - if !slices.Contains(supportedProtocolVersions, protocolVersion) { - http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest) + + sessInfo, ok := h.lookupSession(w, req, sessionID) + if !ok { + return + } + + sessInfo.transport.ServeHTTP(w, req) +} + +// serveStatefulDELETE handles DELETE requests for session termination. +// DELETE requires a valid Mcp-Session-Id header. +func (h *StreamableHTTPHandler) serveStatefulDELETE(w http.ResponseWriter, req *http.Request) { + sessionID := req.Header.Get(sessionIDHeader) + if sessionID == "" { + http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest) + return + } + + sessInfo, ok := h.lookupSession(w, req, sessionID) + if !ok { + return + } + + sessInfo.session.Close() + w.WriteHeader(http.StatusNoContent) +} + +// serveStatefulPOST handles POST requests for stateful servers. +// POST may arrive with or without a Mcp-Session-Id header. Without a session +// ID, a new session is created (this is the normal path for the first +// initialize request). +func (h *StreamableHTTPHandler) serveStatefulPOST(w http.ResponseWriter, req *http.Request) { + if disablecontenttypecheck != "1" && baseMediaType(req.Header.Get("Content-Type")) != "application/json" { + http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) return } - if sessInfo == nil { - server := h.getServer(req) - if server == nil { - // The getServer argument to NewStreamableHTTPHandler returned nil. - http.Error(w, "no server available", http.StatusBadRequest) + jsonOK, streamOK := streamableAccepts(req.Header.Values("Accept")) + if !jsonOK || !streamOK { + http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest) + return + } + + sessionID := req.Header.Get(sessionIDHeader) + + // Look up existing session if a session ID was provided. + if sessionID != "" { + sessInfo, ok := h.lookupSession(w, req, sessionID) + if !ok { return } - if sessionID == "" { - // In stateless mode, sessionID may be nonempty even if there's no - // existing transport. - sessionID = server.opts.GetSessionID() - } - transport := &StreamableServerTransport{ - SessionID: sessionID, - Stateless: h.opts.Stateless, - EventStore: h.opts.EventStore, - jsonResponse: h.opts.JSONResponse, - logger: h.opts.Logger, - } - - // Sessions without a session ID are also stateless: there's no way to - // address them. - stateless := h.opts.Stateless || sessionID == "" - // To support stateless mode, we initialize the session with a default - // state, so that it doesn't reject subsequent requests. - var connectOpts *ServerSessionOptions - if stateless { - // Peek at the body to see if it is initialize or initialized. - // We want those to be handled as usual. - var hasInitialize, hasInitialized bool - { - // TODO: verify that this allows protocol version negotiation for - // stateless servers. - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(w, "failed to read body", http.StatusBadRequest) - return - } - req.Body.Close() - - // Reset the body so that it can be read later. - req.Body = io.NopCloser(bytes.NewBuffer(body)) - - msgs, _, err := readBatch(body) - if err == nil { - for _, msg := range msgs { - if req, ok := msg.(*jsonrpc.Request); ok { - switch req.Method { - case methodInitialize: - hasInitialize = true - case notificationInitialized: - hasInitialized = true - } - } - } - } - } + sessInfo.startPOST() + defer sessInfo.endPOST() + sessInfo.transport.ServeHTTP(w, req) + return + } - // If we don't have InitializeParams or InitializedParams in the request, - // set the initial state to a default value. - state := new(ServerSessionState) - if !hasInitialize { - state.InitializeParams = &InitializeParams{ - ProtocolVersion: protocolVersion, - } - } - if !hasInitialized { - state.InitializedParams = new(InitializedParams) - } - state.LogLevel = "info" - connectOpts = &ServerSessionOptions{ - State: state, - } - } else { - // Cleanup is only required in stateful mode, as transportation is - // not stored in the map otherwise. - connectOpts = &ServerSessionOptions{ - onClose: func() { - h.mu.Lock() - defer h.mu.Unlock() - if info, ok := h.sessions[transport.SessionID]; ok { - info.stopTimer() - delete(h.sessions, transport.SessionID) - if h.onTransportDeletion != nil { - h.onTransportDeletion(transport.SessionID) - } - } - }, - } - } + // No session ID: create a new session. + server := h.getServer(req) + if server == nil { + http.Error(w, "no server available", http.StatusBadRequest) + return + } + sessionID = server.opts.GetSessionID() - // Pass req.Context() here, to allow middleware to add context values. - // The context is detached in the jsonrpc2 library when handling the - // long-running stream. - session, err := server.Connect(req.Context(), transport, connectOpts) + transport := &StreamableServerTransport{ + SessionID: sessionID, + Stateless: false, + EventStore: h.opts.EventStore, + jsonResponse: h.opts.JSONResponse, + logger: h.opts.Logger, + } + + // Sessions without a session ID (GetSessionID returned "") are ephemeral: + // there's no way to address them, so they are closed after the request. + // This can happen when ServerOptions.GetSessionID is explicitly set to + // return "" to suppress session IDs entirely. It also covers any request + // that arrives before a session exists (e.g. initialize or ping) on a + // server configured this way. + if sessionID == "" { + info, err := h.ephemeralConnectOpts(req) if err != nil { - http.Error(w, "failed connection", http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } - // Capture the user ID from the token info to enable session hijacking - // prevention on subsequent requests. - var userID string - if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil { - userID = tokenInfo.UserID - } - sessInfo = &sessionInfo{ - session: session, - transport: transport, - userID: userID, + session, err := connectStreamable(req.Context(), server, transport, info.opts) + if err != nil { + h.opts.Logger.Error(fmt.Sprintf("failed to connect: %v", err)) + http.Error(w, "failed connection", http.StatusInternalServerError) + return } + defer session.Close() + transport.ServeHTTP(w, req) + return + } - if stateless { - // Stateless mode: close the session when the request exits. - defer session.Close() // close the fake session after handling the request - } else { - // Otherwise, save the transport so that it can be reused - - // Clean up the session when it times out. - // - // Note that the timer here may fire multiple times, but - // sessInfo.session.Close is idempotent. - if h.opts.SessionTimeout > 0 { - sessInfo.timeout = h.opts.SessionTimeout - sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() { - sessInfo.session.Close() - }) - } + connectOpts := &ServerSessionOptions{ + onClose: func() { h.mu.Lock() - h.sessions[transport.SessionID] = sessInfo - h.mu.Unlock() - defer func() { - // If initialization failed, clean up the session (#578). - if session.InitializeParams() == nil { - // Initialization failed. - session.Close() + defer h.mu.Unlock() + if info, ok := h.sessions[transport.SessionID]; ok { + info.stopTimer() + delete(h.sessions, transport.SessionID) + if h.onTransportDeletion != nil { + h.onTransportDeletion(transport.SessionID) } - }() - } + } + }, } - if req.Method == http.MethodPost { - sessInfo.startPOST() - defer sessInfo.endPOST() + // Pass req.Context() here, to allow middleware to add context values. + // The context is detached in the jsonrpc2 library when handling the + // long-running stream. + session, err := connectStreamable(req.Context(), server, transport, connectOpts) + if err != nil { + h.opts.Logger.Error(fmt.Sprintf("failed to connect: %v", err)) + http.Error(w, "failed connection", http.StatusInternalServerError) + return + } + // Capture the user ID from the token info to enable session hijacking + // prevention on subsequent requests. + var userID string + if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil { + userID = tokenInfo.UserID + } + sessInfo := &sessionInfo{ + session: session, + transport: transport, + userID: userID, } + if h.opts.SessionTimeout > 0 { + sessInfo.timeout = h.opts.SessionTimeout + sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() { + sessInfo.session.Close() + }) + } + h.mu.Lock() + h.sessions[transport.SessionID] = sessInfo + h.mu.Unlock() + defer func() { + // If initialization failed, clean up the session (#578). + if session.InitializeParams() == nil { + session.Close() + } + }() + + sessInfo.startPOST() + defer sessInfo.endPOST() sessInfo.transport.ServeHTTP(w, req) } +func streamableAccepts(values []string) (jsonOK, streamOK bool) { + for _, value := range values { + for _, raw := range strings.Split(value, ",") { + token := strings.TrimSpace(raw) + // Ignore Accept parameters like ";charset=utf-8"; match the base media type. + base, _, _ := strings.Cut(token, ";") + switch strings.ToLower(strings.TrimSpace(base)) { + case "application/json", "application/*": + jsonOK = true + case "text/event-stream", "text/*": + streamOK = true + case "*/*": + jsonOK = true + streamOK = true + } + } + } + return jsonOK, streamOK +} + +func baseMediaType(value string) string { + mediaType, _, err := mime.ParseMediaType(value) + if err != nil { + return "" + } + return mediaType +} + // A StreamableServerTransport implements the server side of the MCP streamable // transport. // @@ -611,6 +828,10 @@ type StreamableServerTransport struct { // to write their own streamable HTTP handler. logger *slog.Logger + // shouldPropagateCancellation is forwarded to the underlying + // [streamableServerConn]. See its docstring. + shouldPropagateCancellation bool + // connection is non-nil if and only if the transport has been connected. connection *streamableServerConn } @@ -621,15 +842,16 @@ func (t *StreamableServerTransport) Connect(ctx context.Context) (Connection, er return nil, fmt.Errorf("transport already connected") } t.connection = &streamableServerConn{ - sessionID: t.SessionID, - stateless: t.Stateless, - eventStore: t.EventStore, - jsonResponse: t.jsonResponse, - logger: ensureLogger(t.logger), // see #556: must be non-nil - incoming: make(chan jsonrpc.Message, 10), - done: make(chan struct{}), - streams: make(map[string]*stream), - requestStreams: make(map[jsonrpc.ID]string), + sessionID: t.SessionID, + stateless: t.Stateless, + eventStore: t.EventStore, + jsonResponse: t.jsonResponse, + logger: ensureLogger(t.logger), // see #556: must be non-nil + shouldPropagateCancellation: t.shouldPropagateCancellation, + incoming: make(chan jsonrpc.Message, 10), + done: make(chan struct{}), + streams: make(map[string]*stream), + requestStreams: make(map[jsonrpc.ID]string), } // Stream 0 corresponds to the standalone SSE stream. // @@ -643,14 +865,34 @@ func (t *StreamableServerTransport) Connect(ctx context.Context) (Connection, er return t.connection, nil } +// The streamable HTTP transport supports every legacy SDK protocol version, +// but the SEP-2575 >= 2026-07-28 protocol is only supported when the +// transport is configured as stateless. +func (t *StreamableServerTransport) SupportsProtocolVersion(version string) bool { + if version >= protocolVersion20260728 { + return t.Stateless && slices.Contains(supportedProtocolVersions, version) + } + return slices.Contains(supportedProtocolVersions, version) +} + type streamableServerConn struct { sessionID string stateless bool jsonResponse bool eventStore EventStore + // shouldPropagateCancellation is true when the underlying HTTP request's + // lifetime IS the connection's cancellation signal (e.g., a stateless + // POST that owns a long-lived subscriptions/listen stream). It is read + // by the [cancellationPropagator] interface so the jsonrpc2 layer wires + // handler contexts to observe the carrier's cancellation. + shouldPropagateCancellation bool + logger *slog.Logger + server *Server + toolLookup func(name string) (*serverTool, bool) + incoming chan jsonrpc.Message // messages from the client to the server mu sync.Mutex // guards all fields below @@ -684,6 +926,15 @@ func (c *streamableServerConn) SessionID() string { return c.sessionID } +// propagateCancellation implements [cancellationPropagator]. It returns true +// when this connection is bound to a single HTTP request whose lifetime +// should drive request-handler cancellation — for example, a stateless POST +// carrying a long-lived subscriptions/listen stream that must unwind when +// the client TCP-disconnects. +func (c *streamableServerConn) propagateCancellation() bool { + return c.shouldPropagateCancellation +} + // A stream is a single logical stream of SSE events within a server session. // A stream begins with a client request, or with a client GET that has // no Last-Event-ID header. @@ -738,6 +989,12 @@ type stream struct { // the spec and earlier there was a concept of batching, in which POST // payloads could hold multiple requests or responses. requests map[jsonrpc.ID]struct{} + + // isListen reports whether this stream was opened by a + // subscriptions/listen request. Listen streams are always SSE, live for + // the duration of the subscription, and act as the target for + // out-of-band notifications routed through this connection. + isListen bool } // close sends a 'close' event to the client (if protocolVersion >= 2025-11-25 @@ -773,6 +1030,36 @@ func (s *stream) release() { s.done = nil // may already be nil, if the stream is done or closed } +// extractErrorStatus reports the HTTP status to send when the given +// outgoing message is a JSON-RPC error response under the SEP-2575 protocol +// (>= 2026-07-28). +// +// Per SEP-2575: +// - MethodNotFound (-32601) MUST return HTTP 404. +// - InvalidParams (-32602), UnsupportedProtocolVersion (-32022) and +// CodeMissingRequiredClientCapabilities (-32021) MUST +// return HTTP 400. +func extractErrorStatus(ctx context.Context, msg jsonrpc.Message) int { + if protocolVersionFromContext(ctx) < protocolVersion20260728 { + return 0 + } + resp, ok := msg.(*jsonrpc.Response) + if !ok || resp.Error == nil { + return 0 + } + var jerr *jsonrpc.Error + if !errors.As(resp.Error, &jerr) { + return 0 + } + switch jerr.Code { + case jsonrpc.CodeMethodNotFound: + return http.StatusNotFound + case jsonrpc.CodeInvalidParams, CodeUnsupportedProtocolVersion, CodeMissingRequiredClientCapabilities: + return http.StatusBadRequest + } + return 0 +} + // deliverLocked writes data to the stream (for SSE) or stores it in // pendingJSONMessages (for JSON mode). The eventID is used for SSE event ID; // pass "" to omit. @@ -780,11 +1067,16 @@ func (s *stream) release() { // If responseTo is valid, it is removed from the requests map. When all // requests have been responded to, the done channel is closed and set to nil. // +// If overrideStatus is non-zero, data is treated as a SEP-2575 protocol-level +// error response (>= 2026-07-28): it is written as a single raw JSON-RPC +// response body with Content-Type: application/json and HTTP status +// overrideStatus. +// // Returns true if the stream is now done (all requests have been responded to). // The done value is always accurate, even if an error is returned. // // s.mu must be held when calling this method. -func (s *stream) deliverLocked(data []byte, eventID string, responseTo jsonrpc.ID) (done bool, err error) { +func (s *stream) deliverLocked(data []byte, eventID string, responseTo jsonrpc.ID, overrideStatus int) (done bool, err error) { // First, record the response. We must do this *before* returning an error // below, as even if the stream is disconnected we want to update our // accounting. @@ -799,6 +1091,17 @@ func (s *stream) deliverLocked(data []byte, eventID string, responseTo jsonrpc.I if done { defer func() { close(s.done); s.done = nil }() } + // SEP-2575 protocol-level error override: write the error as a raw + // JSON-RPC response with the spec-mandated HTTP status, bypassing any + // SSE framing. + if overrideStatus != 0 { + s.w.Header().Set("Content-Type", "application/json") + s.w.WriteHeader(overrideStatus) + if _, err := s.w.Write(data); err != nil { + return done, err + } + return done, nil + } // Try to write to the response. // // If we get here, the request is still hanging (because s.done != nil @@ -844,7 +1147,7 @@ func (s *stream) doneLocked() bool { } func (c *streamableServerConn) newStream(ctx context.Context, requests map[jsonrpc.ID]struct{}, id string) (*stream, error) { - if c.eventStore != nil { + if c.eventStore != nil && protocolVersionFromContext(ctx) < protocolVersion20260728 { if err := c.eventStore.Open(ctx, c.sessionID, id); err != nil { return nil, err } @@ -880,6 +1183,18 @@ func (c *streamableServerConn) newStream(ctx context.Context, requests map[jsonr // ID, we avoid having to make this API decision. type idContextKey struct{} +// protocolVersionContextKey stores the protocol version extracted from the +// MCP-Protocol-Version HTTP header for use by lower layers. +type protocolVersionContextKey struct{} + +// protocolVersionFromContext returns the protocol version from the context, or +// the empty string if not set. An empty string means the version is unknown +// (e.g. the header was absent). +func protocolVersionFromContext(ctx context.Context) string { + v, _ := ctx.Value(protocolVersionContextKey{}).(string) + return v +} + // ServeHTTP handles a single HTTP request for the session. func (t *StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request) { if t.connection == nil { @@ -925,9 +1240,7 @@ func (c *streamableServerConn) serveGET(w http.ResponseWriter, req *http.Request ctx := req.Context() - // Read the protocol version from the header. For GET requests, this should - // always be present since GET only happens after initialization. - protocolVersion := req.Header.Get(protocolVersionHeader) + protocolVersion := protocolVersionFromContext(ctx) if protocolVersion == "" { protocolVersion = protocolVersion20250326 } @@ -1043,10 +1356,26 @@ func (c *streamableServerConn) acquireStream(ctx context.Context, w http.Respons if s.id == "" { // Issue #410: the standalone SSE stream is likely not to receive messages // for a long time. Ensure that headers are flushed. + // + // On HTTP/2, headers and body travel as separate frames (HEADERS and + // DATA). Reverse proxies (e.g. Envoy, Caddy, net/http/httputil) + // commonly buffer the HEADERS frame until they have a DATA frame to + // coalesce it with — there is no HTTP/2 equivalent of HTTP/1.1's + // Transfer-Encoding: chunked signal that says "this is streaming, send + // headers now". Calling Flush() alone is not sufficient: it pushes + // the kernel buffer to the proxy, but the proxy still holds the + // HEADERS frame. + // + // Write an SSE comment (lines starting with ":" are ignored by + // clients per RFC) so a DATA frame is produced, which forces the + // proxy to forward both frames. See: + // https://github.com/golang/go/issues/31125 + // https://github.com/caddyserver/caddy/issues/4247 w.WriteHeader(http.StatusOK) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } + fmt.Fprint(w, ": ok\n\n") + rc := http.NewResponseController(w) + // Ignore returned error as flushing is best-effort. + _ = rc.Flush() } for _, data := range toReplay { @@ -1088,6 +1417,11 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques // Read incoming messages. body, err := io.ReadAll(req.Body) if err != nil { + var mbe *http.MaxBytesError + if errors.As(err, &mbe) { + http.Error(w, fmt.Sprintf("request body exceeds %d bytes", mbe.Limit), http.StatusRequestEntityTooLarge) + return + } http.Error(w, "failed to read body", http.StatusBadRequest) return } @@ -1104,7 +1438,7 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques return } - protocolVersion := req.Header.Get(protocolVersionHeader) + protocolVersion := protocolVersionFromContext(req.Context()) if protocolVersion == "" { protocolVersion = protocolVersion20250326 } @@ -1124,13 +1458,29 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques calls := make(map[jsonrpc.ID]struct{}) tokenInfo := auth.TokenInfoFromContext(req.Context()) isInitialize := false + isSubscriptionsListen := false var initializeProtocolVersion string for _, msg := range incoming { if jreq, ok := msg.(*jsonrpc.Request); ok { // Preemptively check that this is a valid request, so that we can fail // the HTTP request. If we didn't do this, a request with a bad method or // missing ID could be silently swallowed. - if _, err := checkRequest(jreq, serverMethodInfos); err != nil { + // Use the server's receiving method infos (which include any custom + // methods registered via AddReceivingCustomMethod) when available; + // fall back to the standard methods otherwise, e.g. in tests that + // exercise streamableServerConn directly without a server. + methodInfos := serverMethodInfos + if c.server != nil { + methodInfos = c.server.receivingMethodInfos() + } + if _, err := checkRequest(jreq, methodInfos); err != nil { + if protocolVersion >= protocolVersion20260728 && errors.Is(err, jsonrpc2.ErrNotHandled) && jreq.IsCall() { + writeJSONRPCError(w, http.StatusNotFound, jreq.ID, &jsonrpc.Error{ + Code: jsonrpc.CodeMethodNotFound, + Message: err.Error(), + }) + return + } http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -1142,6 +1492,63 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques initializeProtocolVersion = params.ProtocolVersion } } + if jreq.Method == methodSubscriptionsListen { + isSubscriptionsListen = true + } + // SEP-2575: requests carrying `_meta.protocolVersion` require the + // Mcp-Protocol-Version HTTP header to be present and to match the + // per-request `_meta.protocolVersion` value. + // The new (>= 2026-07-28) protocol is supported on the HTTP transport + // only when [StreamableHTTPOptions.Stateless] is true. + // + // TODO: this validation can be moved within validateMcpHeaders. + var metaVersion string + if meta := extractRequestMeta(jreq.Params); meta != nil { + metaVersion, _ = meta[MetaKeyProtocolVersion].(string) + } + if protocolVersion >= protocolVersion20260728 || metaVersion != "" { + // Extract again the protcol version from the context to see what the client + // is advertising in the Mcp-Protocol-Version HTTP header. + headerVersion := protocolVersionFromContext(req.Context()) + // server/discover is exempt from the stateful + // rejection as it should learn about the supported protocols from the + // DiscoverResult response. + if !c.stateless && jreq.Method != methodDiscover { + http.Error(w, fmt.Sprintf( + "Bad Request: protocol version %q is only supported on stateless HTTP servers (set StreamableHTTPOptions.Stateless = true)", + protocolVersion), + http.StatusBadRequest) + return + } + if headerVersion == "" { + writeJSONRPCError(w, http.StatusBadRequest, jreq.ID, &jsonrpc.Error{ + Code: CodeHeaderMismatch, + Message: fmt.Sprintf( + "%s header is required for requests carrying %q", + protocolVersionHeader, MetaKeyProtocolVersion), + }) + return + } + if metaVersion == "" { + writeJSONRPCError(w, http.StatusBadRequest, jreq.ID, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidParams, + Message: fmt.Sprintf( + "missing or invalid _meta field %q", + MetaKeyProtocolVersion), + }) + return + } + if headerVersion != metaVersion { + writeJSONRPCError(w, http.StatusBadRequest, jreq.ID, &jsonrpc.Error{ + Code: CodeHeaderMismatch, + Message: fmt.Sprintf( + "%s header %q does not match request %s %q", + protocolVersionHeader, headerVersion, + MetaKeyProtocolVersion, metaVersion), + }) + return + } + } // Include metadata for all requests (including notifications). jreq.Extra = &RequestExtra{ TokenInfo: tokenInfo, @@ -1152,6 +1559,12 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques // See the doc for CloseSSEStream: allow the request handler to // explicitly close the ongoing stream. jreq.Extra.(*RequestExtra).CloseSSEStream = func(args CloseSSEStreamArgs) { + // This mechanism was designed to trigger client reconnection with + // Last-Event-ID for server-initiated disconnect scenarios. It is + // deprecated in protocol version 2026-07-28. + if protocolVersion >= protocolVersion20260728 { + return + } c.mu.Lock() streamID, ok := c.requestStreams[jreq.ID] var stream *stream @@ -1168,6 +1581,24 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques } } + // Validate MCP standard headers (Mcp-Method, Mcp-Name, Mcp-Param-*) + if !isBatch && len(incoming) == 1 { + if err := validateMcpHeaders(req.Header, incoming[0], c.toolLookup); err != nil { + resp := &jsonrpc.Response{ + Error: jsonrpc2.NewError(CodeHeaderMismatch, err.Error()), + } + if jreq, ok := incoming[0].(*jsonrpc.Request); ok { + resp.ID = jreq.ID + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + if data, err := jsonrpc2.EncodeMessage(resp); err == nil { + w.Write(data) + } + return + } + } + // The prime and close events were added in protocol version 2025-11-25 (SEP-1699). // Use the version from InitializeParams if this is an initialize request, // otherwise use the protocol version header. @@ -1202,21 +1633,29 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques // // Create a logical stream to track its responses. // Important: don't publish the incoming messages until the stream is - // registered, as the server may attempt to respond to imcoming messages as + // registered, as the server may attempt to respond to incoming messages as // soon as they're published. stream, err := c.newStream(req.Context(), calls, crand.Text()) if err != nil { http.Error(w, fmt.Sprintf("storing stream: %v", err), http.StatusInternalServerError) return } + stream.isListen = isSubscriptionsListen + + // subscriptions/listen is inherently a long-lived SSE endpoint (SEP-2575): + // it has no synchronous result, the response stream stays open until the + // client cancels, and the server pushes notifications on it as they occur. + // Force SSE mode (bypassing JSONResponse) so the buffered application/json + // path doesn't deadlock waiting for a completion that won't come. + useSSE := !c.jsonResponse || isSubscriptionsListen // Set response headers. Accept was checked in [StreamableHTTPHandler]. w.Header().Set("Cache-Control", "no-cache, no-transform") - if c.jsonResponse { - w.Header().Set("Content-Type", "application/json") - } else { + if useSSE { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Connection", "keep-alive") + } else { + w.Header().Set("Content-Type", "application/json") } if c.sessionID != "" && isInitialize { w.Header().Set(sessionIDHeader, c.sessionID) @@ -1227,14 +1666,41 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques done := make(chan struct{}) stream.done = done stream.protocolVersion = effectiveVersion - if c.jsonResponse { + + // Reject any call whose ID is already in flight on this session, + // atomically and without partial registration. + c.mu.Lock() + for reqID := range calls { + if _, ok := c.requestStreams[reqID]; ok { + c.mu.Unlock() + writeJSONRPCError(w, http.StatusBadRequest, reqID, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidRequest, + Message: fmt.Sprintf("duplicate in-flight request ID %v", reqID.Raw()), + }) + return + } + } + c.streams[stream.id] = stream + for reqID := range calls { + c.requestStreams[reqID] = stream.id + } + c.mu.Unlock() + + // TODO(rfindley): if we have no event store, we should really cancel all + // remaining requests here, since the client will never get the results. + defer stream.release() + + if !useSSE { // JSON mode: collect messages in pendingJSONMessages until done. // Set pendingJSONMessages to a non-nil value to signal that this is an // application/json stream. stream.pendingJSONMessages = []json.RawMessage{} } else { // SSE mode: write a priming event if supported. - if c.eventStore != nil && effectiveVersion >= protocolVersion20251125 { + // + // SEP-2575 removes Last-Event-ID-based resumable streams for protocol + // version >= 2026-07-28. + if c.eventStore != nil && effectiveVersion >= protocolVersion20251125 && effectiveVersion < protocolVersion20260728 { // Write a priming event, as defined by [§2.1.6] of the spec. // // [§2.1.6]: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server @@ -1252,20 +1718,6 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques } } - // TODO(rfindley): if we have no event store, we should really cancel all - // remaining requests here, since the client will never get the results. - defer stream.release() - - // The stream is now set up to deliver messages. - // - // Register it before publishing incoming messages. - c.mu.Lock() - c.streams[stream.id] = stream - for reqID := range calls { - c.requestStreams[reqID] = stream.id - } - c.mu.Unlock() - // Publish incoming messages. for _, msg := range incoming { select { @@ -1378,7 +1830,18 @@ func (c *streamableServerConn) Write(ctx context.Context, msg jsonrpc.Message) e s = c.streams[streamID] } } else { - s = c.streams[""] // standalone SSE stream + // In stateless mode there will always be only one stream per connection. + // If that stream was open to listen for subscription notifications, + // automatically select as the one to write the notification to. + for _, stream := range c.streams { + if stream.isListen { + s = stream + break + } + } + if s == nil { + s = c.streams[""] // standalone SSE stream + } } if responseTo.IsValid() { // Once we've responded to a request, disallow related messages by removing @@ -1408,7 +1871,8 @@ func (c *streamableServerConn) Write(ctx context.Context, msg jsonrpc.Message) e // pushing down into the delivery layer. delivered := false var errs []error - if c.eventStore != nil { + protocolVersion := protocolVersionFromContext(ctx) + if c.eventStore != nil && protocolVersion < protocolVersion20260728 { if err := c.eventStore.Append(ctx, c.sessionID, s.id, data); err != nil { errs = append(errs, err) } else { @@ -1419,11 +1883,16 @@ func (c *streamableServerConn) Write(ctx context.Context, msg jsonrpc.Message) e // Compute eventID for SSE streams with event store. // Use s.lastIdx + 1 because deliverLocked increments before writing. var eventID string - if c.eventStore != nil { + if c.eventStore != nil && protocolVersion < protocolVersion20260728 { eventID = formatEventID(s.id, s.lastIdx+1) } - done, err := s.deliverLocked(data, eventID, responseTo) + // SEP-2575: map protocol-level JSON-RPC error codes to HTTP status codes + // on the new protocol (>= 2026-07-28). When non-zero, deliverLocked will + // write the body as raw application/json with the override status. + overrideStatus := extractErrorStatus(ctx, msg) + + done, err := s.deliverLocked(data, eventID, responseTo, overrideStatus) if err != nil { errs = append(errs, err) } else { @@ -1515,17 +1984,23 @@ var ( // reconnectInitialDelay is the base delay for the first reconnect attempt. // // Mutable for testing. - reconnectInitialDelay = 1 * time.Second + reconnectInitialDelay atomic.Int64 ) +func init() { + reconnectInitialDelay.Store(int64(1 * time.Second)) +} + // Connect implements the [Transport] interface. // // The resulting [Connection] writes messages via POST requests to the -// transport URL with the Mcp-Session-Id header set, and reads messages from -// hanging requests. +// transport URL, and reads messages from hanging requests. If the server +// provides a session ID via the Mcp-Session-Id response header, subsequent +// requests include it; sessionless servers that omit the header are fully +// supported. // -// When closed, the connection issues a DELETE request to terminate the logical -// session. +// When closed, the connection issues a DELETE request to terminate the +// session, unless no session was established. func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, error) { client := t.HTTPClient if client == nil { @@ -1617,6 +2092,13 @@ func (c *streamableClientConn) sessionUpdated(state clientSessionState) { c.initializedResult = state.InitializeResult c.mu.Unlock() + // Under SEP-2575 (protocol version >= 2026-07-28) the standalone HTTP GET + // SSE stream is removed. + if state.InitializeResult == nil || + state.InitializeResult.ProtocolVersion >= protocolVersion20260728 { + return + } + // Start the standalone SSE stream as soon as we have the initialized // result, if continuous listening is enabled. // @@ -1657,7 +2139,7 @@ func (c *streamableClientConn) connectStandaloneSSE() { resp.Body.Close() return } - if resp.Header.Get("Content-Type") != "text/event-stream" { + if baseMediaType(resp.Header.Get("Content-Type")) != "text/event-stream" { // modelcontextprotocol/go-sdk#736: some servers return 200 OK or redirect with // non-SSE content type instead of text/event-stream for the standalone // SSE stream. @@ -1676,7 +2158,7 @@ func (c *streamableClientConn) connectStandaloneSSE() { return } summary := "standalone SSE stream" - if err := c.checkResponse(summary, resp); err != nil { + if err := c.checkResponse(c.ctx, summary, resp); err != nil { c.fail(err) return } @@ -1739,12 +2221,14 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e } var requestSummary string + var requestMethod string var forCall *jsonrpc.Request switch msg := msg.(type) { case *jsonrpc.Request: requestSummary = fmt.Sprintf("sending %q", msg.Method) if msg.IsCall() { forCall = msg + requestMethod = msg.Method } case *jsonrpc.Response: requestSummary = fmt.Sprintf("sending jsonrpc response #%d", msg.ID) @@ -1764,18 +2248,22 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") - if err := c.setMCPHeaders(req); err != nil { + + if err := c.setMCPHeaders(req, msg); err != nil { // Failure to set headers means that the request was not sent. // Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr // and permanently break the connection. - return nil, nil, fmt.Errorf("%s: %w: %v", requestSummary, jsonrpc2.ErrRejected, err) + return nil, nil, fmt.Errorf("%s: %w: %w", requestSummary, jsonrpc2.ErrRejected, err) } + // Keep this after the setMCPHeaders call to ensure that the + // protocol version header is set. + setStandardHeaders(ctx, req.Header, msg) resp, err := c.client.Do(req) if err != nil { // Any error from client.Do means the request didn't reach the server. // Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr // and permanently break the connection. - err = fmt.Errorf("%s: %w: %v", requestSummary, jsonrpc2.ErrRejected, err) + err = fmt.Errorf("%s: %w: %w", requestSummary, jsonrpc2.ErrRejected, err) } return req, resp, err } @@ -1787,6 +2275,22 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e if (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) && c.oauthHandler != nil { if err := c.oauthHandler.Authorize(ctx, req, resp); err != nil { + // If the caller's context was cancelled while we were running the + // authorization flow, treat the connection as failed so subsequent + // operations on it (e.g. the cancellation notify the call layer + // sends in response to ctx cancellation) short-circuit instead of + // re-invoking the OAuth handler. Otherwise the user gets prompted + // to authorize a request they have already abandoned. See #882. + // + // We check ctx.Err() rather than the error returned by Authorize, + // because the handler is user-implemented and may return an error + // that does not wrap context.Canceled (e.g. a custom sentinel or + // a fmt.Errorf with %v). The context itself is the authoritative + // source for whether the caller abandoned the request. + ctxErr := ctx.Err() + if errors.Is(ctxErr, context.Canceled) || errors.Is(ctxErr, context.DeadlineExceeded) { + c.fail(fmt.Errorf("%s: authorization cancelled: %w", requestSummary, err)) + } // Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr // and permanently break the connection. // Wrap the authorization error as well for client inspection. @@ -1799,10 +2303,15 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e } } - if err := c.checkResponse(requestSummary, resp); err != nil { - // Only fail the connection for non-transient errors. - // Transient errors (wrapped with ErrRejected) should not break the connection. - if !errors.Is(err, jsonrpc2.ErrRejected) { + if err := c.checkResponse(ctx, requestSummary, resp); err != nil { + if requestMethod == methodDiscover { + // Wrap the discover failure with ErrRejected so the jsonrpc2 layer + // doesn't set writeErr, which would prevent the legacy initialize + // fallback from succeeding on the same connection. + err = fmt.Errorf("%w: %w", err, jsonrpc2.ErrRejected) + } else if !errors.Is(err, jsonrpc2.ErrRejected) { + // Only fail the connection for non-transient errors. + // Transient errors (wrapped with ErrRejected) should not break the connection. c.fail(err) } return err @@ -1841,7 +2350,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e return nil } - contentType := strings.TrimSpace(strings.SplitN(resp.Header.Get("Content-Type"), ";", 2)[0]) + contentType := baseMediaType(resp.Header.Get("Content-Type")) switch contentType { case "application/json": go c.handleJSON(requestSummary, resp) @@ -1864,7 +2373,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e return nil } -func (c *streamableClientConn) setMCPHeaders(req *http.Request) error { +func (c *streamableClientConn) setMCPHeaders(req *http.Request, msg jsonrpc.Message) error { c.mu.Lock() defer c.mu.Unlock() @@ -1876,22 +2385,55 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error { if ts != nil { token, err := ts.Token() if err != nil { - return err - } - if token != nil { + // If the error is an invalid_grant oauth2.RetrieveError it indicates + // that the token source doesn't have valid authorization for the token + // endpoint, per RFC 6749 section 5.2. For example, the refresh token + // may be expired or invalid. + // + // In that case, ignore the error, skip setting the Authorization + // header, and proceed with the request. Callers that support + // authorization flows get a 401/403 response and trigger the + // Authorize() flow to refresh their token. + var retrieveErr *oauth2.RetrieveError + if !errors.As(err, &retrieveErr) || retrieveErr.ErrorCode != "invalid_grant" { + return err + } + } else if token != nil { req.Header.Set("Authorization", "Bearer "+token.AccessToken) } } } - if c.initializedResult != nil { + if pv := protocolVersionFromMessage(msg); pv != "" { + req.Header.Set(protocolVersionHeader, pv) + } else if pv := protocolVersionFromContext(req.Context()); pv != "" { + req.Header.Set(protocolVersionHeader, pv) + } else if c.initializedResult != nil { req.Header.Set(protocolVersionHeader, c.initializedResult.ProtocolVersion) } if c.sessionID != "" { req.Header.Set(sessionIDHeader, c.sessionID) } + return nil } +// protocolVersionFromMessage recovers the SEP-2575 `_meta.protocolVersion` +// value from an outgoing JSON-RPC request, if present. It returns "" for +// notifications, responses, requests without a `_meta.protocolVersion`, or a +// nil msg. +func protocolVersionFromMessage(msg jsonrpc.Message) string { + req, ok := msg.(*jsonrpc.Request) + if !ok || req == nil { + return "" + } + meta := extractRequestMeta(req.Params) + if meta == nil { + return "" + } + v, _ := meta[MetaKeyProtocolVersion].(string) + return v +} + func (c *streamableClientConn) handleJSON(requestSummary string, resp *http.Response) { body, err := io.ReadAll(resp.Body) resp.Body.Close() @@ -1969,7 +2511,7 @@ func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary str } resp = newResp - if err := c.checkResponse(requestSummary, resp); err != nil { + if err := c.checkResponse(ctx, requestSummary, resp); err != nil { c.fail(err) return } @@ -1980,7 +2522,7 @@ func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary str // translates it into an error if the request was unsuccessful. // // The response body is close if a non-nil error is returned. -func (c *streamableClientConn) checkResponse(requestSummary string, resp *http.Response) (err error) { +func (c *streamableClientConn) checkResponse(ctx context.Context, requestSummary string, resp *http.Response) (err error) { defer func() { if err != nil { resp.Body.Close() @@ -2000,6 +2542,17 @@ func (c *streamableClientConn) checkResponse(requestSummary string, resp *http.R return fmt.Errorf("%w: %s: %v", jsonrpc2.ErrRejected, requestSummary, http.StatusText(resp.StatusCode)) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // By default, always try to decode the body and surface the underlying + // JSON-RPC error. + // Setting MCPGODEBUG=noprotocolerrorbody=1 restores the previous behavior. + if noprotocolerrorbody == "1" { + return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode)) + } + body, _ := io.ReadAll(resp.Body) + msg, _ := jsonrpc.DecodeMessage(body) + if response, ok := msg.(*jsonrpc.Response); ok && response.Error != nil { + return fmt.Errorf("%s: %w: %v", requestSummary, response.Error, http.StatusText(resp.StatusCode)) + } return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode)) } return nil @@ -2142,7 +2695,7 @@ func (c *streamableClientConn) connectSSE(ctx context.Context, lastEventID strin if err != nil { return nil, err } - if err := c.setMCPHeaders(req); err != nil { + if err := c.setMCPHeaders(req, nil); err != nil { return nil, err } if lastEventID != "" { @@ -2170,15 +2723,20 @@ func (c *streamableClientConn) Close() error { c.closeOnce.Do(func() { if errors.Is(c.failure(), ErrSessionMissing) { // If the session is missing, no need to delete it. + } else if c.SessionID() == "" { + // No session was established (e.g. the server is stateless), + // so there is nothing to delete. } else { req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.url, nil) if err != nil { c.closeErr = err } else { - if err := c.setMCPHeaders(req); err != nil { + if err := c.setMCPHeaders(req, nil); err != nil { c.closeErr = err - } else if _, err := c.client.Do(req); err != nil { + } else if resp, err := c.client.Do(req); err != nil { c.closeErr = err + } else { + resp.Body.Close() } } } @@ -2196,7 +2754,7 @@ func calculateReconnectDelay(attempt int) time.Duration { return 0 } // Calculate the exponential backoff using the grow factor. - backoffDuration := time.Duration(float64(reconnectInitialDelay) * math.Pow(reconnectGrowFactor, float64(attempt-1))) + backoffDuration := time.Duration(float64(reconnectInitialDelay.Load()) * math.Pow(reconnectGrowFactor, float64(attempt-1))) // Cap the backoffDuration at maxDelay. backoffDuration = min(backoffDuration, reconnectMaxDelay) diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_client.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_client.go index c2cc25b8a..74940b3ad 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_client.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_client.go @@ -40,8 +40,12 @@ components: # Sessions -The client maintains a session with the server, identified by a session ID -(Mcp-Session-Id header): +The client optionally maintains a session with the server, identified by a +session ID (Mcp-Session-Id header). Sessionless servers (those that never +send Mcp-Session-Id) are fully supported; the client simply omits the +header from subsequent requests and skips the DELETE on close. + +When a session is present: - Session ID is received from the server after initialization - Client includes the session ID in all subsequent requests @@ -162,6 +166,7 @@ The client must handle two response formats from POST requests: - DELETE: Terminate the session - Used by [streamableClientConn.Close] - Skipped if session is already known to be gone ([ErrSessionMissing]) + or if no session was established (sessionless server) # Error Handling @@ -176,7 +181,7 @@ Errors are categorized and handled differently: - 404 Not Found: Session terminated by server ([ErrSessionMissing]) - Message decode errors: Protocol violation - Context cancellation: Client closed connection - - Mismatched session IDs: Protocol error + - Mismatched session IDs: Protocol error (only relevant for servers that use sessions) - See issue #683: our terminal errors are too strict. Terminal errors are stored via [streamableClientConn.fail] and returned by @@ -211,7 +216,7 @@ This header (set by [streamableClientConn.setMCPHeaders]): State management: - [streamableClientConn.incoming]: Buffered channel for received messages - - [streamableClientConn.sessionID]: Server-assigned session identifier + - [streamableClientConn.sessionID]: Server-assigned session identifier (empty for sessionless servers) - [streamableClientConn.initializedResult]: Cached for protocol version header - [streamableClientConn.failed]: Channel closed on terminal error - [streamableClientConn.done]: Channel closed on graceful shutdown diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_headers.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_headers.go new file mode 100644 index 000000000..3989c0169 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable_headers.go @@ -0,0 +1,529 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package mcp + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log/slog" + "math" + "net/http" + "strconv" + "strings" + + internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" + "github.com/modelcontextprotocol/go-sdk/jsonrpc" +) + +const ( + protocolVersionHeader = "Mcp-Protocol-Version" + sessionIDHeader = "Mcp-Session-Id" + lastEventIDHeader = "Last-Event-ID" + methodHeader = "Mcp-Method" + nameHeader = "Mcp-Name" + paramHeaderPrefix = "Mcp-Param-" + minVersionForStandardHeaders = protocolVersion20260728 + base64Prefix = "=?base64?" + base64Suffix = "?=" +) + +func extractName(method string, params json.RawMessage) (string, bool) { + switch method { + case "tools/call": + var p CallToolParams + if err := internaljson.Unmarshal(params, &p); err == nil { + return p.Name, true + } + case "prompts/get": + var p GetPromptParams + if err := internaljson.Unmarshal(params, &p); err == nil { + return p.Name, true + } + case "resources/read": + var p ReadResourceParams + if err := internaljson.Unmarshal(params, &p); err == nil { + return p.URI, true + } + } + + return "", false +} + +// headerSchemaProperty captures the fields needed for x-mcp-header processing. +type headerSchemaProperty struct { + Type string `json:"type"` + XMCPHeader json.RawMessage `json:"x-mcp-header,omitempty"` + Properties map[string]headerSchemaProperty `json:"properties,omitempty"` +} + +// unmarshalSchemaProperties normalizes any InputSchema type +// (*jsonschema.Schema, map[string]any, or json.RawMessage) into a common +// representation by marshaling to JSON and unmarshaling only the fields we need. +func unmarshalSchemaProperties(schema any) map[string]headerSchemaProperty { + var s headerSchemaProperty + if err := remarshal(schema, &s); err != nil { + return nil + } + return s.Properties +} + +// paramHeaderBinding maps a (possibly nested) input-schema property to the +// HTTP header it carries. +type paramHeaderBinding struct { + Path []string + Header string +} + +// extractParamHeaderAnnotations returns the bindings for every property in +// the tool's InputSchema that has an x-mcp-header annotation +func extractParamHeaderAnnotations(tool *Tool) []paramHeaderBinding { + props := unmarshalSchemaProperties(tool.InputSchema) + if len(props) == 0 { + return nil + } + var result []paramHeaderBinding + result = collectParamHeaderAnnotations(props, nil, result) + if len(result) == 0 { + return nil + } + return result +} + +// collectParamHeaderAnnotations walks the schema properties and records every +// x-mcp-header annotation it finds, keyed by the property-name path. +func collectParamHeaderAnnotations(props map[string]headerSchemaProperty, prefix []string, out []paramHeaderBinding) []paramHeaderBinding { + for propName, prop := range props { + path := make([]string, len(prefix)+1) + copy(path, prefix) + path[len(prefix)] = propName + + var headerName string + if err := json.Unmarshal(prop.XMCPHeader, &headerName); err == nil && headerName != "" { + out = append(out, paramHeaderBinding{Path: path, Header: headerName}) + } + if len(prop.Properties) > 0 { + out = collectParamHeaderAnnotations(prop.Properties, path, out) + } + } + return out +} + +// lookupArgument navigates the arguments object using the given property-name +// path and returns the raw JSON value at that location. It reports whether +// the value was found. +func lookupArgument(args map[string]json.RawMessage, path []string) (json.RawMessage, bool) { + if len(path) == 0 { + return nil, false + } + cur, ok := args[path[0]] + if !ok { + return nil, false + } + for _, part := range path[1:] { + var obj map[string]json.RawMessage + if err := internaljson.Unmarshal(cur, &obj); err != nil { + return nil, false + } + cur, ok = obj[part] + if !ok { + return nil, false + } + } + return cur, true +} + +// maxSafeInteger and minSafeInteger bound the integer values that can be +// faithfully represented as IEEE-754 double-precision floats. +const ( + maxSafeInteger = 1<<53 - 1 // 2^53 - 1 = 9007199254740991 + minSafeInteger = -(1<<53 - 1) // -(2^53 - 1) = -9007199254740991 +) + +// unmarshalPrimitive unmarshals a JSON value into the Go representation used +// for x-mcp-header processing per SEP-2243: +// +// - JSON string -> string +// - JSON boolean -> bool +// - JSON integer (within the JavaScript safe-integer range) -> int64 +// +// JSON numbers that are non-integers (have a fractional part, NaN, or ±Inf) +// or integers outside the safe range are rejected because the `number` type +// is not permitted for x-mcp-header parameters; only integer, string, boolean +// are allowed. +func unmarshalPrimitive(raw json.RawMessage) any { + var val any + if err := internaljson.Unmarshal(raw, &val); err != nil { + return nil + } + switch v := val.(type) { + case string, bool: + return v + case float64: + if math.IsNaN(v) || math.IsInf(v, 0) || v != math.Trunc(v) { + return nil + } + if v < minSafeInteger || v > maxSafeInteger { + return nil + } + return int64(v) + default: + return nil + } +} + +// primitiveToString formats an x-mcp-header value (as produced by +// [unmarshalPrimitive]) to its canonical header string representation per +// SEP-2243. Returns false if value is not one of the permitted primitive +// types (string, bool, int64). +func primitiveToString(value any) (string, bool) { + switch v := value.(type) { + case string: + return v, true + case bool: + return fmt.Sprintf("%t", v), true + case int64: + return strconv.FormatInt(v, 10), true + default: + return "", false + } +} + +// setStandardHeaders populates standard MCP headers. +// It requires the protocol version header to be set. +func setStandardHeaders(ctx context.Context, header http.Header, msg jsonrpc.Message) { + if msg == nil { + return + } + if header.Get(protocolVersionHeader) == "" || header.Get(protocolVersionHeader) < minVersionForStandardHeaders { + return + } + + switch msg := msg.(type) { + case *jsonrpc.Request: + header.Set(methodHeader, msg.Method) + if name, ok := extractName(msg.Method, msg.Params); ok { + header.Set(nameHeader, name) + } + if msg.Method == "tools/call" { + if tool, ok := ctx.Value(toolContextKey).(*Tool); ok && tool != nil { + for k, v := range generateParamHeaders(tool, msg.Params) { + header.Set(k, v) + } + } + } + } +} + +// generateParamHeaders reads x-mcp-header annotations from the tool's InputSchema +// and returns the Mcp-Param-{Name} headers to be set on the HTTP request. +func generateParamHeaders(tool *Tool, params json.RawMessage) map[string]string { + paramHeaders := extractParamHeaderAnnotations(tool) + if len(paramHeaders) == 0 { + return nil + } + + var raw struct { + Arguments map[string]json.RawMessage `json:"arguments"` + } + if err := internaljson.Unmarshal(params, &raw); err != nil || raw.Arguments == nil { + return nil + } + + res := make(map[string]string) + for _, b := range paramHeaders { + argRaw, ok := lookupArgument(raw.Arguments, b.Path) + if !ok { + continue + } + if string(argRaw) == "null" { + continue + } + val := unmarshalPrimitive(argRaw) + if val == nil { + continue + } + encoded, ok := encodeHeaderValue(val) + if !ok { + continue + } + res[paramHeaderPrefix+b.Header] = encoded + } + return res +} + +// filterValidTools returns only tools that have valid +// x-mcp-header annotations. Invalid tools are logged and excluded. +func filterValidTools(logger *slog.Logger, tools []*Tool) []*Tool { + logger = ensureLogger(logger) + result := make([]*Tool, 0, len(tools)) + for _, tool := range tools { + if err := validateParamHeaderAnnotations(tool); err != nil { + logger.Error("excluding tool from tools/list", "tool", tool.Name, "error", err) + continue + } + result = append(result, tool) + } + return result +} + +// validateParamHeaderAnnotations checks that a tool's x-mcp-header annotations +// are valid. Annotations may appear on properties at any nesting +// depth within the inputSchema and must be unique across all of them. +func validateParamHeaderAnnotations(tool *Tool) error { + props := unmarshalSchemaProperties(tool.InputSchema) + if len(props) == 0 { + return nil + } + seen := make(map[string]bool) + return validateParamHeadersIn(props, "", seen) +} + +func validateParamHeadersIn(props map[string]headerSchemaProperty, prefix string, seen map[string]bool) error { + for propName, prop := range props { + path := propName + if prefix != "" { + path = prefix + "." + propName + } + if prop.XMCPHeader != nil { + if prop.Type != "string" && prop.Type != "integer" && prop.Type != "boolean" { + return fmt.Errorf("property %q: x-mcp-header can only be applied to primitive types (integer, string, boolean), got %q", path, prop.Type) + } + var headerName string + if err := json.Unmarshal(prop.XMCPHeader, &headerName); err != nil || headerName == "" { + return fmt.Errorf("property %q: x-mcp-header must be a non-empty string", path) + } + if err := validateHeaderName(headerName); err != nil { + return fmt.Errorf("property %q: %w", path, err) + } + lower := strings.ToLower(headerName) + if seen[lower] { + return fmt.Errorf("property %q: duplicate x-mcp-header value %q (case-insensitive)", path, headerName) + } + seen[lower] = true + } + if len(prop.Properties) > 0 { + if err := validateParamHeadersIn(prop.Properties, path, seen); err != nil { + return err + } + } + } + return nil +} + +// validateHeaderName checks that a header name matches the HTTP field-name +// token syntax (1*tchar). +func validateHeaderName(name string) error { + if name == "" { + return fmt.Errorf("x-mcp-header value must be a non-empty string") + } + for _, c := range name { + if !isTChar(c) { + return fmt.Errorf("x-mcp-header value %q contains invalid character %q", name, c) + } + } + return nil +} + +// isTChar reports whether c is a valid HTTP token character (tchar) +// +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +func isTChar(c rune) bool { + switch { + case c >= '0' && c <= '9': + return true + case c >= 'A' && c <= 'Z': + return true + case c >= 'a' && c <= 'z': + return true + } + switch c { + case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', + '^', '_', '`', '|', '~': + return true + } + return false +} + +func validateMcpHeaders(header http.Header, msg jsonrpc.Message, toolLookup func(string) (*serverTool, bool)) error { + protocolVersion := header.Get(protocolVersionHeader) + if protocolVersion == "" || protocolVersion < minVersionForStandardHeaders { + return nil + } + + switch msg := msg.(type) { + case *jsonrpc.Request: + methodInHeader := header.Get(methodHeader) + if methodInHeader == "" { + return errors.New("missing required Mcp-Method header") + } + if methodInHeader != msg.Method { + return fmt.Errorf("header mismatch: Mcp-Method header value '%s' does not match body value '%s'", methodInHeader, msg.Method) + } + + var nameInBody string + if msg.Method == "tools/call" || msg.Method == "resources/read" || msg.Method == "prompts/get" { + nameInHeader := header.Get(nameHeader) + if nameInHeader == "" { + return fmt.Errorf("missing required Mcp-Name header for method %q", msg.Method) + } + var ok bool + nameInBody, ok = extractName(msg.Method, msg.Params) + if !ok { + return fmt.Errorf("failed to extract name from parameters for method %q", msg.Method) + } + if nameInHeader != nameInBody { + return fmt.Errorf("header mismatch: Mcp-Name header value '%s' does not match body value '%s'", nameInHeader, nameInBody) + } + } + + if msg.Method == "tools/call" && toolLookup != nil { + if st, ok := toolLookup(nameInBody); ok && st != nil { + if err := validateParamHeaders(header, msg, st.tool); err != nil { + return err + } + } + } + } + return nil +} + +func validateParamHeaders(header http.Header, msg *jsonrpc.Request, tool *Tool) error { + paramHeaders := extractParamHeaderAnnotations(tool) + if len(paramHeaders) == 0 { + return nil + } + + var raw struct { + Arguments map[string]json.RawMessage `json:"arguments"` + } + if err := internaljson.Unmarshal(msg.Params, &raw); err != nil { + return nil + } + + for _, b := range paramHeaders { + fullHeader := paramHeaderPrefix + b.Header + headerVal := header.Get(fullHeader) + argRaw, argExists := lookupArgument(raw.Arguments, b.Path) + + if !argExists || string(argRaw) == "null" { + if headerVal != "" { + return fmt.Errorf("header mismatch: unexpected %s header for absent or null parameter %q", fullHeader, strings.Join(b.Path, ".")) + } + continue + } + + if headerVal == "" { + return fmt.Errorf("header mismatch: missing %s header for parameter %q", fullHeader, strings.Join(b.Path, ".")) + } + + decoded, ok := decodeHeaderValue(headerVal) + if !ok { + return fmt.Errorf("header mismatch: %s header contains invalid Base64 encoding", fullHeader) + } + + bodyVal := unmarshalPrimitive(argRaw) + if bodyVal == nil { + return fmt.Errorf("header mismatch: %s header present but body parameter %q is not a primitive type", fullHeader, strings.Join(b.Path, ".")) + } + + if !primitiveEqual(decoded, bodyVal) { + return fmt.Errorf("header mismatch: %s header value '%s' does not match body value", fullHeader, headerVal) + } + } + return nil +} + +// primitiveEqual reports whether the (decoded) header string equals the +// JSON-derived body value. +func primitiveEqual(headerStr string, bodyVal any) bool { + if bodyInt, ok := bodyVal.(int64); ok { + headerNum, err := strconv.ParseFloat(headerStr, 64) + if err != nil { + return false + } + if math.IsNaN(headerNum) || math.IsInf(headerNum, 0) || headerNum != math.Trunc(headerNum) { + return false + } + if headerNum < minSafeInteger || headerNum > maxSafeInteger { + return false + } + return int64(headerNum) == bodyInt + } + expected, ok := primitiveToString(bodyVal) + if !ok { + return false + } + return headerStr == expected +} + +// encodeHeaderValue converts a parameter value to an HTTP header-safe string +// per the SEP-2243 encoding rules: +// - string: used as-is if safe ASCII, otherwise Base64 encoded +// - int64: decimal string representation +// - bool: lowercase "true" or "false" +// +// Values that contain non-ASCII characters, control characters, or +// leading/trailing whitespace are Base64-encoded with the =?base64?...?= wrapper. +// +// The second return value is false if the value is not a supported primitive type. +func encodeHeaderValue(value any) (string, bool) { + s, ok := primitiveToString(value) + if !ok { + return "", false + } + if requiresBase64Encoding(s) { + return encodeBase64(s), true + } + return s, true +} + +// decodeHeaderValue decodes a header value that may be Base64-encoded +// with the =?base64?...?= wrapper. +// +// The second return value is false if the header value is not a valid Base64 encoded value. +func decodeHeaderValue(headerValue string) (string, bool) { + if len(headerValue) == 0 { + return headerValue, true + } + + if encoded, ok := strings.CutPrefix(headerValue, base64Prefix); ok { + if encoded, ok = strings.CutSuffix(encoded, base64Suffix); ok { + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "", false + } + return string(decoded), true + } + } + return headerValue, true +} + +func requiresBase64Encoding(s string) bool { + if len(s) == 0 { + return false + } + if s[0] == ' ' || s[0] == '\t' || s[len(s)-1] == ' ' || s[len(s)-1] == '\t' { + return true + } + for _, c := range s { + if c < 0x20 || c > 0x7E { + return true + } + } + // Per SEP-2243, plain-ASCII values that match the base64 sentinel pattern + // must also be base64-encoded to avoid ambiguity with already-encoded values. + if strings.HasPrefix(s, base64Prefix) && strings.HasSuffix(s, base64Suffix) { + return true + } + return false +} + +func encodeBase64(s string) string { + return base64Prefix + base64.StdEncoding.EncodeToString([]byte(s)) + base64Suffix +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/tool.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/tool.go index 3ecb59d3e..04cf551a9 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/tool.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/tool.go @@ -65,8 +65,14 @@ type serverTool struct { // applySchema validates whether data is valid JSON according to the provided // schema, after applying schema defaults. // -// Returns the JSON value augmented with defaults. -func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawMessage, error) { +// If forOutput is false, the data is treated as tool input: the schema's root +// type must be "object" and the value is unmarshaled into a map. +// +// If forOutput is true, the data is treated as tool output: the schema's root +// may be of any type (object, array, primitive, composition). +// +// Returns the JSON value, augmented with defaults where applicable. +func applySchema(data json.RawMessage, resolved *jsonschema.Resolved, forOutput bool) (json.RawMessage, error) { // TODO: use reflection to create the struct type to unmarshal into. // Separate validation from assignment. @@ -75,33 +81,81 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved) (json.RawM // This avoids inconsistent representation due to custom marshallers, such as // time.Time (issue #449). // - // Additionally, unmarshalling into a map ensures that the resulting JSON is + // For input, unmarshalling into a map ensures that the resulting JSON is // at least {}, even if data is empty. For example, arguments is technically // an optional property of callToolParams, and we still want to apply the // defaults in this case. // // TODO(rfindley): in which cases can resolved be nil? - if resolved != nil { + if resolved == nil { + return data, nil + } + + var unmarshaled any + if !forOutput { v := make(map[string]any) if len(data) > 0 { if err := internaljson.Unmarshal(data, &v); err != nil { return nil, fmt.Errorf("unmarshaling arguments: %w", err) } } - if err := resolved.ApplyDefaults(&v); err != nil { + unmarshaled = v + } else { + if len(data) > 0 { + if err := internaljson.Unmarshal(data, &unmarshaled); err != nil { + return nil, fmt.Errorf("unmarshaling output: %w", err) + } + } + } + + // Apply defaults only when the value is a map: jsonschema.Resolved.ApplyDefaults + // only operates on object properties. For object-rooted output schemas, + // coerce a nil result (from "null" or empty data) into {} so handlers that + // return a typed-nil map still validate. + appliedDefaults := false + if _, ok := unmarshaled.(map[string]any); ok { + if err := resolved.ApplyDefaults(&unmarshaled); err != nil { return nil, fmt.Errorf("applying schema defaults:\n%w", err) } - if err := resolved.Validate(&v); err != nil { - return nil, err + appliedDefaults = true + } else if forOutput && unmarshaled == nil && resolved.Schema().Type == "object" { + unmarshaled = make(map[string]any) + if err := resolved.ApplyDefaults(&unmarshaled); err != nil { + return nil, fmt.Errorf("applying schema defaults:\n%w", err) } - // We must re-marshal with the default values applied. - var err error - data, err = json.Marshal(v) - if err != nil { - return nil, fmt.Errorf("marshalling with defaults: %v", err) + appliedDefaults = true + } + + if err := resolved.Validate(&unmarshaled); err != nil { + return nil, err + } + + // Re-marshal only when defaults may have changed the value. + if !appliedDefaults { + return data, nil + } + out, err := json.Marshal(unmarshaled) + if err != nil { + return nil, fmt.Errorf("marshalling with defaults: %v", err) + } + return out, nil +} + +// isObjectJSON reports whether data is a JSON object (i.e., starts with '{' +// after any leading whitespace). Returns false for arrays, primitives, null, +// or empty input. +func isObjectJSON(data json.RawMessage) bool { + for _, b := range data { + switch b { + case ' ', '\t', '\n', '\r': + continue + case '{': + return true + default: + return false } } - return data, nil + return false } // validateToolName checks whether name is a valid tool name, reporting a diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go index 5f2a50072..55c73d745 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go @@ -10,17 +10,24 @@ import ( "errors" "fmt" "io" - "log" + "log/slog" "net" "os" "sync" + "time" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" - "github.com/modelcontextprotocol/go-sdk/internal/xcontext" "github.com/modelcontextprotocol/go-sdk/jsonrpc" ) +// notifyCancellationTimeout bounds the cancellation notification we send to +// the peer when the caller's context is cancelled. The notification is +// best-effort: a degraded connection (e.g. an OAuth flow that has been +// abandoned) must not be able to block the caller's return path or +// re-trigger expensive recovery on its behalf. See issue #882. +const notifyCancellationTimeout = 5 * time.Second + // ErrConnectionClosed is returned when sending a message to a connection that // is closed or in the process of closing. var ErrConnectionClosed = errors.New("connection closed") @@ -41,6 +48,19 @@ type Transport interface { Connect(ctx context.Context) (Connection, error) } +// ProtocolVersionSupporter is an optional capability that a [Transport] may +// implement to declare which MCP protocol versions it can serve. +// +// [Server.Connect] consults this interface to filter the +// list of versions advertised in server/discover responses. Transports that +// do not implement this interface are assumed to support every protocol +// version known to the SDK. +type ProtocolVersionSupporter interface { + // SupportsProtocolVersion reports whether the transport can serve + // requests using the given protocol version. + SupportsProtocolVersion(version string) bool +} + // A Connection is a logical bidirectional JSON-RPC connection. type Connection interface { // Read reads the next message to process off the connection. @@ -152,7 +172,9 @@ type handler interface { handle(ctx context.Context, req *jsonrpc.Request) (any, error) } -func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, State], s State, onClose func()) (H, error) { +// connect wires a transport to a binder. logger must be non-nil; it receives +// jsonrpc2 internal errors that would otherwise be dropped (see #218). +func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, State], s State, onClose func(), logger *slog.Logger) (H, error) { var zero H mcpConn, err := t.Connect(ctx) if err != nil { @@ -169,6 +191,13 @@ func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, preempter.conn = conn return jsonrpc2.HandlerFunc(h.handle) } + // Transports may opt in to propagating cancellation of ctx into request + // handler contexts when their own lifecycle IS the cancellation signal + // (e.g., a connection bound to a single HTTP request). + var propagateCancellation bool + if cp, ok := mcpConn.(cancellationPropagator); ok { + propagateCancellation = cp.propagateCancellation() + } _ = jsonrpc2.NewConnection(ctx, jsonrpc2.ConnectionConfig{ Reader: reader, Writer: writer, @@ -178,12 +207,25 @@ func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, OnDone: func() { b.disconnect(h) }, - OnInternalError: func(err error) { log.Printf("jsonrpc2 error: %v", err) }, + OnInternalError: func(err error) { + logger.Error("jsonrpc2 internal error", "error", err) + }, + PropagateCancellation: propagateCancellation, }) assert(preempter.conn != nil, "unbound preempter") return h, nil } +// cancellationPropagator is an optional interface implemented by a +// [Connection] whose own lifecycle should propagate cancellation into request +// handler contexts. The default jsonrpc2 behavior is to suppress propagation; +// transports that bind a connection to a single short-lived carrier (such as +// a one-shot HTTP request) should return true here so that handlers unwind +// when the carrier observes the peer going away. +type cancellationPropagator interface { + propagateCancellation() bool +} + // A canceller is a jsonrpc2.Preempter that cancels in-flight requests on MCP // cancelled notifications. type canceller struct { @@ -206,6 +248,24 @@ func (c *canceller) Preempt(ctx context.Context, req *jsonrpc.Request) (result a return nil, jsonrpc2.ErrNotHandled } +// callSubscriptionsListen issues a "subscriptions/listen" call (SEP-2575) +// without awaiting its JSON-RPC response. The call's logical lifetime is the +// stream of notifications that follow on the same channel — the empty +// response, if ever delivered, only marks subscription teardown — so the +// caller has nothing useful to block on. +// +// Cancellation is driven by ctx: when it is cancelled, a background goroutine +// sends a "notifications/cancelled" notification referencing the listen's +// request ID and retires the call from the connection's outgoing-calls map. +func callSubscriptionsListen(ctx context.Context, conn *jsonrpc2.Connection, method string, params Params) { + call := conn.Call(ctx, method, params) + + go func() { + <-ctx.Done() + _ = cancelCall(ctx, conn, call) + }() +} + // call executes and awaits a jsonrpc2 call on the given connection, // translating errors into the mcp domain. func call(ctx context.Context, conn *jsonrpc2.Connection, method string, params Params, result Result) error { @@ -216,23 +276,7 @@ func call(ctx context.Context, conn *jsonrpc2.Connection, method string, params case errors.Is(err, jsonrpc2.ErrClientClosing), errors.Is(err, jsonrpc2.ErrServerClosing): return fmt.Errorf("%w: calling %q: %v", ErrConnectionClosed, method, err) case ctx.Err() != nil: - // Notify the peer of cancellation. - err := conn.Notify(xcontext.Detach(ctx), notificationCancelled, &CancelledParams{ - Reason: ctx.Err().Error(), - RequestID: call.ID().Raw(), - }) - // By default, the jsonrpc2 library waits for graceful shutdown when the - // connection is closed, meaning it expects all outgoing and incoming - // requests to complete. However, for MCP this expectation is unrealistic, - // and can lead to hanging shutdown. For example, if a streamable client is - // killed, the server will not be able to detect this event, except via - // keepalive pings (if they are configured), and so outgoing calls may hang - // indefinitely. - // - // Therefore, we choose to eagerly retire calls, removing them from the - // outgoingCalls map, when the caller context is cancelled: if the caller - // will never receive the response, there's no need to track it. - conn.Retire(call, ctx.Err()) + err := cancelCall(ctx, conn, call) return errors.Join(ctx.Err(), err) case err != nil: return fmt.Errorf("calling %q: %w", method, err) @@ -240,6 +284,30 @@ func call(ctx context.Context, conn *jsonrpc2.Connection, method string, params return nil } +// cancelCall sends a "notifications/cancelled" notification for call and eagerly +// retires it from conn. +// +// By default, the jsonrpc2 library waits for graceful shutdown when the +// connection is closed, meaning it expects all outgoing and incoming requests +// to complete. However, for MCP this expectation is unrealistic, and can lead +// to hanging shutdown. For example, if a streamable client is killed, the +// server will not be able to detect this event, except via keepalive pings (if +// they are configured), and so outgoing calls may hang indefinitely. +// +// Therefore, we choose to eagerly retire calls, removing them from the +// outgoingCalls map, when the caller context is cancelled: if the caller will +// never receive the response, there's no need to track it. +func cancelCall(ctx context.Context, conn *jsonrpc2.Connection, call *jsonrpc2.AsyncCall) error { + notifyCtx, cancelNotify := context.WithTimeout(context.WithoutCancel(ctx), notifyCancellationTimeout) + defer cancelNotify() + err := conn.Notify(notifyCtx, notificationCancelled, &CancelledParams{ + Reason: ctx.Err().Error(), + RequestID: call.ID().Raw(), + }) + conn.Retire(call, ctx.Err()) + return err +} + // A LoggingTransport is a [Transport] that delegates to another transport, // writing RPC logs to an io.Writer. type LoggingTransport struct { @@ -344,7 +412,11 @@ func (r rwc) Close() error { // // See [msgBatch] for more discussion of message batching. type ioConn struct { - protocolVersion string // negotiated version, set during session initialization. + // protocolVersion is the negotiated version of the protocol, + // set during session initialization. + // Since writes may be concurrent to reads, we need to guard this with a mutex. + sessionMu sync.Mutex + protocolVersion string writeMu sync.Mutex // guards Write, which must be concurrency safe. rwc io.ReadWriteCloser // the underlying stream @@ -430,9 +502,15 @@ func (c *ioConn) sessionUpdated(state ServerSessionState) { protocolVersion = state.InitializeParams.ProtocolVersion } if protocolVersion == "" { + // 2025-03-26 is used, because it's the last spec version + // where specifying the protocol version in the HTTP header + // was not required. protocolVersion = protocolVersion20250326 } - c.protocolVersion = negotiatedVersion(protocolVersion) + protocolVersion = negotiatedVersion(protocolVersion) + c.sessionMu.Lock() + c.protocolVersion = protocolVersion + c.sessionMu.Unlock() } // addBatch records a msgBatch for an incoming batch payload. @@ -533,8 +611,12 @@ func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) { if err != nil { return nil, err } - if batch && t.protocolVersion >= protocolVersion20250618 { - return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, t.protocolVersion) + var protocolVersion string + t.sessionMu.Lock() + protocolVersion = t.protocolVersion + t.sessionMu.Unlock() + if batch && protocolVersion >= protocolVersion20250618 { + return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, protocolVersion) } t.queue = msgs[1:] diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/audience.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/audience.go new file mode 100644 index 000000000..cad1303d7 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/audience.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package oauthex + +import "strings" + +// MatchesResource reports whether any of claims matches resource under +// RFC 3986 §6.2.3 scheme-based normalization, narrowed to the empty-path +// case: a URI with an empty path is treated as equivalent to one with a +// path of "/". All other URI components (scheme, host case, port, query, +// fragment) must match exactly — this is intentionally narrower than the +// full §6.2.3 rung, which would also fold scheme/host case and default +// ports. +// +// RFC 9728 §3.3 normatively requires "simple string comparison" per +// RFC 3986 §6.2.1 (byte-equal). Callers that need strict byte-equal +// semantics should compare claims to resource directly: +// +// for _, c := range claims { if c == resource { return true } } +// +// Background: RFC 9728 §3.3 canonicalises the protected-resource +// identifier with a trailing slash, but RFC 8707 resource indicators +// sometimes omit it, and upstream IdPs vary in which form they emit in +// `aud` claims (Google trims, Auth0 retains, claude.ai round-trips +// whichever it received). Strict byte equality therefore fails routinely +// on legitimate setups; this helper is the most common pragmatic relaxation +// while keeping path/scheme/host strict so token confusion across distinct +// resources still fails closed. +// +// Returns false when claims is empty. +func MatchesResource(claims []string, resource string) bool { + expected := strings.TrimSuffix(resource, "/") + for _, c := range claims { + if c == resource { + return true + } + if strings.TrimSuffix(c, "/") == expected { + return true + } + } + return false +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go index 36210576b..711b17a43 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go @@ -5,8 +5,6 @@ // This file implements Authorization Server Metadata. // See https://www.rfc-editor.org/rfc/rfc8414.html. -//go:build mcp_go_client_oauth - package oauthex import ( @@ -14,6 +12,8 @@ import ( "errors" "fmt" "net/http" + + "github.com/modelcontextprotocol/go-sdk/internal/authutil" ) // AuthServerMeta represents the metadata for an OAuth 2.0 authorization server, @@ -115,6 +115,13 @@ type AuthServerMeta struct { // ClientIDMetadataDocumentSupported is a boolean indicating whether the authorization server // supports client ID metadata documents. ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported,omitempty"` + + // AuthorizationResponseIssParameterSupported indicates whether the authorization server + // provides the "iss" parameter in authorization responses per [RFC 9207]. + // When true, clients must verify the "iss" parameter is present and matches the Issuer field. + // + // [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207 + AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` } // GetAuthServerMeta issues a GET request to retrieve authorization server metadata @@ -147,8 +154,7 @@ func GetAuthServerMeta(ctx context.Context, metadataURL, issuer string, c *http. } return nil, fmt.Errorf("%v", err) // Do not expose error types. } - if asm.Issuer != issuer { - // Validate the Issuer field (see RFC 8414, section 3.3). + if !authutil.IssuersEqual(asm.Issuer, issuer) { return nil, fmt.Errorf("metadata issuer %q does not match issuer URL %q", asm.Issuer, issuer) } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go new file mode 100644 index 000000000..1b19eed5e --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go @@ -0,0 +1,66 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package oauthex + +import "errors" + +// ClientCredentials holds client authentication credentials for OAuth token requests. +// It supports multiple authentication methods, but only one method should be set at a time. +// Use the Validate method to ensure proper configuration. +type ClientCredentials struct { + // ClientID is the OAuth2 client identifier. + // REQUIRED for all authentication methods. + ClientID string + + // ClientSecretAuth configures client authentication using a client secret. + // This is the most common authentication method for confidential clients. + // OPTIONAL. If not provided, the client is treated as a public client. + ClientSecretAuth *ClientSecretAuth + + // Issuer is the issuer identifier of the authorization server these + // credentials are registered with. Pre-registered credentials are bound + // to a specific authorization server; when set, an error is returned if + // the discovered authorization server does not match, per SEP-2352. + // The comparison ignores a single trailing slash, matching the + // tolerance applied during RFC 8414 Section 3.3 metadata validation. + // OPTIONAL. + Issuer string +} + +// ClientSecretAuth holds client secret authentication credentials. +// This authentication method supports both "client_secret_basic" and "client_secret_post" +// methods as defined in RFC 6749 Section 2.3.1. +type ClientSecretAuth struct { + // ClientSecret is the OAuth2 client secret for confidential clients. + // REQUIRED when using ClientSecretAuth. + ClientSecret string +} + +// Validate checks that the ClientCredentials are properly configured. +// It ensures that: +// - ClientID is not empty. +// - At most one authentication method is configured. +// - If ClientSecretAuth is set, ClientSecret is not empty. +func (c *ClientCredentials) Validate() error { + if c.ClientID == "" { + return errors.New("ClientID is required") + } + + // Count how many auth methods are configured. + authMethodCount := 0 + if c.ClientSecretAuth != nil { + authMethodCount++ + if c.ClientSecretAuth.ClientSecret == "" { + return errors.New("ClientSecret is required when using ClientSecretAuth") + } + } + + // Allow zero auth methods (public client) or exactly one auth method. + if authMethodCount > 1 { + return errors.New("only one client authentication method can be configured") + } + + return nil +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go index 6db30255f..f46d0e8e7 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go @@ -5,8 +5,6 @@ // This file implements Authorization Server Metadata. // See https://www.rfc-editor.org/rfc/rfc8414.html. -//go:build mcp_go_client_oauth - package oauthex import ( @@ -91,6 +89,11 @@ type ClientRegistrationMetadata struct { // SoftwareStatement is an OPTIONAL JWT that asserts client metadata values. // Values in the software statement take precedence over other metadata values. SoftwareStatement string `json:"software_statement,omitempty"` + + // ApplicationType is an OPTIONAL string that indicates the type of application. + // Valid values are "native" and "web". + // If omitted, OIDC-compliant authorization servers default to "web". + ApplicationType string `json:"application_type,omitempty"` } // ClientRegistrationResponse represents the fields returned by the Authorization Server @@ -206,7 +209,7 @@ func RegisterClient(ctx context.Context, registrationEndpoint string, clientMeta return nil, fmt.Errorf("failed to read registration response body: %w", err) } - if resp.StatusCode == http.StatusCreated { + if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK { var regResponse ClientRegistrationResponse if err := internaljson.Unmarshal(body, ®Response); err != nil { return nil, fmt.Errorf("failed to decode successful registration response: %w (%s)", err, string(body)) diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go index d8aeb3c27..689f17aea 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go @@ -4,13 +4,10 @@ // Package oauthex implements extensions to OAuth2. -//go:build mcp_go_client_oauth - package oauthex import ( "context" - "encoding/json" "fmt" "io" "mime" @@ -18,6 +15,7 @@ import ( "net/url" "strings" + "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/util" ) @@ -66,6 +64,7 @@ func getJSON[T any](ctx context.Context, c *http.Client, url string, limit int64 // checkURLScheme ensures that its argument is a valid URL with a scheme // that prevents XSS attacks. // See #526. +// Note: a copy of this function exists in auth/extauth/oidc_login.go; keep these in sync. func checkURLScheme(u string) error { if u == "" { return nil diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go index 4680c1538..75066345c 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go @@ -5,8 +5,6 @@ // This file implements Protected Resource Metadata. // See https://www.rfc-editor.org/rfc/rfc9728.html. -//go:build mcp_go_client_oauth - package oauthex import ( @@ -14,81 +12,108 @@ import ( "errors" "fmt" "net/http" - "net/url" - "path" "strings" "unicode" "github.com/modelcontextprotocol/go-sdk/internal/util" ) -const defaultProtectedResourceMetadataURI = "/.well-known/oauth-protected-resource" - -// GetProtectedResourceMetadataFromID issues a GET request to retrieve protected resource -// metadata from a resource server by its ID. -// The resource ID is an HTTPS URL, typically with a host:port and possibly a path. -// For example: -// -// https://example.com/server -// -// This function, following the spec (§3), inserts the default well-known path into the -// URL. In our example, the result would be +// ProtectedResourceMetadata is the metadata for an OAuth 2.0 protected resource, +// as defined in section 2 of https://www.rfc-editor.org/rfc/rfc9728.html. // -// https://example.com/.well-known/oauth-protected-resource/server -// -// It then retrieves the metadata at that location using the given client (or the -// default client if nil) and validates its resource field against resourceID. -// -// Deprecated: Use [GetProtectedResourceMetadata] instead. This function will be removed in v1.5.0. -func GetProtectedResourceMetadataFromID(ctx context.Context, resourceID string, c *http.Client) (_ *ProtectedResourceMetadata, err error) { - defer util.Wrapf(&err, "GetProtectedResourceMetadataFromID(%q)", resourceID) +// The following features are not supported: +// - additional keys (§2, last sentence) +// - human-readable metadata (§2.1) +// - signed metadata (§2.2) +type ProtectedResourceMetadata struct { + // Resource (resource) is the protected resource's resource identifier. + // Required. + Resource string `json:"resource"` - u, err := url.Parse(resourceID) - if err != nil { - return nil, err - } - // Insert well-known URI into URL. - u.Path = path.Join(defaultProtectedResourceMetadataURI, u.Path) - return GetProtectedResourceMetadata(ctx, u.String(), resourceID, c) -} + // AuthorizationServers (authorization_servers) is an optional slice containing a list of + // OAuth authorization server issuer identifiers (as defined in RFC 8414) that can be + // used with this protected resource. + AuthorizationServers []string `json:"authorization_servers,omitempty"` -// GetProtectedResourceMetadataFromHeader retrieves protected resource metadata -// using information in the given header, using the given client (or the default -// client if nil). -// It issues a GET request to a URL discovered by parsing the WWW-Authenticate headers in the given request. -// Per RFC 9728 section 3.3, it validates that the resource field of the resulting metadata -// matches the serverURL (the URL that the client used to make the original request to the resource server). -// If there is no metadata URL in the header, it returns nil, nil. -// -// Deprecated: Use [GetProtectedResourceMetadata] instead. This function will be removed in v1.5.0. -func GetProtectedResourceMetadataFromHeader(ctx context.Context, serverURL string, header http.Header, c *http.Client) (_ *ProtectedResourceMetadata, err error) { - headers := header[http.CanonicalHeaderKey("WWW-Authenticate")] - if len(headers) == 0 { - return nil, nil - } - cs, err := ParseWWWAuthenticate(headers) - if err != nil { - return nil, err - } - metadataURL := resourceMetadataURL(cs) - if metadataURL == "" { - return nil, nil - } - return GetProtectedResourceMetadata(ctx, metadataURL, serverURL, c) + // JWKSURI (jwks_uri) is an optional URL of the protected resource's JSON Web Key (JWK) Set + // document. This contains public keys belonging to the protected resource, such as + // signing key(s) that the resource server uses to sign resource responses. + JWKSURI string `json:"jwks_uri,omitempty"` + + // ScopesSupported (scopes_supported) is a recommended slice containing a list of scope + // values (as defined in RFC 6749) used in authorization requests to request access + // to this protected resource. + ScopesSupported []string `json:"scopes_supported,omitempty"` + + // BearerMethodsSupported (bearer_methods_supported) is an optional slice containing + // a list of the supported methods of sending an OAuth 2.0 bearer token to the + // protected resource. Defined values are "header", "body", and "query". + BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` + + // ResourceSigningAlgValuesSupported (resource_signing_alg_values_supported) is an optional + // slice of JWS signing algorithms (alg values) supported by the protected + // resource for signing resource responses. + ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"` + + // ResourceName (resource_name) is a human-readable name of the protected resource + // intended for display to the end user. It is RECOMMENDED that this field be included. + // This value may be internationalized. + ResourceName string `json:"resource_name,omitempty"` + + // ResourceDocumentation (resource_documentation) is an optional URL of a page containing + // human-readable information for developers using the protected resource. + // This value may be internationalized. + ResourceDocumentation string `json:"resource_documentation,omitempty"` + + // ResourcePolicyURI (resource_policy_uri) is an optional URL of a page containing + // human-readable policy information on how a client can use the data provided. + // This value may be internationalized. + ResourcePolicyURI string `json:"resource_policy_uri,omitempty"` + + // ResourceTOSURI (resource_tos_uri) is an optional URL of a page containing the protected + // resource's human-readable terms of service. This value may be internationalized. + ResourceTOSURI string `json:"resource_tos_uri,omitempty"` + + // TLSClientCertificateBoundAccessTokens (tls_client_certificate_bound_access_tokens) is an + // optional boolean indicating support for mutual-TLS client certificate-bound + // access tokens (RFC 8705). Defaults to false if omitted. + TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` + + // AuthorizationDetailsTypesSupported (authorization_details_types_supported) is an optional + // slice of 'type' values supported by the resource server for the + // 'authorization_details' parameter (RFC 9396). + AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"` + + // DPOPSigningAlgValuesSupported (dpop_signing_alg_values_supported) is an optional + // slice of JWS signing algorithms supported by the resource server for validating + // DPoP proof JWTs (RFC 9449). + DPOPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"` + + // DPOPBoundAccessTokensRequired (dpop_bound_access_tokens_required) is an optional boolean + // specifying whether the protected resource always requires the use of DPoP-bound + // access tokens (RFC 9449). Defaults to false if omitted. + DPOPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"` + + // SignedMetadata (signed_metadata) is an optional JWT containing metadata parameters + // about the protected resource as claims. If present, these values take precedence + // over values conveyed in plain JSON. + // TODO:implement. + // Note that §2.2 says it's okay to ignore this. + // SignedMetadata string `json:"signed_metadata,omitempty"` } -// resourceMetadataURL returns a resource metadata URL from the given "WWW-Authenticate" header challenges, -// or the empty string if there is none. -func resourceMetadataURL(cs []Challenge) string { - for _, c := range cs { - if u := c.Params["resource_metadata"]; u != "" { - return u - } - } - return "" +// Challenge represents a single authentication challenge from a WWW-Authenticate header. +// As per RFC 9110, Section 11.6.1, a challenge consists of a scheme and optional parameters. +type Challenge struct { + // Scheme is the authentication scheme (e.g., "Bearer", "Basic"). + // It is case-insensitive. A parsed value will always be lower-case. + Scheme string + // Params is a map of authentication parameters. + // Keys are case-insensitive. Parsed keys are always lower-case. + Params map[string]string } -// GetProtectedResourceMetadataFromID issues a GET request to retrieve protected resource +// GetProtectedResourceMetadata issues a GET request to retrieve protected resource // metadata from a resource server. // The metadataURL is typically a URL with a host:port and possibly a path. // The resourceURL is the resource URI the metadataURL is for. diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go deleted file mode 100644 index 3bf7d9aca..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2025 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -// This file implements Protected Resource Metadata. -// See https://www.rfc-editor.org/rfc/rfc9728.html. - -// This is a temporary file to expose the required objects to the main package. - -package oauthex - -// ProtectedResourceMetadata is the metadata for an OAuth 2.0 protected resource, -// as defined in section 2 of https://www.rfc-editor.org/rfc/rfc9728.html. -// -// The following features are not supported: -// - additional keys (§2, last sentence) -// - human-readable metadata (§2.1) -// - signed metadata (§2.2) -type ProtectedResourceMetadata struct { - // Resource (resource) is the protected resource's resource identifier. - // Required. - Resource string `json:"resource"` - - // AuthorizationServers (authorization_servers) is an optional slice containing a list of - // OAuth authorization server issuer identifiers (as defined in RFC 8414) that can be - // used with this protected resource. - AuthorizationServers []string `json:"authorization_servers,omitempty"` - - // JWKSURI (jwks_uri) is an optional URL of the protected resource's JSON Web Key (JWK) Set - // document. This contains public keys belonging to the protected resource, such as - // signing key(s) that the resource server uses to sign resource responses. - JWKSURI string `json:"jwks_uri,omitempty"` - - // ScopesSupported (scopes_supported) is a recommended slice containing a list of scope - // values (as defined in RFC 6749) used in authorization requests to request access - // to this protected resource. - ScopesSupported []string `json:"scopes_supported,omitempty"` - - // BearerMethodsSupported (bearer_methods_supported) is an optional slice containing - // a list of the supported methods of sending an OAuth 2.0 bearer token to the - // protected resource. Defined values are "header", "body", and "query". - BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` - - // ResourceSigningAlgValuesSupported (resource_signing_alg_values_supported) is an optional - // slice of JWS signing algorithms (alg values) supported by the protected - // resource for signing resource responses. - ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"` - - // ResourceName (resource_name) is a human-readable name of the protected resource - // intended for display to the end user. It is RECOMMENDED that this field be included. - // This value may be internationalized. - ResourceName string `json:"resource_name,omitempty"` - - // ResourceDocumentation (resource_documentation) is an optional URL of a page containing - // human-readable information for developers using the protected resource. - // This value may be internationalized. - ResourceDocumentation string `json:"resource_documentation,omitempty"` - - // ResourcePolicyURI (resource_policy_uri) is an optional URL of a page containing - // human-readable policy information on how a client can use the data provided. - // This value may be internationalized. - ResourcePolicyURI string `json:"resource_policy_uri,omitempty"` - - // ResourceTOSURI (resource_tos_uri) is an optional URL of a page containing the protected - // resource's human-readable terms of service. This value may be internationalized. - ResourceTOSURI string `json:"resource_tos_uri,omitempty"` - - // TLSClientCertificateBoundAccessTokens (tls_client_certificate_bound_access_tokens) is an - // optional boolean indicating support for mutual-TLS client certificate-bound - // access tokens (RFC 8705). Defaults to false if omitted. - TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` - - // AuthorizationDetailsTypesSupported (authorization_details_types_supported) is an optional - // slice of 'type' values supported by the resource server for the - // 'authorization_details' parameter (RFC 9396). - AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"` - - // DPOPSigningAlgValuesSupported (dpop_signing_alg_values_supported) is an optional - // slice of JWS signing algorithms supported by the resource server for validating - // DPoP proof JWTs (RFC 9449). - DPOPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"` - - // DPOPBoundAccessTokensRequired (dpop_bound_access_tokens_required) is an optional boolean - // specifying whether the protected resource always requires the use of DPoP-bound - // access tokens (RFC 9449). Defaults to false if omitted. - DPOPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"` - - // SignedMetadata (signed_metadata) is an optional JWT containing metadata parameters - // about the protected resource as claims. If present, these values take precedence - // over values conveyed in plain JSON. - // TODO:implement. - // Note that §2.2 says it's okay to ignore this. - // SignedMetadata string `json:"signed_metadata,omitempty"` -} - -// Challenge represents a single authentication challenge from a WWW-Authenticate header. -// As per RFC 9110, Section 11.6.1, a challenge consists of a scheme and optional parameters. -type Challenge struct { - // Scheme is the authentication scheme (e.g., "Bearer", "Basic"). - // It is case-insensitive. A parsed value will always be lower-case. - Scheme string - // Params is a map of authentication parameters. - // Keys are case-insensitive. Parsed keys are always lower-case. - Params map[string]string -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go new file mode 100644 index 000000000..0c7b24eb4 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go @@ -0,0 +1,183 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +// This file implements Token Exchange (RFC 8693) for Enterprise Managed Authorization. +// See https://datatracker.ietf.org/doc/html/rfc8693 + +package oauthex + +import ( + "context" + "fmt" + "net/http" + "strings" + + "golang.org/x/oauth2" +) + +// Token type identifiers defined by RFC 8693 and SEP-990. +const ( + // TokenTypeIDToken is the URN for OpenID Connect ID Tokens. + TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token" + + // TokenTypeSAML2 is the URN for SAML 2.0 assertions. + TokenTypeSAML2 = "urn:ietf:params:oauth:token-type:saml2" + + // TokenTypeIDJAG is the URN for Identity Assertion JWT Authorization Grants. + // This is the token type returned by IdP during token exchange for SEP-990. + TokenTypeIDJAG = "urn:ietf:params:oauth:token-type:id-jag" + + // GrantTypeTokenExchange is the grant type for RFC 8693 token exchange. + GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" +) + +// TokenExchangeRequest represents a Token Exchange request per RFC 8693. +// This is used for Enterprise Managed Authorization (SEP-990) where an MCP Client +// exchanges an ID Token from an enterprise IdP for an ID-JAG that can be used +// to obtain an access token from an MCP Server's authorization server. +type TokenExchangeRequest struct { + // RequestedTokenType indicates the type of security token being requested. + // For SEP-990, this MUST be TokenTypeIDJAG. + RequestedTokenType string + + // Audience is the logical name of the target service where the client + // intends to use the requested token. For SEP-990, this MUST be the + // Issuer URL of the MCP Server's authorization server. + Audience string + + // Resource is the physical location or identifier of the target resource. + // For SEP-990, this MUST be the RFC9728 Resource Identifier of the MCP Server. + Resource string + + // Scope is a list of space-separated scopes for the requested token. + // This is OPTIONAL per RFC 8693 but commonly used in SEP-990. + Scope []string + + // SubjectToken is the security token that represents the identity of the + // party on behalf of whom the request is being made. For SEP-990, this is + // typically an OpenID Connect ID Token. + SubjectToken string + + // SubjectTokenType is the type of the security token in SubjectToken. + // For SEP-990 with OIDC, this MUST be TokenTypeIDToken. + SubjectTokenType string +} + +// ExchangeToken performs a token exchange request per RFC 8693 for Enterprise +// Managed Authorization (SEP-990). It exchanges an identity assertion (typically +// an ID Token) for an Identity Assertion JWT Authorization Grant (ID-JAG) that +// can be used to obtain an access token from an MCP Server. +// +// The tokenEndpoint parameter should be the IdP's token endpoint (typically +// obtained from the IdP's authorization server metadata). +// +// Returns an oauth2.Token where: +// - Extra("issued_token_type") contains the type of the issued token (e.g., TokenTypeIDJAG) +// - AccessToken contains the ID-JAG JWT (despite the name, this is not an OAuth access token) +// - TokenType is typically "N_A" for SEP-990 +// - Extra("scope") may contain the scope if different from the request +// - Expiry is when the token expires +func ExchangeToken( + ctx context.Context, + tokenEndpoint string, + req *TokenExchangeRequest, + clientCreds *ClientCredentials, + httpClient *http.Client, +) (*oauth2.Token, error) { + if tokenEndpoint == "" { + return nil, fmt.Errorf("token endpoint is required") + } + if req == nil { + return nil, fmt.Errorf("token exchange request is required") + } + if clientCreds == nil { + return nil, fmt.Errorf("client credentials are required") + } + if err := clientCreds.Validate(); err != nil { + return nil, fmt.Errorf("invalid client credentials: %w", err) + } + + // Validate required fields per SEP-990 Section 4. + if req.RequestedTokenType == "" { + return nil, fmt.Errorf("requested_token_type is required") + } + if req.Audience == "" { + return nil, fmt.Errorf("audience is required") + } + if req.Resource == "" { + return nil, fmt.Errorf("resource is required") + } + if req.SubjectToken == "" { + return nil, fmt.Errorf("subject_token is required") + } + if req.SubjectTokenType == "" { + return nil, fmt.Errorf("subject_token_type is required") + } + + // Validate URL schemes to prevent XSS attacks (see #526). + if err := checkURLScheme(tokenEndpoint); err != nil { + return nil, fmt.Errorf("invalid token endpoint: %w", err) + } + if err := checkURLScheme(req.Audience); err != nil { + return nil, fmt.Errorf("invalid audience: %w", err) + } + if err := checkURLScheme(req.Resource); err != nil { + return nil, fmt.Errorf("invalid resource: %w", err) + } + + // Per RFC 6749 Section 3.2, parameters sent without a value (like the empty + // "code" parameter) MUST be treated as if they were omitted from the request. + // The oauth2 library's Exchange method sends an empty code, but compliant + // servers should ignore it. + cfg := &oauth2.Config{ + ClientID: clientCreds.ClientID, + Endpoint: oauth2.Endpoint{ + TokenURL: tokenEndpoint, + AuthStyle: oauth2.AuthStyleInParams, + }, + } + // Set ClientSecret if ClientSecretAuth is configured. + if clientCreds.ClientSecretAuth != nil { + cfg.ClientSecret = clientCreds.ClientSecretAuth.ClientSecret + } + + // Use custom HTTP client if provided. + if httpClient == nil { + httpClient = http.DefaultClient + } + ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, httpClient) + + // Build token exchange parameters per RFC 8693. + opts := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("grant_type", GrantTypeTokenExchange), + oauth2.SetAuthURLParam("requested_token_type", req.RequestedTokenType), + oauth2.SetAuthURLParam("audience", req.Audience), + oauth2.SetAuthURLParam("resource", req.Resource), + oauth2.SetAuthURLParam("subject_token", req.SubjectToken), + oauth2.SetAuthURLParam("subject_token_type", req.SubjectTokenType), + } + if len(req.Scope) > 0 { + opts = append(opts, oauth2.SetAuthURLParam("scope", strings.Join(req.Scope, " "))) + } + + // Exchange with token exchange grant type. + // SetAuthURLParam overrides the default grant_type and adds all required parameters. + token, err := cfg.Exchange( + ctxWithClient, + "", // empty code - per RFC 6749 Section 3.2, empty params should be ignored + opts..., + ) + if err != nil { + return nil, fmt.Errorf("token exchange request failed: %w", err) + } + + // Validate that issued_token_type is present in the response. + // The oauth2 library stores additional response fields in Extra. + issuedTokenType, _ := token.Extra("issued_token_type").(string) + if issuedTokenType == "" { + return nil, fmt.Errorf("response missing required field: issued_token_type") + } + + return token, nil +} diff --git a/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go b/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go index 8547c8dfd..820bf436a 100644 --- a/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go +++ b/vendor/github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header/header.go @@ -90,7 +90,7 @@ loop: s = skipSpace(s[1:]) } } - return + return specs } func skipSpace(s string) (rest string) { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_go116.go b/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_go116.go deleted file mode 100644 index effc57840..000000000 --- a/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_go116.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2021 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !go1.17 -// +build !go1.17 - -package collectors - -import "github.com/prometheus/client_golang/prometheus" - -// NewGoCollector returns a collector that exports metrics about the current Go -// process. This includes memory stats. To collect those, runtime.ReadMemStats -// is called. This requires to “stop the world”, which usually only happens for -// garbage collection (GC). Take the following implications into account when -// deciding whether to use the Go collector: -// -// 1. The performance impact of stopping the world is the more relevant the more -// frequently metrics are collected. However, with Go1.9 or later the -// stop-the-world time per metrics collection is very short (~25µs) so that the -// performance impact will only matter in rare cases. However, with older Go -// versions, the stop-the-world duration depends on the heap size and can be -// quite significant (~1.7 ms/GiB as per -// https://go-review.googlesource.com/c/go/+/34937). -// -// 2. During an ongoing GC, nothing else can stop the world. Therefore, if the -// metrics collection happens to coincide with GC, it will only complete after -// GC has finished. Usually, GC is fast enough to not cause problems. However, -// with a very large heap, GC might take multiple seconds, which is enough to -// cause scrape timeouts in common setups. To avoid this problem, the Go -// collector will use the memstats from a previous collection if -// runtime.ReadMemStats takes more than 1s. However, if there are no previously -// collected memstats, or their collection is more than 5m ago, the collection -// will block until runtime.ReadMemStats succeeds. -// -// NOTE: The problem is solved in Go 1.15, see -// https://github.com/golang/go/issues/19812 for the related Go issue. -func NewGoCollector() prometheus.Collector { - return prometheus.NewGoCollector() -} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_latest.go b/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_latest.go index cc4ef1077..d29ade654 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_latest.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/collectors/go_collector_latest.go @@ -90,13 +90,13 @@ func WithGoCollectorMemStatsMetricsDisabled() func(options *internal.GoCollector // GoRuntimeMetricsRule allow enabling and configuring particular group of runtime/metrics. // TODO(bwplotka): Consider adding ability to adjust buckets. type GoRuntimeMetricsRule struct { - // Matcher represents RE2 expression will match the runtime/metrics from https://golang.bg/src/runtime/metrics/description.go + // Matcher represents RE2 expression will match the runtime/metrics from https://pkg.go.dev/runtime/metrics // Use `regexp.MustCompile` or `regexp.Compile` to create this field. Matcher *regexp.Regexp } // WithGoCollectorRuntimeMetrics allows enabling and configuring particular group of runtime/metrics. -// See the list of metrics https://golang.bg/src/runtime/metrics/description.go (pick the Go version you use there!). +// See the list of metrics https://pkg.go.dev/runtime/metrics (pick the Go version you use there!). // You can use this option in repeated manner, which will add new rules. The order of rules is important, the last rule // that matches particular metrics is applied. func WithGoCollectorRuntimeMetrics(rules ...GoRuntimeMetricsRule) func(options *internal.GoCollectorOptions) { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/counter.go b/vendor/github.com/prometheus/client_golang/prometheus/counter.go index 4ce84e7a8..7d963d3af 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/counter.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/counter.go @@ -85,11 +85,12 @@ type CounterVecOpts struct { // Both internal tracking values are added up in the Write method. This has to // be taken into account when it comes to precision and overflow behavior. func NewCounter(opts CounterOpts) Counter { - desc := NewDesc( + desc := V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ) if opts.now == nil { opts.now = time.Now @@ -205,6 +206,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + WithUnit(opts.Unit), ) if opts.now == nil { opts.now = time.Now @@ -349,10 +351,11 @@ type CounterFunc interface { // // Check out the ExampleGaugeFunc examples for the similar GaugeFunc. func NewCounterFunc(opts CounterOpts, function func() float64) CounterFunc { - return newValueFunc(NewDesc( + return newValueFunc(V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ), CounterValue, function) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/desc.go b/vendor/github.com/prometheus/client_golang/prometheus/desc.go index 2331b8b4f..a3c92e7a4 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/desc.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/desc.go @@ -47,6 +47,8 @@ type Desc struct { fqName string // help provides some helpful information about this metric. help string + // unit provides the unit of this metric. + unit string // constLabelPairs contains precalculated DTO label pairs based on // the constant labels. constLabelPairs []*dto.LabelPair @@ -66,6 +68,16 @@ type Desc struct { err error } +// DescOpt allows setting optional fields for NewDesc. +type DescOpt func(*Desc) + +// WithUnit sets the unit for a Desc. +func WithUnit(unit string) DescOpt { + return func(d *Desc) { + d.unit = unit + } +} + // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc // and will be reported on registration time. variableLabels and constLabels can // be nil if no such labels should be set. fqName must not be empty. @@ -89,14 +101,17 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * // // For constLabels, the label values are constant. Therefore, they are fully // specified in the Desc. See the Collector example for a usage pattern. -func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc { +func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, opts ...DescOpt) *Desc { d := &Desc{ fqName: fqName, help: help, variableLabels: variableLabels.compile(), } - //nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme. - if !model.NameValidationScheme.IsValidMetricName(fqName) { + + for _, opt := range opts { + opt(d) + } + if !model.UTF8Validation.IsValidMetricName(fqName) { d.err = fmt.Errorf("%q is not a valid metric name", fqName) return d } @@ -150,11 +165,13 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const d.id = xxh.Sum64() // Sort labelNames so that order doesn't matter for the hash. sort.Strings(labelNames) - // Now hash together (in this order) the help string and the sorted + // Now hash together (in this order) the help string, the unit string and the sorted // label names. xxh.Reset() xxh.WriteString(help) xxh.Write(separatorByteSlice) + xxh.WriteString(d.unit) + xxh.Write(separatorByteSlice) for _, labelName := range labelNames { xxh.WriteString(labelName) xxh.Write(separatorByteSlice) @@ -182,6 +199,15 @@ func NewInvalidDesc(err error) *Desc { } } +// Err returns an error that occurred during construction, if any. +// +// Calling this method is optional. It can be used to detect construction +// errors early, before invoking other methods on the Desc. If an error is +// present, later operations may not behave as expected. +func (d *Desc) Err() error { + return d.err +} + func (d *Desc) String() string { lpStrings := make([]string, 0, len(d.constLabelPairs)) for _, lp := range d.constLabelPairs { @@ -202,9 +228,10 @@ func (d *Desc) String() string { } } return fmt.Sprintf( - "Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}", + "Desc{fqName: %q, help: %q, unit: %q, constLabels: {%s}, variableLabels: {%s}}", d.fqName, d.help, + d.unit, strings.Join(lpStrings, ","), strings.Join(vlStrings, ","), ) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector.go b/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector.go index de5a85629..327746f43 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/expvar_collector.go @@ -47,14 +47,14 @@ func (e *expvarCollector) Collect(ch chan<- Metric) { if expVar == nil { continue } - var v interface{} + var v any labels := make([]string, len(desc.variableLabels.names)) if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil { ch <- NewInvalidMetric(desc, err) continue } - var processValue func(v interface{}, i int) - processValue = func(v interface{}, i int) { + var processValue func(v any, i int) + processValue = func(v any, i int) { if i >= len(labels) { copiedLabels := append(make([]string, 0, len(labels)), labels...) switch v := v.(type) { @@ -72,7 +72,7 @@ func (e *expvarCollector) Collect(ch chan<- Metric) { ch <- m return } - vm, ok := v.(map[string]interface{}) + vm, ok := v.(map[string]any) if !ok { return } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/gauge.go b/vendor/github.com/prometheus/client_golang/prometheus/gauge.go index dd2eac940..41e54bf27 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/gauge.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/gauge.go @@ -76,11 +76,12 @@ type GaugeVecOpts struct { // scenarios for Gauges and Counters, where the former tends to be Set-heavy and // the latter Inc-heavy. func NewGauge(opts GaugeOpts) Gauge { - desc := NewDesc( + desc := V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ) result := &gauge{desc: desc, labelPairs: desc.constLabelPairs} result.init(result) // Init self-collection. @@ -163,6 +164,7 @@ func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + WithUnit(opts.Unit), ) return &GaugeVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { @@ -302,10 +304,11 @@ type GaugeFunc interface { // value of 1. Example: // https://github.com/prometheus/common/blob/8558a5b7db3c84fa38b4766966059a7bd5bfa2ee/version/info.go#L36-L56 func NewGaugeFunc(opts GaugeOpts, function func() float64) GaugeFunc { - return newValueFunc(NewDesc( + return newValueFunc(V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ), GaugeValue, function) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_go116.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_go116.go deleted file mode 100644 index 897a6e906..000000000 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_go116.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2021 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !go1.17 -// +build !go1.17 - -package prometheus - -import ( - "runtime" - "sync" - "time" -) - -type goCollector struct { - base baseGoCollector - - // ms... are memstats related. - msLast *runtime.MemStats // Previously collected memstats. - msLastTimestamp time.Time - msMtx sync.Mutex // Protects msLast and msLastTimestamp. - msMetrics memStatsMetrics - msRead func(*runtime.MemStats) // For mocking in tests. - msMaxWait time.Duration // Wait time for fresh memstats. - msMaxAge time.Duration // Maximum allowed age of old memstats. -} - -// NewGoCollector is the obsolete version of collectors.NewGoCollector. -// See there for documentation. -// -// Deprecated: Use collectors.NewGoCollector instead. -func NewGoCollector() Collector { - msMetrics := goRuntimeMemStats() - msMetrics = append(msMetrics, struct { - desc *Desc - eval func(*runtime.MemStats) float64 - valType ValueType - }{ - // This metric is omitted in Go1.17+, see https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034 - desc: NewDesc( - memstatNamespace("gc_cpu_fraction"), - "The fraction of this program's available CPU time used by the GC since the program started.", - nil, nil, - ), - eval: func(ms *runtime.MemStats) float64 { return ms.GCCPUFraction }, - valType: GaugeValue, - }) - return &goCollector{ - base: newBaseGoCollector(), - msLast: &runtime.MemStats{}, - msRead: runtime.ReadMemStats, - msMaxWait: time.Second, - msMaxAge: 5 * time.Minute, - msMetrics: msMetrics, - } -} - -// Describe returns all descriptions of the collector. -func (c *goCollector) Describe(ch chan<- *Desc) { - c.base.Describe(ch) - for _, i := range c.msMetrics { - ch <- i.desc - } -} - -// Collect returns the current state of all metrics of the collector. -func (c *goCollector) Collect(ch chan<- Metric) { - var ( - ms = &runtime.MemStats{} - done = make(chan struct{}) - ) - // Start reading memstats first as it might take a while. - go func() { - c.msRead(ms) - c.msMtx.Lock() - c.msLast = ms - c.msLastTimestamp = time.Now() - c.msMtx.Unlock() - close(done) - }() - - // Collect base non-memory metrics. - c.base.Collect(ch) - - timer := time.NewTimer(c.msMaxWait) - select { - case <-done: // Our own ReadMemStats succeeded in time. Use it. - timer.Stop() // Important for high collection frequencies to not pile up timers. - c.msCollect(ch, ms) - return - case <-timer.C: // Time out, use last memstats if possible. Continue below. - } - c.msMtx.Lock() - if time.Since(c.msLastTimestamp) < c.msMaxAge { - // Last memstats are recent enough. Collect from them under the lock. - c.msCollect(ch, c.msLast) - c.msMtx.Unlock() - return - } - // If we are here, the last memstats are too old or don't exist. We have - // to wait until our own ReadMemStats finally completes. For that to - // happen, we have to release the lock. - c.msMtx.Unlock() - <-done - c.msCollect(ch, ms) -} - -func (c *goCollector) msCollect(ch chan<- Metric, ms *runtime.MemStats) { - for _, i := range c.msMetrics { - ch <- MustNewConstMetric(i.desc, i.valType, i.eval(ms)) - } -} diff --git a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go index 6b8684731..1db1c4be0 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/go_collector_latest.go @@ -98,7 +98,7 @@ type goCollector struct { // snapshot is always produced by Collect. mu sync.Mutex - // Contains all samples that has to retrieved from runtime/metrics (not all of them will be exposed). + // Contains all samples that have to be retrieved from runtime/metrics (not all of them will be exposed). sampleBuf []metrics.Sample // sampleMap allows lookup for MemStats metrics and runtime/metrics histograms for exact sums. sampleMap map[string]*metrics.Sample @@ -210,16 +210,26 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name}) sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1] + // Extract unit from the runtime/metrics name (e.g., "/gc/heap/allocs:bytes" -> "bytes") + // and sanitize to match Prometheus naming conventions (e.g., "cpu-seconds" -> "cpu_seconds") + var unit string + if idx := strings.IndexRune(d.Name, ':'); idx >= 0 { + unit = d.Name[idx+1:] + unit = strings.ReplaceAll(unit, "-", "_") + unit = strings.ReplaceAll(unit, "*", "_") + unit = strings.ReplaceAll(unit, "/", "_per_") + } + var m collectorMetric if d.Kind == metrics.KindFloat64Histogram { _, hasSum := opt.RuntimeMetricSumForHist[d.Name] - unit := d.Name[strings.IndexRune(d.Name, ':')+1:] m = newBatchHistogram( - NewDesc( + V2.NewDesc( BuildFQName(namespace, subsystem, name), help, + UnconstrainedLabels(nil), nil, - nil, + WithUnit(unit), ), internal.RuntimeMetricsBucketsForUnit(bucketsMap[d.Name], unit), hasSum, @@ -230,6 +240,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { Subsystem: subsystem, Name: name, Help: help, + Unit: unit, }, ) } else { @@ -238,6 +249,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { Subsystem: subsystem, Name: name, Help: help, + Unit: unit, }) } metricSet = append(metricSet, m) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go index c453b754a..88bae3b32 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/histogram.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/histogram.go @@ -378,6 +378,9 @@ type HistogramOpts struct { // string. Help string + // Unit provides the unit of this Histogram. + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. @@ -522,11 +525,12 @@ type HistogramVecOpts struct { // for each bucket. func NewHistogram(opts HistogramOpts) Histogram { return newHistogram( - NewDesc( + V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ), opts, ) @@ -966,7 +970,7 @@ func (h *histogram) maybeReset( // We are using the possibly mocked h.now() rather than // time.Since(h.lastResetTime) to enable testing. if h.nativeHistogramMinResetDuration == 0 || // No reset configured. - h.resetScheduled || // Do not interefere if a reset is already scheduled. + h.resetScheduled || // Do not interfere if a reset is already scheduled. h.now().Sub(h.lastResetTime) < h.nativeHistogramMinResetDuration { return false } @@ -1053,8 +1057,8 @@ func (h *histogram) maybeWidenZeroBucket(hot, cold *histogramCounts) bool { atomic.StoreUint64(&cold.nativeHistogramZeroThresholdBits, math.Float64bits(newZeroThreshold)) // ...and then merge the newly deleted buckets into the wider zero // bucket. - mergeAndDeleteOrAddAndReset := func(hotBuckets, coldBuckets *sync.Map) func(k, v interface{}) bool { - return func(k, v interface{}) bool { + mergeAndDeleteOrAddAndReset := func(hotBuckets, coldBuckets *sync.Map) func(k, v any) bool { + return func(k, v any) bool { key := k.(int) bucket := v.(*int64) if key == smallestKey { @@ -1107,8 +1111,8 @@ func (h *histogram) doubleBucketWidth(hot, cold *histogramCounts) { // ...adjust the schema in the cold counts, too... atomic.StoreInt32(&cold.nativeHistogramSchema, coldSchema) // ...and then merge the cold buckets into the wider hot buckets. - merge := func(hotBuckets *sync.Map) func(k, v interface{}) bool { - return func(k, v interface{}) bool { + merge := func(hotBuckets *sync.Map) func(k, v any) bool { + return func(k, v any) bool { key := k.(int) bucket := v.(*int64) // Adjust key to match the bucket to merge into. @@ -1190,6 +1194,7 @@ func (v2) NewHistogramVec(opts HistogramVecOpts) *HistogramVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + WithUnit(opts.Unit), ) return &HistogramVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { @@ -1476,7 +1481,7 @@ func pickSchema(bucketFactor float64) int32 { func makeBuckets(buckets *sync.Map) ([]*dto.BucketSpan, []int64) { var ii []int - buckets.Range(func(k, v interface{}) bool { + buckets.Range(func(k, v any) bool { ii = append(ii, k.(int)) return true }) @@ -1553,8 +1558,8 @@ func addToBucket(buckets *sync.Map, key int, increment int64) bool { // according to the buckets ranged through. It then resets all buckets ranged // through to 0 (but leaves them in place so that they don't need to get // recreated on the next scrape). -func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v interface{}) bool { - return func(k, v interface{}) bool { +func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v any) bool { + return func(k, v any) bool { bucket := v.(*int64) if addToBucket(hotBuckets, k.(int), atomic.LoadInt64(bucket)) { atomic.AddUint32(bucketNumber, 1) @@ -1565,7 +1570,7 @@ func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v interface } func deleteSyncMap(m *sync.Map) { - m.Range(func(k, v interface{}) bool { + m.Range(func(k, v any) bool { m.Delete(k) return true }) @@ -1573,7 +1578,7 @@ func deleteSyncMap(m *sync.Map) { func findSmallestKey(m *sync.Map) int { result := math.MaxInt32 - m.Range(func(k, v interface{}) bool { + m.Range(func(k, v any) bool { key := k.(int) if key < result { result = key diff --git a/vendor/github.com/prometheus/client_golang/prometheus/internal/difflib.go b/vendor/github.com/prometheus/client_golang/prometheus/internal/difflib.go index 7bac0da33..2db270f21 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/internal/difflib.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/internal/difflib.go @@ -78,7 +78,7 @@ type OpCode struct { // notion, pairing up elements that appear uniquely in each sequence. // That, and the method here, appear to yield more intuitive difference // reports than does diff. This method appears to be the least vulnerable -// to synching up on blocks of "junk lines", though (like blank lines in +// to syncing up on blocks of "junk lines", though (like blank lines in // ordinary text files, or maybe "

" lines in HTML files). That may be // because this is the only method of the 3 that has a *concept* of // "junk" . @@ -567,7 +567,7 @@ type UnifiedDiff struct { func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { buf := bufio.NewWriter(writer) defer buf.Flush() - wf := func(format string, args ...interface{}) error { + wf := func(format string, args ...any) error { _, err := fmt.Fprintf(buf, format, args...) return err } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/labels.go b/vendor/github.com/prometheus/client_golang/prometheus/labels.go index 5fe8d3b4d..a0285489a 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/labels.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/labels.go @@ -184,6 +184,5 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error { } func checkLabelName(l string) bool { - //nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme. - return model.NameValidationScheme.IsValidLabelName(l) && !strings.HasPrefix(l, reservedLabelPrefix) + return model.UTF8Validation.IsValidLabelName(l) && !strings.HasPrefix(l, reservedLabelPrefix) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/metric.go b/vendor/github.com/prometheus/client_golang/prometheus/metric.go index 76e59f128..c5cb90adf 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/metric.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/metric.go @@ -81,6 +81,9 @@ type Opts struct { // string. Help string + // Unit provides the unit of this metric as per https://prometheus.io/docs/specs/om + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go index b32c95fa3..2b16298f4 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_darwin.go @@ -72,7 +72,13 @@ func getOpenFileCount() (float64, error) { } func (c *processCollector) processCollect(ch chan<- Metric) { - if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", os.Getpid()); err == nil { + pid, err := c.pidFn() + if err != nil { + c.reportError(ch, nil, err) + return + } + + if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", pid); err == nil { if len(procs) == 1 { startTime := float64(procs[0].Proc.P_starttime.Nano() / 1e9) ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime) @@ -84,6 +90,11 @@ func (c *processCollector) processCollect(ch chan<- Metric) { c.reportError(ch, c.startTime, err) } + if pid != os.Getpid() { + c.reportError(ch, nil, fmt.Errorf("collecting metrics for pid %d is not supported on darwin: process metrics collection is limited to the current process (pid %d)", pid, os.Getpid())) + return + } + // The proc structure returned by kern.proc.pid above has an Rusage member, // but it is not filled in, so it needs to be fetched by getrusage(2). For // that call, the UTime, STime, and Maxrss members are filled out, but not diff --git a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_windows.go b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_windows.go index fa474289e..c08dd05f0 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/process_collector_windows.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/process_collector_windows.go @@ -30,6 +30,10 @@ var ( procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") procGetProcessHandleCount = modkernel32.NewProc("GetProcessHandleCount") + + openProcess = windows.OpenProcess + closeHandle = windows.CloseHandle + getProcessTimes = windows.GetProcessTimes ) type processMemoryCounters struct { @@ -79,10 +83,21 @@ func getProcessHandleCount(handle windows.Handle) (uint32, error) { } func (c *processCollector) processCollect(ch chan<- Metric) { - h := windows.CurrentProcess() + pid, err := c.pidFn() + if err != nil { + c.reportError(ch, nil, err) + return + } + + h, err := openProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, uint32(pid)) + if err != nil { + c.reportError(ch, nil, err) + return + } + defer closeHandle(h) var startTime, exitTime, kernelTime, userTime windows.Filetime - err := windows.GetProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime) + err = getProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime) if err != nil { c.reportError(ch, nil, err) return diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go index 763d99e36..c28af5ce2 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go @@ -37,10 +37,12 @@ import ( "fmt" "io" "net/http" + "slices" "strconv" "sync" "time" + dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil" @@ -74,11 +76,118 @@ func defaultCompressionFormats() []Compression { } var gzipPool = sync.Pool{ - New: func() interface{} { + New: func() any { return gzip.NewWriter(nil) }, } +// coalescingGatherer wraps a TransactionalGatherer to deduplicate concurrent +// Gather calls. When a Gather is already in flight, new callers join the +// existing cycle and receive the same result once it completes. The underlying +// done function is called exactly once, when the last joined caller releases. +// +// This prevents goroutine pile-up when the scrape rate is faster than the +// time collectors need to produce metrics. +type coalescingGatherer struct { + g prometheus.TransactionalGatherer + mu sync.Mutex + cycle *gatherCycle +} + +// gatherCycle tracks a single in-flight Gather and all HTTP handlers sharing it. +type gatherCycle struct { + ready chan struct{} // closed when Gather completes; happens-before reads of mfs/err/done + mfs []*dto.MetricFamily // canonical result, set before ready is closed; callers get a slices.Clone, the element values stay shared and must not be mutated + err error // set before ready is closed + done func() // underlying done callback; set before ready is closed + refs int // number of handlers using this cycle; protected by coalescingGatherer.mu +} + +var _ prometheus.TransactionalGatherer = (*coalescingGatherer)(nil) // compile-time interface check + +// errGatherPanicked is returned to callers that joined an in-flight coalesced +// Gather whose underlying gatherer panicked. See the panic guard in Gather for +// why joiners receive this error instead of the panic itself. +var errGatherPanicked = errors.New("coalesced gather panicked") + +func (c *coalescingGatherer) Gather() ([]*dto.MetricFamily, func(), error) { + c.mu.Lock() + if cy := c.cycle; cy != nil { + // c.cycle is non-nil while Gather runs or handlers are still consuming its results. + cy.refs++ + c.mu.Unlock() + <-cy.ready + // Each caller gets its own slice header so it can filter or reorder + // without racing other callers sharing this cycle. The *dto.MetricFamily + // values remain shared and must not be mutated in place. + return slices.Clone(cy.mfs), c.releaseFunc(cy), cy.err + } + cy := &gatherCycle{ + ready: make(chan struct{}), + done: func() {}, + refs: 1, + } + c.cycle = cy + c.mu.Unlock() + + // Guard against a panic in c.g.Gather. The common case, a panicking + // Collector, never reaches here: Registry.Gather recovers Collector panics + // and returns them as an error. This guard only covers the rare case where + // the wrapped gatherer itself panics. + // + // We deliberately do not recover: the leader's panic propagates and is + // handled by net/http exactly as it would be without coalescing. We only + // set cy.err before closing cy.ready so joiners waiting on <-cy.ready fail + // with that error instead of silently returning an empty, successful + // response, and we clear c.cycle so the next Gather starts a fresh cycle. + // + // The leader never runs its own releaseFunc on this path, so its ref is + // not decremented; that is harmless because the cycle is detached (c.cycle + // = nil) and cy.done is still the no-op set at construction (c.g.Gather + // panicked before assigning a real done). If cy.done is ever made non-nil + // before c.g.Gather runs, this path would need to release it. + panicked := true + defer func() { + if panicked { + c.mu.Lock() + if c.cycle == cy { + c.cycle = nil + } + c.mu.Unlock() + cy.err = errGatherPanicked // set before close: happens-before joiners' reads + close(cy.ready) + } + }() + cy.mfs, cy.done, cy.err = c.g.Gather() + panicked = false + close(cy.ready) // happens-before joiners' reads of cy.mfs/err/done + + // Clone here too so cy.mfs stays the write-once canonical slice: joiners + // read it concurrently via slices.Clone, so the leader must not hand out + // (and potentially reorder) the same backing array. + return slices.Clone(cy.mfs), c.releaseFunc(cy), cy.err +} + +// releaseFunc returns the done callback for one caller sharing cy. +// When the last caller releases, the underlying done is invoked and the +// cycle is cleared so the next Gather starts fresh. +func (c *coalescingGatherer) releaseFunc(cy *gatherCycle) func() { + return func() { + c.mu.Lock() + cy.refs-- + if cy.refs > 0 { + c.mu.Unlock() + return + } + // Last caller. + if c.cycle == cy { + c.cycle = nil + } + c.mu.Unlock() + cy.done() // called outside the lock to avoid holding it during done + } +} + // Handler returns an http.Handler for the prometheus.DefaultGatherer, using // default HandlerOpts, i.e. it reports the first error as an HTTP error, it has // no error logging, and it applies compression if requested by the client. @@ -89,6 +198,10 @@ var gzipPool = sync.Pool{ // metrics used for instrumentation will be shared between them, providing // global scrape counts. // +// The handler supports filtering metrics by name using the `name[]` query parameter. +// Multiple metric names can be specified by providing the parameter multiple times. +// When no name[] parameters are provided, all metrics are returned. +// // This function is meant to cover the bulk of basic use cases. If you are doing // anything that requires more customization (including using a non-default // Gatherer, different instrumentation, and non-default HandlerOpts), use the @@ -105,6 +218,10 @@ func Handler() http.Handler { // Gatherers, with non-default HandlerOpts, and/or with custom (or no) // instrumentation. Use the InstrumentMetricHandler function to apply the same // kind of instrumentation as it is used by the Handler function. +// +// The handler supports filtering metrics by name using the `name[]` query parameter. +// Multiple metric names can be specified by providing the parameter multiple times. +// When no name[] parameters are provided, all metrics are returned. func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { return HandlerForTransactional(prometheus.ToTransactionalGatherer(reg), opts) } @@ -112,7 +229,15 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { // HandlerForTransactional is like HandlerFor, but it uses transactional gather, which // can safely change in-place returned *dto.MetricFamily before call to `Gather` and after // call to `done` of that `Gather`. +// +// The handler supports filtering metrics by name using the `name[]` query parameter. +// Multiple metric names can be specified by providing the parameter multiple times. +// When no name[] parameters are provided, all metrics are returned. func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerOpts) http.Handler { + if opts.CoalesceGather { + reg = &coalescingGatherer{g: reg} + } + var ( inFlightSem chan struct{} errCnt = prometheus.NewCounterVec( @@ -214,12 +339,14 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO rsp.Header().Set(contentEncodingHeader, encodingHeader) } - var enc expfmt.Encoder + var ( + enc expfmt.Encoder + encOpts []expfmt.EncoderOption + ) if opts.EnableOpenMetricsTextCreatedSamples { - enc = expfmt.NewEncoder(w, contentType, expfmt.WithCreatedLines()) - } else { - enc = expfmt.NewEncoder(w, contentType) + encOpts = append(encOpts, expfmt.WithCreatedLines()) } + enc = expfmt.NewEncoder(w, contentType, encOpts...) // handleError handles the error according to opts.ErrorHandling // and returns true if we have to abort after the handling. @@ -245,7 +372,24 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO return false } + // Build metric name filter set from query params (if any). The URL + // can be nil on hand-constructed requests. + var metricFilter map[string]struct{} + if req.URL != nil { + if metricNames := req.URL.Query()["name[]"]; len(metricNames) > 0 { + metricFilter = make(map[string]struct{}, len(metricNames)) + for _, name := range metricNames { + metricFilter[name] = struct{}{} + } + } + } + for _, mf := range mfs { + if metricFilter != nil { + if _, ok := metricFilter[mf.GetName()]; !ok { + continue + } + } if handleError(enc.Encode(mf)) { return } @@ -353,7 +497,7 @@ const ( // log.Logger from the standard library implements this interface, and it is // easy to implement by custom loggers, if they don't do so already anyway. type Logger interface { - Println(v ...interface{}) + Println(v ...any) } // HandlerOpts specifies options how to serve metrics via an http.Handler. The @@ -400,6 +544,40 @@ type HandlerOpts struct { // Service Unavailable and a suitable message in the body. If // MaxRequestsInFlight is 0 or negative, no limit is applied. MaxRequestsInFlight int + // CoalesceGather, if true, deduplicates concurrent Gather calls so that + // only one collection runs at a time. Additional requests that arrive + // while a Gather is in flight will receive the same result once it + // completes. This prevents goroutine pile-up when the scrape rate is + // faster than the time collectors need to produce metrics. + // + // When enabled, concurrent scrapers share a single metric snapshot per + // collection cycle. Each request receives its own copy of the returned + // slice, so filtering or reordering it (for example via name[] query + // parameters) is safe. The pointed-to MetricFamily values are still + // shared: the built-in handler only reads them, so this is safe in + // practice, but a custom TransactionalGatherer that mutates the returned + // families in place after Gather returns must not use this option. + // + // Because the snapshot is shared, a request that arrives while a cycle is + // in flight receives that cycle's result even though collection began + // before the request; two scrapers joined to one cycle observe the same + // timestamps rather than independently gathered data. + // + // Consider using CoalesceGather together with Timeout. Timeout bounds the + // client-facing response time and keeps at most one collection running at + // a time, but it does not cancel the underlying Gather: a joined request + // that times out still holds a MaxRequestsInFlight slot until the shared + // collection completes. + // + // Panic handling: a panicking Collector is already turned into an error by + // the registry, so joiners receive that error like any other. In the rare + // case where the wrapped gatherer itself panics, the panicking request's + // panic propagates as usual (handled by net/http), while requests that + // joined the same cycle receive an error rather than an empty response. + // + // NOTE: This option is experimental and may change or be removed in a + // future release. + CoalesceGather bool // If handling a request takes longer than Timeout, it is responded to // with 503 ServiceUnavailable and a suitable Message. No timeout is // applied if Timeout is 0 or negative. Note that with the current @@ -407,8 +585,9 @@ type HandlerOpts struct { // described above (and even that only if sending of the body hasn't // started yet), while the bulk work of gathering all the metrics keeps // running in the background (with the eventual result to be thrown - // away). Until the implementation is improved, it is recommended to - // implement a separate timeout in potentially slow Collectors. + // away). When CoalesceGather is enabled, only one such background Gather + // can be in flight at a time. It is also recommended to implement a + // separate timeout in potentially slow Collectors. Timeout time.Duration // If true, the experimental OpenMetrics encoding is added to the // possible options during content negotiation. Note that Prometheus @@ -460,7 +639,7 @@ func httpError(rsp http.ResponseWriter, err error) { // negotiateEncodingWriter reads the Accept-Encoding header from a request and // selects the right compression based on an allow-list of supported -// compressions. It returns a writer implementing the compression and an the +// compressions. It returns a writer implementing the compression and the // correct value that the caller can set in the response header. func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []string) (_ io.Writer, encodingHeaderValue string, closeWriter func(), _ error) { if len(compressions) == 0 { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go index d3482c40c..024857974 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_client.go @@ -75,10 +75,10 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou resp, err := next.RoundTrip(r) if err == nil { l := labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...) - for label, resolve := range rtOpts.extraLabelsFromCtx { - l[label] = resolve(resp.Request.Context()) + for label, resolve := range rtOpts.extraLabelsFromRequest { + l[label] = resolve(resp.Request) } - addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r)) } return resp, err } @@ -119,10 +119,10 @@ func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundT resp, err := next.RoundTrip(r) if err == nil { l := labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...) - for label, resolve := range rtOpts.extraLabelsFromCtx { - l[label] = resolve(resp.Request.Context()) + for label, resolve := range rtOpts.extraLabelsFromRequest { + l[label] = resolve(resp.Request) } - observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r)) } return resp, err } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go index 9332b0249..9dec091ac 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/instrument_server.go @@ -28,24 +28,36 @@ import ( // magicString is used for the hacky label test in checkLabels. Remove once fixed. const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" -// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver], -// which falls back to [prometheus.Observer.Observe] if no labels are provided. +// observeWithExemplar records val on obs. If labels is non-nil and obs +// implements [prometheus.ExemplarObserver], the exemplar is attached via +// ObserveWithExemplar; otherwise the exemplar is dropped and the value is +// recorded with a plain [prometheus.Observer.Observe]. This mirrors the +// safe-cast pattern in [prometheus.Timer.ObserveDurationWithExemplar] and +// ensures we never panic when callers pass an ObserverVec backed by a +// summary, which cannot carry exemplars in the Prometheus exposition format. func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string) { - if labels == nil { - obs.Observe(val) - return + if labels != nil { + if eo, ok := obs.(prometheus.ExemplarObserver); ok { + eo.ObserveWithExemplar(val, labels) + return + } } - obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels) + obs.Observe(val) } -// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar], -// which falls back to [prometheus.Counter.Add] if no labels are provided. -func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string) { - if labels == nil { - obs.Add(val) - return +// addWithExemplar records val on c. If labels is non-nil and c implements +// [prometheus.ExemplarAdder], the exemplar is attached via AddWithExemplar; +// otherwise the exemplar is dropped and the value is recorded with a plain +// [prometheus.Counter.Add]. The safe-cast keeps the helper robust against +// custom Counter implementations that do not advertise exemplar support. +func addWithExemplar(c prometheus.Counter, val float64, labels map[string]string) { + if labels != nil { + if ea, ok := c.(prometheus.ExemplarAdder); ok { + ea.AddWithExemplar(val, labels) + return + } } - obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels) + c.Add(val) } // InstrumentHandlerInFlight is a middleware that wraps the provided @@ -97,10 +109,10 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op next.ServeHTTP(d, r) l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r)) } } @@ -108,10 +120,10 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op now := time.Now() next.ServeHTTP(w, r) l := labels(code, method, r.Method, 0, hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r)) } } @@ -147,10 +159,10 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, next.ServeHTTP(d, r) l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r)) } } @@ -158,10 +170,10 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, next.ServeHTTP(w, r) l := labels(code, method, r.Method, 0, hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r)) } } @@ -200,10 +212,10 @@ func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Ha now := time.Now() d := newDelegator(w, func(status int) { l := labels(code, method, r.Method, status, hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r)) }) next.ServeHTTP(d, r) } @@ -244,10 +256,10 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, size := computeApproximateRequestSize(r) l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r)) } } @@ -256,10 +268,10 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, size := computeApproximateRequestSize(r) l := labels(code, method, r.Method, 0, hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r)) } } @@ -296,10 +308,10 @@ func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler next.ServeHTTP(d, r) l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...) - for label, resolve := range hOpts.extraLabelsFromCtx { - l[label] = resolve(r.Context()) + for label, resolve := range hOpts.extraLabelsFromRequest { + l[label] = resolve(r) } - observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r)) }) } @@ -366,7 +378,7 @@ func checkLabels(c prometheus.Collector) (code, method bool) { panic("metric partitioned with non-supported labels") } } - return + return code, method } func isLabelCurried(c prometheus.Collector, label string) bool { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go index 5d4383aa1..d4c0954f3 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/promhttp/option.go @@ -15,6 +15,7 @@ package promhttp import ( "context" + "net/http" "github.com/prometheus/client_golang/prometheus" ) @@ -24,28 +25,31 @@ type Option interface { apply(*options) } +// LabelValueFromRequest is used to compute the label value from request. +type LabelValueFromRequest func(request *http.Request) string + // LabelValueFromCtx are used to compute the label value from request context. // Context can be filled with values from request through middleware. type LabelValueFromCtx func(ctx context.Context) string // options store options for both a handler or round tripper. type options struct { - extraMethods []string - getExemplarFn func(requestCtx context.Context) prometheus.Labels - extraLabelsFromCtx map[string]LabelValueFromCtx + extraMethods []string + getExemplarFn func(req *http.Request) prometheus.Labels + extraLabelsFromRequest map[string]LabelValueFromRequest } func defaultOptions() *options { return &options{ - getExemplarFn: func(ctx context.Context) prometheus.Labels { return nil }, - extraLabelsFromCtx: map[string]LabelValueFromCtx{}, + getExemplarFn: func(req *http.Request) prometheus.Labels { return nil }, + extraLabelsFromRequest: map[string]LabelValueFromRequest{}, } } func (o *options) emptyDynamicLabels() prometheus.Labels { labels := prometheus.Labels{} - for label := range o.extraLabelsFromCtx { + for label := range o.extraLabelsFromRequest { labels[label] = "" } @@ -66,19 +70,39 @@ func WithExtraMethods(methods ...string) Option { }) } -// WithExemplarFromContext allows to inject function that will get exemplar from context that will be put to counter and histogram metrics. +// WithExemplarFromRequest allows you to inject a function that will get exemplar from request that will be put to counter and histogram metrics. // If the function returns nil labels or the metric does not support exemplars, no exemplar will be added (noop), but // metric will continue to observe/increment. -func WithExemplarFromContext(getExemplarFn func(requestCtx context.Context) prometheus.Labels) Option { +func WithExemplarFromRequest(getExemplarFn func(req *http.Request) prometheus.Labels) Option { return optionApplyFunc(func(o *options) { o.getExemplarFn = getExemplarFn }) } +// WithExemplarFromContext allows you to inject a function that will get exemplar from context that will be put to counter and histogram metrics. +// If the function returns nil labels or the metric does not support exemplars, no exemplar will be added (noop), but +// metric will continue to observe/increment. +func WithExemplarFromContext(getExemplarFn func(requestCtx context.Context) prometheus.Labels) Option { + return optionApplyFunc(func(o *options) { + o.getExemplarFn = func(req *http.Request) prometheus.Labels { + return getExemplarFn(req.Context()) + } + }) +} + +// WithLabelFromRequest registers a label for dynamic resolution with access to the request. +func WithLabelFromRequest(name string, valueFn LabelValueFromRequest) Option { + return optionApplyFunc(func(o *options) { + o.extraLabelsFromRequest[name] = valueFn + }) +} + // WithLabelFromCtx registers a label for dynamic resolution with access to context. // See the example for ExampleInstrumentHandlerWithLabelResolver for example usage func WithLabelFromCtx(name string, valueFn LabelValueFromCtx) Option { return optionApplyFunc(func(o *options) { - o.extraLabelsFromCtx[name] = valueFn + o.extraLabelsFromRequest[name] = func(req *http.Request) string { + return valueFn(req.Context()) + } }) } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/registry.go b/vendor/github.com/prometheus/client_golang/prometheus/registry.go index c6fd2f58b..ed0681c8b 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/registry.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/registry.go @@ -214,6 +214,19 @@ func (err AlreadyRegisteredError) Error() string { // by a Gatherer to report multiple errors during MetricFamily gathering. type MultiError []error +// SafeMultiError is a thread-safe wrapper around MultiError using a mutex. +type SafeMultiError struct { + mu sync.Mutex + errs MultiError +} + +// Appends the provided error to the contained MultiError in a thread-safe way. +func (s *SafeMultiError) Append(err error) { + s.mu.Lock() + s.errs.Append(err) + s.mu.Unlock() +} + // Error formats the contained errors as a bullet point list, preceded by the // total number of errors. Note that this results in a multi-line string. func (errs MultiError) Error() string { @@ -408,6 +421,16 @@ func (r *Registry) MustRegister(cs ...Collector) { } } +// MustGather implements Gatherer. +// Wraps around Gather and panics if Gather fails for any reason. +func (r *Registry) MustGather() []*dto.MetricFamily { + mfs, err := r.Gather() + if err != nil { + panic(err) + } + return mfs +} + // Gather implements Gatherer. func (r *Registry) Gather() ([]*dto.MetricFamily, error) { r.mtx.RLock() @@ -423,7 +446,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { uncheckedMetricChan = make(chan Metric, capMetricChan) metricHashes = map[uint64]struct{}{} wg sync.WaitGroup - errs MultiError // The collected errors to return in the end. + safeErrs = &SafeMultiError{} // To collect errors in a threadsafe way registeredDescIDs map[uint64]struct{} // Only used for pedantic checks ) @@ -453,9 +476,9 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { for { select { case collector := <-checkedCollectors: - collector.Collect(checkedMetricChan) + safeErrs.Append((safeCollect(collector, checkedMetricChan))) case collector := <-uncheckedCollectors: - collector.Collect(uncheckedMetricChan) + safeErrs.Append(safeCollect(collector, uncheckedMetricChan)) default: return } @@ -499,7 +522,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { cmc = nil break } - errs.Append(processMetric( + safeErrs.Append(processMetric( metric, metricFamiliesByName, metricHashes, registeredDescIDs, @@ -509,7 +532,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { umc = nil break } - errs.Append(processMetric( + safeErrs.Append(processMetric( metric, metricFamiliesByName, metricHashes, nil, @@ -526,7 +549,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { cmc = nil break } - errs.Append(processMetric( + safeErrs.Append(processMetric( metric, metricFamiliesByName, metricHashes, registeredDescIDs, @@ -536,7 +559,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { umc = nil break } - errs.Append(processMetric( + safeErrs.Append(processMetric( metric, metricFamiliesByName, metricHashes, nil, @@ -556,7 +579,8 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { break } } - return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() + + return internal.NormalizeMetricFamilies(metricFamiliesByName), safeErrs.errs.MaybeUnwrap() } // Describe implements Collector. @@ -571,6 +595,24 @@ func (r *Registry) Describe(ch chan<- *Desc) { } } +// Helper wrapper around Collector.Collect. +// It tries to collect from the channel, recovers on panic and +// if it has recovered from a panic, then it sends an InvalidMetric into +// the channel with an InvalidDesc, and an error that includes a stack trace. +func safeCollect(c Collector, ch chan<- Metric) (err error) { + defer func() { + if r := recover(); r != nil { + buf := make([]byte, 64<<10) // 64 KB + n := runtime.Stack(buf, false) + err = fmt.Errorf("prometheus collector panic recovered: type=%T: error=%v\nstack trace=%s", c, r, buf[:n]) + ch <- NewInvalidMetric(NewInvalidDesc(err), err) + } + }() + c.Collect(ch) + + return err +} + // Collect implements Collector. func (r *Registry) Collect(ch chan<- Metric) { r.mtx.RLock() @@ -599,10 +641,12 @@ func WriteToTextfile(filename string, g Gatherer) error { mfs, err := g.Gather() if err != nil { + tmp.Close() return err } for _, mf := range mfs { if _, err := expfmt.MetricFamilyToText(tmp, mf); err != nil { + tmp.Close() return err } } @@ -685,6 +729,9 @@ func processMetric( metricFamily = &dto.MetricFamily{} metricFamily.Name = proto.String(desc.fqName) metricFamily.Help = proto.String(desc.help) + if desc.unit != "" { + metricFamily.Unit = proto.String(desc.unit) + } // TODO(beorn7): Simplify switch once Desc has type. switch { case dtoMetric.Gauge != nil: diff --git a/vendor/github.com/prometheus/client_golang/prometheus/summary.go b/vendor/github.com/prometheus/client_golang/prometheus/summary.go index ac5203c6f..c12b8d13d 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/summary.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/summary.go @@ -101,6 +101,9 @@ type SummaryOpts struct { // string. Help string + // Unit provides the unit of this Summary. + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. @@ -181,11 +184,12 @@ type SummaryVecOpts struct { // NewSummary creates a new Summary based on the provided SummaryOpts. func NewSummary(opts SummaryOpts) Summary { return newSummary( - NewDesc( + V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, - nil, + UnconstrainedLabels(nil), opts.ConstLabels, + WithUnit(opts.Unit), ), opts, ) @@ -578,6 +582,7 @@ func (v2) NewSummaryVec(opts SummaryVecOpts) *SummaryVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + WithUnit(opts.Unit), ) return &SummaryVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { diff --git a/vendor/github.com/prometheus/client_golang/prometheus/timer.go b/vendor/github.com/prometheus/client_golang/prometheus/timer.go index 52344fef5..c1318ffb5 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/timer.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/timer.go @@ -37,10 +37,10 @@ type Timer struct { // or // // func TimeMeWithExemplar() { -// timer := NewTimer(myHistogram) -// defer timer.ObserveDurationWithExemplar(exemplar) -// // Do actual work. -// } +// timer := NewTimer(myHistogram) +// defer timer.ObserveDurationWithExemplar(exemplar) +// // Do actual work. +// } func NewTimer(o Observer) *Timer { return &Timer{ begin: time.Now(), @@ -66,7 +66,7 @@ func (t *Timer) ObserveDuration() time.Duration { // ObserveDurationWithExemplar is like ObserveDuration, but it will also // observe exemplar with the duration unless exemplar is nil or provided Observer can't -// be casted to ExemplarObserver. +// be cast to ExemplarObserver. func (t *Timer) ObserveDurationWithExemplar(exemplar Labels) time.Duration { d := time.Since(t.begin) eo, ok := t.observer.(ExemplarObserver) diff --git a/vendor/github.com/prometheus/client_golang/prometheus/vec.go b/vendor/github.com/prometheus/client_golang/prometheus/vec.go index 487b46656..121d2a963 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/vec.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/vec.go @@ -193,9 +193,11 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) { // // Keeping the Metric for later use is possible (and should be considered if // performance is critical), but keep in mind that Reset, DeleteLabelValues and -// Delete can be used to delete the Metric from the MetricVec. In that case, the -// Metric will still exist, but it will not be exported anymore, even if a -// Metric with the same label values is created later. +// Delete can be used to delete the Metric from the MetricVec. In that case, if +// you have previously kept a reference to that Metric, the Metric object still +// exists and can be used, but it will not be exported anymore. If a Metric with +// the same label values is created later, updates to the old Metric reference +// will not be exported. // // An error is returned if the number of label values is not the same as the // number of variable labels in Desc (minus any curried labels). @@ -657,7 +659,7 @@ func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string { } var labelsPool = &sync.Pool{ - New: func() interface{} { + New: func() any { return make(Labels) }, } diff --git a/vendor/github.com/prometheus/client_golang/prometheus/wrap.go b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go index 2ed128506..697f55558 100644 --- a/vendor/github.com/prometheus/client_golang/prometheus/wrap.go +++ b/vendor/github.com/prometheus/client_golang/prometheus/wrap.go @@ -230,6 +230,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { return &Desc{ fqName: desc.fqName, help: desc.help, + unit: desc.unit, variableLabels: desc.variableLabels, constLabelPairs: desc.constLabelPairs, err: fmt.Errorf("attempted wrapping with already existing label name %q", ln), @@ -238,8 +239,8 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { constLabels[ln] = lv } // NewDesc will do remaining validations. - newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels) - // Propagate errors if there was any. This will override any errer + newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels, WithUnit(desc.unit)) + // Propagate errors if there was any. This will override any error // created by NewDesc above, i.e. earlier errors get precedence. if desc.err != nil { newDesc.err = desc.err diff --git a/vendor/github.com/prometheus/procfs/Makefile.common b/vendor/github.com/prometheus/procfs/Makefile.common index cce3ef1d1..a7c5f553e 100644 --- a/vendor/github.com/prometheus/procfs/Makefile.common +++ b/vendor/github.com/prometheus/procfs/Makefile.common @@ -55,13 +55,13 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),) endif endif -PROMU_VERSION ?= 0.18.0 +PROMU_VERSION ?= 0.20.0 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v2.10.1 +GOLANGCI_LINT_VERSION ?= v2.11.4 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. @@ -90,8 +90,8 @@ ifdef DOCKERFILE_PATH $(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile) endif -DOCKER_ARCHS ?= amd64 -DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*) +DOCKER_ARCHS ?= amd64 arm64 armv7 ppc64le riscv64 s390x +DOCKERFILE_VARIANTS ?= $(wildcard Dockerfile Dockerfile.*) # Function to extract variant from Dockerfile label. # Returns the variant name from io.prometheus.image.variant label, or "default" if not found. @@ -109,24 +109,6 @@ endif # Build variant:dockerfile pairs for shell iteration. DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df)) -# Shell helper to check whether a dockerfile/arch pair is excluded. -define dockerfile_arch_is_excluded -case " $(DOCKERFILE_ARCH_EXCLUSIONS) " in \ - *" $$dockerfile:$(1) "*) true ;; \ - *) false ;; \ -esac -endef - -# Shell helper to check whether a registry/arch pair is excluded. -# Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io) -define registry_arch_is_excluded -registry=$$(echo "$(DOCKER_REPO)" | cut -d'/' -f1); \ -case " $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) " in \ - *" $$registry:$(1) "*) true ;; \ - *) false ;; \ -esac -endef - BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) @@ -268,10 +250,6 @@ $(BUILD_DOCKER_ARCHS): common-docker-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ - if $(call dockerfile_arch_is_excluded,$*); then \ - echo "Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ distroless_arch="$*"; \ if [ "$*" = "armv7" ]; then \ distroless_arch="arm"; \ @@ -306,14 +284,6 @@ $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ - if $(call dockerfile_arch_is_excluded,$*); then \ - echo "Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$*); then \ - echo "Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Pushing $$variant_name variant for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ @@ -341,14 +311,6 @@ $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ - if $(call dockerfile_arch_is_excluded,$*); then \ - echo "Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$*); then \ - echo "Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Tagging $$variant_name variant for linux-$* as latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \ @@ -370,14 +332,6 @@ common-docker-manifest: echo "Creating manifest for $$variant_name variant"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ - if $(call dockerfile_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ @@ -391,14 +345,6 @@ common-docker-manifest: echo "Creating default variant ($$variant_name) manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ - if $(call dockerfile_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ @@ -413,14 +359,6 @@ common-docker-manifest: echo "Creating manifest for $$variant_name variant version tag"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ - if $(call dockerfile_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ @@ -434,14 +372,6 @@ common-docker-manifest: echo "Creating default variant version tag manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ - if $(call dockerfile_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ - if $(call registry_arch_is_excluded,$$arch); then \ - echo " Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ - continue; \ - fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ @@ -495,9 +425,3 @@ $(1)_precheck: exit 1; \ fi endef - -govulncheck: install-govulncheck - govulncheck ./... - -install-govulncheck: - command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest diff --git a/vendor/github.com/prometheus/procfs/README.md b/vendor/github.com/prometheus/procfs/README.md index 0718239cf..363524094 100644 --- a/vendor/github.com/prometheus/procfs/README.md +++ b/vendor/github.com/prometheus/procfs/README.md @@ -7,7 +7,7 @@ metrics from the pseudo-filesystems /proc and /sys. backwards-incompatible ways without warnings. Use it at your own risk. [![Go Reference](https://pkg.go.dev/badge/github.com/prometheus/procfs.svg)](https://pkg.go.dev/github.com/prometheus/procfs) -[![CircleCI](https://circleci.com/gh/prometheus/procfs/tree/master.svg?style=svg)](https://circleci.com/gh/prometheus/procfs/tree/master) +[![Build Status](https://github.com/prometheus/procfs/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus/procfs/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/prometheus/procfs)](https://goreportcard.com/report/github.com/prometheus/procfs) ## Usage diff --git a/vendor/github.com/prometheus/procfs/SECURITY.md b/vendor/github.com/prometheus/procfs/SECURITY.md index fed02d85c..5e6f976db 100644 --- a/vendor/github.com/prometheus/procfs/SECURITY.md +++ b/vendor/github.com/prometheus/procfs/SECURITY.md @@ -3,4 +3,4 @@ The Prometheus security policy, including how to report vulnerabilities, can be found here: - +[https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/) diff --git a/vendor/github.com/prometheus/procfs/crypto.go b/vendor/github.com/prometheus/procfs/crypto.go index e4a5876ea..d93b712e0 100644 --- a/vendor/github.com/prometheus/procfs/crypto.go +++ b/vendor/github.com/prometheus/procfs/crypto.go @@ -48,11 +48,13 @@ type Crypto struct { Walksize *uint64 } +var cryptoFile = "crypto" + // Crypto parses an crypto-file (/proc/crypto) and returns a slice of // structs containing the relevant info. More information available here: // https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html func (fs FS) Crypto() ([]Crypto, error) { - path := fs.proc.Path("crypto") + path := fs.proc.Path(cryptoFile) b, err := util.ReadFileNoStat(path) if err != nil { return nil, fmt.Errorf("%w: Cannot read file %v: %w", ErrFileRead, b, err) @@ -82,6 +84,10 @@ func parseCrypto(r io.Reader) ([]Crypto, error) { continue } + if len(out) == 0 { + return nil, fmt.Errorf("%w: parsed invalid line before name parsed: %q", ErrFileParse, text) + } + kv := strings.Split(text, ":") if len(kv) != 2 { return nil, fmt.Errorf("%w: Cannot parse line: %q", ErrFileParse, text) diff --git a/vendor/github.com/prometheus/procfs/mountinfo.go b/vendor/github.com/prometheus/procfs/mountinfo.go index 9414a12f4..8594ae7f1 100644 --- a/vendor/github.com/prometheus/procfs/mountinfo.go +++ b/vendor/github.com/prometheus/procfs/mountinfo.go @@ -17,10 +17,10 @@ import ( "bufio" "bytes" "fmt" + "io" + "os" "strconv" "strings" - - "github.com/prometheus/procfs/internal/util" ) // A MountInfo is a type that describes the details, options @@ -160,9 +160,19 @@ func mountOptionsParser(mountOptions string) map[string]string { return opts } +// readMountInfo reads a full mountinfo file (no 1 MiB cap, unlike util.ReadFileNoStat). +func readMountInfo(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + return io.ReadAll(f) +} + // GetMounts retrieves mountinfo information from `/proc/self/mountinfo`. func GetMounts() ([]*MountInfo, error) { - data, err := util.ReadFileNoStat("/proc/self/mountinfo") + data, err := readMountInfo("/proc/self/mountinfo") if err != nil { return nil, err } @@ -171,7 +181,7 @@ func GetMounts() ([]*MountInfo, error) { // GetProcMounts retrieves mountinfo information from a processes' `/proc//mountinfo`. func GetProcMounts(pid int) ([]*MountInfo, error) { - data, err := util.ReadFileNoStat(fmt.Sprintf("/proc/%d/mountinfo", pid)) + data, err := readMountInfo(fmt.Sprintf("/proc/%d/mountinfo", pid)) if err != nil { return nil, err } @@ -180,7 +190,7 @@ func GetProcMounts(pid int) ([]*MountInfo, error) { // GetMounts retrieves mountinfo information from `/proc/self/mountinfo`. func (fs FS) GetMounts() ([]*MountInfo, error) { - data, err := util.ReadFileNoStat(fs.proc.Path("self/mountinfo")) + data, err := readMountInfo(fs.proc.Path("self/mountinfo")) if err != nil { return nil, err } @@ -189,7 +199,7 @@ func (fs FS) GetMounts() ([]*MountInfo, error) { // GetProcMounts retrieves mountinfo information from a processes' `/proc//mountinfo`. func (fs FS) GetProcMounts(pid int) ([]*MountInfo, error) { - data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid))) + data, err := readMountInfo(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid))) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/net_wireless.go b/vendor/github.com/prometheus/procfs/net_wireless.go index 69d079445..f74dd3bed 100644 --- a/vendor/github.com/prometheus/procfs/net_wireless.go +++ b/vendor/github.com/prometheus/procfs/net_wireless.go @@ -114,47 +114,47 @@ func parseWireless(r io.Reader) ([]*Wireless, error) { qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], ".")) if err != nil { - return nil, fmt.Errorf("%w: parse Quality:link as integer %q: %w", ErrFileParse, qlink, err) + return nil, fmt.Errorf("%w: parse Quality:link as integer %q: %w", ErrFileParse, stats[1], err) } qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], ".")) if err != nil { - return nil, fmt.Errorf("%w: Quality:level as integer %q: %w", ErrFileParse, qlevel, err) + return nil, fmt.Errorf("%w: Quality:level as integer %q: %w", ErrFileParse, stats[2], err) } qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], ".")) if err != nil { - return nil, fmt.Errorf("%w: Quality:noise as integer %q: %w", ErrFileParse, qnoise, err) + return nil, fmt.Errorf("%w: Quality:noise as integer %q: %w", ErrFileParse, stats[3], err) } dnwid, err := strconv.Atoi(stats[4]) if err != nil { - return nil, fmt.Errorf("%w: Discarded:nwid as integer %q: %w", ErrFileParse, dnwid, err) + return nil, fmt.Errorf("%w: Discarded:nwid as integer %q: %w", ErrFileParse, stats[4], err) } dcrypt, err := strconv.Atoi(stats[5]) if err != nil { - return nil, fmt.Errorf("%w: Discarded:crypt as integer %q: %w", ErrFileParse, dcrypt, err) + return nil, fmt.Errorf("%w: Discarded:crypt as integer %q: %w", ErrFileParse, stats[5], err) } dfrag, err := strconv.Atoi(stats[6]) if err != nil { - return nil, fmt.Errorf("%w: Discarded:frag as integer %q: %w", ErrFileParse, dfrag, err) + return nil, fmt.Errorf("%w: Discarded:frag as integer %q: %w", ErrFileParse, stats[6], err) } dretry, err := strconv.Atoi(stats[7]) if err != nil { - return nil, fmt.Errorf("%w: Discarded:retry as integer %q: %w", ErrFileParse, dretry, err) + return nil, fmt.Errorf("%w: Discarded:retry as integer %q: %w", ErrFileParse, stats[7], err) } dmisc, err := strconv.Atoi(stats[8]) if err != nil { - return nil, fmt.Errorf("%w: Discarded:misc as integer %q: %w", ErrFileParse, dmisc, err) + return nil, fmt.Errorf("%w: Discarded:misc as integer %q: %w", ErrFileParse, stats[8], err) } mbeacon, err := strconv.Atoi(stats[9]) if err != nil { - return nil, fmt.Errorf("%w: Missed:beacon as integer %q: %w", ErrFileParse, mbeacon, err) + return nil, fmt.Errorf("%w: Missed:beacon as integer %q: %w", ErrFileParse, stats[9], err) } w := &Wireless{ diff --git a/vendor/github.com/prometheus/procfs/proc_cgroup.go b/vendor/github.com/prometheus/procfs/proc_cgroup.go index 535c08d6f..7e8a12297 100644 --- a/vendor/github.com/prometheus/procfs/proc_cgroup.go +++ b/vendor/github.com/prometheus/procfs/proc_cgroup.go @@ -60,7 +60,7 @@ func parseCgroupString(cgroupStr string) (*Cgroup, error) { } cgroup.HierarchyID, err = strconv.Atoi(fields[0]) if err != nil { - return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, cgroup.HierarchyID) + return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, fields[0]) } if fields[1] != "" { ssNames := strings.Split(fields[1], ",") diff --git a/vendor/github.com/stretchr/objx/README.md b/vendor/github.com/stretchr/objx/README.md index 78dc1f8b0..e9ba83073 100644 --- a/vendor/github.com/stretchr/objx/README.md +++ b/vendor/github.com/stretchr/objx/README.md @@ -1,8 +1,6 @@ # Objx [![Build Status](https://travis-ci.org/stretchr/objx.svg?branch=master)](https://travis-ci.org/stretchr/objx) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/objx)](https://goreportcard.com/report/github.com/stretchr/objx) -[![Maintainability](https://api.codeclimate.com/v1/badges/1d64bc6c8474c2074f2b/maintainability)](https://codeclimate.com/github/stretchr/objx/maintainability) -[![Test Coverage](https://api.codeclimate.com/v1/badges/1d64bc6c8474c2074f2b/test_coverage)](https://codeclimate.com/github/stretchr/objx/test_coverage) [![Sourcegraph](https://sourcegraph.com/github.com/stretchr/objx/-/badge.svg)](https://sourcegraph.com/github.com/stretchr/objx) [![GoDoc](https://pkg.go.dev/badge/github.com/stretchr/objx?utm_source=godoc)](https://pkg.go.dev/github.com/stretchr/objx) @@ -19,49 +17,62 @@ Objx provides the `objx.Map` type, which is a `map[string]interface{}` that expo ### Pattern Objx uses a predictable pattern to make access data from within `map[string]interface{}` easy. Call one of the `objx.` functions to create your `objx.Map` to get going: - m, err := objx.FromJSON(json) +```go +m, err := objx.FromJSON(json) +``` NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong, the rest will be optimistic and try to figure things out without panicking. Use `Get` to access the value you're interested in. You can use dot and array notation too: - m.Get("places[0].latlng") +```go +m.Get("places[0].latlng") +``` Once you have sought the `Value` you're interested in, you can use the `Is*` methods to determine its type. - if m.Get("code").IsStr() { // Your code... } +```go +if m.Get("code").IsStr() { // Your code... } +``` Or you can just assume the type, and use one of the strong type methods to extract the real value: - m.Get("code").Int() +```go +m.Get("code").Int() +``` If there's no value there (or if it's the wrong type) then a default value will be returned, or you can be explicit about the default value. - Get("code").Int(-1) - +```go +Get("code").Int(-1) +``` If you're dealing with a slice of data as a value, Objx provides many useful methods for iterating, manipulating and selecting that data. You can find out more by exploring the index below. ### Reading data A simple example of how to use Objx: - // Use MustFromJSON to make an objx.Map from some JSON - m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`) +```go +// Use MustFromJSON to make an objx.Map from some JSON +m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`) - // Get the details - name := m.Get("name").Str() - age := m.Get("age").Int() +// Get the details +name := m.Get("name").Str() +age := m.Get("age").Int() - // Get their nickname (or use their name if they don't have one) - nickname := m.Get("nickname").Str(name) +// Get their nickname (or use their name if they don't have one) +nickname := m.Get("nickname").Str(name) +``` ### Ranging Since `objx.Map` is a `map[string]interface{}` you can treat it as such. For example, to `range` the data, do what you would expect: - m := objx.MustFromJSON(json) - for key, value := range m { - // Your code... - } +```go +m := objx.MustFromJSON(json) +for key, value := range m { + // Your code... +} +``` ## Installation To install Objx, use go get: diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index c592f6ad5..a19a89279 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -84,7 +84,7 @@ func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, ar return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -124,7 +124,7 @@ func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg stri return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // assert.Errorf(t, err, "error message %s", "formatted") @@ -144,8 +144,8 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") @@ -190,10 +190,10 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// assert.EventuallyWithTf(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -552,7 +552,7 @@ func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) bool return NoDirExists(t, path, append([]interface{}{msg}, args...)...) } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if assert.NoErrorf(t, err, "error message %s", "formatted") { @@ -849,7 +849,19 @@ func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// assert.YAMLEqf(t, expected, actual, "error message %s", "formatted") func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 58db92845..cd2a86061 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -146,7 +146,7 @@ func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs return Equal(a.t, expected, actual, msgAndArgs...) } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -158,7 +158,7 @@ func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ... return EqualError(a.t, theError, errString, msgAndArgs...) } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -240,7 +240,7 @@ func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string return Equalf(a.t, expected, actual, msg, args...) } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Error(err) @@ -269,8 +269,8 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContains(err, expectedErrorSubString) @@ -281,8 +281,8 @@ func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs . return ErrorContains(a.t, theError, contains, msgAndArgs...) } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") @@ -311,7 +311,7 @@ func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...inter return ErrorIsf(a.t, err, target, msg, args...) } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Errorf(err, "error message %s", "formatted") @@ -372,10 +372,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyWithTf(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1096,7 +1096,7 @@ func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) return NoDirExistsf(a.t, path, msg, args...) } -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if a.NoError(err) { @@ -1109,7 +1109,7 @@ func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { return NoError(a.t, err, msgAndArgs...) } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if a.NoErrorf(err, "error message %s", "formatted") { @@ -1690,7 +1690,19 @@ func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Ti return WithinRangef(a.t, actual, start, end, msg, args...) } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEq(expected, actual) func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1698,7 +1710,19 @@ func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interf return YAMLEq(a.t, expected, actual, msgAndArgs...) } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEqf(expected, actual, "error message %s", "formatted") func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 2fdf80fdd..a44b40ed3 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -9,7 +9,7 @@ import ( func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { objKind := reflect.TypeOf(object).Kind() if objKind != reflect.Slice && objKind != reflect.Array { - return false + return Fail(t, fmt.Sprintf("object %T is not an ordered collection", object), msgAndArgs...) } objValue := reflect.ValueOf(object) @@ -50,6 +50,9 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareR // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } @@ -59,6 +62,9 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } @@ -68,6 +74,9 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } @@ -77,5 +86,8 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index de8de0cb6..1419e4776 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -17,11 +17,10 @@ import ( "unicode" "unicode/utf8" - "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" - // Wrapper around gopkg.in/yaml.v3 "github.com/stretchr/testify/assert/yaml" + "github.com/stretchr/testify/internal/difflib" + "github.com/stretchr/testify/internal/spew" ) //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl" @@ -33,19 +32,19 @@ type TestingT interface { // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful // for table driven tests. -type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool +type ComparisonAssertionFunc = func(TestingT, interface{}, interface{}, ...interface{}) bool // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful // for table driven tests. -type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool +type ValueAssertionFunc = func(TestingT, interface{}, ...interface{}) bool // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful // for table driven tests. -type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool +type BoolAssertionFunc = func(TestingT, bool, ...interface{}) bool // ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful // for table driven tests. -type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool +type ErrorAssertionFunc = func(TestingT, error, ...interface{}) bool // PanicAssertionFunc is a common function prototype when validating a panic value. Can be useful // for table driven tests. @@ -325,13 +324,15 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { func indentMessageLines(message string, longestLabelLen int) string { outBuf := new(bytes.Buffer) - for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { - // no need to align first line because it starts at the correct location (after the label) - if i != 0 { - // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab - outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + scanner := bufio.NewScanner(strings.NewReader(message)) + for firstLine := true; scanner.Scan(); firstLine = false { + if !firstLine { + fmt.Fprint(outBuf, "\n\t"+strings.Repeat(" ", longestLabelLen+1)+"\t") } - outBuf.WriteString(scanner.Text()) + fmt.Fprint(outBuf, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return fmt.Sprintf("cannot display message: %s", err) } return outBuf.String() @@ -544,9 +545,8 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b if !same { // both are pointers but not the same type & pointing to the same address return Fail(t, fmt.Sprintf("Not same: \n"+ - "expected: %p %#[1]v\n"+ - "actual : %p %#[2]v", - expected, actual), msgAndArgs...) + "expected: %[2]s (%[1]T)(%[1]p)\n"+ + "actual : %[4]s (%[3]T)(%[3]p)", expected, truncatingFormat("%#v", expected), actual, truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -571,8 +571,8 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} if same { return Fail(t, fmt.Sprintf( - "Expected and actual point to the same object: %p %#[1]v", - expected), msgAndArgs...) + "Expected and actual point to the same object: %p %s", + expected, truncatingFormat("%#v", expected)), msgAndArgs...) } return true } @@ -604,25 +604,26 @@ func samePointers(first, second interface{}) (same bool, ok bool) { // to a type conversion in the Go grammar. func formatUnequalValues(expected, actual interface{}) (e string, a string) { if reflect.TypeOf(expected) != reflect.TypeOf(actual) { - return fmt.Sprintf("%T(%s)", expected, truncatingFormat(expected)), - fmt.Sprintf("%T(%s)", actual, truncatingFormat(actual)) + return fmt.Sprintf("%T(%s)", expected, truncatingFormat("%#v", expected)), + fmt.Sprintf("%T(%s)", actual, truncatingFormat("%#v", actual)) } switch expected.(type) { case time.Duration: return fmt.Sprintf("%v", expected), fmt.Sprintf("%v", actual) } - return truncatingFormat(expected), truncatingFormat(actual) + return truncatingFormat("%#v", expected), truncatingFormat("%#v", actual) } // truncatingFormat formats the data and truncates it if it's too long. // // This helps keep formatted error messages lines from exceeding the // bufio.MaxScanTokenSize max line length that the go testing framework imposes. -func truncatingFormat(data interface{}) string { - value := fmt.Sprintf("%#v", data) - max := bufio.MaxScanTokenSize - 100 // Give us some space the type info too if needed. - if len(value) > max { - value = value[0:max] + "<... truncated>" +func truncatingFormat(format string, data interface{}) string { + value := fmt.Sprintf(format, data) + // Give us space for two truncated objects and the surrounding sentence. + maxMessageSize := bufio.MaxScanTokenSize/2 - 100 + if len(value) > maxMessageSize { + value = value[0:maxMessageSize] + "<... truncated>" } return value } @@ -743,7 +744,7 @@ func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) + return Fail(t, fmt.Sprintf("Expected nil, but got: %s", truncatingFormat("%#v", object)), msgAndArgs...) } // isEmpty gets whether the specified object is considered empty or not. @@ -793,7 +794,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + Fail(t, fmt.Sprintf("Should be empty, but was %s", truncatingFormat("%v", object)), msgAndArgs...) } return pass @@ -836,11 +837,11 @@ func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) } l, ok := getLen(object) if !ok { - return Fail(t, fmt.Sprintf("\"%v\" could not be applied builtin len()", object), msgAndArgs...) + return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", truncatingFormat("%v", object)), msgAndArgs...) } if l != length { - return Fail(t, fmt.Sprintf("\"%v\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + return Fail(t, fmt.Sprintf("%q should have %d item(s), but has %d", truncatingFormat("%v", object), length, l), msgAndArgs...) } return true } @@ -889,7 +890,7 @@ func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{ } if ObjectsAreEqual(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -904,7 +905,7 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte } if ObjectsAreEqualValues(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...) } return true @@ -964,10 +965,10 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo ok, found := containsElement(s, contains) if !ok { - return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...) } if !found { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", s, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...) } return true @@ -986,10 +987,10 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) ok, found := containsElement(s, contains) if !ok { - return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...) } if found { - return Fail(t, fmt.Sprintf("%#v should not contain %#v", s, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s should not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...) } return true @@ -1031,10 +1032,10 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok av := actualMap.MapIndex(k) if !av.IsValid() { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...) } if !ObjectsAreEqual(ev.Interface(), av.Interface()) { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...) } } @@ -1056,7 +1057,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { - return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, element), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", list), element), msgAndArgs...) } } @@ -1082,12 +1083,12 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) listKind := reflect.TypeOf(list).Kind() if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...) } subsetKind := reflect.TypeOf(subset).Kind() if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...) } if subsetKind == reflect.Map && listKind == reflect.Map { @@ -1106,7 +1107,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...) } subsetList := reflect.ValueOf(subset) @@ -1121,14 +1122,14 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) element := subsetList.Index(i).Interface() ok, found := containsElement(list, element) if !ok { - return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { return true } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...) } // ElementsMatch asserts that the specified listA(array, slice...) is equal to specified @@ -1343,9 +1344,15 @@ func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs . if !funcDidPanic { return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) } - panicErr, ok := panicValue.(error) - if !ok || panicErr.Error() != errString { - return Fail(t, fmt.Sprintf("func %#v should panic with error message:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, errString, panicValue, panickedStack), msgAndArgs...) + panicErr, isError := panicValue.(error) + if !isError || panicErr.Error() != errString { + msg := fmt.Sprintf("func %#v should panic with error message:\t%#v\n", f, errString) + if isError { + msg += fmt.Sprintf("\tError message:\t%#v\n", panicErr.Error()) + } + msg += fmt.Sprintf("\tPanic value:\t%#v\n", panicValue) + msg += fmt.Sprintf("\tPanic stack:\t%s\n", panickedStack) + return Fail(t, msg, msgAndArgs...) } return true @@ -1624,7 +1631,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m Errors */ -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // // actualObj, err := SomeFunction() // if assert.NoError(t, err) { @@ -1635,13 +1642,13 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...) + return Fail(t, fmt.Sprintf("Received unexpected error:\n%s", truncatingFormat("%+v", err)), msgAndArgs...) } return true } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // assert.Error(t, err) @@ -1656,7 +1663,7 @@ func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { return true } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -1674,13 +1681,13 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte if expected != actual { return Fail(t, fmt.Sprintf("Error message not equal:\n"+ "expected: %q\n"+ - "actual : %q", expected, actual), msgAndArgs...) + "actual : %s", expected, truncatingFormat("%q", actual)), msgAndArgs...) } return true } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // assert.ErrorContains(t, err, expectedErrorSubString) @@ -1694,7 +1701,7 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in actual := theError.Error() if !strings.Contains(actual, contains) { - return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("Error %s does not contain %#v", truncatingFormat("%#v", actual), contains), msgAndArgs...) } return true @@ -1760,7 +1767,7 @@ func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { h.Helper() } if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { - return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should be zero, but was %s", truncatingFormat("%v", i)), msgAndArgs...) } return true } @@ -1874,7 +1881,19 @@ func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{ return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// assert.YAMLEq(t, expected, actual) func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -2188,8 +2207,8 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ - "expected: %q\n"+ - "in chain: %s", expectedText, chain, + "expected: %s\n"+ + "in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2211,8 +2230,8 @@ func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ - "found: %q\n"+ - "in chain: %s", expectedText, chain, + "found: %s\n"+ + "in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2236,7 +2255,7 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{ return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ "expected: %s\n"+ - "in chain: %s", expectedType, chain, + "in chain: %s", expectedType, truncatingFormat("%s", chain), ), msgAndArgs...) } @@ -2254,7 +2273,7 @@ func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interfa return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ "found: %s\n"+ - "in chain: %s", reflect.TypeOf(target).Elem().String(), chain, + "in chain: %s", reflect.TypeOf(target).Elem().String(), truncatingFormat("%s", chain), ), msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go index a0b953aa5..c111589c7 100644 --- a/vendor/github.com/stretchr/testify/assert/doc.go +++ b/vendor/github.com/stretchr/testify/assert/doc.go @@ -40,8 +40,8 @@ // // # Assertions // -// Assertions allow you to easily write test code, and are global funcs in the `assert` package. -// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// Assertions allow you to easily write test code, and are global funcs in the assert package. +// All assertion functions take, as the first argument, the [*testing.T] object provided by the // testing framework. This allows the assertion funcs to write the failings and other details to // the correct place. // diff --git a/vendor/github.com/stretchr/testify/internal/difflib/LICENSE b/vendor/github.com/stretchr/testify/internal/difflib/LICENSE new file mode 100644 index 000000000..485be13c6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/github.com/stretchr/testify/internal/difflib/difflib.go b/vendor/github.com/stretchr/testify/internal/difflib/difflib.go new file mode 100644 index 000000000..9984599b4 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/difflib/difflib.go @@ -0,0 +1,592 @@ +// Package difflib is a partial port of Python difflib module. +// +// It provides tools to compare sequences of strings and generate textual diffs. +// +// The following class and functions have been ported: +// +// - SequenceMatcher +// +// - unified_diff +// +// Getting unified diffs was the main goal of the port. Keep in mind this code +// is mostly suitable to output text differences in a human friendly way, there +// are no guarantees generated diffs are consumable by patch(1). +// +// This package was adopted from [github.com/pmezard/go-difflib] which +// is no longer maintained. +// +// [github.com/pmezard/go-difflib]: https://github.com/pmezard/go-difflib +package difflib + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +type Match struct { + A int + B int + Size int +} + +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "

" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" . +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +// Set two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// Set the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// Set the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s, _ := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s, _ := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s, _ := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// +// and for all (i',j',k') meeting those conditions, +// +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// Return list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// Return list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// Isolate change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +// Unified diff parameters +type UnifiedDiff struct { + A []string // First sequence lines + FromFile string // First file name + FromDate string // First file time + B []string // Second sequence lines + ToFile string // Second file name + ToDate string // Second file time + Eol string // Headers end of line, defaults to LF + Context int // Number of context lines +} + +// Compare two sequences of lines; generate the delta as a unified diff. +// +// Unified diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by 'n' which +// defaults to three. +// +// By default, the diff control lines (those with ---, +++, or @@) are +// created with a trailing newline. This is helpful so that inputs +// created from file.readlines() result in diffs that are suitable for +// file.writelines() since both the inputs and outputs have trailing +// newlines. +// +// For inputs that do not have trailing newlines, set the lineterm +// argument to "" so that the output will be uniformly newline free. +// +// The unidiff format normally has a header for filenames and modification +// times. Any or all of these may be specified using strings for +// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. +// The modification times are normally expressed in the ISO 8601 format. +func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + wf := func(format string, args ...interface{}) error { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + return err + } + ws := func(s string) error { + _, err := buf.WriteString(s) + return err + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + if err != nil { + return err + } + err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + if err != nil { + return err + } + } + } + first, last := g[0], g[len(g)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + return err + } + for _, c := range g { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + if c.Tag == 'e' { + for _, line := range diff.A[i1:i2] { + if err := ws(" " + line); err != nil { + return err + } + } + continue + } + if c.Tag == 'r' || c.Tag == 'd' { + for _, line := range diff.A[i1:i2] { + if err := ws("-" + line); err != nil { + return err + } + } + } + if c.Tag == 'r' || c.Tag == 'i' { + for _, line := range diff.B[j1:j2] { + if err := ws("+" + line); err != nil { + return err + } + } + } + } + } + return nil +} + +// Like WriteUnifiedDiff but returns the diff a string. +func GetUnifiedDiffString(diff UnifiedDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteUnifiedDiff(w, diff) + return string(w.Bytes()), err +} + +// Convert range to the "ed" format. +func formatRangeContext(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + if length <= 1 { + return fmt.Sprintf("%d", beginning) + } + return fmt.Sprintf("%d,%d", beginning, beginning+length-1) +} + +// Split a string on "\n" while preserving them. The output can be used +// as input for UnifiedDiff and ContextDiff structures. +func SplitLines(s string) []string { + lines := strings.SplitAfter(s, "\n") + lines[len(lines)-1] += "\n" + return lines +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/LICENSE b/vendor/github.com/stretchr/testify/internal/spew/LICENSE new file mode 100644 index 000000000..bc52e96f2 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/internal/spew/README.md b/vendor/github.com/stretchr/testify/internal/spew/README.md new file mode 100644 index 000000000..51a909e2e --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/README.md @@ -0,0 +1,12 @@ +go-spew +======= + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) + +Go-spew implements a deep pretty printer for Go data structures to aid in +debugging. A comprehensive suite of tests with 100% test coverage is provided +to ensure proper functionality. + +## License + +Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/vendor/github.com/stretchr/testify/internal/spew/bypass.go b/vendor/github.com/stretchr/testify/internal/spew/bypass.go new file mode 100644 index 000000000..70ddeaad3 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/bypass.go @@ -0,0 +1,146 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// Go versions prior to 1.4 are disabled because they use a different layout +// for interfaces which make the implementation of unsafeReflectValue more complex. +//go:build !js && !appengine && !safe && !disableunsafe && go1.4 +// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +package spew + +import ( + "reflect" + "unsafe" +) + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = false + + // ptrSize is the size of a pointer on the current arch. + ptrSize = unsafe.Sizeof((*byte)(nil)) +) + +type flag uintptr + +var ( + // flagRO indicates whether the value field of a reflect.Value + // is read-only. + flagRO flag + + // flagAddr indicates whether the address of the reflect.Value's + // value may be taken. + flagAddr flag +) + +// flagKindMask holds the bits that make up the kind +// part of the flags field. In all the supported versions, +// it is in the lower 5 bits. +const flagKindMask = flag(0x1f) + +// Different versions of Go have used different +// bit layouts for the flags type. This table +// records the known combinations. +var okFlags = []struct { + ro, addr flag +}{{ + // From Go 1.4 to 1.5 + ro: 1 << 5, + addr: 1 << 7, +}, { + // Up to Go tip. + ro: 1<<5 | 1<<6, + addr: 1 << 8, +}} + +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + return field.Offset +}() + +// flagField returns a pointer to the flag field of a reflect.Value. +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) +} + +// unsafeReflectValue converts the passed reflect.Value into a one that bypasses +// the typical safety restrictions preventing access to unaddressable and +// unexported data. It works by digging the raw pointer to the underlying +// value out of the protected value and generating a new unprotected (unsafe) +// reflect.Value to it. +// +// This allows us to check for implementations of the Stringer and error +// interfaces to be used for pretty printing ordinarily unaddressable and +// inaccessible values such as unexported struct fields. +func unsafeReflectValue(v reflect.Value) reflect.Value { + if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { + return v + } + flagFieldPtr := flagField(&v) + *flagFieldPtr &^= flagRO + *flagFieldPtr |= flagAddr + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + type t0 int + var t struct { + A t0 + // t0 will have flagEmbedRO set. + t0 + // a will have flagStickyRO set + a t0 + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + vt0 := reflect.ValueOf(t).FieldByName("t0") + + // Infer flagRO from the difference between the flags + // for the (otherwise identical) fields in t. + flagPublic := *flagField(&vA) + flagWithRO := *flagField(&va) | *flagField(&vt0) + flagRO = flagPublic ^ flagWithRO + + // Infer flagAddr from the difference between a value + // taken from a pointer and not. + vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") + flagNoPtr := *flagField(&vA) + flagPtr := *flagField(&vPtrA) + flagAddr = flagNoPtr ^ flagPtr + + // Check that the inferred flags tally with one of the known versions. + for _, f := range okFlags { + if flagRO == f.ro && flagAddr == f.addr { + return + } + } + panic("reflect.Value read-only flag has changed semantics") +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go b/vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go new file mode 100644 index 000000000..5e2d890d6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/bypasssafe.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is running on Google App Engine, compiled by GopherJS, or +// "-tags safe" is added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +//go:build js || appengine || safe || disableunsafe || !go1.4 +// +build js appengine safe disableunsafe !go1.4 + +package spew + +import "reflect" + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = true +) + +// unsafeReflectValue typically converts the passed reflect.Value into a one +// that bypasses the typical safety restrictions preventing access to +// unaddressable and unexported data. However, doing this relies on access to +// the unsafe package. This is a stub version which simply returns the passed +// reflect.Value when the unsafe package is not available. +func unsafeReflectValue(v reflect.Value) reflect.Value { + return v +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/common.go b/vendor/github.com/stretchr/testify/internal/spew/common.go new file mode 100644 index 000000000..1be8ce945 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/common.go @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "reflect" + "sort" + "strconv" +) + +// Some constants in the form of bytes to avoid string overhead. This mirrors +// the technique used in the fmt package. +var ( + panicBytes = []byte("(PANIC=") + plusBytes = []byte("+") + iBytes = []byte("i") + trueBytes = []byte("true") + falseBytes = []byte("false") + interfaceBytes = []byte("(interface {})") + commaNewlineBytes = []byte(",\n") + newlineBytes = []byte("\n") + openBraceBytes = []byte("{") + openBraceNewlineBytes = []byte("{\n") + closeBraceBytes = []byte("}") + asteriskBytes = []byte("*") + colonBytes = []byte(":") + colonSpaceBytes = []byte(": ") + openParenBytes = []byte("(") + closeParenBytes = []byte(")") + spaceBytes = []byte(" ") + pointerChainBytes = []byte("->") + nilAngleBytes = []byte("") + maxNewlineBytes = []byte("\n") + maxShortBytes = []byte("") + circularBytes = []byte("") + circularShortBytes = []byte("") + invalidAngleBytes = []byte("") + openBracketBytes = []byte("[") + closeBracketBytes = []byte("]") + percentBytes = []byte("%") + precisionBytes = []byte(".") + openAngleBytes = []byte("<") + closeAngleBytes = []byte(">") + openMapBytes = []byte("map[") + closeMapBytes = []byte("]") + lenEqualsBytes = []byte("len=") + capEqualsBytes = []byte("cap=") +) + +// hexDigits is used to map a decimal value to a hex digit. +var hexDigits = "0123456789abcdef" + +// catchPanic handles any panics that might occur during the handleMethods +// calls. +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + w.Write(panicBytes) + fmt.Fprintf(w, "%v", err) + w.Write(closeParenBytes) + } +} + +// handleMethods attempts to call the Error and String methods on the underlying +// type the passed reflect.Value represents and outputes the result to Writer w. +// +// It handles panics in any called methods by catching and displaying the error +// as the formatted value. +func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { + // We need an interface to check if the type implements the error or + // Stringer interface. However, the reflect package won't give us an + // interface on certain things like unexported struct fields in order + // to enforce visibility rules. We use unsafe, when it's available, + // to bypass these restrictions since this package does not mutate the + // values. + if !v.CanInterface() { + if UnsafeDisabled { + return false + } + + v = unsafeReflectValue(v) + } + + // Choose whether or not to do error and Stringer interface lookups against + // the base type or a pointer to the base type depending on settings. + // Technically calling one of these methods with a pointer receiver can + // mutate the value, however, types which choose to satisify an error or + // Stringer interface with a pointer receiver should not be mutating their + // state inside these interface methods. + if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { + v = unsafeReflectValue(v) + } + if v.CanAddr() { + v = v.Addr() + } + + // Is it an error or Stringer? + switch iface := v.Interface().(type) { + case error: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.Error())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + + w.Write([]byte(iface.Error())) + return true + + case fmt.Stringer: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.String())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + w.Write([]byte(iface.String())) + return true + } + return false +} + +// printBool outputs a boolean value as true or false to Writer w. +func printBool(w io.Writer, val bool) { + if val { + w.Write(trueBytes) + } else { + w.Write(falseBytes) + } +} + +// printInt outputs a signed integer value to Writer w. +func printInt(w io.Writer, val int64, base int) { + w.Write([]byte(strconv.FormatInt(val, base))) +} + +// printUint outputs an unsigned integer value to Writer w. +func printUint(w io.Writer, val uint64, base int) { + w.Write([]byte(strconv.FormatUint(val, base))) +} + +// printFloat outputs a floating point value using the specified precision, +// which is expected to be 32 or 64bit, to Writer w. +func printFloat(w io.Writer, val float64, precision int) { + w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) +} + +// printComplex outputs a complex value using the specified float precision +// for the real and imaginary parts to Writer w. +func printComplex(w io.Writer, c complex128, floatPrecision int) { + r := real(c) + w.Write(openParenBytes) + w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) + i := imag(c) + if i >= 0 { + w.Write(plusBytes) + } + w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) + w.Write(iBytes) + w.Write(closeParenBytes) +} + +// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' +// prefix to Writer w. +func printHexPtr(w io.Writer, p uintptr) { + // Null pointer. + num := uint64(p) + if num == 0 { + w.Write(nilAngleBytes) + return + } + + // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix + buf := make([]byte, 18) + + // It's simpler to construct the hex string right to left. + base := uint64(16) + i := len(buf) - 1 + for num >= base { + buf[i] = hexDigits[num%base] + num /= base + i-- + } + buf[i] = hexDigits[num] + + // Add '0x' prefix. + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + + // Strip unused leading bytes. + buf = buf[i:] + w.Write(buf) +} + +// valuesSorter implements sort.Interface to allow a slice of reflect.Value +// elements to be sorted. +type valuesSorter struct { + values []reflect.Value + strings []string // either nil or same len and values + cs *ConfigState +} + +// newValuesSorter initializes a valuesSorter instance, which holds a set of +// surrogate keys on which the data should be sorted. It uses flags in +// ConfigState to decide if and how to populate those surrogate keys. +func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { + vs := &valuesSorter{values: values, cs: cs} + if canSortSimply(vs.values[0].Kind()) { + return vs + } + if !cs.DisableMethods { + vs.strings = make([]string, len(values)) + for i := range vs.values { + b := bytes.Buffer{} + if !handleMethods(cs, &b, vs.values[i]) { + vs.strings = nil + break + } + vs.strings[i] = b.String() + } + } + if vs.strings == nil && cs.SpewKeys { + vs.strings = make([]string, len(values)) + for i := range vs.values { + vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) + } + } + return vs +} + +// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted +// directly, or whether it should be considered for sorting by surrogate keys +// (if the ConfigState allows it). +func canSortSimply(kind reflect.Kind) bool { + // This switch parallels valueSortLess, except for the default case. + switch kind { + case reflect.Bool: + return true + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return true + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Uintptr: + return true + case reflect.Array: + return true + } + return false +} + +// Len returns the number of values in the slice. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Len() int { + return len(s.values) +} + +// Swap swaps the values at the passed indices. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] + if s.strings != nil { + s.strings[i], s.strings[j] = s.strings[j], s.strings[i] + } +} + +// valueSortLess returns whether the first value should sort before the second +// value. It is used by valueSorter.Less as part of the sort.Interface +// implementation. +func valueSortLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Bool: + return !a.Bool() && b.Bool() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return a.Int() < b.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + case reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Array: + // Compare the contents of both arrays. + l := a.Len() + for i := 0; i < l; i++ { + av := a.Index(i) + bv := b.Index(i) + if av.Interface() == bv.Interface() { + continue + } + return valueSortLess(av, bv) + } + } + return a.String() < b.String() +} + +// Less returns whether the value at index i should sort before the +// value at index j. It is part of the sort.Interface implementation. +func (s *valuesSorter) Less(i, j int) bool { + if s.strings == nil { + return valueSortLess(s.values[i], s.values[j]) + } + return s.strings[i] < s.strings[j] +} + +// sortValues is a sort function that handles both native types and any type that +// can be converted to error or Stringer. Other inputs are sorted according to +// their Value.String() value to ensure display stability. +func sortValues(values []reflect.Value, cs *ConfigState) { + if len(values) == 0 { + return + } + sort.Sort(newValuesSorter(values, cs)) +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/config.go b/vendor/github.com/stretchr/testify/internal/spew/config.go new file mode 100644 index 000000000..161895fc6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/config.go @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// ConfigState houses the configuration options used by spew to format and +// display values. There is a global instance, Config, that is used to control +// all top-level Formatter and Dump functionality. Each ConfigState instance +// provides methods equivalent to the top-level functions. +// +// The zero value for ConfigState provides no indentation. You would typically +// want to set it to a space or a tab. +// +// Alternatively, you can use NewDefaultConfig to get a ConfigState instance +// with default settings. See the documentation of NewDefaultConfig for default +// values. +type ConfigState struct { + // Indent specifies the string to use for each indentation level. The + // global config instance that all top-level functions use set this to a + // single space by default. If you would like more indentation, you might + // set this to a tab with "\t" or perhaps two spaces with " ". + Indent string + + // MaxDepth controls the maximum number of levels to descend into nested + // data structures. The default, 0, means there is no limit. + // + // NOTE: Circular data structures are properly detected, so it is not + // necessary to set this value unless you specifically want to limit deeply + // nested data structures. + MaxDepth int + + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + + // DisablePointerMethods specifies whether or not to check for and invoke + // error and Stringer interfaces on types which only accept a pointer + // receiver when the current type is not a pointer. + // + // NOTE: This might be an unsafe action since calling one of these methods + // with a pointer receiver could technically mutate the value, however, + // in practice, types which choose to satisify an error or Stringer + // interface with a pointer receiver should not be mutating their state + // inside these interface methods. As a result, this option relies on + // access to the unsafe package, so it will not have any effect when + // running in environments without access to the unsafe package such as + // Google App Engine or with the "safe" build tag specified. + DisablePointerMethods bool + + // DisablePointerAddresses specifies whether to disable the printing of + // pointer addresses. This is useful when diffing data structures in tests. + DisablePointerAddresses bool + + // DisableCapacities specifies whether to disable the printing of capacities + // for arrays, slices, maps and channels. This is useful when diffing + // data structures in tests. + DisableCapacities bool + + // ContinueOnMethod specifies whether or not recursion should continue once + // a custom error or Stringer interface is invoked. The default, false, + // means it will print the results of invoking the custom error or Stringer + // interface and return immediately instead of continuing to recurse into + // the internals of the data type. + // + // NOTE: This flag does not have any effect if method invocation is disabled + // via the DisableMethods or DisablePointerMethods options. + ContinueOnMethod bool + + // SortKeys specifies map keys should be sorted before being printed. Use + // this to have a more deterministic, diffable output. Note that only + // native types (bool, int, uint, floats, uintptr and string) and types + // that support the error or Stringer interfaces (if methods are + // enabled) are supported, with other types sorted according to the + // reflect.Value.String() output which guarantees display stability. + SortKeys bool + + // SpewKeys specifies that, as a last resort attempt, map keys should + // be spewed to strings and sorted by those strings. This is only + // considered if SortKeys is true. + SpewKeys bool +} + +// Config is the active configuration of the top-level functions. +// The configuration can be changed by modifying the contents of spew.Config. +var Config = ConfigState{Indent: " "} + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the formatted string as a value that satisfies error. See NewFormatter +// for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, c.convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, c.convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, c.convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a Formatter interface returned by c.NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Print(a ...interface{}) (n int, err error) { + return fmt.Print(c.convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, c.convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Println(a ...interface{}) (n int, err error) { + return fmt.Println(c.convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprint(a ...interface{}) string { + return fmt.Sprint(c.convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, c.convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a Formatter interface returned by c.NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.convertArgs(a)...) +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +c.Printf, c.Println, or c.Printf. +*/ +func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(c, v) +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { + fdump(c, w, a...) +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by modifying the public members +of c. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func (c *ConfigState) Dump(a ...interface{}) { + fdump(c, os.Stdout, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func (c *ConfigState) Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(c, &buf, a...) + return buf.String() +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a spew Formatter interface using +// the ConfigState associated with s. +func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = newFormatter(c, arg) + } + return formatters +} + +// NewDefaultConfig returns a ConfigState with the following default settings. +// +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false +func NewDefaultConfig() *ConfigState { + return &ConfigState{Indent: " "} +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/doc.go b/vendor/github.com/stretchr/testify/internal/spew/doc.go new file mode 100644 index 000000000..722e9aa79 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/doc.go @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package spew implements a deep pretty printer for Go data structures to aid in +debugging. + +A quick overview of the additional features spew provides over the built-in +printing facilities for Go data types are as follows: + + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) + +There are two different approaches spew allows for dumping Go data structures: + + - Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + - A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt + +# Quick Start + +This section demonstrates how to quickly get started with spew. See the +sections below for further details on formatting and configuration options. + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + + spew.Dump(myVar1, myVar2, ...) + spew.Fdump(someWriter, myVar1, myVar2, ...) + str := spew.Sdump(myVar1, myVar2, ...) + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with +%v (most compact), %+v (adds pointer addresses), %#v (adds types), or +%#+v (adds types and pointer addresses): + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +# Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available +via the spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +The following configuration options are available: + + - Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + - MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + - DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + - DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + - DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + + - DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. + + - ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + - SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + - SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +# Dump Usage + +Simply call spew.Dump with a list of variables you want to dump: + + spew.Dump(myVar1, myVar2, ...) + +You may also call spew.Fdump if you would prefer to output to an arbitrary +io.Writer. For example, to dump to standard error: + + spew.Fdump(os.Stderr, myVar1, myVar2, ...) + +A third option is to call spew.Sdump to get the formatted output as a string: + + str := spew.Sdump(myVar1, myVar2, ...) + +# Sample Dump Output + +See the Dump example for details on the setup of the types and variables being +shown here. + + (main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) + }), + ExportedField: (map[interface {}]interface {}) (len=1) { + (string) (len=3) "one": (bool) true + } + } + +Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C +command as shown. + + ([]uint8) (len=32 cap=32) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| + } + +# Custom Formatter + +Spew provides a custom formatter that implements the fmt.Formatter interface +so that it integrates cleanly with standard fmt package printing functions. The +formatter is useful for inline printing of smaller data types similar to the +standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +# Custom Formatter Usage + +The simplest way to make use of the spew custom formatter is to call one of the +convenience functions such as spew.Printf, spew.Println, or spew.Printf. The +functions have syntax you are most likely already familiar with: + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Println(myVar, myVar2) + spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +See the Index for the full list convenience functions. + +# Sample Formatter Output + +Double pointer to a uint8: + + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 + +Pointer to circular struct with a uint8 field and a pointer to itself: + + %v: <*>{1 <*>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} + +See the Printf example for details on the setup of variables being shown +here. + +# Errors + +Since it is possible for custom Stringer/error interfaces to panic, spew +detects them and handles them internally by printing the panic information +inline with the output. Since spew is intended to provide deep pretty printing +capabilities on structures, it intentionally does not return any errors. +*/ +package spew diff --git a/vendor/github.com/stretchr/testify/internal/spew/dump.go b/vendor/github.com/stretchr/testify/internal/spew/dump.go new file mode 100644 index 000000000..8323041a4 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/dump.go @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +var ( + // uint8Type is a reflect.Type representing a uint8. It is used to + // convert cgo types to uint8 slices for hexdumping. + uint8Type = reflect.TypeOf(uint8(0)) + + // cCharRE is a regular expression that matches a cgo char. + // It is used to detect character arrays to hexdump them. + cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) + + // cUnsignedCharRE is a regular expression that matches a cgo unsigned + // char. It is used to detect unsigned character arrays to hexdump + // them. + cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) + + // cUint8tCharRE is a regular expression that matches a cgo uint8_t. + // It is used to detect uint8_t arrays to hexdump them. + cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) +) + +// dumpState contains information about the state of a dump operation. +type dumpState struct { + w io.Writer + depth int + pointers map[uintptr]int + ignoreNextType bool + ignoreNextIndent bool + cs *ConfigState +} + +// indent performs indentation according to the depth level and cs.Indent +// option. +func (d *dumpState) indent() { + if d.ignoreNextIndent { + d.ignoreNextIndent = false + return + } + d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) +} + +// unpackValue returns values inside of non-nil interfaces when possible. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v +} + +// dumpPtr handles formatting of pointers by indirecting them as necessary. +func (d *dumpState) dumpPtr(v reflect.Value) { + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range d.pointers { + if depth >= d.depth { + delete(d.pointers, k) + } + } + + // Keep list of all dereferenced pointers to show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by dereferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := d.pointers[addr]; ok && pd < d.depth { + cycleFound = true + indirects-- + break + } + d.pointers[addr] = d.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type information. + d.w.Write(openParenBytes) + d.w.Write(bytes.Repeat(asteriskBytes, indirects)) + d.w.Write([]byte(ve.Type().String())) + d.w.Write(closeParenBytes) + + // Display pointer information. + if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { + d.w.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + d.w.Write(pointerChainBytes) + } + printHexPtr(d.w, addr) + } + d.w.Write(closeParenBytes) + } + + // Display dereferenced value. + d.w.Write(openParenBytes) + switch { + case nilFound: + d.w.Write(nilAngleBytes) + + case cycleFound: + d.w.Write(circularBytes) + + default: + d.ignoreNextType = true + d.dump(ve) + } + d.w.Write(closeParenBytes) +} + +// dumpSlice handles formatting of arrays and slices. Byte (uint8 under +// reflection) arrays and slices are dumped in hexdump -C fashion. +func (d *dumpState) dumpSlice(v reflect.Value) { + // Determine whether this type should be hex dumped or not. Also, + // for types which should be hexdumped, try to use the underlying data + // first, then fall back to trying to convert them to a uint8 slice. + var buf []uint8 + doConvert := false + doHexDump := false + numEntries := v.Len() + if numEntries > 0 { + vt := v.Index(0).Type() + vts := vt.String() + switch { + // C types that need to be converted. + case cCharRE.MatchString(vts): + fallthrough + case cUnsignedCharRE.MatchString(vts): + fallthrough + case cUint8tCharRE.MatchString(vts): + doConvert = true + + // Try to use existing uint8 slices and fall back to converting + // and copying if that fails. + case vt.Kind() == reflect.Uint8: + // We need an addressable interface to convert the type + // to a byte slice. However, the reflect package won't + // give us an interface on certain things like + // unexported struct fields in order to enforce + // visibility rules. We use unsafe, when available, to + // bypass these restrictions since this package does not + // mutate the values. + vs := v + if !vs.CanInterface() || !vs.CanAddr() { + vs = unsafeReflectValue(vs) + } + if !UnsafeDisabled { + vs = vs.Slice(0, numEntries) + + // Use the existing uint8 slice if it can be + // type asserted. + iface := vs.Interface() + if slice, ok := iface.([]uint8); ok { + buf = slice + doHexDump = true + break + } + } + + // The underlying data needs to be converted if it can't + // be type asserted to a uint8 slice. + doConvert = true + } + + // Copy and convert the underlying type if needed. + if doConvert && vt.ConvertibleTo(uint8Type) { + // Convert and copy each element into a uint8 byte + // slice. + buf = make([]uint8, numEntries) + for i := 0; i < numEntries; i++ { + vv := v.Index(i) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) + } + doHexDump = true + } + } + + // Hexdump the entire slice as needed. + if doHexDump { + indent := strings.Repeat(d.cs.Indent, d.depth) + str := indent + hex.Dump(buf) + str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.TrimRight(str, d.cs.Indent) + d.w.Write([]byte(str)) + return + } + + // Recursively call dump for each item. + for i := 0; i < numEntries; i++ { + d.dump(d.unpackValue(v.Index(i))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } +} + +// dump is the main workhorse for dumping a value. It uses the passed reflect +// value to figure out what kind of object we are dealing with and formats it +// appropriately. It is a recursive function, however circular data structures +// are detected and handled properly. +func (d *dumpState) dump(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + d.w.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + d.indent() + d.dumpPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !d.ignoreNextType { + d.indent() + d.w.Write(openParenBytes) + d.w.Write([]byte(v.Type().String())) + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + d.ignoreNextType = false + + // Display length and capacity if the built-in len and cap functions + // work with the value's kind and the len/cap itself is non-zero. + valueLen, valueCap := 0, 0 + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.Chan: + valueLen, valueCap = v.Len(), v.Cap() + case reflect.Map, reflect.String: + valueLen = v.Len() + } + if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { + d.w.Write(openParenBytes) + if valueLen != 0 { + d.w.Write(lenEqualsBytes) + printInt(d.w, int64(valueLen), 10) + } + if !d.cs.DisableCapacities && valueCap != 0 { + if valueLen != 0 { + d.w.Write(spaceBytes) + } + d.w.Write(capEqualsBytes) + printInt(d.w, int64(valueCap), 10) + } + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + + // Call Stringer/error interfaces if they exist and the handle methods flag + // is enabled + if !d.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(d.cs, d.w, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(d.w, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(d.w, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(d.w, v.Uint(), 10) + + case reflect.Float32: + printFloat(d.w, v.Float(), 32) + + case reflect.Float64: + printFloat(d.w, v.Float(), 64) + + case reflect.Complex64: + printComplex(d.w, v.Complex(), 32) + + case reflect.Complex128: + printComplex(d.w, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + d.dumpSlice(v) + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.String: + d.w.Write([]byte(strconv.Quote(v.String()))) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + d.w.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + numEntries := v.Len() + keys := v.MapKeys() + if d.cs.SortKeys { + sortValues(keys, d.cs) + } + for i, key := range keys { + d.dump(d.unpackValue(key)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.MapIndex(key))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Struct: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + vt := v.Type() + numFields := v.NumField() + for i := 0; i < numFields; i++ { + d.indent() + vtf := vt.Field(i) + d.w.Write([]byte(vtf.Name)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.Field(i))) + if i < (numFields - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(d.w, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(d.w, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it in case any new + // types are added. + default: + if v.CanInterface() { + fmt.Fprintf(d.w, "%v", v.Interface()) + } else { + fmt.Fprintf(d.w, "%v", v.String()) + } + } +} + +// fdump is a helper function to consolidate the logic from the various public +// methods which take varying writers and config states. +func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { + for _, arg := range a { + if arg == nil { + w.Write(interfaceBytes) + w.Write(spaceBytes) + w.Write(nilAngleBytes) + w.Write(newlineBytes) + continue + } + + d := dumpState{w: w, cs: cs} + d.pointers = make(map[uintptr]int) + d.dump(reflect.ValueOf(arg)) + d.w.Write(newlineBytes) + } +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func Fdump(w io.Writer, a ...interface{}) { + fdump(&Config, w, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(&Config, &buf, a...) + return buf.String() +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by an exported package global, +spew.Config. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func Dump(a ...interface{}) { + fdump(&Config, os.Stdout, a...) +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/format.go b/vendor/github.com/stretchr/testify/internal/spew/format.go new file mode 100644 index 000000000..b04edb7d7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/format.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" +) + +// supportedFlags is a list of all the character flags supported by fmt package. +const supportedFlags = "0-+# " + +// formatState implements the fmt.Formatter interface and contains information +// about the state of a formatting operation. The NewFormatter function can +// be used to get a new Formatter which can be used directly as arguments +// in standard fmt package printing calls. +type formatState struct { + value interface{} + fs fmt.State + depth int + pointers map[uintptr]int + ignoreNextType bool + cs *ConfigState +} + +// buildDefaultFormat recreates the original format string without precision +// and width information to pass in to fmt.Sprintf in the case of an +// unrecognized type. Unless new types are added to the language, this +// function won't ever be called. +func (f *formatState) buildDefaultFormat() (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + buf.WriteRune('v') + + format = buf.String() + return format +} + +// constructOrigFormat recreates the original format string including precision +// and width information to pass along to the standard fmt package. This allows +// automatic deferral of all format strings this package doesn't support. +func (f *formatState) constructOrigFormat(verb rune) (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + if width, ok := f.fs.Width(); ok { + buf.WriteString(strconv.Itoa(width)) + } + + if precision, ok := f.fs.Precision(); ok { + buf.Write(precisionBytes) + buf.WriteString(strconv.Itoa(precision)) + } + + buf.WriteRune(verb) + + format = buf.String() + return format +} + +// unpackValue returns values inside of non-nil interfaces when possible and +// ensures that types for values which have been unpacked from an interface +// are displayed when the show types flag is also set. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (f *formatState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface { + f.ignoreNextType = false + if !v.IsNil() { + v = v.Elem() + } + } + return v +} + +// formatPtr handles formatting of pointers by indirecting them as necessary. +func (f *formatState) formatPtr(v reflect.Value) { + // Display nil if top level pointer is nil. + showTypes := f.fs.Flag('#') + if v.IsNil() && (!showTypes || f.ignoreNextType) { + f.fs.Write(nilAngleBytes) + return + } + + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range f.pointers { + if depth >= f.depth { + delete(f.pointers, k) + } + } + + // Keep list of all dereferenced pointers to possibly show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by derferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := f.pointers[addr]; ok && pd < f.depth { + cycleFound = true + indirects-- + break + } + f.pointers[addr] = f.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type or indirection level depending on flags. + if showTypes && !f.ignoreNextType { + f.fs.Write(openParenBytes) + f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) + f.fs.Write([]byte(ve.Type().String())) + f.fs.Write(closeParenBytes) + } else { + if nilFound || cycleFound { + indirects += strings.Count(ve.Type().String(), "*") + } + f.fs.Write(openAngleBytes) + f.fs.Write([]byte(strings.Repeat("*", indirects))) + f.fs.Write(closeAngleBytes) + } + + // Display pointer information depending on flags. + if f.fs.Flag('+') && (len(pointerChain) > 0) { + f.fs.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + f.fs.Write(pointerChainBytes) + } + printHexPtr(f.fs, addr) + } + f.fs.Write(closeParenBytes) + } + + // Display dereferenced value. + switch { + case nilFound: + f.fs.Write(nilAngleBytes) + + case cycleFound: + f.fs.Write(circularShortBytes) + + default: + f.ignoreNextType = true + f.format(ve) + } +} + +// format is the main workhorse for providing the Formatter interface. It +// uses the passed reflect value to figure out what kind of object we are +// dealing with and formats it appropriately. It is a recursive function, +// however circular data structures are detected and handled properly. +func (f *formatState) format(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + f.fs.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + f.formatPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !f.ignoreNextType && f.fs.Flag('#') { + f.fs.Write(openParenBytes) + f.fs.Write([]byte(v.Type().String())) + f.fs.Write(closeParenBytes) + } + f.ignoreNextType = false + + // Call Stringer/error interfaces if they exist and the handle methods + // flag is enabled. + if !f.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(f.cs, f.fs, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(f.fs, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(f.fs, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(f.fs, v.Uint(), 10) + + case reflect.Float32: + printFloat(f.fs, v.Float(), 32) + + case reflect.Float64: + printFloat(f.fs, v.Float(), 64) + + case reflect.Complex64: + printComplex(f.fs, v.Complex(), 32) + + case reflect.Complex128: + printComplex(f.fs, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + f.fs.Write(openBracketBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + numEntries := v.Len() + for i := 0; i < numEntries; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(v.Index(i))) + } + } + f.depth-- + f.fs.Write(closeBracketBytes) + + case reflect.String: + f.fs.Write([]byte(v.String())) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + f.fs.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + + f.fs.Write(openMapBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + keys := v.MapKeys() + if f.cs.SortKeys { + sortValues(keys, f.cs) + } + for i, key := range keys { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(key)) + f.fs.Write(colonBytes) + f.ignoreNextType = true + f.format(f.unpackValue(v.MapIndex(key))) + } + } + f.depth-- + f.fs.Write(closeMapBytes) + + case reflect.Struct: + numFields := v.NumField() + f.fs.Write(openBraceBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + vt := v.Type() + for i := 0; i < numFields; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + vtf := vt.Field(i) + if f.fs.Flag('+') || f.fs.Flag('#') { + f.fs.Write([]byte(vtf.Name)) + f.fs.Write(colonBytes) + } + f.format(f.unpackValue(v.Field(i))) + } + } + f.depth-- + f.fs.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(f.fs, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(f.fs, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it if any get added. + default: + format := f.buildDefaultFormat() + if v.CanInterface() { + fmt.Fprintf(f.fs, format, v.Interface()) + } else { + fmt.Fprintf(f.fs, format, v.String()) + } + } +} + +// Format satisfies the fmt.Formatter interface. See NewFormatter for usage +// details. +func (f *formatState) Format(fs fmt.State, verb rune) { + f.fs = fs + + // Use standard formatting for verbs that are not v. + if verb != 'v' { + format := f.constructOrigFormat(verb) + fmt.Fprintf(fs, format, f.value) + return + } + + if f.value == nil { + if fs.Flag('#') { + fs.Write(interfaceBytes) + } + fs.Write(nilAngleBytes) + return + } + + f.format(reflect.ValueOf(f.value)) +} + +// newFormatter is a helper function to consolidate the logic from the various +// public methods which take varying config states. +func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { + fs := &formatState{value: v, cs: cs} + fs.pointers = make(map[uintptr]int) + return fs +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +Printf, Println, or Fprintf. +*/ +func NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(&Config, v) +} diff --git a/vendor/github.com/stretchr/testify/internal/spew/spew.go b/vendor/github.com/stretchr/testify/internal/spew/spew.go new file mode 100644 index 000000000..32c0e3388 --- /dev/null +++ b/vendor/github.com/stretchr/testify/internal/spew/spew.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "fmt" + "io" +) + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the formatted string as a value that satisfies error. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a default Formatter interface returned by NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprint(a ...interface{}) string { + return fmt.Sprint(convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(convertArgs(a)...) +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a default spew Formatter interface. +func convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = NewFormatter(arg) + } + return formatters +} diff --git a/vendor/github.com/stretchr/testify/mock/mock.go b/vendor/github.com/stretchr/testify/mock/mock.go index efc89deff..7f4d28d5e 100644 --- a/vendor/github.com/stretchr/testify/mock/mock.go +++ b/vendor/github.com/stretchr/testify/mock/mock.go @@ -11,11 +11,11 @@ import ( "sync" "time" - "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" "github.com/stretchr/objx" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/internal/difflib" + "github.com/stretchr/testify/internal/spew" ) // regex for GCCGO functions @@ -470,7 +470,7 @@ func callString(method string, arguments Arguments, includeArgumentValues bool) // Called tells the mock object that a method has been called, and gets an array // of arguments to return. Panics if the call is unexpected (i.e. not preceded by -// appropriate .On .Return() calls) +// appropriate [Mock.On] calls) // If Call.WaitFor is set, blocks until the channel is closed or receives a message. func (m *Mock) Called(arguments ...interface{}) Arguments { // get the calling function's name @@ -493,7 +493,7 @@ func (m *Mock) Called(arguments ...interface{}) Arguments { // MethodCalled tells the mock object that the given method has been called, and gets // an array of arguments to return. Panics if the call is unexpected (i.e. not preceded -// by appropriate .On .Return() calls) +// by appropriate [Mock.On] calls) // If Call.WaitFor is set, blocks until the channel is closed or receives a message. func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Arguments { m.mutex.Lock() @@ -510,8 +510,7 @@ func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Argumen // as the return arguments. This is because: // // a) this is a totally unexpected call to this method, - // b) the arguments are not what was expected, or - // c) the developer has forgotten to add an accompanying On...Return pair. + // b) the arguments are not what was expected closestCall, mismatch := m.findClosestCall(methodName, arguments...) m.mutex.Unlock() @@ -595,7 +594,7 @@ type assertExpectationiser interface { AssertExpectations(TestingT) bool } -// AssertExpectationsForObjects asserts that everything specified with On and Return +// AssertExpectationsForObjects asserts that everything specified with [Mock.On] // of the specified objects was in fact called as expected. // // Calls may have occurred in any order. @@ -604,11 +603,12 @@ func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool { h.Helper() } for _, obj := range testObjects { - if m, ok := obj.(*Mock); ok { - t.Logf("Deprecated mock.AssertExpectationsForObjects(myMock.Mock) use mock.AssertExpectationsForObjects(myMock)") - obj = m + m, ok := obj.(assertExpectationiser) + if !ok { + t.Errorf("Invalid test object type %T. Expected reference to a mock.Mock, eg: 'AssertExpectationsForObjects(t, myMock)' or 'AssertExpectationsForObjects(t, &myMock.Mock)'", obj) + continue + } - m := obj.(assertExpectationiser) if !m.AssertExpectations(t) { t.Logf("Expectations didn't match for Mock: %+v", reflect.TypeOf(m)) return false @@ -617,7 +617,7 @@ func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool { return true } -// AssertExpectations asserts that everything specified with On and Return was +// AssertExpectations asserts that everything specified with [Mock.On] was // in fact called as expected. Calls may have occurred in any order. func (m *Mock) AssertExpectations(t TestingT) bool { if s, ok := t.(interface{ Skipped() bool }); ok && s.Skipped() { @@ -712,8 +712,8 @@ func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...inter return true } -// IsMethodCallable checking that the method can be called -// If the method was called more than `Repeatability` return false +// IsMethodCallable returns true if given methodName and arguments have an +// unsatisfied expected call registered in the Mock. func (m *Mock) IsMethodCallable(t TestingT, methodName string, arguments ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -833,6 +833,10 @@ type IsTypeArgument struct { // For example: // // args.Assert(t, IsType(""), IsType(0)) +// +// Mock cannot match interface types because the contained type will be passed +// to both IsType and Mock.Called, for the zero value of all interfaces this +// will be type. func IsType(t interface{}) *IsTypeArgument { return &IsTypeArgument{t: reflect.TypeOf(t)} } @@ -1012,7 +1016,7 @@ func (args Arguments) Diff(objects []interface{}) (string, int) { actualT := reflect.TypeOf(actual) if actualT != expected.t { differences++ - output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, expected.t.Name(), actualT.Name(), actualFmt) + output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, safeTypeName(expected.t), safeTypeName(actualT), actualFmt) } case *FunctionalOptionsArgument: var name string @@ -1141,6 +1145,15 @@ func (args Arguments) Bool(index int) bool { return s } +// safeTypeName returns the reflect.Type's name without causing a panic. +// If the provided reflect.Type is nil, it returns the placeholder string "" +func safeTypeName(t reflect.Type) string { + if t == nil { + return "" + } + return t.Name() +} + func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { t := reflect.TypeOf(v) k := t.Kind() diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go index c8e3f94a8..ee84cc479 100644 --- a/vendor/github.com/stretchr/testify/require/doc.go +++ b/vendor/github.com/stretchr/testify/require/doc.go @@ -1,4 +1,4 @@ -// Package require implements the same assertions as the `assert` package but +// Package require implements the same assertions as the assert package but // stops test execution when a test fails. // // # Example Usage @@ -21,8 +21,8 @@ // // # Assertions // -// The `require` package have same global functions as in the `assert` package, -// but instead of returning a boolean result they call `t.FailNow()`. +// The require package have same global functions as in the assert package, +// but instead of returning a boolean result they call [testing.T.FailNow]. // A consequence of this is that it must be called from the goroutine running // the test function, not from other goroutines created during the test. // diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go index 2d02f9bce..652871f2e 100644 --- a/vendor/github.com/stretchr/testify/require/require.go +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -180,7 +180,7 @@ func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...i t.FailNow() } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -195,7 +195,7 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte t.FailNow() } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -295,7 +295,7 @@ func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, ar t.FailNow() } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // require.Error(t, err) @@ -333,8 +333,8 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int t.FailNow() } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // require.ErrorContains(t, err, expectedErrorSubString) @@ -348,8 +348,8 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in t.FailNow() } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // require.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") @@ -387,7 +387,7 @@ func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface t.FailNow() } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // require.Errorf(t, err, "error message %s", "formatted") @@ -429,7 +429,7 @@ func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick t // time.Sleep(8*time.Second) // externalValue = true // }() -// require.EventuallyWithT(t, func(c *require.CollectT) { +// require.EventuallyWithT(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // require.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") @@ -457,10 +457,10 @@ func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitF // time.Sleep(8*time.Second) // externalValue = true // }() -// require.EventuallyWithTf(t, func(c *require.CollectT, "error message %s", "formatted") { +// require.EventuallyWithTf(t, func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // require.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -671,8 +671,6 @@ func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...in // body that contains a string. // // require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -687,8 +685,6 @@ func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url s // body that contains a string. // // require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -703,8 +699,6 @@ func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url // body that does not contain a string. // // require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -719,8 +713,6 @@ func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, ur // body that does not contain a string. // // require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -734,8 +726,6 @@ func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, u // HTTPError asserts that a specified handler returns an error status code. // // require.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -749,8 +739,6 @@ func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, // HTTPErrorf asserts that a specified handler returns an error status code. // // require.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -764,8 +752,6 @@ func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, // HTTPRedirect asserts that a specified handler returns a redirect status code. // // require.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -779,8 +765,6 @@ func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url strin // HTTPRedirectf asserts that a specified handler returns a redirect status code. // // require.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -794,8 +778,6 @@ func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url stri // HTTPStatusCode asserts that a specified handler returns a specified status code. // // require.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) -// -// Returns whether the assertion was successful (true) or not (false). func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -809,8 +791,6 @@ func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url str // HTTPStatusCodef asserts that a specified handler returns a specified status code. // // require.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -824,8 +804,6 @@ func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url st // HTTPSuccess asserts that a specified handler returns a success status code. // // require.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) -// -// Returns whether the assertion was successful (true) or not (false). func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -839,8 +817,6 @@ func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string // HTTPSuccessf asserts that a specified handler returns a success status code. // // require.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1385,12 +1361,11 @@ func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) { t.FailNow() } -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if require.NoError(t, err) { -// require.Equal(t, expectedObj, actualObj) -// } +// actualObj, err := SomeFunction() +// require.NoError(t, err) +// require.Equal(t, expectedObj, actualObj) func NoError(t TestingT, err error, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1401,12 +1376,11 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) { t.FailNow() } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if require.NoErrorf(t, err, "error message %s", "formatted") { -// require.Equal(t, expectedObj, actualObj) -// } +// actualObj, err := SomeFunction() +// require.NoErrorf(t, err, "error message %s", "formatted") +// require.Equal(t, expectedObj, actualObj) func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1515,9 +1489,8 @@ func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg str // NotEmpty asserts that the specified object is NOT [Empty]. // -// if require.NotEmpty(t, obj) { -// require.Equal(t, "two", obj[1]) -// } +// require.NotEmpty(t, obj) +// require.Equal(t, "two", obj[1]) func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1530,9 +1503,8 @@ func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { // NotEmptyf asserts that the specified object is NOT [Empty]. // -// if require.NotEmptyf(t, obj, "error message %s", "formatted") { -// require.Equal(t, "two", obj[1]) -// } +// require.NotEmptyf(t, obj, "error message %s", "formatted") +// require.Equal(t, "two", obj[1]) func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2135,7 +2107,19 @@ func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, t.FailNow() } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// require.YAMLEq(t, expected, actual) func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -2146,7 +2130,19 @@ func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{ t.FailNow() } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// require.YAMLEqf(t, expected, actual, "error message %s", "formatted") func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl index 8b3283685..6a975501e 100644 --- a/vendor/github.com/stretchr/testify/require/require.go.tmpl +++ b/vendor/github.com/stretchr/testify/require/require.go.tmpl @@ -1,4 +1,4 @@ -{{ replace .Comment "assert." "require."}} +{{.CommentRequire}} func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { if h, ok := t.(tHelper); ok { h.Helper() } if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return } diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go index e6f7e9446..edac147ef 100644 --- a/vendor/github.com/stretchr/testify/require/require_forward.go +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -147,7 +147,7 @@ func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs Equal(a.t, expected, actual, msgAndArgs...) } -// EqualError asserts that a function returned an error (i.e. not `nil`) +// EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -159,7 +159,7 @@ func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ... EqualError(a.t, theError, errString, msgAndArgs...) } -// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// EqualErrorf asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // // actualObj, err := SomeFunction() @@ -241,7 +241,7 @@ func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string Equalf(a.t, expected, actual, msg, args...) } -// Error asserts that a function returned an error (i.e. not `nil`). +// Error asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Error(err) @@ -270,8 +270,8 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. ErrorAsf(a.t, err, target, msg, args...) } -// ErrorContains asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContains asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContains(err, expectedErrorSubString) @@ -282,8 +282,8 @@ func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs . ErrorContains(a.t, theError, contains, msgAndArgs...) } -// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) -// and that the error contains the specified substring. +// ErrorContainsf asserts that a function returned a non-nil error (i.e. an +// error) and that the error contains the specified substring. // // actualObj, err := SomeFunction() // a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") @@ -312,7 +312,7 @@ func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...inter ErrorIsf(a.t, err, target, msg, args...) } -// Errorf asserts that a function returned an error (i.e. not `nil`). +// Errorf asserts that a function returned a non-nil error (ie. an error). // // actualObj, err := SomeFunction() // a.Errorf(err, "error message %s", "formatted") @@ -373,10 +373,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), w // time.Sleep(8*time.Second) // externalValue = true // }() -// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// a.EventuallyWithTf(func(c *assert.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick // assert.True(c, externalValue, "expected 'externalValue' to be true") -// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +// }, 10*time.Second, 1*time.Second, "error message %s", "formatted") func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -539,8 +539,6 @@ func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args . // body that contains a string. // // a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -552,8 +550,6 @@ func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, u // body that contains a string. // // a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -565,8 +561,6 @@ func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, // body that does not contain a string. // // a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -578,8 +572,6 @@ func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string // body that does not contain a string. // // a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -590,8 +582,6 @@ func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method strin // HTTPError asserts that a specified handler returns an error status code. // // a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -602,8 +592,6 @@ func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url stri // HTTPErrorf asserts that a specified handler returns an error status code. // // a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -614,8 +602,6 @@ func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url str // HTTPRedirect asserts that a specified handler returns a redirect status code. // // a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -626,8 +612,6 @@ func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url s // HTTPRedirectf asserts that a specified handler returns a redirect status code. // // a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -638,8 +622,6 @@ func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url // HTTPStatusCode asserts that a specified handler returns a specified status code. // // a.HTTPStatusCode(myHandler, "GET", "/notImplemented", nil, 501) -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -650,8 +632,6 @@ func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url // HTTPStatusCodef asserts that a specified handler returns a specified status code. // // a.HTTPStatusCodef(myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -662,8 +642,6 @@ func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, ur // HTTPSuccess asserts that a specified handler returns a success status code. // // a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -674,8 +652,6 @@ func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url st // HTTPSuccessf asserts that a specified handler returns a success status code. // // a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") -// -// Returns whether the assertion was successful (true) or not (false). func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1097,12 +1073,11 @@ func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) NoDirExistsf(a.t, path, msg, args...) } -// NoError asserts that a function returned no error (i.e. `nil`). +// NoError asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if a.NoError(err) { -// assert.Equal(t, expectedObj, actualObj) -// } +// actualObj, err := SomeFunction() +// a.NoError(err) +// a.Equal(expectedObj, actualObj) func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1110,12 +1085,11 @@ func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { NoError(a.t, err, msgAndArgs...) } -// NoErrorf asserts that a function returned no error (i.e. `nil`). +// NoErrorf asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if a.NoErrorf(err, "error message %s", "formatted") { -// assert.Equal(t, expectedObj, actualObj) -// } +// actualObj, err := SomeFunction() +// a.NoErrorf(err, "error message %s", "formatted") +// a.Equal(expectedObj, actualObj) func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1203,9 +1177,8 @@ func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg // NotEmpty asserts that the specified object is NOT [Empty]. // -// if a.NotEmpty(obj) { -// assert.Equal(t, "two", obj[1]) -// } +// a.NotEmpty(obj) +// a.Equal("two", obj[1]) func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1215,9 +1188,8 @@ func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { // NotEmptyf asserts that the specified object is NOT [Empty]. // -// if a.NotEmptyf(obj, "error message %s", "formatted") { -// assert.Equal(t, "two", obj[1]) -// } +// a.NotEmptyf(obj, "error message %s", "formatted") +// a.Equal("two", obj[1]) func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1691,7 +1663,19 @@ func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Ti WithinRangef(a.t, actual, start, end, msg, args...) } -// YAMLEq asserts that two YAML strings are equivalent. +// YAMLEq asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEq(expected, actual) func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1699,7 +1683,19 @@ func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interf YAMLEq(a.t, expected, actual, msgAndArgs...) } -// YAMLEqf asserts that two YAML strings are equivalent. +// YAMLEqf asserts that the first documents in the two YAML strings are equivalent. +// +// expected := `--- +// key: value +// --- +// key: this is a second document, it is not evaluated +// ` +// actual := `--- +// key: value +// --- +// key: this is a subsequent document, it is not evaluated +// ` +// a.YAMLEqf(expected, actual, "error message %s", "formatted") func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl index 54124df1d..b3b751de4 100644 --- a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl +++ b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl @@ -1,4 +1,4 @@ -{{.CommentWithoutT "a"}} +{{.CommentRequireWithoutT "a"}} func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) { if h, ok := a.t.(tHelper); ok { h.Helper() } {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go index 6b7ce929e..375adb0a6 100644 --- a/vendor/github.com/stretchr/testify/require/requirements.go +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -12,18 +12,18 @@ type tHelper = interface { // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful // for table driven tests. -type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) +type ComparisonAssertionFunc = func(TestingT, interface{}, interface{}, ...interface{}) // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful // for table driven tests. -type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) +type ValueAssertionFunc = func(TestingT, interface{}, ...interface{}) // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful // for table driven tests. -type BoolAssertionFunc func(TestingT, bool, ...interface{}) +type BoolAssertionFunc = func(TestingT, bool, ...interface{}) // ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful // for table driven tests. -type ErrorAssertionFunc func(TestingT, error, ...interface{}) +type ErrorAssertionFunc = func(TestingT, error, ...interface{}) //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs" diff --git a/vendor/github.com/valyala/fasthttp/fasthttputil/inmemory_listener.go b/vendor/github.com/valyala/fasthttp/fasthttputil/inmemory_listener.go index 3d345bd7a..cfe14d775 100644 --- a/vendor/github.com/valyala/fasthttp/fasthttputil/inmemory_listener.go +++ b/vendor/github.com/valyala/fasthttp/fasthttputil/inmemory_listener.go @@ -7,7 +7,7 @@ import ( ) // ErrInmemoryListenerClosed indicates that the InmemoryListener is already closed. -var ErrInmemoryListenerClosed = errors.New("InmemoryListener is already closed: use of closed network connection") +var ErrInmemoryListenerClosed = errors.New("fasthttputil: inmemorylistener is already closed: use of closed network connection") // InmemoryListener provides in-memory dialer<->net.Listener implementation. // diff --git a/vendor/github.com/valyala/fasthttp/fasthttputil/pipeconns.go b/vendor/github.com/valyala/fasthttp/fasthttputil/pipeconns.go index 5dedfe697..d8b18d005 100644 --- a/vendor/github.com/valyala/fasthttp/fasthttputil/pipeconns.go +++ b/vendor/github.com/valyala/fasthttp/fasthttputil/pipeconns.go @@ -221,12 +221,12 @@ func (c *pipeConn) readNextByteBuffer(mayBlock bool) error { var errWouldBlock = errors.New("would block") // ErrConnectionClosed indicates that the underlying connection is closed. It could mean that the client has disconnected. -var ErrConnectionClosed = errors.New("connection closed") +var ErrConnectionClosed = errors.New("fasthttputil: connection closed") type timeoutError struct{} func (e *timeoutError) Error() string { - return "timeout" + return "fasthttputil: timeout" } // Timeout implements the Timeout method of the net.Error interface. @@ -330,7 +330,7 @@ type byteBuffer struct { } func acquireByteBuffer() *byteBuffer { - return byteBufferPool.Get().(*byteBuffer) + return byteBufferPool.Get().(*byteBuffer) //nolint:forcetypeassert } func releaseByteBuffer(b *byteBuffer) { diff --git a/vendor/github.com/zclconf/go-cty/cty/ctystrings/prefix.go b/vendor/github.com/zclconf/go-cty/cty/ctystrings/prefix.go index bbf045233..3359c9e45 100644 --- a/vendor/github.com/zclconf/go-cty/cty/ctystrings/prefix.go +++ b/vendor/github.com/zclconf/go-cty/cty/ctystrings/prefix.go @@ -4,8 +4,9 @@ import ( "fmt" "unicode/utf8" - "github.com/apparentlymart/go-textseg/v15/textseg" "golang.org/x/text/unicode/norm" + + "github.com/zclconf/go-cty/cty/internal/graphemes" ) // SafeKnownPrefix takes a string intended to represent a known prefix of @@ -65,19 +66,29 @@ func SafeKnownPrefix(prefix string) string { prevBoundary := 0 thisBoundary := 0 for len(remain) > 0 { - advance, _, err := textseg.ScanGraphemeClusters(remain, false) + advance, _, err := graphemes.ScanGraphemeClusters(remain, false) if err != nil { // ScanGraphemeClusters should never return an error because // any sequence of valid UTF-8 encodings is valid input. - panic(fmt.Sprintf("textseg.ScanGraphemeClusters returned error: %s", err)) + panic(fmt.Sprintf("graphemes.ScanGraphemeClusters returned error: %s", err)) } if advance == 0 { // If we have at least one byte remaining but the scanner cannot // advance then that means the remainder might be an incomplete // grapheme cluster and so we need to stop here, discarding the - // rest of the input. However, we do now know that we can safely - // include what we found on the previous iteration of this loop. + // rest of the input. However, there are some characters that we + // just assume will never become a prefix for something that can + // be transformed by normalization and so we'll consider including + // whatever the next grapheme cluster would be if we pretend we're + // at the end of input. + advance, _, err := graphemes.ScanGraphemeClusters(remain, true) + if err != nil { + // ScanGraphemeClusters should never return an error because + // any sequence of valid UTF-8 encodings is valid input. + panic(fmt.Sprintf("graphemes.ScanGraphemeClusters returned error: %s", err)) + } prevBoundary = thisBoundary + thisBoundary += advance break } prevBoundary = thisBoundary diff --git a/vendor/github.com/zclconf/go-cty/cty/function/stdlib/format.go b/vendor/github.com/zclconf/go-cty/cty/function/stdlib/format.go index 9da529b18..ab3f8db77 100644 --- a/vendor/github.com/zclconf/go-cty/cty/function/stdlib/format.go +++ b/vendor/github.com/zclconf/go-cty/cty/function/stdlib/format.go @@ -1,16 +1,16 @@ package stdlib import ( + "bufio" "bytes" "fmt" "math/big" "strings" - "github.com/apparentlymart/go-textseg/v15/textseg" - "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/internal/graphemes" "github.com/zclconf/go-cty/cty/json" ) @@ -466,7 +466,7 @@ func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) erro // ran out of characters before we hit our max width break } - d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true) + d, _, _ := graphemes.ScanGraphemeClusters(strB[pos:], true) pos += d } str = str[:pos] @@ -497,7 +497,7 @@ func formatPadWidth(verb *formatVerb, fmted string) string { } // Safe to ignore errors because ScanGraphemeClusters cannot produce errors - givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters) + givenLen, _ := tokenCount([]byte(fmted), graphemes.ScanGraphemeClusters) wantLen := verb.Width if givenLen >= wantLen { return fmted @@ -532,3 +532,15 @@ func formatStripIndexSegment(rawVerb string) string { return rawVerb[:start] + rawVerb[end+1:] } + +// tokenCount is a utility that uses a bufio.SplitFunc to count the number of +// recognized tokens in the given buffer. +func tokenCount(buf []byte, splitFunc bufio.SplitFunc) (int, error) { + scanner := bufio.NewScanner(bytes.NewReader(buf)) + scanner.Split(splitFunc) + var ret int + for scanner.Scan() { + ret++ + } + return ret, scanner.Err() +} diff --git a/vendor/github.com/zclconf/go-cty/cty/function/stdlib/string.go b/vendor/github.com/zclconf/go-cty/cty/function/stdlib/string.go index f79bf98df..39cc72588 100644 --- a/vendor/github.com/zclconf/go-cty/cty/function/stdlib/string.go +++ b/vendor/github.com/zclconf/go-cty/cty/function/stdlib/string.go @@ -6,11 +6,10 @@ import ( "sort" "strings" - "github.com/apparentlymart/go-textseg/v15/textseg" - "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/gocty" + "github.com/zclconf/go-cty/cty/internal/graphemes" ) var UpperFunc = function.New(&function.Spec{ @@ -67,7 +66,7 @@ var ReverseFunc = function.New(&function.Spec{ inB := []byte(in) for i := 0; i < len(in); { - d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true) + d, _, _ := graphemes.ScanGraphemeClusters(inB[i:], true) cluster := in[i : i+d] pos -= len(cluster) copy(out[pos:], cluster) @@ -117,7 +116,7 @@ func graphemeClusterCount(in string) int { l := 0 inB := []byte(in) for i := 0; i < len(in); { - d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true) + d, _, _ := graphemes.ScanGraphemeClusters(inB[i:], true) l++ i += d } @@ -191,7 +190,7 @@ var SubstrFunc = function.New(&function.Spec{ // First we'll seek forward to our offset if offset > 0 { for i = 0; i < len(sub); { - d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true) + d, _, _ := graphemes.ScanGraphemeClusters(sub[i:], true) i += d pos++ if pos == offset { @@ -215,7 +214,7 @@ var SubstrFunc = function.New(&function.Spec{ // reach the length we want. pos = 0 for i = 0; i < len(sub); { - d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true) + d, _, _ := graphemes.ScanGraphemeClusters(sub[i:], true) i += d pos++ if pos == length { diff --git a/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode15.go b/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode15.go new file mode 100644 index 000000000..fe178fdc6 --- /dev/null +++ b/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode15.go @@ -0,0 +1,12 @@ +//go:build !go1.27 +// +build !go1.27 + +package graphemes + +import ( + v15 "github.com/apparentlymart/go-textseg/v15/textseg" +) + +func ScanGraphemeClusters(data []byte, atEOF bool) (int, []byte, error) { + return v15.ScanGraphemeClusters(data, atEOF) +} diff --git a/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode17.go b/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode17.go new file mode 100644 index 000000000..309001ca4 --- /dev/null +++ b/vendor/github.com/zclconf/go-cty/cty/internal/graphemes/unicode17.go @@ -0,0 +1,11 @@ +//go:build go1.27 + +package graphemes + +import ( + v17 "github.com/apparentlymart/go-textseg/v17/textseg" +) + +func ScanGraphemeClusters(data []byte, atEOF bool) (int, []byte, error) { + return v17.ScanGraphemeClusters(data, atEOF) +} diff --git a/vendor/github.com/zclconf/go-cty/cty/marks.go b/vendor/github.com/zclconf/go-cty/cty/marks.go index 44287f519..e18e977b1 100644 --- a/vendor/github.com/zclconf/go-cty/cty/marks.go +++ b/vendor/github.com/zclconf/go-cty/cty/marks.go @@ -59,6 +59,23 @@ func NewValueMarks(marks ...any) ValueMarks { return ret } +// Insert adds each of the values produced by the given sequence to the +// [ValueMarks] set, if it isn't already present. +// +// All values produced by the sequence must be valid to use as marks, or this +// function will panic. +func (m ValueMarks) Insert(seq iter.Seq[any]) { + for mark := range seq { + m[mark] = struct{}{} + } +} + +// Has returns true if the given value is in the [ValueMarks] set. +func (m ValueMarks) Has(mark any) bool { + _, ok := m[mark] + return ok +} + // Equal returns true if the receiver and the given ValueMarks both contain // the same marks. func (m ValueMarks) Equal(o ValueMarks) bool { diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/common.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/common.go index 3ae082434..733b75dca 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/common.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/common.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "net/http" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go index a7d4b2a81..08ba0778f 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/config.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "context" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/doc.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/doc.go index 1c9aa3ff4..09a2103fe 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/doc.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/doc.go @@ -3,4 +3,4 @@ // Package otelhttp provides an http.Handler and functions that are intended // to be used to add tracing by wrapping existing handlers. -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go index 204588b84..844363a09 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "net/http" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go index d032aa841..aeff41604 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go @@ -1,12 +1,12 @@ -// Code generated by gotmpl. DO NOT MODIFY. -// source: internal/shared/request/body_wrapper.go.tmpl - // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/request/body_wrapper.go.tmpl + // Package request provides types and functionality to handle HTTP request // handling. -package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" +package request import ( "io" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go index 9e00dd2fc..98f7bdbfc 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/gen.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" +package request // Generate request package: //go:generate gotmpl --body=../../../../../../internal/shared/request/body_wrapper.go.tmpl "--data={}" --out=body_wrapper.go diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go index f29f9b7c9..6b523cb74 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go @@ -1,10 +1,10 @@ -// Code generated by gotmpl. DO NOT MODIFY. -// source: internal/shared/request/resp_writer_wrapper.go.tmpl - // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/request/resp_writer_wrapper.go.tmpl + +package request import ( "net/http" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/client.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/client.go index 0241b08e8..c275b5f05 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/client.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/client.go @@ -1,12 +1,12 @@ -// Code generated by gotmpl. DO NOT MODIFY. -// source: internal/shared/semconv/client.go.tmpl - // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/semconv/client.go.tmpl + // Package semconv provides OpenTelemetry semantic convention types and // functionality. -package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" +package semconv import ( "context" @@ -20,8 +20,8 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/semconv/v1.41.0" - "go.opentelemetry.io/otel/semconv/v1.41.0/httpconv" + semconv "go.opentelemetry.io/otel/semconv/v1.43.0" + "go.opentelemetry.io/otel/semconv/v1.43.0/httpconv" ) type HTTPClient struct { @@ -45,7 +45,7 @@ func NewHTTPClient(meter metric.Meter) HTTPClient { return client } -func (n HTTPClient) Status(code int) (codes.Code, string) { +func (HTTPClient) Status(code int) (codes.Code, string) { if code < 100 || code >= 600 { return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code) } @@ -120,9 +120,7 @@ func (n HTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { // Restore any username/password info that was removed. req.URL.User = userinfo } - attrs = append(attrs, semconv.URLFull(u)) - - attrs = append(attrs, semconv.ServerAddress(requestHost)) + attrs = append(attrs, semconv.URLFull(u), semconv.ServerAddress(requestHost)) if eligiblePort > 0 { attrs = append(attrs, semconv.ServerPort(eligiblePort)) } @@ -138,11 +136,13 @@ func (n HTTPClient) RequestTraceAttrs(req *http.Request) []attribute.KeyValue { } // ResponseTraceAttrs returns trace attributes for an HTTP response made by a client. -func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { +func (HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue { /* below attributes are returned: - http.response.status_code - error.type + - network.protocol.name (only when it is not "http") + - network.protocol.version */ var count int if resp.StatusCode > 0 { @@ -153,6 +153,21 @@ func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue count++ } + // The protocol version is only known for certain once the response + // arrives: req.Proto reflects what the caller asked for, but the + // transport may negotiate a different version (for example HTTP/2 via + // ALPN). Report resp.Proto here so it overrides the request-time value + // on the span. As in RequestTraceAttrs, network.protocol.name is omitted + // when it is "http": the spec only requires it when the name differs from + // the protocol implied by http.request.method. + protoName, protoVersion := netProtocol(resp.Proto) + if protoName != "" && protoName != "http" { + count++ + } + if protoVersion != "" { + count++ + } + attrs := make([]attribute.KeyValue, 0, count) if resp.StatusCode > 0 { attrs = append(attrs, semconv.HTTPResponseStatusCode(resp.StatusCode)) @@ -162,10 +177,17 @@ func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue errorType := strconv.Itoa(resp.StatusCode) attrs = append(attrs, semconv.ErrorTypeKey.String(errorType)) } + + if protoName != "" && protoName != "http" { + attrs = append(attrs, semconv.NetworkProtocolName(protoName)) + } + if protoVersion != "" { + attrs = append(attrs, semconv.NetworkProtocolVersion(protoVersion)) + } return attrs } -func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) { +func (HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) { if method == "" { return semconv.HTTPRequestMethodOther, attribute.KeyValue{} } @@ -180,7 +202,7 @@ func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValu return semconv.HTTPRequestMethodOther, orig } -func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { +func (n HTTPClient) MetricAttributes(req *http.Request, resp *http.Response, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue { num := len(additionalAttributes) + 2 var h string if req.URL != nil { @@ -200,7 +222,14 @@ func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, addition num++ } - protoName, protoVersion := netProtocol(req.Proto) + // Prefer the negotiated protocol reported on the response: req.Proto is + // only what the caller asked for, and the transport may have upgraded + // the connection (for example HTTP/2 via ALPN). + proto := req.Proto + if resp != nil && resp.Proto != "" { + proto = resp.Proto + } + protoName, protoVersion := netProtocol(proto) if protoName != "" { num++ } @@ -213,7 +242,8 @@ func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, addition } attributes := slices.Grow(additionalAttributes, num) - attributes = append(attributes, + attributes = append( + attributes, semconv.HTTPRequestMethodKey.String(standardizeHTTPMethod(req.Method)), semconv.ServerAddress(requestHost), n.scheme(req), @@ -249,7 +279,7 @@ func (o MetricOpts) AddOptions() metric.AddOption { } func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts { - attributes := n.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes) + attributes := n.MetricAttributes(ma.Req, ma.Resp, ma.StatusCode, ma.AdditionalAttributes) if ma.StatusCode == 0 && ma.Err != nil { attributes = append(attributes, n.ErrorType(ma.Err)) } @@ -269,12 +299,12 @@ func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts { // type per failure keeps attribute cardinality bounded, as required by the // OTel spec (error.type SHOULD have low cardinality). Callers that need // finer-grained error distinctions should inspect the error themselves. -func (n HTTPClient) ErrorType(err error) attribute.KeyValue { +func (HTTPClient) ErrorType(err error) attribute.KeyValue { t := reflect.TypeOf(err) if t == nil { return semconv.ErrorTypeOther } - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } var value string @@ -307,13 +337,13 @@ func (n HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts Metri } // TraceAttributes returns attributes for httptrace. -func (n HTTPClient) TraceAttributes(host string) []attribute.KeyValue { +func (HTTPClient) TraceAttributes(host string) []attribute.KeyValue { return []attribute.KeyValue{ semconv.ServerAddress(host), } } -func (n HTTPClient) scheme(req *http.Request) attribute.KeyValue { +func (HTTPClient) scheme(req *http.Request) attribute.KeyValue { if req.URL != nil && req.URL.Scheme != "" { return semconv.URLScheme(req.URL.Scheme) } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go index a8a0d58df..fbcd4f6b2 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/gen.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" +package semconv // Generate semconv package: //go:generate gotmpl --body=../../../../../../internal/shared/semconv/bench_test.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\" }" --out=bench_test.go diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go index 8e0430863..35e744e41 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go @@ -1,12 +1,12 @@ -// Code generated by gotmpl. DO NOT MODIFY. -// source: internal/shared/semconv/server.go.tmpl - // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/semconv/server.go.tmpl + // Package semconv provides OpenTelemetry semantic convention types and // functionality. -package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" +package semconv import ( "context" @@ -20,8 +20,8 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/semconv/v1.41.0" - "go.opentelemetry.io/otel/semconv/v1.41.0/httpconv" + semconv "go.opentelemetry.io/otel/semconv/v1.43.0" + "go.opentelemetry.io/otel/semconv/v1.43.0/httpconv" ) type RequestTraceAttrsOpts struct { @@ -67,7 +67,7 @@ func NewHTTPServer(meter metric.Meter) HTTPServer { // Status returns a span status code and message for an HTTP status code // value returned by a server. Status codes in the 400-499 range are not // returned as errors. -func (n HTTPServer) Status(code int) (codes.Code, string) { +func (HTTPServer) Status(code int) (codes.Code, string) { if code < 100 || code >= 600 { return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code) } @@ -168,7 +168,8 @@ func (n HTTPServer) RequestTraceAttrs(server string, req *http.Request, opts Req } attrs := make([]attribute.KeyValue, 0, count) - attrs = append(attrs, + attrs = append( + attrs, semconv.ServerAddress(host), method, scheme, @@ -216,7 +217,7 @@ func (n HTTPServer) RequestTraceAttrs(server string, req *http.Request, opts Req return attrs } -func (s HTTPServer) NetworkTransportAttr(network string) []attribute.KeyValue { +func (HTTPServer) NetworkTransportAttr(network string) []attribute.KeyValue { attr := semconv.NetworkTransportPipe switch network { case "tcp", "tcp4", "tcp6": @@ -239,7 +240,11 @@ type ServerMetricData struct { } type MetricAttributes struct { - Req *http.Request + Req *http.Request + // Resp is the client response, if any. It is only consulted by + // HTTPClient.MetricAttributes to source the negotiated protocol version; + // server-side metric recording ignores it. + Resp *http.Response StatusCode int Route string AdditionalAttributes []attribute.KeyValue @@ -274,7 +279,7 @@ func (n HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) { // It returns "{method} {route}" when the request has a pattern, // or just "{method}" when no route is available. // Non-standard HTTP methods are replaced by "HTTP". -func (n HTTPServer) SpanName(r *http.Request) string { +func (HTTPServer) SpanName(r *http.Request) string { method := strings.ToUpper(r.Method) if _, ok := methodLookup[method]; !ok { method = "HTTP" @@ -287,7 +292,7 @@ func (n HTTPServer) SpanName(r *http.Request) string { return method } -func (n HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) { +func (HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) { if method == "" { return semconv.HTTPRequestMethodOther, attribute.KeyValue{} } @@ -314,7 +319,7 @@ func (n HTTPServer) scheme(https bool) attribute.KeyValue { //nolint:revive // i // // If any of the fields in the ResponseTelemetry are not set the attribute will // be omitted. -func (n HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { +func (HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyValue { var count int if resp.ReadBytes > 0 { @@ -330,17 +335,20 @@ func (n HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyVa attributes := make([]attribute.KeyValue, 0, count) if resp.ReadBytes > 0 { - attributes = append(attributes, + attributes = append( + attributes, semconv.HTTPRequestBodySize(int(resp.ReadBytes)), ) } if resp.WriteBytes > 0 { - attributes = append(attributes, + attributes = append( + attributes, semconv.HTTPResponseBodySize(int(resp.WriteBytes)), ) } if resp.StatusCode > 0 { - attributes = append(attributes, + attributes = append( + attributes, semconv.HTTPResponseStatusCode(resp.StatusCode), ) } @@ -349,7 +357,7 @@ func (n HTTPServer) ResponseTraceAttrs(resp ResponseTelemetry) []attribute.KeyVa } // Route returns the attribute for the route. -func (n HTTPServer) Route(route string) attribute.KeyValue { +func (HTTPServer) Route(route string) attribute.KeyValue { return semconv.HTTPRoute(route) } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go index d6842ed2a..0de0bd83c 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/util.go @@ -1,10 +1,10 @@ -// Code generated by gotmpl. DO NOT MODIFY. -// source: internal/shared/semconv/util.go.tmpl - // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package semconv // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv" +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/semconv/util.go.tmpl + +package semconv import ( "net" @@ -15,7 +15,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - semconvNew "go.opentelemetry.io/otel/semconv/v1.41.0" + semconvNew "go.opentelemetry.io/otel/semconv/v1.43.0" ) // SplitHostPort splits a network address hostport of the form "host", @@ -32,29 +32,29 @@ func SplitHostPort(hostport string) (host string, port int) { addrEnd := strings.LastIndexByte(hostport, ']') if addrEnd < 0 { // Invalid hostport. - return + return host, port } if i := strings.LastIndexByte(hostport[addrEnd:], ':'); i < 0 { host = hostport[1:addrEnd] - return + return host, port } } else { if i := strings.LastIndexByte(hostport, ':'); i < 0 { host = hostport - return + return host, port } } host, pStr, err := net.SplitHostPort(hostport) if err != nil { - return + return host, port } p, err := strconv.ParseUint(pStr, 10, 16) if err != nil { - return + return host, port } - return host, int(p) //nolint:gosec // Byte size checked 16 above. + return host, int(p) } func requiredHTTPPort(https bool, port int) int { //nolint:revive // ignore linter @@ -84,7 +84,7 @@ func httpRoute(pattern string) string { return "" } -func netProtocol(proto string) (name string, version string) { +func netProtocol(proto string) (name, version string) { name, version, _ = strings.Cut(proto, "/") switch name { case "HTTP": diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go index d62ce44b0..8362b8246 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/labeler.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "context" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/start_time_context.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/start_time_context.go index 9476ef01b..9b6c0a889 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/start_time_context.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/start_time_context.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "context" diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go index b15895cee..82501eab3 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/transport.go @@ -1,10 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp import ( "context" + "fmt" "io" "net/http" "net/http/httptrace" @@ -15,7 +16,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" - otelsemconv "go.opentelemetry.io/otel/semconv/v1.41.0" + otelsemconv "go.opentelemetry.io/otel/semconv/v1.43.0" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request" @@ -142,6 +143,9 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header)) res, err := t.rt.RoundTrip(r) + if err == nil { + res, err = ensureResponseBody(t.rt, r, res) + } // Record the metrics on error or no error. statusCode := 0 @@ -160,6 +164,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { }, t.semconv.MetricOptions(semconv.MetricAttributes{ Req: r, + Resp: res, StatusCode: statusCode, Err: err, AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...), @@ -183,6 +188,20 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { return res, nil } +func ensureResponseBody(rt http.RoundTripper, r *http.Request, res *http.Response) (*http.Response, error) { + switch { + case res == nil: + return nil, fmt.Errorf("http: RoundTripper implementation (%T) returned a nil *Response with a nil error", rt) + case res.Body != nil: + return res, nil + case res.ContentLength > 0 && r.Method != http.MethodHead: + return nil, fmt.Errorf("http: RoundTripper implementation (%T) returned a *Response with content length %d but a nil Body", rt, res.ContentLength) + default: + res.Body = http.NoBody + return res, nil + } +} + func (t *Transport) metricAttributesFromRequest(r *http.Request) []attribute.KeyValue { var attributeForRequest []attribute.KeyValue if t.metricAttributesFn != nil { diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go index 2feb88500..b757d6e9d 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +package otelhttp // Version is the current release version of the otelhttp instrumentation. -const Version = "0.69.0" +const Version = "0.70.0" diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.41.0/httpconv/metric.go b/vendor/go.opentelemetry.io/otel/semconv/v1.43.0/httpconv/metric.go similarity index 100% rename from vendor/go.opentelemetry.io/otel/semconv/v1.41.0/httpconv/metric.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.43.0/httpconv/metric.go index 734cf2409..1eb8c9e2a 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.41.0/httpconv/metric.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.43.0/httpconv/metric.go @@ -438,13 +438,6 @@ func (m ClientConnectionDuration) RecordSet(ctx context.Context, val float64, se m.Float64Histogram.Record(ctx, val, *o...) } -// AttrNetworkPeerAddress returns an optional attribute for the -// "network.peer.address" semantic convention. It represents the peer address of -// the network connection - IP address or Unix domain socket name. -func (ClientConnectionDuration) AttrNetworkPeerAddress(val string) attribute.KeyValue { - return attribute.String("network.peer.address", val) -} - // AttrNetworkProtocolVersion returns an optional attribute for the // "network.protocol.version" semantic convention. It represents the actual // version of the protocol used for network communication. @@ -452,6 +445,13 @@ func (ClientConnectionDuration) AttrNetworkProtocolVersion(val string) attribute return attribute.String("network.protocol.version", val) } +// AttrNetworkPeerAddress returns an optional attribute for the +// "network.peer.address" semantic convention. It represents the peer address of +// the network connection - IP address or Unix domain socket name. +func (ClientConnectionDuration) AttrNetworkPeerAddress(val string) attribute.KeyValue { + return attribute.String("network.peer.address", val) +} + // AttrURLScheme returns an optional attribute for the "url.scheme" semantic // convention. It represents the [URI scheme] component identifying the used // protocol. @@ -586,13 +586,6 @@ func (m ClientOpenConnections) AddSet(ctx context.Context, incr int64, set attri m.Int64UpDownCounter.Add(ctx, incr, *o...) } -// AttrNetworkPeerAddress returns an optional attribute for the -// "network.peer.address" semantic convention. It represents the peer address of -// the network connection - IP address or Unix domain socket name. -func (ClientOpenConnections) AttrNetworkPeerAddress(val string) attribute.KeyValue { - return attribute.String("network.peer.address", val) -} - // AttrNetworkProtocolVersion returns an optional attribute for the // "network.protocol.version" semantic convention. It represents the actual // version of the protocol used for network communication. @@ -600,6 +593,13 @@ func (ClientOpenConnections) AttrNetworkProtocolVersion(val string) attribute.Ke return attribute.String("network.protocol.version", val) } +// AttrNetworkPeerAddress returns an optional attribute for the +// "network.peer.address" semantic convention. It represents the peer address of +// the network connection - IP address or Unix domain socket name. +func (ClientOpenConnections) AttrNetworkPeerAddress(val string) attribute.KeyValue { + return attribute.String("network.peer.address", val) +} + // AttrURLScheme returns an optional attribute for the "url.scheme" semantic // convention. It represents the [URI scheme] component identifying the used // protocol. @@ -689,13 +689,6 @@ func (ClientOpenConnectionsObservable) AttrServerPort(val int) attribute.KeyValu return attribute.Int("server.port", val) } -// AttrNetworkPeerAddress returns an optional attribute for the -// "network.peer.address" semantic convention. It represents the peer address of -// the network connection - IP address or Unix domain socket name. -func (ClientOpenConnectionsObservable) AttrNetworkPeerAddress(val string) attribute.KeyValue { - return attribute.String("network.peer.address", val) -} - // AttrNetworkProtocolVersion returns an optional attribute for the // "network.protocol.version" semantic convention. It represents the actual // version of the protocol used for network communication. @@ -703,6 +696,13 @@ func (ClientOpenConnectionsObservable) AttrNetworkProtocolVersion(val string) at return attribute.String("network.protocol.version", val) } +// AttrNetworkPeerAddress returns an optional attribute for the +// "network.peer.address" semantic convention. It represents the peer address of +// the network connection - IP address or Unix domain socket name. +func (ClientOpenConnectionsObservable) AttrNetworkPeerAddress(val string) attribute.KeyValue { + return attribute.String("network.peer.address", val) +} + // AttrURLScheme returns an optional attribute for the "url.scheme" semantic // convention. It represents the [URI scheme] component identifying the used // protocol. diff --git a/vendor/modules.txt b/vendor/modules.txt index 16ad89cbd..54311a096 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -325,13 +325,18 @@ github.com/antlr4-go/antlr/v4 # github.com/apparentlymart/go-textseg/v15 v15.0.0 ## explicit; go 1.16 github.com/apparentlymart/go-textseg/v15/textseg +# github.com/apparentlymart/go-textseg/v17 v17.0.1 +## explicit; go 1.25 +github.com/apparentlymart/go-textseg/v17/textseg +github.com/apparentlymart/go-textseg/v17/textseg/internal/charprops +github.com/apparentlymart/go-textseg/v17/textseg/internal/machine # github.com/ashanbrown/forbidigo/v2 v2.3.0 ## explicit; go 1.24.0 github.com/ashanbrown/forbidigo/v2/forbidigo # github.com/ashanbrown/makezero/v2 v2.1.0 ## explicit; go 1.24.0 github.com/ashanbrown/makezero/v2/makezero -# github.com/aws/aws-sdk-go-v2 v1.42.0 +# github.com/aws/aws-sdk-go-v2 v1.43.4 ## explicit; go 1.24 github.com/aws/aws-sdk-go-v2/aws/arn # github.com/aymanbagabas/go-osc52/v2 v2.0.1 @@ -443,7 +448,7 @@ github.com/clipperhouse/displaywidth # github.com/clipperhouse/uax29/v2 v2.6.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/coder/coder/v2 v2.35.8 +# github.com/coder/coder/v2 v2.37.3 ## explicit; go 1.26.5 github.com/coder/coder/v2/apiversion github.com/coder/coder/v2/buildinfo @@ -468,7 +473,7 @@ github.com/coder/pretty # github.com/coder/serpent v0.15.0 ## explicit; go 1.21.4 github.com/coder/serpent -# github.com/coder/terraform-provider-coder/v2 v2.18.0 +# github.com/coder/terraform-provider-coder/v2 v2.19.0 ## explicit; go 1.26.0 github.com/coder/terraform-provider-coder/v2/provider github.com/coder/terraform-provider-coder/v2/provider/helpers @@ -480,7 +485,7 @@ github.com/coder/websocket/internal/errd github.com/coder/websocket/internal/util github.com/coder/websocket/internal/wsjs github.com/coder/websocket/wsjson -# github.com/coreos/go-oidc/v3 v3.19.0 +# github.com/coreos/go-oidc/v3 v3.20.0 ## explicit; go 1.25.0 github.com/coreos/go-oidc/v3/oidc # github.com/coreos/go-semver v0.3.1 @@ -526,7 +531,7 @@ github.com/dlclark/regexp2/syntax # github.com/dustin/go-humanize v1.0.1 ## explicit; go 1.16 github.com/dustin/go-humanize -# github.com/ebitengine/purego v0.10.1 +# github.com/ebitengine/purego v0.10.2 ## explicit; go 1.18 github.com/ebitengine/purego github.com/ebitengine/purego/internal/cgo @@ -956,7 +961,7 @@ github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function github.com/google/go-cmp/cmp/internal/value -# github.com/google/jsonschema-go v0.4.2 +# github.com/google/jsonschema-go v0.4.3 ## explicit; go 1.23.0 github.com/google/jsonschema-go/jsonschema # github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 @@ -1049,8 +1054,8 @@ github.com/hashicorp/logutils ## explicit; go 1.25.0 github.com/hashicorp/terraform-plugin-go/tfprotov5 github.com/hashicorp/terraform-plugin-go/tftypes -# github.com/hashicorp/terraform-plugin-log v0.10.0 -## explicit; go 1.24.0 +# github.com/hashicorp/terraform-plugin-log v0.10.1-0.20260721164921-2f73dc013b2c +## explicit; go 1.25.0 github.com/hashicorp/terraform-plugin-log/internal/fieldutils github.com/hashicorp/terraform-plugin-log/internal/hclogutils github.com/hashicorp/terraform-plugin-log/internal/logging @@ -1117,7 +1122,7 @@ github.com/kisielk/errcheck/errcheck # github.com/kkHAIKE/contextcheck v1.1.6 ## explicit; go 1.23.0 github.com/kkHAIKE/contextcheck -# github.com/klauspost/compress v1.18.7 +# github.com/klauspost/compress v1.19.1 ## explicit; go 1.24 github.com/klauspost/compress github.com/klauspost/compress/flate @@ -1263,9 +1268,10 @@ github.com/mitchellh/mapstructure # github.com/mitchellh/reflectwalk v1.0.2 ## explicit github.com/mitchellh/reflectwalk -# github.com/modelcontextprotocol/go-sdk v1.4.1 +# github.com/modelcontextprotocol/go-sdk v1.7.0 ## explicit; go 1.25.0 github.com/modelcontextprotocol/go-sdk/auth +github.com/modelcontextprotocol/go-sdk/internal/authutil github.com/modelcontextprotocol/go-sdk/internal/json github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2 github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug @@ -1363,8 +1369,8 @@ github.com/pmezard/go-difflib/difflib # github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 ## explicit; go 1.14 github.com/power-devops/perfstat -# github.com/prometheus/client_golang v1.23.2 -## explicit; go 1.23.0 +# github.com/prometheus/client_golang v1.24.1 +## explicit; go 1.25.0 github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header github.com/prometheus/client_golang/prometheus @@ -1378,11 +1384,11 @@ github.com/prometheus/client_golang/prometheus/testutil/promlint/validations # github.com/prometheus/client_model v0.6.2 ## explicit; go 1.22.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.69.0 +# github.com/prometheus/common v0.70.1 ## explicit; go 1.25.0 github.com/prometheus/common/expfmt github.com/prometheus/common/model -# github.com/prometheus/procfs v0.20.1 +# github.com/prometheus/procfs v0.21.1 ## explicit; go 1.25.0 github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs @@ -1558,13 +1564,15 @@ github.com/ssgreg/nlreturn/v2/pkg/nlreturn # github.com/stbenjam/no-sprintf-host-port v0.3.1 ## explicit; go 1.24.0 github.com/stbenjam/no-sprintf-host-port/pkg/analyzer -# github.com/stretchr/objx v0.5.2 +# github.com/stretchr/objx v0.5.3 ## explicit; go 1.20 github.com/stretchr/objx -# github.com/stretchr/testify v1.11.1 +# github.com/stretchr/testify v1.12.0 ## explicit; go 1.17 github.com/stretchr/testify/assert github.com/stretchr/testify/assert/yaml +github.com/stretchr/testify/internal/difflib +github.com/stretchr/testify/internal/spew github.com/stretchr/testify/mock github.com/stretchr/testify/require # github.com/subosito/gotenv v1.4.2 @@ -1620,7 +1628,7 @@ github.com/uudashr/iface/internal/directive github.com/uudashr/iface/opaque github.com/uudashr/iface/unexported github.com/uudashr/iface/unused -# github.com/valyala/fasthttp v1.72.0 +# github.com/valyala/fasthttp v1.73.0 ## explicit; go 1.25.0 github.com/valyala/fasthttp/fasthttputil # github.com/vmihailenco/msgpack v4.0.4+incompatible @@ -1674,7 +1682,7 @@ github.com/yuin/gopher-lua/pm # github.com/yusufpapurcu/wmi v1.2.4 ## explicit; go 1.16 github.com/yusufpapurcu/wmi -# github.com/zclconf/go-cty v1.18.1 +# github.com/zclconf/go-cty v1.19.0 ## explicit; go 1.25 github.com/zclconf/go-cty/cty github.com/zclconf/go-cty/cty/convert @@ -1683,6 +1691,7 @@ github.com/zclconf/go-cty/cty/ctystrings github.com/zclconf/go-cty/cty/function github.com/zclconf/go-cty/cty/function/stdlib github.com/zclconf/go-cty/cty/gocty +github.com/zclconf/go-cty/cty/internal/graphemes github.com/zclconf/go-cty/cty/json github.com/zclconf/go-cty/cty/set # github.com/zeebo/errs v1.4.0 @@ -1757,7 +1766,7 @@ go.opentelemetry.io/collector/pdata/pprofile ## explicit; go 1.25.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal -# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 +# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 ## explicit; go 1.25.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request @@ -1788,9 +1797,9 @@ go.opentelemetry.io/otel/semconv/v1.27.0 go.opentelemetry.io/otel/semconv/v1.37.0 go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv go.opentelemetry.io/otel/semconv/v1.41.0 -go.opentelemetry.io/otel/semconv/v1.41.0/httpconv go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv go.opentelemetry.io/otel/semconv/v1.43.0 +go.opentelemetry.io/otel/semconv/v1.43.0/httpconv go.opentelemetry.io/otel/semconv/v1.43.0/otelconv go.opentelemetry.io/otel/semconv/v1.6.1 # go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 @@ -2109,7 +2118,7 @@ google.golang.org/genproto/googleapis/api google.golang.org/genproto/googleapis/api/annotations google.golang.org/genproto/googleapis/api/expr/v1alpha1 google.golang.org/genproto/googleapis/api/httpbody -# google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d +# google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea ## explicit; go 1.25.0 google.golang.org/genproto/googleapis/rpc/code google.golang.org/genproto/googleapis/rpc/errdetails