From 9a4cf0e4ee5d9513e0281bf8c044778fe14ced95 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 16 Sep 2026 23:02:58 +0200 Subject: [PATCH 1/2] Give cursor-paginated operations no page parameter basecamp-sdk#914 declares the event feed's two poll lanes, PollEvents and PollInbox, as cursor-style pagination. They page by an opaque position the response body carries. Bumping the SDK to 12cbad5b (the revision #729 needs for eventfeed.NewLive) and re-syncing the vendored model brings that declaration in, and synthesizePageParams then gave both operations a Link-style `page` parameter. For those two operations a page number means nothing. BC3 ignores it, so a caller passing page=2 is served the first page again, with nothing in the answer to say so. describe would still advertise the parameter as if it did something. Three tests caught it: TestFeedLaneParamsMatchTheModel, TestCatalogPaginatedActionsTakePage (61 expected, 63 found) and TestCatalogSnapshot. `page` is now synthesized only for Link-style operations. The decision reads the style the behavior model declares, not any operation's name, because the toolkit catalog records only whether an operation is paginated, not how. Cursor-style operations keep exactly the parameters the model gives them: since and position on the feed lanes. A paginated operation with a style the loader does not recognize now stops the load. Either default would be a silent guess: a page parameter that does nothing, or a listing whose later pages cannot be reached. So a model sync that brings a new style has to be decided about here. No other operation gained or lost `page`. The set of operations declaring it is identical, name for name, to main's (62), and differs from the unfixed re-sync only by PollEvents and PollInbox. The snapshot changes only by marking the two feed lanes "(RO, paginated)", which the model now says they are. The paginated-operation test now counts Link-style operations (61) and says why the model's total is 63. Two new tests, each red against the code they guard: before this change (cursor operations given page): --- FAIL: TestCatalogCursorPaginatedActionsTakeNoPage cursor-style operation "PollEvents" must not take a Link-style page parameter cursor-style operation "PollInbox" must not take a Link-style page parameter undecided style defaulted instead of refused: --- FAIL: TestCatalogRefusesAnUndecidedPaginationStyle an undecided style must stop the load The SDK bump, model re-sync and Nix vendorHash come from the repo's own tooling: scripts/bump-sdk.sh, scripts/sync-mcp-model.sh against a basecamp-sdk checkout at 12cbad5b, and make update-nix-hash, which verified the Docker Nix build. --- go.mod | 2 +- go.sum | 4 +- internal/mcpserver/catalog.go | 75 +++++++++++++- internal/mcpserver/catalog_test.go | 97 +++++++++++++++++-- internal/mcpserver/model/PROVENANCE.json | 4 +- internal/mcpserver/model/behavior-model.json | 8 ++ internal/mcpserver/model/openapi.json | 14 ++- .../mcpserver/testdata/catalog_snapshot.txt | 4 +- internal/version/sdk-provenance.json | 6 +- nix/package.nix | 2 +- 10 files changed, 191 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 51b6b0782..cae612e59 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/bubbles/v2 v2.2.1 charm.land/bubbletea/v2 v2.0.9 charm.land/lipgloss/v2 v2.0.6 - github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c + github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5 github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/basecamp/surfguard/go v0.1.0 diff --git a/go.sum b/go.sum index 208cf859d..1530c1ed2 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c h1:FJQx85IjtQkA2rLAHrO77op3VRA9b5U4S6qtdjSNdIU= -github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916130353-a8046263835c/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q= +github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5 h1:sM2bbD5qQv5wjS/geKRC7tedj1+e3QGYiKLf2R2h1l4= +github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q= github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d h1:jAzDrCCzDpIwhbFT1xVVs0z2xpXoDEkomHfKB2bUUp8= github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d/go.mod h1:iTBTaWvsPEFIcZfkxQHEfISyJ6sZ7036K6bNx0RY3EE= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= diff --git a/internal/mcpserver/catalog.go b/internal/mcpserver/catalog.go index f455571ac..0f591ec52 100644 --- a/internal/mcpserver/catalog.go +++ b/internal/mcpserver/catalog.go @@ -14,6 +14,7 @@ package mcpserver import ( "embed" + "encoding/json" "fmt" "io/fs" "strings" @@ -42,7 +43,13 @@ func loadCatalog() (*catalog.Catalog, error) { if err := rescopeToAccount(cat); err != nil { return nil, err } - synthesizePageParams(cat) + styles, err := paginationStyles(model) + if err != nil { + return nil, err + } + if err := synthesizePageParams(cat, styles); err != nil { + return nil, err + } if err := installComposites(cat); err != nil { return nil, err } @@ -82,7 +89,44 @@ func rescopeToAccount(cat *catalog.Catalog) error { return nil } -// synthesizePageParams gives every paginated operation a page query +// Pagination styles the behavior model declares, as basecamp-sdk spells them. +const ( + // paginationLink pages by number, through a Link rel="next" header the + // dispatcher reads back into next_page. + paginationLink = "link" + // paginationCursor pages by an opaque position the response body + // carries (basecamp-sdk#914): the event feed's poll lanes. + paginationCursor = "cursor" +) + +// paginationStyles reads each paginated operation's declared style from the +// behavior model. The toolkit's catalog keeps only whether an operation is +// paginated, not how, and the page parameter depends on how. +func paginationStyles(model fs.FS) (map[string]string, error) { + raw, err := fs.ReadFile(model, "behavior-model.json") + if err != nil { + return nil, fmt.Errorf("embedded behavior model: %w", err) + } + var doc struct { + Operations map[string]struct { + Pagination *struct { + Style string `json:"style"` + } `json:"pagination"` + } `json:"operations"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + return nil, fmt.Errorf("embedded behavior model: %w", err) + } + styles := make(map[string]string, len(doc.Operations)) + for id, op := range doc.Operations { + if op.Pagination != nil { + styles[id] = op.Pagination.Style + } + } + return styles, nil +} + +// synthesizePageParams gives every Link-style paginated operation a page query // parameter. The SDK export marks a handful of operations paginated without // declaring one (ListWebhooks, ListChatbots, ...); left alone, that makes // every page after the first unreachable over MCP — the dispatcher rejects @@ -90,10 +134,32 @@ func rescopeToAccount(cat *catalog.Catalog) error { // returns could never be passed back. Synthesizing from the paginated trait // covers whatever the model marks, and no-ops once the export declares the // parameter itself. -func synthesizePageParams(cat *catalog.Catalog) { +// +// Only Link style, keyed off the style the model declares rather than any +// operation's name. A cursor-style operation pages by a position its own +// response carries, and a page number means nothing to it: BC3 ignores the +// parameter, so a caller passing page=2 would be served the first page again +// with nothing in the answer to say so. Those operations keep the entry-point +// parameters the model gives them (since, position) and nothing else. +// +// A paginated operation with a style this function does not recognize stops +// the load. Defaulting it either way is a silent guess — a page parameter that +// does nothing, or a listing whose later pages cannot be reached — so a new +// style has to be decided about here before the server will start. +func synthesizePageParams(cat *catalog.Catalog, styles map[string]string) error { for _, d := range cat.Domains { for _, op := range d.Operations { - if !op.Paginated || declaresPage(op) { + if !op.Paginated { + continue + } + switch style := styles[op.ID]; style { + case paginationCursor: + continue + case paginationLink: + default: + return fmt.Errorf("operation %q declares pagination style %q, which the page parameter has not been decided for", op.ID, style) + } + if declaresPage(op) { continue } op.Params = append(op.Params, catalog.Param{ @@ -104,6 +170,7 @@ func synthesizePageParams(cat *catalog.Catalog) { }) } } + return nil } func declaresPage(op *catalog.Operation) bool { diff --git a/internal/mcpserver/catalog_test.go b/internal/mcpserver/catalog_test.go index ee8b89420..b8bec2c99 100644 --- a/internal/mcpserver/catalog_test.go +++ b/internal/mcpserver/catalog_test.go @@ -155,20 +155,51 @@ func TestCatalogIsAccountScoped(t *testing.T) { } } +// modelPaginationStylesForTest reads each operation's declared pagination style +// straight from the vendored behavior model, independently of the loader, so +// the page-parameter tests check the catalog against the model rather than +// against the code under test. +func modelPaginationStylesForTest(t *testing.T) map[string]string { + t.Helper() + raw, err := modelFS.ReadFile("model/behavior-model.json") + require.NoError(t, err) + var model struct { + Operations map[string]struct { + Pagination *struct { + Style string `json:"style"` + } `json:"pagination"` + } `json:"operations"` + } + require.NoError(t, json.Unmarshal(raw, &model)) + styles := map[string]string{} + for id, op := range model.Operations { + if op.Pagination != nil { + styles[id] = op.Pagination.Style + } + } + return styles +} + // TestCatalogPaginatedActionsTakePage pins the synthesized page parameter: -// every operation the behavior model marks paginated must declare a page -// query parameter, whether the OpenAPI export supplies it or loadCatalog -// synthesizes it. Otherwise the next_page value a listing returns could -// never be passed back — the dispatcher rejects undeclared parameters. +// every Link-style paginated operation must declare exactly one page query +// parameter, whether the OpenAPI export supplies it or loadCatalog +// synthesizes it. Otherwise the next_page value a listing returns could never +// be passed back — the dispatcher rejects undeclared parameters. +// +// 61 is the Link-style count. The model marks 63 operations paginated; the +// other two are the event feed's poll lanes (PollEvents, PollInbox), which +// basecamp-sdk#914 declares cursor-style. They page by position, not by page +// number, and are pinned separately below. func TestCatalogPaginatedActionsTakePage(t *testing.T) { cat := loadForTest(t) - paginated := 0 + styles := modelPaginationStylesForTest(t) + link := 0 for _, d := range cat.Domains { for _, op := range d.Operations { - if !op.Paginated { + if !op.Paginated || styles[op.ID] != "link" { continue } - paginated++ + link++ pages := 0 for _, p := range op.Params { if p.In != "query" || p.Name != "page" { @@ -181,7 +212,39 @@ func TestCatalogPaginatedActionsTakePage(t *testing.T) { assert.Equal(t, 1, pages, "operation %q must declare exactly one page query parameter", op.ID) } } - assert.Equal(t, 61, paginated, "paginated operation count") + assert.Equal(t, 61, link, "Link-style paginated operation count") +} + +// TestCatalogCursorPaginatedActionsTakeNoPage pins the other half. A +// cursor-style operation pages by an opaque position its own response +// carries, and a Link-style page number means nothing to it: BC3 ignores it, +// so a caller who passes page=2 is served page one again and told nothing. +// The page parameter is keyed off the style the model declares, so this also +// checks that every operation the model calls paginated has a style the +// loader recognizes — a new style must be decided about, not defaulted. +func TestCatalogCursorPaginatedActionsTakeNoPage(t *testing.T) { + cat := loadForTest(t) + styles := modelPaginationStylesForTest(t) + cursor := 0 + for _, d := range cat.Domains { + for _, op := range d.Operations { + if !op.Paginated { + continue + } + style := styles[op.ID] + assert.Contains(t, []string{"link", "cursor"}, style, + "operation %q declares pagination style %q, which the loader has not decided about", op.ID, style) + if style != "cursor" { + continue + } + cursor++ + for _, p := range op.Params { + assert.False(t, p.In == "query" && p.Name == "page", + "cursor-style operation %q must not take a Link-style page parameter", op.ID) + } + } + } + assert.Equal(t, 2, cursor, "cursor-style operation count (the event feed's two poll lanes)") } // TestCatalogSnapshot renders the full served surface — every tool @@ -256,3 +319,21 @@ func pseudoVersionCommit(version string) (string, bool) { } return match[2], true } + +// A paginated operation whose declared style the loader has not decided about +// stops the load rather than being defaulted. Either default is a silent +// guess: a page parameter the server ignores, or a listing whose later pages +// cannot be reached. The load failing is how a model sync that brings a new +// style gets a decision instead of a guess. +func TestCatalogRefusesAnUndecidedPaginationStyle(t *testing.T) { + cat := &catalog.Catalog{Domains: []*catalog.Domain{{ + Key: "probe", + Operations: []*catalog.Operation{{ID: "ListThings", Paginated: true}}, + }}} + + err := synthesizePageParams(cat, map[string]string{"ListThings": "offset"}) + + require.Error(t, err, "an undecided style must stop the load") + assert.Contains(t, err.Error(), `"offset"`) + assert.Empty(t, cat.Domains[0].Operations[0].Params, "nothing may be synthesized for an undecided style") +} diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 7fb80ac03..8c9c9c53d 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/basecamp-sdk", - "commit": "a8046263835c559be89e3b081a0428c131a4c663", - "ref": "go/v0.18.0-51-ga8046263", + "commit": "12cbad5b3af5a3f9ce126d37b003f84267b0464a", + "ref": "go/v0.18.0-60-g12cbad5b", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh", "patches": "binary-upload operations dropped because the toolkit refuses their non-JSON bodies (EXCLUDED_OPERATIONS) and the stream-ticket mint dropped by policy (POLICY_EXCLUDED_OPERATIONS); no tag patches applied (PATCHED_TAGS is empty — the export tags every operation) — see the sync script" diff --git a/internal/mcpserver/model/behavior-model.json b/internal/mcpserver/model/behavior-model.json index f1b4727c5..8913ae7e7 100644 --- a/internal/mcpserver/model/behavior-model.json +++ b/internal/mcpserver/model/behavior-model.json @@ -2534,6 +2534,10 @@ }, "PollEvents": { "readonly": true, + "pagination": { + "style": "cursor", + "maxPageSize": 100 + }, "retry": { "max": 3, "base_delay_ms": 1000, @@ -2546,6 +2550,10 @@ }, "PollInbox": { "readonly": true, + "pagination": { + "style": "cursor", + "maxPageSize": 100 + }, "retry": { "max": 3, "base_delay_ms": 1000, diff --git a/internal/mcpserver/model/openapi.json b/internal/mcpserver/model/openapi.json index 0499916a3..b885bae54 100644 --- a/internal/mcpserver/model/openapi.json +++ b/internal/mcpserver/model/openapi.json @@ -8377,7 +8377,7 @@ }, "/{accountId}/events.json": { "get": { - "description": "Poll the account event feed for events after a position (oldest first, strict event-id order, up to 100 per page).\n\n**Entry.** With neither `since` nor `position` the feed begins at the present\n(equivalent to `since=now`). `since=` starts after that id and\n`since=0` replays all served history back to the feed's epoch; `since` is a\nsigned 64-bit integer written in decimal, or the literal `now`. `position`\nresumes from a token a previous page issued \u2014 signed, opaque, bound to the\naccount and the filter set.\n\n**Pagination**: the body envelope, not the Link header. `position` is the\ndurable cursor (persist it only after processing the page's events); `next`\nis an absolute continuation URL present only while this walk has more to\nserve. Not wired into the generic Link paginator \u2014 see the section note.\n\n**Errors.** 400 (FeedRequestError) for a malformed position (resume with\n`since=`) or a malformed filter (fix the filters; a position reset will not\nhelp), told apart by its optional `reason` and undifferentiated when `reason`\nis absent. 409 (FeedFilterMismatchError) when the position was minted for a\ndifferent filter set. 410 (FeedPositionGoneError) when the position predates\nthe feed's epoch; its `resume` re-enters at the epoch.", + "description": "Poll the account event feed for events after a position (oldest first, strict event-id order, up to 100 per page).\n\n**Entry.** With neither `since` nor `position` the feed begins at the present\n(equivalent to `since=now`). `since=` starts after that id and\n`since=0` replays all served history back to the feed's epoch; `since` is a\nsigned 64-bit integer written in decimal, or the literal `now`. `position`\nresumes from a token a previous page issued \u2014 signed, opaque, bound to the\naccount and the filter set.\n\n**Pagination**: cursor style \u2014 the body envelope, not the Link header.\n`position` is the durable cursor (persist it only after processing the\npage's events); `next` is an absolute continuation URL present only while\nthis walk has more to serve. One call answers one page; no generator emits\na walk for the cursor style \u2014 see the section note.\n\n**Errors.** 400 (FeedRequestError) for a malformed position (resume with\n`since=`) or a malformed filter (fix the filters; a position reset will not\nhelp), told apart by its optional `reason` and undifferentiated when `reason`\nis absent. 409 (FeedFilterMismatchError) when the position was minted for a\ndifferent filter set. 410 (FeedPositionGoneError) when the position predates\nthe feed's epoch; its `resume` re-enters at the epoch.", "operationId": "PollEvents", "parameters": [ { @@ -8549,6 +8549,11 @@ "tags": [ "EventFeed" ], + "x-basecamp-pagination": { + "style": "cursor", + "key": "events", + "maxPageSize": 100 + }, "x-basecamp-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -9302,7 +9307,7 @@ }, "/{accountId}/inbox.json": { "get": { - "description": "Poll the authenticated agent's inbox for the items that addressed it (oldest first, strict item-id order); people receive 403.\n\nThe inbox is the low-noise \"someone addressed you\" lane as its own resource\nrather than a filter over the account feed. **Agents only for now**: any\nother principal receives 403.\n\nAn item is a first-class delivery with its own identity: one event can\naddress the same principal for several reasons, and each reason is its own\nitem. Deduplicate by `addressing_id`, never by event id. Items are never\nself-addressed, are kept for 30 days, and are dropped at read time when the\nevent is no longer readable.\n\n**Entry**: `since=0` replays the earliest retained items, `since=now` enters\nat the present, `position` resumes. Inbox positions are bound to the\naccount, the principal, and the filter set, and are never interchangeable\nwith feed positions.\n\n**Pagination**: the body envelope (`items`, `position`, `next`), exactly as\nPollEvents \u2014 not the Link header, and not the generic paginator.\n\n**Errors** follow PollEvents (FeedRequestError 400, FeedFilterMismatchError\n409), except that 403 carries no body \u2014 the agent guard's bare\n`head :forbidden` (BareForbiddenError) \u2014 and that the 410 is the inbox's own\nInboxPositionGoneError: the position fell behind the retention window, there\nis no epoch, and `resume` re-enters at `since=0`, the earliest retained item\n\u2014 not the feed's recovery, and not interchangeable with it.", + "description": "Poll the authenticated agent's inbox for the items that addressed it (oldest first, strict item-id order); people receive 403.\n\nThe inbox is the low-noise \"someone addressed you\" lane as its own resource\nrather than a filter over the account feed. **Agents only for now**: any\nother principal receives 403.\n\nAn item is a first-class delivery with its own identity: one event can\naddress the same principal for several reasons, and each reason is its own\nitem. Deduplicate by `addressing_id`, never by event id. Items are never\nself-addressed, are kept for 30 days, and are dropped at read time when the\nevent is no longer readable.\n\n**Entry**: `since=0` replays the earliest retained items, `since=now` enters\nat the present, `position` resumes. Inbox positions are bound to the\naccount, the principal, and the filter set, and are never interchangeable\nwith feed positions.\n\n**Pagination**: cursor style, exactly as PollEvents \u2014 the body envelope\n(`items`, `position`, `next`), not the Link header and not the generic\npaginator. Up to 100 items per page.\n\n**Errors** follow PollEvents (FeedRequestError 400, FeedFilterMismatchError\n409), except that 403 carries no body \u2014 the agent guard's bare\n`head :forbidden` (BareForbiddenError) \u2014 and that the 410 is the inbox's own\nInboxPositionGoneError: the position fell behind the retention window, there\nis no epoch, and `resume` re-enters at `since=0`, the earliest retained item\n\u2014 not the feed's recovery, and not interchangeable with it.", "operationId": "PollInbox", "parameters": [ { @@ -9440,6 +9445,11 @@ "tags": [ "EventFeed" ], + "x-basecamp-pagination": { + "style": "cursor", + "key": "items", + "maxPageSize": 100 + }, "x-basecamp-retry": { "maxAttempts": 3, "baseDelayMs": 1000, diff --git a/internal/mcpserver/testdata/catalog_snapshot.txt b/internal/mcpserver/testdata/catalog_snapshot.txt index f76b22ad3..e5caab5ce 100644 --- a/internal/mcpserver/testdata/catalog_snapshot.txt +++ b/internal/mcpserver/testdata/catalog_snapshot.txt @@ -320,8 +320,8 @@ Gateway tool: call with {"action": "...", "params": {...}}. Call {"action": "describe", "params": {"action": "NAME"}} for an action's full parameter schema. ACTIONS (RO = read-only): -- poll_events (RO): Poll the account event feed for events after a position (oldest first, strict event-id order, up to 100 per page) -- poll_inbox (RO): Poll the authenticated agent's inbox for the items that addressed it (oldest first, strict item-id order); people receive 403 +- poll_events (RO, paginated): Poll the account event feed for events after a position (oldest first, strict event-id order, up to 100 per page) +- poll_inbox (RO, paginated): Poll the authenticated agent's inbox for the items that addressed it (oldest first, strict item-id order); people receive 403 == basecamp_clientside The Clientside: client approvals, correspondences, replies, and client visibility of recordings. diff --git a/internal/version/sdk-provenance.json b/internal/version/sdk-provenance.json index fa44c345d..d1cfed6f9 100644 --- a/internal/version/sdk-provenance.json +++ b/internal/version/sdk-provenance.json @@ -1,9 +1,9 @@ { "sdk": { "module": "github.com/basecamp/basecamp-sdk/go", - "version": "v0.18.1-0.20260916130353-a8046263835c", - "revision": "a8046263835c", - "updated_at": "2026-09-16T13:03:53Z" + "version": "v0.18.1-0.20260916184634-12cbad5b3af5", + "revision": "12cbad5b3af5", + "updated_at": "2026-09-16T18:46:34Z" }, "api": { "repo": "basecamp/bc3", diff --git a/nix/package.nix b/nix/package.nix index 0ec678cc7..e8630e86c 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -8,7 +8,7 @@ buildGoModule.override { go = go_1_26; } (finalAttrs: { src = lib.cleanSource ./..; # To update: set to lib.fakeHash, run `nix build`, use the hash from the error. - vendorHash = "sha256-eTROZTeU4FbooBGNSKiuqH8ohtXhvnDrHTB69pACHL4="; + vendorHash = "sha256-1NhGOQaSd0QP87GJ/umEg1Ff898KJrq9hLI1TqL19rg="; subPackages = [ "cmd/basecamp" ]; From 3f4da78edab64c5151493f450d7ad72f146f6a71 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 16 Sep 2026 23:08:33 +0200 Subject: [PATCH 2/2] Pin the SDK to main at 4523eac, which carries #919 basecamp-sdk#919 (bounded connector capacities) merged after the first bump here, and a release is being cut. No release tag exists yet, and the latest is go/v0.18.0. Waiting for one would hold up #729, so this pins the newest basecamp-sdk main, 4523eac, which contains both #919 and 12cbad5b. #729 and the connector work then inherit #919 through this PR. The vendored model is byte-identical between 12cbad5b and 4523eac. The re-sync moves only provenance, so the pagination change in the previous commit is unaffected. The Nix vendorHash is refreshed and verified through the Docker Nix build. --- go.mod | 2 +- go.sum | 4 ++-- internal/mcpserver/model/PROVENANCE.json | 4 ++-- internal/version/sdk-provenance.json | 6 +++--- nix/package.nix | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index cae612e59..f91d73a23 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/bubbles/v2 v2.2.1 charm.land/bubbletea/v2 v2.0.9 charm.land/lipgloss/v2 v2.0.6 - github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5 + github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916210227-4523eac74cfa github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/basecamp/surfguard/go v0.1.0 diff --git a/go.sum b/go.sum index 1530c1ed2..da24d7918 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5 h1:sM2bbD5qQv5wjS/geKRC7tedj1+e3QGYiKLf2R2h1l4= -github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916184634-12cbad5b3af5/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q= +github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916210227-4523eac74cfa h1:efZJJSiwKn5lBih05oVfLM6S+ZYN6kB0vcH1DQmd8a8= +github.com/basecamp/basecamp-sdk/go v0.18.1-0.20260916210227-4523eac74cfa/go.mod h1:kIBDYwPMMD59PadNGxpH0YTQuI+blFPZ8MelGI0RK5Q= github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d h1:jAzDrCCzDpIwhbFT1xVVs0z2xpXoDEkomHfKB2bUUp8= github.com/basecamp/cli v0.2.2-0.20260828230226-767413fc712d/go.mod h1:iTBTaWvsPEFIcZfkxQHEfISyJ6sZ7036K6bNx0RY3EE= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 8c9c9c53d..25c72af63 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/basecamp-sdk", - "commit": "12cbad5b3af5a3f9ce126d37b003f84267b0464a", - "ref": "go/v0.18.0-60-g12cbad5b", + "commit": "4523eac74cfa6452ec85ea5f8f633baa4dc487a5", + "ref": "go/v0.18.0-63-g4523eac7", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh", "patches": "binary-upload operations dropped because the toolkit refuses their non-JSON bodies (EXCLUDED_OPERATIONS) and the stream-ticket mint dropped by policy (POLICY_EXCLUDED_OPERATIONS); no tag patches applied (PATCHED_TAGS is empty — the export tags every operation) — see the sync script" diff --git a/internal/version/sdk-provenance.json b/internal/version/sdk-provenance.json index d1cfed6f9..3ff436f25 100644 --- a/internal/version/sdk-provenance.json +++ b/internal/version/sdk-provenance.json @@ -1,9 +1,9 @@ { "sdk": { "module": "github.com/basecamp/basecamp-sdk/go", - "version": "v0.18.1-0.20260916184634-12cbad5b3af5", - "revision": "12cbad5b3af5", - "updated_at": "2026-09-16T18:46:34Z" + "version": "v0.18.1-0.20260916210227-4523eac74cfa", + "revision": "4523eac74cfa", + "updated_at": "2026-09-16T21:02:27Z" }, "api": { "repo": "basecamp/bc3", diff --git a/nix/package.nix b/nix/package.nix index e8630e86c..4847f5d16 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -8,7 +8,7 @@ buildGoModule.override { go = go_1_26; } (finalAttrs: { src = lib.cleanSource ./..; # To update: set to lib.fakeHash, run `nix build`, use the hash from the error. - vendorHash = "sha256-1NhGOQaSd0QP87GJ/umEg1Ff898KJrq9hLI1TqL19rg="; + vendorHash = "sha256-uBf5VeKAMnOAPHLIwvaKd/4arzVPhmsa4kCWizKXEn0="; subPackages = [ "cmd/basecamp" ];