From 89e2b62126d5479af408791107c5f81d05fbbf9b Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Tue, 29 Sep 2026 19:32:21 -0700 Subject: [PATCH 1/3] Declare what every operation destroys, sends and reads from strangers Three explicit declarations on every operation, emitted into openapi.json as x-hey-* extensions and into behavior-model.json for consumers such as the MCP toolkit: - @heyDestructive (writes) -> destructive: a path that destroys data, or the caller's access to it, with no way back for the caller. - @heyOpenWorld (writes) -> open_world: the call can deliver mail, publish to HEY World, or send calendar cancellations. @heyDraftWhen -> draft_when names the request-body conditions under which a send saves a draft instead. - @heyUntrustedContent (all) -> untrusted_content: the response can carry text someone other than the caller wrote. EmitEachSelector validators make each declaration mandatory and forbid resending an open-world POST/PUT; scripts/test-behavior-traits breaks the model on purpose to prove they fire, and runs in make check and the Smithy CI job. --- .github/workflows/smithy-verify.yml | 4 + AGENTS.md | 32 +- Makefile | 12 +- behavior-model.json | 685 ++++++++++++++++++++++------ openapi.json | 575 +++++++++++++++++------ scripts/generate-behavior-model | 26 +- scripts/test-behavior-traits | 111 +++++ spec/hey-traits.smithy | 118 +++++ spec/hey.smithy | 299 ++++++++++++ 9 files changed, 1594 insertions(+), 268 deletions(-) create mode 100755 scripts/test-behavior-traits diff --git a/.github/workflows/smithy-verify.yml b/.github/workflows/smithy-verify.yml index b55ab9cb..72118ce8 100644 --- a/.github/workflows/smithy-verify.yml +++ b/.github/workflows/smithy-verify.yml @@ -54,3 +54,7 @@ jobs: - name: Verify behavior model is up to date working-directory: . run: make behavior-model-check + + - name: Verify behavior-trait tripwires fire + working-directory: . + run: make behavior-traits-test diff --git a/AGENTS.md b/AGENTS.md index ae948fc6..ab9f7415 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -338,9 +338,37 @@ Swift has no registry and no publication switch: a `vX.Y.Z` tag is the release, `release-swift.yml` runs the same gate on the tag before `release-github.yml` creates the GitHub release. +## Effect and provenance traits + +Every operation declares what it does beyond its own record, in `spec/hey-traits.smithy`'s +vocabulary, and `behavior-model.json` carries the answers for consumers such as the MCP +toolkit (`github.com/basecamp/mcp`): + +- `@heyDestructive(true|false)` on every write → `destructive`. True when some path destroys + data, or the caller's own access to it, with no way back for the caller: a hard delete, + emptying the trash or spam, erasing a note, `TrashPostings` on a shared thread (the default + JSON path revokes your access). Trashing is not destructive (HEY restores for 30 days), nor + is a toggle with an inverse or an ordinary edit. +- `@heyOpenWorld(true|false)` on every write → `open_world`. True when the call can reach + people outside the mailbox: delivering mail, publishing to HEY World, calendar + invitations or cancellations. An open-world operation may not be `@idempotent` or + `@heyIdempotent(natural: true)` unless it is a DELETE. +- `@heyDraftWhen([...])` on an open-world operation that can save instead of send → + `draft_when`: request-body conditions under which the call delivers nothing. +- `@heyUntrustedContent(true|false)` on every operation → `untrusted_content`. True when the + response can carry text someone other than the caller wrote (subjects, bodies, summaries, + filenames, correspondents' names, others' calendar events, clips). + +A read is never destructive or open-world, and the model says `false` for it without the +trait. EmitEachSelector validators make each declaration mandatory, so an operation added +without one fails `smithy validate`; `make behavior-traits-test` breaks the model on +purpose to prove those validators still fire. Decide from haystack's controller, not the +verb or the name: `HideContact` is a DELETE and reversible, `TrashPostings` is a POST and +can be irreversible, `DeleteCalendarEvent` emails cancellations. + ## Adding an operation -1. Edit `spec/hey.smithy` +1. Edit `spec/hey.smithy`, declaring the operation's effect and provenance traits (above) 2. `make smithy-build` -- regenerates `openapi.json` 3. Refresh the three artifacts `smithy-build` leaves behind: @@ -375,7 +403,7 @@ GitHub release. has to be the shape the model says. 11. `make check` -`make check` resolves to `check-mvp`: `smithy-check`, `behavior-model-check`, +`make check` resolves to `check-mvp`: `smithy-check`, `behavior-model-check`, `behavior-traits-test`, `drift-check-mvp`, `url-routes-check`, `go-check`, `go-check-drift`, `rs-check`, `rs-check-drift`, `ts-check`, `kt-check`, `kt-check-drift`, `swift-check`, `swift-check-drift`, `sync-api-version-check` and `conformance-mvp` (Go, Rust, TypeScript, diff --git a/Makefile b/Makefile index cc4fd23d..594a78cf 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ smithy-clean: # Behavior model #------------------------------------------------------------------------------ -.PHONY: behavior-model behavior-model-check +.PHONY: behavior-model behavior-model-check behavior-traits-test behavior-model: @echo "==> Generating behavior model..." @@ -52,6 +52,12 @@ behavior-model-check: @echo "==> Checking behavior model freshness..." @./scripts/generate-behavior-model --check +# Break the model one declaration at a time and require smithy validate to refuse +# each break: proves the effect/provenance tripwires in spec/hey-traits.smithy fire. +behavior-traits-test: + @echo "==> Testing behavior-trait tripwires..." + @./scripts/test-behavior-traits + #------------------------------------------------------------------------------ # URL routes #------------------------------------------------------------------------------ @@ -427,14 +433,14 @@ audit-check: #------------------------------------------------------------------------------ # Supported gate: Smithy + shipped Go, Rust, TypeScript, Kotlin and Swift SDKs -check-mvp: smithy-check behavior-model-check drift-check-mvp \ +check-mvp: smithy-check behavior-model-check behavior-traits-test drift-check-mvp \ url-routes-check go-check go-check-drift rs-check rs-check-drift \ ts-check kt-check kt-check-drift swift-check swift-check-drift \ sync-api-version-check provenance-check conformance-mvp @echo "==> MVP gate passed" # Phase 3: Full surface, all languages -check-full: smithy-check behavior-model-check drift-check-full \ +check-full: smithy-check behavior-model-check behavior-traits-test drift-check-full \ sync-api-version-check provenance-check \ go-check-drift rs-check-drift kt-check-drift swift-check-drift \ go-check rs-check ts-check rb-check swift-check kt-check \ diff --git a/behavior-model.json b/behavior-model.json index 9da45d77..c645d2c3 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -3,7 +3,9 @@ "generated": true, "operations": { "AddPostingsToBoxGroup": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -13,10 +15,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "AdvancedSearch": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link" }, @@ -29,10 +34,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "BubbleUpPostingsNow": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -42,10 +50,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "BulkUpdateClearances": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -55,10 +66,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "BundleContact": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -68,10 +82,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CancelPostingsBubbleUp": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -81,10 +98,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CompleteCalendarTodo": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -94,10 +114,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CompleteHabit": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -107,10 +130,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateBoxDesignation": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -120,10 +146,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateBoxGroup": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -133,17 +162,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateBulkReply": { + "destructive": false, "idempotent": false, + "open_world": true, "readonly": false, "retry": { "max": 0 - } + }, + "untrusted_content": false }, "CreateCalendarTodo": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -153,10 +188,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateContact": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -166,17 +204,23 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "CreateDirectUpload": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "max": 0 - } + }, + "untrusted_content": false }, "CreateFolderForPostings": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -186,10 +230,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateHabit": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -199,10 +246,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateMessage": { + "destructive": false, + "draft_when": [ + { + "equals": "drafted", + "pointer": "/entry/status" + }, + { + "not_equals": "true", + "pointer": "/entry/scheduled_delivery" + } + ], "idempotent": false, + "open_world": true, "readonly": false, "retry": { "backoff": "exponential", @@ -212,10 +272,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateReply": { + "destructive": false, + "draft_when": [ + { + "equals": "drafted", + "pointer": "/entry/status" + }, + { + "not_equals": "true", + "pointer": "/entry/scheduled_delivery" + } + ], "idempotent": false, + "open_world": true, "readonly": false, "retry": { "backoff": "exponential", @@ -225,10 +298,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateSticky": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -238,10 +314,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateTimeTrack": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -251,17 +330,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "CreateWorkflowStaging": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "max": 0 - } + }, + "untrusted_content": false }, "DeleteBoxDesignation": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -271,10 +356,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteBoxGroup": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -284,10 +372,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteCalendarEvent": { + "destructive": true, "idempotent": true, + "open_world": true, "readonly": false, "retry": { "backoff": "exponential", @@ -297,10 +388,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteCalendarEventOccurrence": { + "destructive": true, "idempotent": true, + "open_world": true, "readonly": false, "retry": { "backoff": "exponential", @@ -310,10 +404,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteCalendarTodo": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -323,10 +420,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteContactNote": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -336,10 +436,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteDraft": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -349,10 +452,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteExtenzion": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -362,10 +468,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteHabit": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -375,10 +484,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteSticky": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -388,10 +500,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "DeleteTimeTrack": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -401,10 +516,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "EmptySpam": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -414,10 +532,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "EmptyTrash": { + "destructive": true, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -427,10 +548,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "FilePostings": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -440,10 +564,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetAdvancedSearchFilters": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -453,10 +580,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetAsidebox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -466,10 +596,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetBox": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -483,10 +616,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetBoxGroup": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -500,10 +636,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetBoxPostingChanges": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "pageParameter": "page", "style": "link", @@ -518,10 +657,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetBubblebox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -531,10 +673,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetBundleUnseenPostings": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -548,10 +693,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetCalendarDay": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -561,10 +709,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetCalendarRecordings": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "window" }, @@ -577,10 +728,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetCalendarWeek": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -590,10 +744,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetCalendarYear": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -603,10 +760,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetClearances": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -620,10 +780,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetCollection": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -637,10 +800,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetContact": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -654,10 +820,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetContactNote": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -667,10 +836,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetEverythingTopics": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -684,10 +856,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetFeedbox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -697,10 +872,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetFolder": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -714,10 +892,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetIdentity": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -727,10 +908,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetImbox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -740,10 +924,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetImboxSeen": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -753,10 +940,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetJournalEntry": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -766,10 +956,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetLaterbox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -779,10 +972,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetMessage": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -792,10 +988,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetMessageEdit": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -805,10 +1004,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetMyClearances": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -822,10 +1024,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetNavigation": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -835,13 +1040,16 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetOngoingTimeTrack": { + "destructive": false, "empty_on": [ 404 ], "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -851,10 +1059,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetSentTopics": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -868,10 +1079,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetSpamTopics": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -885,10 +1099,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetTopic": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -898,10 +1115,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetTopicEntries": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -915,10 +1135,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetTopicPublication": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -928,10 +1151,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetTrailbox": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -941,10 +1167,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetTrashTopics": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -958,10 +1187,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "GetWorkflow": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -971,10 +1203,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "GetWorkflowStage": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -984,10 +1219,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "HideContact": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -997,7 +1235,8 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListAddressableContacts": { "idempotent": false, @@ -1013,7 +1252,9 @@ } }, "ListBoxGroups": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1023,10 +1264,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListBoxes": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -1040,10 +1284,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListCalendarDays": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1053,10 +1300,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListCalendarWeeks": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1066,10 +1316,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListCalendars": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1079,10 +1332,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListClips": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -1096,10 +1352,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListCollections": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1109,10 +1368,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListContacts": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -1126,10 +1388,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListDrafts": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link", "totalCountHeader": "X-Total-Count" @@ -1143,10 +1408,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListJournalEntries": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link" }, @@ -1159,10 +1427,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListSnippets": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1172,10 +1443,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListStickies": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1185,10 +1459,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListTimeTrackCategories": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1198,10 +1475,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ListTimeTracks": { + "destructive": false, "idempotent": false, + "open_world": false, "pagination": { "style": "link" }, @@ -1214,10 +1494,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkBoxSeen": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1227,10 +1510,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkEntrySpam": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1240,10 +1526,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkPostingsSeen": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1253,10 +1542,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkPostingsSpam": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1266,10 +1558,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkPostingsUnseen": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1279,10 +1574,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MarkTopicHam": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1292,10 +1590,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MovePostings": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1305,10 +1606,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MoveSticky": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1318,10 +1622,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MoveTopic": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1331,17 +1638,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "MoveWorkflowStaging": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "max": 0 - } + }, + "untrusted_content": false }, "MutePostings": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1351,10 +1664,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "NewBulkReply": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1364,10 +1680,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "NewEntryForward": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1377,10 +1696,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "NewEntryReply": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1390,10 +1712,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "PuntClearances": { + "destructive": true, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1403,10 +1728,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "RemovePostingsFromBoxGroup": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1416,10 +1744,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "RestoreTopic": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1429,10 +1760,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ResumeHabit": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1442,10 +1776,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "RevealContact": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1455,10 +1792,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "SchedulePostingsBubbleUp": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1468,10 +1808,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "StartTimeTrack": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1481,10 +1824,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "StopHabit": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1494,10 +1840,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "ToggleCalendar": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1507,10 +1856,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "TrashPostings": { + "destructive": true, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1520,10 +1872,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "TrashTopic": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1533,10 +1888,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UnbundleContact": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1546,10 +1904,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UncompleteCalendarTodo": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1559,10 +1920,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UncompleteHabit": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1572,10 +1936,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UnfilePostings": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1585,10 +1952,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UnmutePostings": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1598,10 +1968,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateCalendarTodo": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1611,10 +1984,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateClearance": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1624,10 +2000,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "UpdateCollection": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1637,10 +2016,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateContact": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1650,10 +2032,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "UpdateContactClearance": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1663,10 +2048,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateContactNote": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1676,10 +2064,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateFirstWeekDay": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1689,10 +2080,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateHabit": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1702,10 +2096,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateJournalEntry": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1715,10 +2112,23 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateMessage": { + "destructive": false, + "draft_when": [ + { + "equals": "drafted", + "pointer": "/entry/status" + }, + { + "not_equals": "true", + "pointer": "/entry/scheduled_delivery" + } + ], "idempotent": true, + "open_world": true, "readonly": false, "retry": { "backoff": "exponential", @@ -1728,10 +2138,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateMyClearance": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1741,10 +2154,13 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "UpdateSticky": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1754,10 +2170,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateTimeFormat": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1767,10 +2186,13 @@ 429, 503 ] - } + }, + "untrusted_content": false }, "UpdateTimeTrack": { + "destructive": false, "idempotent": true, + "open_world": false, "readonly": false, "retry": { "backoff": "exponential", @@ -1780,7 +2202,8 @@ 429, 503 ] - } + }, + "untrusted_content": false } }, "version": "1.0.0" diff --git a/openapi.json b/openapi.json index 66c107f3..73867c73 100644 --- a/openapi.json +++ b/openapi.json @@ -96,6 +96,8 @@ "tags": [ "Extenzions" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -104,7 +106,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/advanced_search.json": { @@ -277,7 +280,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/advanced_search_filters.json": { @@ -337,7 +341,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/autocompletable/contacts/addressable.json": { @@ -473,7 +478,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/boxes/{boxId}": { @@ -566,7 +572,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/boxes/{boxId}/designations.json": { @@ -652,6 +659,8 @@ "tags": [ "Boxes" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -660,7 +669,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/boxes/{boxId}/designations/{designationId}": { @@ -735,6 +745,8 @@ "tags": [ "Boxes" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -743,7 +755,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/boxes/{boxId}/groups.json": { @@ -824,7 +837,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Create a Set Aside group out of a selection of postings.\n\nThis endpoint does not split a comma-joined posting_ids string — send an array.", @@ -905,6 +919,8 @@ "tags": [ "Boxes" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -913,7 +929,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/boxes/{boxId}/groups/{groupId}": { @@ -988,6 +1005,8 @@ "tags": [ "Boxes" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -996,7 +1015,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "get": { "description": "Read one Set Aside group with the postings in it.\n\nThe postings are paged like a folder's: newest observed first, 30 to a page, with the\nnext page in the Link header and the total in X-Total-Count.", @@ -1096,7 +1116,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/boxes/{boxId}/observation.json": { @@ -1162,6 +1183,8 @@ "tags": [ "Boxes" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -1170,7 +1193,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/boxes/{boxId}/postings/changes.json": { @@ -1298,7 +1322,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/bubble_up.json": { @@ -1368,7 +1393,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/bulk_replies.json": { @@ -1439,7 +1465,10 @@ }, "tags": [ "Bulk Reply" - ] + ], + "x-hey-destructive": false, + "x-hey-open-world": true, + "x-hey-untrusted-content": false } }, "/bulk_replies/new.json": { @@ -1521,7 +1550,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendar/days.json": { @@ -1593,7 +1623,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendar/days/{day}": { @@ -1673,7 +1704,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendar/days/{day}/habits/{habitId}/completions": { @@ -1754,6 +1786,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -1762,7 +1796,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Complete a habit for a day", @@ -1841,9 +1876,11 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, "x-hey-idempotent": { "natural": true }, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -1852,7 +1889,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/days/{day}/journal_entry": { @@ -1932,7 +1970,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "patch": { "description": "Update the journal entry for a day: writes it, creating it if the day has none, and\nanswers the entry as a recording. Empty content removes the entry instead, and HEY then\nanswers 204 with no body — which is not this shape, so send that through the SDK's own\njournal wrapper rather than here.", @@ -2022,6 +2061,8 @@ "tags": [ "Calendar Journal" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2030,7 +2071,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/events/{eventId}": { @@ -2096,6 +2138,8 @@ "tags": [ "Calendar Events" ], + "x-hey-destructive": true, + "x-hey-open-world": true, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -2104,7 +2148,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/events/{eventId}/occurrences/{occurrence}": { @@ -2190,6 +2235,8 @@ "tags": [ "Calendar Events" ], + "x-hey-destructive": true, + "x-hey-open-world": true, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -2198,7 +2245,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/habits.json": { @@ -2270,6 +2318,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2278,7 +2328,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/habits/{habitId}": { @@ -2344,6 +2395,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2352,7 +2405,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "patch": { "description": "Edit a habit. habitId is the recording's id.", @@ -2443,6 +2497,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2451,7 +2507,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/habits/{habitId}/stop.json": { @@ -2517,6 +2574,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2525,7 +2584,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Pause a habit, so it stops appearing on the calendar", @@ -2589,6 +2649,8 @@ "tags": [ "Calendar Habits" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2597,7 +2659,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/identity/first_week_day": { @@ -2669,6 +2732,8 @@ "tags": [ "Identity" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -2677,7 +2742,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/journal_entries": { @@ -2758,7 +2824,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/ongoing_time_track.json": { @@ -2823,7 +2890,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Start a new time track. Takes no body: haystack's\nCalendar::OngoingTimeTracksController#create ignores request parameters and\nstarts a track with defaults; use UpdateTimeTrack to set notes and category_title,\nwhich also stops the track.", @@ -2893,6 +2961,8 @@ "tags": [ "Calendar Time Tracks" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -2901,7 +2971,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/time_tracks.json": { @@ -2993,7 +3064,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Record a finished stretch of time.\n\nJSON callers send the fields flat; Rails wraps them into calendar_time_track itself.", @@ -3073,6 +3145,8 @@ "tags": [ "Calendar Time Tracks" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -3081,7 +3155,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/time_tracks/categories.json": { @@ -3141,7 +3216,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/time_tracks/{timeTrackId}": { @@ -3207,6 +3283,8 @@ "tags": [ "Calendar Time Tracks" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -3215,7 +3293,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "put": { "description": "Update a time track (stop by setting ends_at to current time).\n\nEvery update completes the track, whether or not ends_at is sent, so this cannot\nbe used to adjust a running track: it stops it.\n\nOnly the fields sent are written, so a partial update leaves the rest of the track\nalone. A starts_at or ends_at the server cannot parse is a 400, not a 422.", @@ -3316,6 +3395,8 @@ "tags": [ "Calendar Time Tracks" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -3324,7 +3405,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/todos.json": { @@ -3396,6 +3478,8 @@ "tags": [ "Calendar Todos" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -3404,7 +3488,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/todos/{todoId}": { @@ -3470,6 +3555,8 @@ "tags": [ "Calendar Todos" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -3478,7 +3565,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "patch": { "description": "Edit a calendar todo. todoId is the recording's id, and every field of the payload\nis optional: haystack's `wrap_parameters` accepts title, focused and starts_at, and\nchanges only what is sent.", @@ -3569,6 +3657,8 @@ "tags": [ "Calendar Todos" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -3577,7 +3667,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/todos/{todoId}/completions": { @@ -3650,6 +3741,8 @@ "tags": [ "Calendar Todos" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -3658,7 +3751,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Complete a calendar todo", @@ -3729,9 +3823,11 @@ "tags": [ "Calendar Todos" ], + "x-hey-destructive": false, "x-hey-idempotent": { "natural": true }, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -3740,7 +3836,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/calendar/weeks.json": { @@ -3822,7 +3919,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendar/weeks/{week}": { @@ -3904,7 +4002,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendar/years/{year}": { @@ -3986,7 +4085,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendars.json": { @@ -4046,7 +4146,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendars/{calendarId}/recordings": { @@ -4154,7 +4255,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/calendars/{calendarId}/toggle": { @@ -4227,6 +4329,8 @@ "tags": [ "Calendars" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -4235,7 +4339,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/clearances.json": { @@ -4317,7 +4422,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/clearances/bulk.json": { @@ -4389,6 +4495,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -4397,7 +4505,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/clearances/punt.json": { @@ -4442,6 +4551,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -4450,7 +4561,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/clearances/{clearanceId}": { @@ -4543,6 +4655,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -4551,7 +4665,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/clips.json": { @@ -4625,7 +4740,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/collections.json": { @@ -4685,7 +4801,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/collections/{collectionId}": { @@ -4778,7 +4895,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true }, "patch": { "description": "Rename a collection or change its summary", @@ -4862,6 +4980,8 @@ "tags": [ "Collections" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -4870,7 +4990,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/contacts.json": { @@ -4952,7 +5073,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true }, "post": { "description": "Add a contact. Answers the contact that was created.", @@ -5032,6 +5154,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5040,7 +5164,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/contacts/{contactId}": { @@ -5106,6 +5231,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5114,7 +5241,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "get": { "description": "Get a contact, with a page of the threads they are on", @@ -5205,7 +5333,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true }, "patch": { "description": "Edit a contact. HEY rewrites the whole contact, so send every field: a name,\naddress or alias left out is cleared. Answers the contact, which is not always\nthe one addressed — promoting an alias makes the alias primary.", @@ -5306,6 +5435,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5314,7 +5445,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/contacts/{contactId}/bundle.json": { @@ -5380,6 +5512,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5388,7 +5522,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Bundle a contact so their mail arrives grouped", @@ -5462,6 +5597,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5470,7 +5607,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/contacts/{contactId}/clearance.json": { @@ -5556,6 +5694,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5564,7 +5704,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/contacts/{contactId}/note.json": { @@ -5630,6 +5771,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5638,7 +5781,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "get": { "description": "Read the private note kept on a contact", @@ -5717,7 +5861,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "patch": { "description": "Write the private note on a contact, replacing whatever was there", @@ -5808,6 +5953,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5816,7 +5963,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/contacts/{contactId}/reveal.json": { @@ -5889,6 +6037,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -5897,7 +6047,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/entries/drafts.json": { @@ -5971,7 +6122,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/entries/drafts/{entryId}": { @@ -6037,6 +6189,8 @@ "tags": [ "Entries" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -6045,7 +6199,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/entries/{entryId}/forwards/new.json": { @@ -6126,7 +6281,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/entries/{entryId}/replies.json": { @@ -6212,6 +6368,18 @@ "tags": [ "Entries" ], + "x-hey-destructive": false, + "x-hey-draft-when": [ + { + "pointer": "/entry/status", + "equals": "drafted" + }, + { + "pointer": "/entry/scheduled_delivery", + "notEquals": "true" + } + ], + "x-hey-open-world": true, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -6220,7 +6388,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/entries/{entryId}/replies/new.json": { @@ -6301,7 +6470,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/entries/{entryId}/status/spam.json": { @@ -6367,6 +6537,8 @@ "tags": [ "Entries" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -6375,7 +6547,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/feedbox.json": { @@ -6445,7 +6618,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/folders/{folderId}": { @@ -6538,7 +6712,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/identity.json": { @@ -6598,7 +6773,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/identity/time_format": { @@ -6660,6 +6836,8 @@ "tags": [ "Identity" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 3, "baseDelayMs": 1000, @@ -6668,7 +6846,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/imbox.json": { @@ -6738,7 +6917,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/imbox/seen.json": { @@ -6808,7 +6988,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/messages.json": { @@ -6873,6 +7054,18 @@ "tags": [ "Messages" ], + "x-hey-destructive": false, + "x-hey-draft-when": [ + { + "pointer": "/entry/status", + "equals": "drafted" + }, + { + "pointer": "/entry/scheduled_delivery", + "notEquals": "true" + } + ], + "x-hey-open-world": true, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -6881,7 +7074,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/messages/{messageId}": { @@ -6962,7 +7156,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true }, "put": { "description": "Revise a message entry (MessagesController#update). With entry.status \"drafted\" the\nentry is saved as a draft (204 + Location, like CreateMessage); without it a draft is\ndelivered through the undo-delay window. A trashed draft is silently restored first.\nThe revision is not a patch: subject, content and any scheduled delivery are rewritten\nfrom this request (an omitted scheduled delivery clears one), while recipients are\nreplaced only when entry.addressed is present.\n\nNot naturally idempotent despite the PUT: without the drafted status this request\n*delivers*, so a transparent retry after an ambiguous first attempt could send the\nmessage again. The client must not retry it.", @@ -7046,9 +7241,21 @@ "tags": [ "Messages" ], + "x-hey-destructive": false, + "x-hey-draft-when": [ + { + "pointer": "/entry/status", + "equals": "drafted" + }, + { + "pointer": "/entry/scheduled_delivery", + "notEquals": "true" + } + ], "x-hey-idempotent": { "natural": false }, + "x-hey-open-world": true, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7057,7 +7264,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/messages/{messageId}/edit.json": { @@ -7138,7 +7346,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/my/clearances.json": { @@ -7212,7 +7421,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/my/clearances/{clearanceId}": { @@ -7305,6 +7515,8 @@ "tags": [ "Contacts" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7313,7 +7525,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/my/navigation.json": { @@ -7373,7 +7586,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/paper_trail.json": { @@ -7443,7 +7657,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/postings/box_groups.json": { @@ -7500,6 +7715,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7508,7 +7725,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Add a selection of postings to a Set Aside group", @@ -7571,6 +7789,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7579,7 +7799,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/bubble_up.json": { @@ -7646,6 +7867,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7654,7 +7877,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Schedule a selection of postings to bubble up.\n\nHEY's scheduler takes a `slot` — today, tomorrow, weekend, next_week, surprise_me\nor custom — and a custom slot also carries the `date` (YYYY-MM-DD) to bubble up on,\nat HEY's morning hour. The today slot lands at HEY's evening hour of the current\nday instead, and both hours are UTC over JSON. An unknown slot, or a custom slot\nwithout a date, is a server error rather than a validation response, so callers\ncheck both first. Responds 201 Created.", @@ -7717,6 +7941,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7725,7 +7951,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/bulk_bubble_up_now.json": { @@ -7790,6 +8017,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7798,7 +8027,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/filings.json": { @@ -7874,6 +8104,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7882,7 +8114,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "File a selection of postings into an existing folder (label)", @@ -7945,6 +8178,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -7953,7 +8188,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/folders.json": { @@ -8028,6 +8264,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8036,7 +8274,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/moves.json": { @@ -8101,6 +8340,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8109,7 +8350,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/mutings.json": { @@ -8176,6 +8418,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8184,7 +8428,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Mute postings (bulk) — stop notifications for their threads.\nMirrors HEY's Postings::MutingsController#create. Responds 201 Created.", @@ -8247,6 +8492,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8255,7 +8502,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/seen.json": { @@ -8310,6 +8558,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8318,7 +8568,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/spam.json": { @@ -8383,6 +8634,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8391,7 +8644,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/trash.json": { @@ -8456,6 +8710,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8464,7 +8720,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/unseen.json": { @@ -8519,6 +8776,8 @@ "tags": [ "Postings" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -8527,7 +8786,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/postings/{postingId}/bundles/unseen.json": { @@ -8620,7 +8880,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/rails/active_storage/direct_uploads.json": { @@ -8691,7 +8952,10 @@ }, "tags": [ "Attachments" - ] + ], + "x-hey-destructive": false, + "x-hey-open-world": false, + "x-hey-untrusted-content": false } }, "/reply_later.json": { @@ -8761,7 +9025,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/set_aside.json": { @@ -8831,7 +9096,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/snippets.json": { @@ -8891,7 +9157,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/stickies.json": { @@ -8964,7 +9231,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "post": { "description": "Write a new sticky", @@ -9034,6 +9302,8 @@ "tags": [ "Stickies" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9042,7 +9312,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/stickies/moves.json": { @@ -9107,6 +9378,8 @@ "tags": [ "Stickies" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9115,7 +9388,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/stickies/{stickyId}": { @@ -9181,6 +9455,8 @@ "tags": [ "Stickies" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9189,7 +9465,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false }, "patch": { "description": "Edit a sticky", @@ -9280,6 +9557,8 @@ "tags": [ "Stickies" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9288,7 +9567,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/everything.json": { @@ -9362,7 +9642,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/sent.json": { @@ -9436,7 +9717,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/spam.json": { @@ -9510,7 +9792,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/spam/all.json": { @@ -9555,6 +9838,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9563,7 +9848,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/trash.json": { @@ -9637,7 +9923,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/trash/all.json": { @@ -9682,6 +9969,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": true, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9690,7 +9979,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}": { @@ -9771,7 +10061,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/{topicId}/entries": { @@ -9864,7 +10155,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/topics/{topicId}/moves.json": { @@ -9940,6 +10232,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -9948,7 +10242,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}/publication.json": { @@ -10039,7 +10334,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}/status/active.json": { @@ -10105,6 +10401,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -10113,7 +10411,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}/status/ham.json": { @@ -10179,6 +10478,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -10187,7 +10488,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}/status/trashed.json": { @@ -10261,6 +10563,8 @@ "tags": [ "Topics" ], + "x-hey-destructive": false, + "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, "baseDelayMs": 1000, @@ -10269,7 +10573,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/topics/{topicId}/workflows/{workflowId}/stagings": { @@ -10373,7 +10678,10 @@ }, "tags": [ "Workflows" - ] + ], + "x-hey-destructive": false, + "x-hey-open-world": false, + "x-hey-untrusted-content": false }, "post": { "description": "Add a topic to a workflow. HEY places it in the first stage.", @@ -10465,7 +10773,10 @@ }, "tags": [ "Workflows" - ] + ], + "x-hey-destructive": false, + "x-hey-open-world": false, + "x-hey-untrusted-content": false } }, "/workflows/{workflowId}": { @@ -10546,7 +10857,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": false } }, "/workflows/{workflowId}/stages/{stageId}": { @@ -10636,7 +10948,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } } }, diff --git a/scripts/generate-behavior-model b/scripts/generate-behavior-model index b7a2be95..e9c20f58 100755 --- a/scripts/generate-behavior-model +++ b/scripts/generate-behavior-model @@ -19,7 +19,22 @@ if [[ ! -f "$AST_FILE" ]]; then exit 1 fi -GENERATED=$(jq -S '{ +# Effect and provenance declarations (spec/hey-traits.smithy). The Smithy validators +# already refuse a model without them; this refuses too, so a validator edited away +# cannot quietly turn a missing declaration into `false` here. +GENERATED=$(jq -S ' +def declared($op; $trait): + .value.traits["hey.traits#" + $trait] as $v + | if $v == null then error("\($op) does not declare @\($trait); see spec/hey-traits.smithy") else $v end; +def write_effect($trait): + if .value.traits["smithy.api#readonly"] != null then false + else declared(.key | split("#")[1]; $trait) end; +def condition: + if (has("equals") | not) == (has("notEquals") | not) then + error("a heyDraftWhen condition on \(.pointer) needs exactly one of equals and notEquals") + elif has("equals") then { pointer, equals } + else { pointer, not_equals: .notEquals } end; +{ "$schema": "https://hey.com/schemas/behavior-model.json", "version": "1.0.0", "generated": true, @@ -52,6 +67,15 @@ GENERATED=$(jq -S '{ else { retry: { max: 0 } } end) + + # Effects and provenance + { + destructive: write_effect("heyDestructive"), + open_world: write_effect("heyOpenWorld"), + untrusted_content: declared(.key | split("#")[1]; "heyUntrustedContent") + } + + (if .value.traits["hey.traits#heyDraftWhen"] != null then + { draft_when: (.value.traits["hey.traits#heyDraftWhen"] | map(condition)) } + else {} end) + # Empty-on (if .value.traits["hey.traits#heyEmptyOn"] != null then { empty_on: .value.traits["hey.traits#heyEmptyOn"].statusCodes } diff --git a/scripts/test-behavior-traits b/scripts/test-behavior-traits new file mode 100755 index 00000000..ca11f703 --- /dev/null +++ b/scripts/test-behavior-traits @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# test-behavior-traits — prove the effect/provenance tripwires fire +# +# spec/hey-traits.smithy makes every operation declare @heyDestructive, @heyOpenWorld +# and @heyUntrustedContent, and forbids resending an open-world operation, through +# EmitEachSelector validators. A validator whose selector stops matching passes +# silently, so this breaks the model on purpose, one declaration at a time, and +# fails unless `smithy validate` refuses each break under the expected id and shape. +# The unbroken model must validate, or every refusal below proves nothing. +# +# Usage: scripts/test-behavior-traits (from anywhere; needs the smithy CLI and jq) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SPEC_DIR="$(dirname "$SCRIPT_DIR")/spec" + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +# A copy of the build config reading the sources from $WORK, with the vendored +# mapper repository kept absolute. Validation needs the same dependencies as a build. +jq --arg lib "file://$SPEC_DIR/lib" \ + '.sources = ["hey.smithy", "hey-traits.smithy"] + | .maven.repositories |= map(if .url == "file://lib" then .url = $lib else . end) + | del(.projections)' \ + "$SPEC_DIR/smithy-build.json" > "$WORK/smithy-build.json" + +failures=0 + +reset_model() { + cp "$SPEC_DIR/hey.smithy" "$SPEC_DIR/hey-traits.smithy" "$WORK/" +} + +validate() { + (cd "$WORK" && smithy validate -c smithy-build.json 2>&1) +} + +# Edit the trait block of one operation: the lines between its doc comment and +# `operation NAME {`. Modes: drop PREFIX (delete the line starting with PREFIX), +# add LINE (append LINE to the block), sub FROM TO (replace FROM with TO). +edit_operation() { + local operation="$1" mode="$2" a="$3" b="${4:-}" + awk -v op="$operation" -v mode="$mode" -v a="$a" -v b="$b" ' + { lines[NR] = $0 } + $0 ~ "^operation " op " [{]" { target = NR } + END { + if (!target) { print "no operation " op > "/dev/stderr"; exit 1 } + start = target + while (start > 1 && lines[start - 1] !~ /^(\/\/\/|}|$)/) start-- + for (i = 1; i <= NR; i++) { + line = lines[i] + if (i >= start && i < target) { + if (mode == "drop" && index(line, a) == 1) continue + if (mode == "sub" && (at = index(line, a))) + line = substr(line, 1, at - 1) b substr(line, at + length(a)) + } + if (i == target && mode == "add") print a + print line + } + }' "$WORK/hey.smithy" > "$WORK/hey.smithy.new" + mv "$WORK/hey.smithy.new" "$WORK/hey.smithy" +} + +expect_refusal() { + local name="$1" id="$2" shape="$3" output + if output=$(validate); then + echo "FAIL $name: the model validated; expected $id on $shape" + failures=$((failures + 1)) + elif printf '%s\n' "$output" | grep -A1 -- "$id" | grep -q "Shape: $shape\$"; then + echo "ok $name: refused with $id on $shape" + else + echo "FAIL $name: refused, but not with $id on $shape:" + printf '%s\n' "$output" | tail -20 + failures=$((failures + 1)) + fi +} + +reset_model +if output=$(validate); then + echo "ok the unbroken model validates" +else + echo "FAIL the unbroken model does not validate:" + printf '%s\n' "$output" | tail -20 + exit 1 +fi + +reset_model +edit_operation CreateBulkReply drop '@heyOpenWorld(' +expect_refusal "a send without @heyOpenWorld" HeyOpenWorldUndeclared hey#CreateBulkReply + +reset_model +edit_operation EmptyTrash drop '@heyDestructive(' +expect_refusal "a purge without @heyDestructive" HeyDestructiveUndeclared hey#EmptyTrash + +reset_model +edit_operation GetMessage drop '@heyUntrustedContent(' +expect_refusal "a read without @heyUntrustedContent" HeyUntrustedContentUndeclared hey#GetMessage + +reset_model +edit_operation CreateReply add '@idempotent' +expect_refusal "a send marked @idempotent" HeyOpenWorldRetried hey#CreateReply + +reset_model +edit_operation UpdateMessage sub 'natural: false' 'natural: true' +expect_refusal "a send opted into resends" HeyOpenWorldRetried hey#UpdateMessage + +if [[ $failures -gt 0 ]]; then + echo "$failures tripwire check(s) failed" + exit 1 +fi +echo "All behavior-trait tripwires fire." diff --git a/spec/hey-traits.smithy b/spec/hey-traits.smithy index 4f1d8186..c6ea0872 100644 --- a/spec/hey-traits.smithy +++ b/spec/hey-traits.smithy @@ -1,5 +1,49 @@ $version: "2" +// The declarations the effect and provenance traits below make mandatory. An +// operation missing one fails `smithy validate` (and so `make check` and CI). +metadata validators = [ + { + name: "EmitEachSelector" + id: "HeyDestructiveUndeclared" + severity: "DANGER" + message: "Every write must declare @heyDestructive(true|false); see hey-traits.smithy." + configuration: { + selector: "operation :not([trait|readonly]) :not([trait|hey.traits#heyDestructive])" + } + } + { + name: "EmitEachSelector" + id: "HeyOpenWorldUndeclared" + severity: "DANGER" + message: "Every write must declare @heyOpenWorld(true|false); see hey-traits.smithy." + configuration: { + selector: "operation :not([trait|readonly]) :not([trait|hey.traits#heyOpenWorld])" + } + } + { + name: "EmitEachSelector" + id: "HeyUntrustedContentUndeclared" + severity: "DANGER" + message: "Every operation must declare @heyUntrustedContent(true|false); see hey-traits.smithy." + configuration: { + selector: "operation :not([trait|hey.traits#heyUntrustedContent])" + } + } + { + name: "EmitEachSelector" + id: "HeyOpenWorldRetried" + severity: "DANGER" + message: "An open-world operation must not be resent: a retry after an ambiguous first attempt can deliver twice." + // DELETE is exempt: the deliveries HEY makes on a delete (calendar + // cancellations) are keyed to the record it destroys, so a resend finds + // nothing and answers 404 rather than notifying again. + configuration: { + selector: "operation [trait|hey.traits#heyOpenWorld = true] :not([trait|http|method = DELETE]) :is([trait|idempotent], [trait|hey.traits#heyIdempotent|natural = true])" + } + } +] + namespace hey.traits use smithy.api#documentation @@ -141,3 +185,77 @@ structure heyEmptyOn { list HeyEmptyOnStatusCodes { member: Integer } + +// ============================================================================ +// Effect and provenance traits — what an operation does beyond its own record +// ============================================================================ +// +// Three declarations every operation makes explicitly, so that nothing downstream +// (the MCP toolkit, an agent's policy layer) has to guess from a verb or a name. +// behavior-model.json carries them as `destructive`, `open_world`, +// `untrusted_content` and `draft_when`. The validators at the end of this section +// make each declaration mandatory: an operation added without one fails +// `smithy validate`, so a new send or delete cannot arrive unclassified. + +/// Whether a write can destroy data, or the caller's own access to it, with no way +/// back for the caller: a hard delete, emptying the trash or spam, erasing a note. +/// Moving something to the trash is not destructive — HEY restores trashed and spam +/// threads for 30 days — and neither is a toggle with an inverse operation +/// (hide/reveal, mute/unmute, complete/uncomplete). An ordinary edit is not +/// destructive either. `true` when any documented path of the operation destroys, +/// even if another path does not: MCP's destructiveHint means "may". +/// +/// Required on every operation that is not @readonly. A read is never destructive, +/// and the behavior model says so without the trait. +@trait(selector: "operation :not([trait|readonly])") +@specificationExtension(as: "x-hey-destructive") +boolean heyDestructive + +/// Whether a write can reach people outside the caller's own mailbox and calendar: +/// delivering email (a message, a reply, a bulk reply), publishing to HEY World, or +/// sending calendar invitations or cancellations to attendees. This is MCP's +/// openWorldHint, and the half of the "lethal trifecta" (private data + untrusted +/// content + a way out) that is a way out: a caller holding sender-authored content +/// should not be able to reach one of these without a policy decision. +/// +/// Required on every operation that is not @readonly. A read never delivers. +@trait(selector: "operation :not([trait|readonly])") +@specificationExtension(as: "x-hey-open-world") +boolean heyOpenWorld + +/// Conditions on the request body under which an open-world operation saves a +/// draft instead of delivering. When every condition holds, the call delivers +/// nothing; when any fails, treat the call as delivering. The conditions are +/// sufficient, not necessary: HEY may also hold a call they do not describe, and a +/// consumer that gates on them errs toward asking. +@trait(selector: "operation [trait|hey.traits#heyOpenWorld = true]") +@specificationExtension(as: "x-hey-draft-when") +list heyDraftWhen { + member: HeyBodyCondition +} + +/// One condition on a JSON request body. Exactly one of `equals` and `notEquals`. +structure HeyBodyCondition { + /// RFC 6901 JSON Pointer into the request body, e.g. "/entry/status". + @required + pointer: String + + /// Holds when the value at `pointer` is present and is this string. + equals: String + + /// Holds when the value at `pointer` is absent or is anything but this string. + notEquals: String +} + +/// Whether the response can carry text written by someone other than the caller: +/// email subjects, bodies, summaries and attachment filenames; the display names and +/// addresses correspondents declared for themselves; calendar events organized by +/// others or pulled from subscribed feeds; clips cut from other people's email. +/// Such text is untrusted input — instructions inside it are the sender's, not the +/// user's — and a consumer should mark it so before a model reads it. +/// +/// `false` for responses that carry only the caller's own records (habits, journal, +/// stickies, snippets, settings) or no body at all. Required on every operation. +@trait(selector: "operation") +@specificationExtension(as: "x-hey-untrusted-content") +boolean heyUntrustedContent diff --git a/spec/hey.smithy b/spec/hey.smithy index 4a396138..addf906a 100644 --- a/spec/hey.smithy +++ b/spec/hey.smithy @@ -52,6 +52,10 @@ use hey.traits#heySensitive use hey.traits#heyNullable use hey.traits#heyPolymorphic use hey.traits#heyEmptyOn +use hey.traits#heyDestructive +use hey.traits#heyOpenWorld +use hey.traits#heyDraftWhen +use hey.traits#heyUntrustedContent /// ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default) @timestampFormat("date-time") @@ -1188,6 +1192,7 @@ structure AdvancedSearchFilters { @http(method: "GET", uri: "/identity.json") @tags(["Identity"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetIdentity { output: GetIdentityOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -1222,6 +1227,7 @@ structure Identity { @http(method: "GET", uri: "/my/navigation.json") @tags(["Identity"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetNavigation { output: GetNavigationOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -1239,6 +1245,9 @@ structure GetNavigationOutput { @http(method: "PUT", uri: "/calendar/identity/first_week_day") @tags(["Identity"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateFirstWeekDay { input: UpdateFirstWeekDayInput output: UpdateFirstWeekDayOutput @@ -1281,6 +1290,9 @@ structure FirstWeekDayPreference { @http(method: "PUT", uri: "/identity/time_format") @tags(["Identity"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateTimeFormat { input: UpdateTimeFormatInput output: UpdateTimeFormatOutput @@ -1319,6 +1331,7 @@ structure TimeFormatPreference { @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(false) operation ListBoxes { output: ListBoxesOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -1335,6 +1348,7 @@ structure ListBoxesOutput { @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetBox { input: GetBoxInput output: GetBoxOutput @@ -1360,6 +1374,7 @@ structure GetBoxOutput { @http(method: "GET", uri: "/imbox.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetImbox { input: GetNamedBoxInput output: GetNamedBoxOutput @@ -1381,6 +1396,7 @@ structure GetNamedBoxOutput { @http(method: "GET", uri: "/imbox/seen.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetImboxSeen { input: GetNamedBoxInput output: GetImboxSeenOutput @@ -1397,6 +1413,7 @@ structure GetImboxSeenOutput { @http(method: "GET", uri: "/feedbox.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetFeedbox { input: GetNamedBoxInput output: GetFeedboxOutput @@ -1413,6 +1430,7 @@ structure GetFeedboxOutput { @http(method: "GET", uri: "/paper_trail.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetTrailbox { input: GetNamedBoxInput output: GetTrailboxOutput @@ -1429,6 +1447,7 @@ structure GetTrailboxOutput { @http(method: "GET", uri: "/set_aside.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetAsidebox { input: GetNamedBoxInput output: GetAsideboxOutput @@ -1445,6 +1464,7 @@ structure GetAsideboxOutput { @http(method: "GET", uri: "/reply_later.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetLaterbox { input: GetNamedBoxInput output: GetLaterboxOutput @@ -1461,6 +1481,7 @@ structure GetLaterboxOutput { @http(method: "GET", uri: "/bubble_up.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetBubblebox { input: GetNamedBoxInput output: GetBubbleboxOutput @@ -1481,6 +1502,7 @@ structure GetBubbleboxOutput { @http(method: "GET", uri: "/topics/{topicId}") @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetTopic { input: GetTopicInput output: GetTopicOutput @@ -1504,6 +1526,7 @@ structure GetTopicOutput { @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetTopicEntries { input: GetTopicEntriesInput output: GetTopicEntriesOutput @@ -1530,6 +1553,7 @@ structure GetTopicEntriesOutput { @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetSentTopics { input: PagedInput output: GetSentTopicsOutput @@ -1552,6 +1576,7 @@ structure GetSentTopicsOutput { @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetSpamTopics { input: PagedInput output: GetSpamTopicsOutput @@ -1569,6 +1594,7 @@ structure GetSpamTopicsOutput { @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetTrashTopics { input: PagedInput output: GetTrashTopicsOutput @@ -1586,6 +1612,7 @@ structure GetTrashTopicsOutput { @tags(["Topics"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetEverythingTopics { input: PagedInput output: GetEverythingTopicsOutput @@ -1606,6 +1633,7 @@ structure GetEverythingTopicsOutput { @http(method: "GET", uri: "/messages/{messageId}") @tags(["Messages"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetMessage { input: GetMessageInput output: GetMessageOutput @@ -1631,6 +1659,13 @@ structure GetMessageOutput { @http(method: "POST", uri: "/messages.json") @tags(["Messages"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(true) +@heyDraftWhen([ + { pointer: "/entry/status", equals: "drafted" } + { pointer: "/entry/scheduled_delivery", notEquals: "true" } +]) +@heyUntrustedContent(false) operation CreateMessage { input: CreateMessageInput errors: [UnauthorizedError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -1675,6 +1710,13 @@ structure MessagePayload { @tags(["Messages"]) @heyIdempotent(natural: false) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(true) +@heyDraftWhen([ + { pointer: "/entry/status", equals: "drafted" } + { pointer: "/entry/scheduled_delivery", notEquals: "true" } +]) +@heyUntrustedContent(false) operation UpdateMessage { input: UpdateMessageInput errors: [UnauthorizedError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -1696,6 +1738,7 @@ structure UpdateMessageInput { @http(method: "GET", uri: "/messages/{messageId}/edit.json") @tags(["Messages"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetMessageEdit { input: GetMessageEditInput output: GetMessageEditOutput @@ -1742,6 +1785,9 @@ structure MessageEditState { /// The returned URL is self-authenticating and accepts the raw file bytes via PUT. @http(method: "POST", uri: "/rails/active_storage/direct_uploads.json") @tags(["Attachments"]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateDirectUpload { input: CreateDirectUploadInput output: CreateDirectUploadOutput @@ -1846,6 +1892,7 @@ list EmailAddressList { @tags(["Entries"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation ListDrafts { input: PagedInput output: ListDraftsOutput @@ -1863,6 +1910,9 @@ structure ListDraftsOutput { @http(method: "DELETE", uri: "/entries/drafts/{entryId}") @tags(["Entries"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteDraft { input: DeleteDraftInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -1878,6 +1928,13 @@ structure DeleteDraftInput { @http(method: "POST", uri: "/entries/{entryId}/replies.json") @tags(["Entries"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(true) +@heyDraftWhen([ + { pointer: "/entry/status", equals: "drafted" } + { pointer: "/entry/scheduled_delivery", notEquals: "true" } +]) +@heyUntrustedContent(false) operation CreateReply { input: CreateReplyInput errors: [UnauthorizedError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -1890,6 +1947,7 @@ operation CreateReply { @http(method: "GET", uri: "/entries/{entryId}/replies/new.json") @tags(["Entries"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation NewEntryReply { input: EntryStatusInput output: NewEntryReplyOutput @@ -1947,6 +2005,7 @@ structure ReplyMessagePayload { @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation ListContacts { input: ListContactsInput output: ListContactsOutput @@ -1972,6 +2031,7 @@ structure ListContactsOutput { @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetContact { input: GetContactInput output: GetContactOutput @@ -2066,6 +2126,9 @@ list AddressableContactRowList { @http(method: "POST", uri: "/contacts.json", code: 201) @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation CreateContact { input: CreateContactInput output: ContactWriteOutput @@ -2085,6 +2148,9 @@ structure CreateContactInput { @http(method: "PATCH", uri: "/contacts/{contactId}") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation UpdateContact { input: UpdateContactInput output: ContactWriteOutput @@ -2106,6 +2172,9 @@ structure UpdateContactInput { @http(method: "DELETE", uri: "/contacts/{contactId}") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation HideContact { input: ContactActionInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2115,6 +2184,9 @@ operation HideContact { @http(method: "POST", uri: "/contacts/{contactId}/reveal.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation RevealContact { input: ContactActionInput output: ContactWriteOutput @@ -2160,6 +2232,7 @@ structure ContactWriteOutput { @http(method: "GET", uri: "/contacts/{contactId}/note.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetContactNote { input: ContactActionInput output: GetContactNoteOutput @@ -2189,6 +2262,9 @@ structure ContactNote { @http(method: "PATCH", uri: "/contacts/{contactId}/note.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateContactNote { input: UpdateContactNoteInput output: GetContactNoteOutput @@ -2221,6 +2297,9 @@ structure ContactNotePayload { @http(method: "DELETE", uri: "/contacts/{contactId}/note.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteContactNote { input: ContactActionInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2236,6 +2315,9 @@ operation DeleteContactNote { @http(method: "DELETE", uri: "/accounts/{accountId}/domains/extenzions/{extenzionId}") @tags(["Extenzions"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteExtenzion { input: DeleteExtenzionInput errors: [UnauthorizedError, ForbiddenError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2260,6 +2342,7 @@ structure DeleteExtenzionInput { @http(method: "GET", uri: "/calendars.json") @tags(["Calendars"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation ListCalendars { output: ListCalendarsOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -2276,6 +2359,7 @@ structure ListCalendarsOutput { @tags(["Calendars"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "window") +@heyUntrustedContent(true) operation GetCalendarRecordings { input: GetCalendarRecordingsInput output: GetCalendarRecordingsOutput @@ -2308,6 +2392,9 @@ structure GetCalendarRecordingsOutput { @http(method: "POST", uri: "/calendars/{calendarId}/toggle") @tags(["Calendars"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation ToggleCalendar { input: ToggleCalendarInput output: ToggleCalendarOutput @@ -2334,6 +2421,9 @@ structure ToggleCalendarOutput { @http(method: "DELETE", uri: "/calendar/events/{eventId}") @tags(["Calendar Events"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(true) +@heyUntrustedContent(false) operation DeleteCalendarEvent { input: DeleteCalendarEventInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2353,6 +2443,9 @@ structure DeleteCalendarEventInput { @http(method: "DELETE", uri: "/calendar/events/{eventId}/occurrences/{occurrence}") @tags(["Calendar Events"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(true) +@heyUntrustedContent(false) operation DeleteCalendarEventOccurrence { input: DeleteCalendarEventOccurrenceInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2385,6 +2478,7 @@ structure DeleteCalendarEventOccurrenceInput { @http(method: "GET", uri: "/calendar/days/{day}") @tags(["Calendar Periods"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetCalendarDay { input: GetCalendarDayInput output: GetCalendarDayOutput @@ -2408,6 +2502,7 @@ structure GetCalendarDayOutput { @http(method: "GET", uri: "/calendar/days.json") @tags(["Calendar Periods"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation ListCalendarDays { input: ListCalendarDaysInput output: ListCalendarDaysOutput @@ -2430,6 +2525,7 @@ structure ListCalendarDaysOutput { @http(method: "GET", uri: "/calendar/weeks/{week}") @tags(["Calendar Periods"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetCalendarWeek { input: GetCalendarWeekInput output: GetCalendarWeekOutput @@ -2453,6 +2549,7 @@ structure GetCalendarWeekOutput { @http(method: "GET", uri: "/calendar/weeks.json") @tags(["Calendar Periods"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation ListCalendarWeeks { input: ListCalendarWeeksInput output: ListCalendarWeeksOutput @@ -2479,6 +2576,7 @@ structure ListCalendarWeeksOutput { @http(method: "GET", uri: "/calendar/years/{year}") @tags(["Calendar Periods"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetCalendarYear { input: GetCalendarYearInput output: GetCalendarYearOutput @@ -2505,6 +2603,9 @@ structure GetCalendarYearOutput { @http(method: "POST", uri: "/calendar/todos.json") @tags(["Calendar Todos"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateCalendarTodo { input: CreateCalendarTodoInput output: CreateCalendarTodoOutput @@ -2543,6 +2644,9 @@ structure CreateCalendarTodoOutput { @http(method: "PATCH", uri: "/calendar/todos/{todoId}") @tags(["Calendar Todos"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateCalendarTodo { input: UpdateCalendarTodoInput output: UpdateCalendarTodoOutput @@ -2586,6 +2690,9 @@ structure UpdateCalendarTodoOutput { @tags(["Calendar Todos"]) @heyIdempotent(natural: true) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CompleteCalendarTodo { input: CalendarTodoCompletionInput output: CalendarTodoCompletionOutput @@ -2608,6 +2715,9 @@ structure CalendarTodoCompletionOutput { @http(method: "DELETE", uri: "/calendar/todos/{todoId}/completions") @tags(["Calendar Todos"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UncompleteCalendarTodo { input: CalendarTodoCompletionInput output: CalendarTodoCompletionOutput @@ -2619,6 +2729,9 @@ operation UncompleteCalendarTodo { @http(method: "DELETE", uri: "/calendar/todos/{todoId}") @tags(["Calendar Todos"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteCalendarTodo { input: DeleteCalendarTodoInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -2639,6 +2752,9 @@ structure DeleteCalendarTodoInput { @tags(["Calendar Habits"]) @heyIdempotent(natural: true) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CompleteHabit { input: HabitCompletionInput output: HabitCompletionOutput @@ -2665,6 +2781,9 @@ structure HabitCompletionOutput { @http(method: "DELETE", uri: "/calendar/days/{day}/habits/{habitId}/completions") @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UncompleteHabit { input: HabitCompletionInput output: HabitCompletionOutput @@ -2691,6 +2810,7 @@ operation UncompleteHabit { @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link") +@heyUntrustedContent(false) operation ListTimeTracks { input: ListTimeTracksInput output: ListTimeTracksOutput @@ -2723,6 +2843,7 @@ structure TrackedTime { @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyEmptyOn(statusCodes: [404]) +@heyUntrustedContent(false) operation GetOngoingTimeTrack { output: GetOngoingTimeTrackOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -2740,6 +2861,9 @@ structure GetOngoingTimeTrackOutput { @http(method: "POST", uri: "/calendar/ongoing_time_track.json") @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation StartTimeTrack { output: StartTimeTrackOutput errors: [UnauthorizedError, ConflictError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -2765,6 +2889,9 @@ structure StartTimeTrackOutput { @http(method: "PUT", uri: "/calendar/time_tracks/{timeTrackId}") @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateTimeTrack { input: UpdateTimeTrackInput output: UpdateTimeTrackOutput @@ -2824,6 +2951,7 @@ structure UpdateTimeTrackOutput { @tags(["Calendar Journal"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link") +@heyUntrustedContent(false) operation ListJournalEntries { input: ListJournalEntriesInput output: ListJournalEntriesOutput @@ -2848,6 +2976,7 @@ structure ListJournalEntriesOutput { @http(method: "GET", uri: "/calendar/days/{day}/journal_entry") @tags(["Calendar Journal"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetJournalEntry { input: JournalEntryInput output: GetJournalEntryOutput @@ -2872,6 +3001,9 @@ structure GetJournalEntryOutput { @http(method: "PATCH", uri: "/calendar/days/{day}/journal_entry") @tags(["Calendar Journal"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateJournalEntry { input: UpdateJournalEntryInput output: UpdateJournalEntryOutput @@ -2919,6 +3051,7 @@ structure JournalEntryPayload { @tags(["Search"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link") +@heyUntrustedContent(true) operation AdvancedSearch { input: AdvancedSearchInput output: AdvancedSearchOutput @@ -2997,6 +3130,7 @@ list SearchMatchList { @http(method: "GET", uri: "/advanced_search_filters.json") @tags(["Search"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetAdvancedSearchFilters { output: GetAdvancedSearchFiltersOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -3015,6 +3149,9 @@ structure GetAdvancedSearchFiltersOutput { @http(method: "POST", uri: "/postings/seen.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkPostingsSeen { input: MarkPostingsInput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -3024,6 +3161,9 @@ operation MarkPostingsSeen { @http(method: "POST", uri: "/postings/unseen.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkPostingsUnseen { input: MarkPostingsInput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -3047,6 +3187,9 @@ structure MarkPostingsRequestContent { @http(method: "POST", uri: "/postings/moves.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MovePostings { input: MovePostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3073,6 +3216,9 @@ structure MovePostingsRequestContent { @http(method: "POST", uri: "/postings/trash.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation TrashPostings { input: TrashPostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3098,6 +3244,9 @@ structure TrashPostingsRequestContent { @http(method: "POST", uri: "/postings/mutings.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MutePostings { input: MarkPostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3110,6 +3259,9 @@ operation MutePostings { @http(method: "DELETE", uri: "/postings/mutings.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UnmutePostings { input: UnmutePostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3213,6 +3365,7 @@ string PostingIdsParam @tags(["Postings"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetBundleUnseenPostings { input: GetBundleUnseenPostingsInput output: GetBundleUnseenPostingsOutput @@ -3247,6 +3400,9 @@ structure GetBundleUnseenPostingsOutput { @http(method: "POST", uri: "/postings/spam.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkPostingsSpam { input: MarkPostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3263,6 +3419,9 @@ structure PostingSelectionInput { @http(method: "POST", uri: "/postings/box_groups.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation AddPostingsToBoxGroup { input: AddPostingsToBoxGroupInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3290,6 +3449,9 @@ structure AddPostingsToBoxGroupRequestContent { @http(method: "DELETE", uri: "/postings/box_groups.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation RemovePostingsFromBoxGroup { input: PostingSelectionInput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -3299,6 +3461,9 @@ operation RemovePostingsFromBoxGroup { @http(method: "POST", uri: "/postings/filings.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation FilePostings { input: FilePostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3323,6 +3488,9 @@ structure FilePostingsRequestContent { @http(method: "DELETE", uri: "/postings/filings.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UnfilePostings { input: UnfilePostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3341,6 +3509,9 @@ structure UnfilePostingsInput { @http(method: "POST", uri: "/postings/folders.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateFolderForPostings { input: CreateFolderForPostingsInput errors: [UnauthorizedError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -3373,6 +3544,9 @@ structure FolderPayload { @http(method: "DELETE", uri: "/postings/bubble_up.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CancelPostingsBubbleUp { input: PostingSelectionInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3382,6 +3556,9 @@ operation CancelPostingsBubbleUp { @http(method: "POST", uri: "/postings/bulk_bubble_up_now.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation BubbleUpPostingsNow { input: MarkPostingsInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3398,6 +3575,9 @@ operation BubbleUpPostingsNow { @http(method: "POST", uri: "/postings/bubble_up.json") @tags(["Postings"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation SchedulePostingsBubbleUp { input: SchedulePostingsBubbleUpInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3431,6 +3611,9 @@ structure SchedulePostingsBubbleUpRequestContent { @http(method: "PUT", uri: "/topics/{topicId}/status/trashed.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation TrashTopic { input: TrashTopicInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3450,6 +3633,9 @@ structure TrashTopicInput { @http(method: "PUT", uri: "/topics/{topicId}/status/active.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation RestoreTopic { input: TopicStatusInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3460,6 +3646,9 @@ operation RestoreTopic { @http(method: "PUT", uri: "/topics/{topicId}/status/ham.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkTopicHam { input: TopicStatusInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3476,6 +3665,9 @@ structure TopicStatusInput { @http(method: "DELETE", uri: "/topics/trash/all.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation EmptyTrash { errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] } @@ -3485,6 +3677,9 @@ operation EmptyTrash { @http(method: "DELETE", uri: "/topics/spam/all.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation EmptySpam { errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] } @@ -3495,6 +3690,9 @@ operation EmptySpam { @http(method: "POST", uri: "/topics/{topicId}/moves.json") @tags(["Topics"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MoveTopic { input: MoveTopicInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3524,6 +3722,9 @@ structure MoveTopicRequestContent { @http(method: "PUT", uri: "/entries/{entryId}/status/spam.json") @tags(["Entries"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkEntrySpam { input: EntryStatusInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3541,6 +3742,7 @@ structure EntryStatusInput { @http(method: "GET", uri: "/entries/{entryId}/forwards/new.json") @tags(["Entries"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation NewEntryForward { input: EntryStatusInput output: NewEntryForwardOutput @@ -3563,6 +3765,7 @@ structure NewEntryForwardOutput { @http(method: "GET", uri: "/bulk_replies/new.json") @tags(["Bulk Reply"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation NewBulkReply { input: NewBulkReplyInput output: NewBulkReplyOutput @@ -3614,6 +3817,9 @@ structure BulkReplyEntry { /// delivery is queued, and delayed while undo is still possible. @http(method: "POST", uri: "/bulk_replies.json", code: 201) @tags(["Bulk Reply"]) +@heyDestructive(false) +@heyOpenWorld(true) +@heyUntrustedContent(false) operation CreateBulkReply { input: CreateBulkReplyInput output: CreateBulkReplyOutput @@ -3668,6 +3874,9 @@ structure BulkReplyDelivery { @http(method: "POST", uri: "/contacts/{contactId}/bundle.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation BundleContact { input: ContactActionInput errors: [UnauthorizedError, ForbiddenError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3678,6 +3887,9 @@ operation BundleContact { @http(method: "DELETE", uri: "/contacts/{contactId}/bundle.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UnbundleContact { input: ContactActionInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3694,6 +3906,9 @@ structure ContactActionInput { @http(method: "PATCH", uri: "/contacts/{contactId}/clearance.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateContactClearance { input: UpdateContactClearanceInput errors: [UnauthorizedError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -3721,6 +3936,7 @@ structure UpdateContactClearanceRequestContent { @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetClearances { input: GetClearancesInput output: GetClearancesOutput @@ -3748,6 +3964,9 @@ structure GetClearancesOutput { @http(method: "PATCH", uri: "/clearances/{clearanceId}") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation UpdateClearance { input: UpdateClearanceInput output: UpdateClearanceOutput @@ -3784,6 +4003,9 @@ structure UpdateClearanceOutput { @http(method: "PATCH", uri: "/clearances/bulk.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation BulkUpdateClearances { input: BulkUpdateClearancesInput output: BulkUpdateClearancesOutput @@ -3815,6 +4037,9 @@ structure BulkUpdateClearancesOutput { @http(method: "POST", uri: "/clearances/punt.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation PuntClearances { errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] } @@ -3825,6 +4050,7 @@ operation PuntClearances { @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetMyClearances { input: PagedInput output: GetMyClearancesOutput @@ -3841,6 +4067,9 @@ structure GetMyClearancesOutput { @http(method: "PATCH", uri: "/my/clearances/{clearanceId}") @tags(["Contacts"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(true) operation UpdateMyClearance { input: UpdateMyClearanceInput output: UpdateMyClearanceOutput @@ -3875,6 +4104,9 @@ structure UpdateMyClearanceOutput { @http(method: "POST", uri: "/boxes/{boxId}/designations.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateBoxDesignation { input: CreateBoxDesignationInput errors: [UnauthorizedError, ForbiddenError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3900,6 +4132,9 @@ structure CreateBoxDesignationRequestContent { @http(method: "DELETE", uri: "/boxes/{boxId}/designations/{designationId}") @tags(["Boxes"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteBoxDesignation { input: DeleteBoxDesignationInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -3929,6 +4164,7 @@ structure DeleteBoxDesignationInput { @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count", pageParameter: "page") +@heyUntrustedContent(true) operation GetBoxPostingChanges { input: GetBoxPostingChangesInput output: GetBoxPostingChangesOutput @@ -3980,6 +4216,7 @@ list DeletedPostingList { @http(method: "GET", uri: "/boxes/{boxId}/groups.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation ListBoxGroups { input: BoxGroupsInput output: ListBoxGroupsOutput @@ -4006,6 +4243,7 @@ structure ListBoxGroupsOutput { @tags(["Boxes"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetBoxGroup { input: GetBoxGroupInput output: GetBoxGroupOutput @@ -4036,6 +4274,9 @@ structure GetBoxGroupOutput { @http(method: "POST", uri: "/boxes/{boxId}/groups.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateBoxGroup { input: CreateBoxGroupInput output: CreateBoxGroupOutput @@ -4067,6 +4308,9 @@ structure CreateBoxGroupOutput { @http(method: "DELETE", uri: "/boxes/{boxId}/groups/{groupId}") @tags(["Boxes"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteBoxGroup { input: DeleteBoxGroupInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4086,6 +4330,9 @@ structure DeleteBoxGroupInput { @http(method: "POST", uri: "/boxes/{boxId}/observation.json") @tags(["Boxes"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MarkBoxSeen { input: MarkBoxSeenInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4107,6 +4354,7 @@ structure MarkBoxSeenInput { @tags(["Folders"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetFolder { input: GetFolderInput output: GetFolderOutput @@ -4136,6 +4384,7 @@ structure GetFolderOutput { @http(method: "GET", uri: "/collections.json") @tags(["Collections"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation ListCollections { output: ListCollectionsOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -4152,6 +4401,7 @@ structure ListCollectionsOutput { @tags(["Collections"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation GetCollection { input: GetCollectionInput output: GetCollectionOutput @@ -4177,6 +4427,9 @@ structure GetCollectionOutput { @http(method: "PATCH", uri: "/collections/{collectionId}") @tags(["Collections"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateCollection { input: UpdateCollectionInput errors: [UnauthorizedError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -4212,6 +4465,7 @@ structure CollectionPayload { @http(method: "GET", uri: "/stickies.json") @tags(["Stickies"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation ListStickies { input: ListStickiesInput output: ListStickiesOutput @@ -4233,6 +4487,9 @@ structure ListStickiesOutput { @http(method: "POST", uri: "/stickies.json") @tags(["Stickies"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateSticky { input: CreateStickyInput output: StickyOutput @@ -4266,6 +4523,9 @@ structure StickyOutput { @http(method: "PATCH", uri: "/stickies/{stickyId}") @tags(["Stickies"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateSticky { input: UpdateStickyInput output: StickyOutput @@ -4287,6 +4547,9 @@ structure UpdateStickyInput { @http(method: "DELETE", uri: "/stickies/{stickyId}") @tags(["Stickies"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteSticky { input: DeleteStickyInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4302,6 +4565,9 @@ structure DeleteStickyInput { @http(method: "POST", uri: "/stickies/moves.json") @tags(["Stickies"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MoveSticky { input: MoveStickyInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4332,6 +4598,9 @@ structure MoveStickyRequestContent { @http(method: "POST", uri: "/calendar/time_tracks.json") @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateTimeTrack { input: CreateTimeTrackInput output: CreateTimeTrackOutput @@ -4365,6 +4634,9 @@ structure CreateTimeTrackOutput { @http(method: "DELETE", uri: "/calendar/time_tracks/{timeTrackId}") @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteTimeTrack { input: DeleteTimeTrackInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4384,6 +4656,9 @@ structure DeleteTimeTrackInput { @http(method: "POST", uri: "/calendar/habits.json", code: 201) @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateHabit { input: CreateHabitInput output: CreateHabitOutput @@ -4421,6 +4696,9 @@ structure HabitPayload { @http(method: "PATCH", uri: "/calendar/habits/{habitId}") @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation UpdateHabit { input: UpdateHabitInput output: UpdateHabitOutput @@ -4448,6 +4726,9 @@ structure UpdateHabitInput { @http(method: "DELETE", uri: "/calendar/habits/{habitId}") @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(true) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation DeleteHabit { input: HabitInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4463,6 +4744,9 @@ structure HabitInput { @http(method: "POST", uri: "/calendar/habits/{habitId}/stop.json") @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation StopHabit { input: HabitInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4473,6 +4757,9 @@ operation StopHabit { @http(method: "DELETE", uri: "/calendar/habits/{habitId}/stop.json") @tags(["Calendar Habits"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation ResumeHabit { input: HabitInput errors: [UnauthorizedError, NotFoundError, InternalServerError, ServiceUnavailableError] @@ -4487,6 +4774,7 @@ operation ResumeHabit { @http(method: "GET", uri: "/calendar/time_tracks/categories.json") @tags(["Calendar Time Tracks"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation ListTimeTrackCategories { output: ListTimeTrackCategoriesOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -4515,6 +4803,7 @@ list TimeTrackCategoryList { @tags(["Clips"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @heyPagination(style: "link", totalCountHeader: "X-Total-Count") +@heyUntrustedContent(true) operation ListClips { input: PagedInput output: ListClipsOutput @@ -4553,6 +4842,7 @@ list ClipList { @http(method: "GET", uri: "/snippets.json") @tags(["Snippets"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation ListSnippets { output: ListSnippetsOutput errors: [UnauthorizedError, InternalServerError, ServiceUnavailableError] @@ -4584,6 +4874,7 @@ list SnippetList { @http(method: "GET", uri: "/workflows/{workflowId}") @tags(["Workflows"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetWorkflow { input: GetWorkflowInput output: GetWorkflowOutput @@ -4595,6 +4886,7 @@ operation GetWorkflow { @http(method: "GET", uri: "/workflows/{workflowId}/stages/{stageId}") @tags(["Workflows"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation GetWorkflowStage { input: GetWorkflowStageInput output: GetWorkflowStageOutput @@ -4630,6 +4922,9 @@ structure GetWorkflowOutput { /// Add a topic to a workflow. HEY places it in the first stage. @http(method: "POST", uri: "/topics/{topicId}/workflows/{workflowId}/stagings") @tags(["Workflows"]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation CreateWorkflowStaging { input: CreateWorkflowStagingInput errors: [UnauthorizedError, ForbiddenError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -4648,6 +4943,9 @@ structure CreateWorkflowStagingInput { /// Move a staged topic to a workflow stage. @http(method: "PATCH", uri: "/topics/{topicId}/workflows/{workflowId}/stagings") @tags(["Workflows"]) +@heyDestructive(false) +@heyOpenWorld(false) +@heyUntrustedContent(false) operation MoveWorkflowStaging { input: MoveWorkflowStagingInput errors: [UnauthorizedError, ForbiddenError, NotFoundError, UnprocessableEntityError, InternalServerError, ServiceUnavailableError] @@ -4682,6 +4980,7 @@ structure WorkflowStagingPayload { @http(method: "GET", uri: "/topics/{topicId}/publication.json") @tags(["Publications"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(false) operation GetTopicPublication { input: GetTopicPublicationInput output: GetTopicPublicationOutput From d9cff96406ee561c2c66ecd48e94753003faebf9 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Tue, 29 Sep 2026 19:43:34 -0700 Subject: [PATCH 2/3] Close the resend paths the retry tripwire missed, and mark journal erasure destructive - behavior-model.json counted any @heyIdempotent as idempotent, so UpdateMessage, which opts out with natural: false, was advertised as safe to repeat. Only natural decides now; UpdateMessage is the one operation that changes. - HeyOpenWorldRetried now mirrors how every generator decides a resend: an open-world operation that does not say natural: false is refused when @idempotent, natural: true, or a GET/HEAD/PUT verb would resend it. - @heyDraftWhen needs at least one condition; an empty list holds vacuously. - UpdateJournalEntry with empty content destroys the entry: destructive. The tripwire test gains the dropped-opt-out and empty-conditions cases. --- AGENTS.md | 7 ++++--- behavior-model.json | 4 ++-- openapi.json | 2 +- scripts/generate-behavior-model | 8 ++++++-- scripts/test-behavior-traits | 8 ++++++++ spec/hey-traits.smithy | 14 +++++++++++--- spec/hey.smithy | 2 +- 7 files changed, 33 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ab9f7415..e874413e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -346,13 +346,14 @@ toolkit (`github.com/basecamp/mcp`): - `@heyDestructive(true|false)` on every write → `destructive`. True when some path destroys data, or the caller's own access to it, with no way back for the caller: a hard delete, - emptying the trash or spam, erasing a note, `TrashPostings` on a shared thread (the default + emptying the trash or spam, erasing a note or a journal entry, `TrashPostings` on a shared thread (the default JSON path revokes your access). Trashing is not destructive (HEY restores for 30 days), nor is a toggle with an inverse or an ordinary edit. - `@heyOpenWorld(true|false)` on every write → `open_world`. True when the call can reach people outside the mailbox: delivering mail, publishing to HEY World, calendar - invitations or cancellations. An open-world operation may not be `@idempotent` or - `@heyIdempotent(natural: true)` unless it is a DELETE. + invitations or cancellations. An open-world operation other than a DELETE must never be + resent: not `@idempotent`, not `@heyIdempotent(natural: true)`, and a PUT must say + `@heyIdempotent(natural: false)` to opt out of the verb's retries. - `@heyDraftWhen([...])` on an open-world operation that can save instead of send → `draft_when`: request-body conditions under which the call delivers nothing. - `@heyUntrustedContent(true|false)` on every operation → `untrusted_content`. True when the diff --git a/behavior-model.json b/behavior-model.json index c645d2c3..ef2d0d7b 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -2100,7 +2100,7 @@ "untrusted_content": false }, "UpdateJournalEntry": { - "destructive": false, + "destructive": true, "idempotent": false, "open_world": false, "readonly": false, @@ -2127,7 +2127,7 @@ "pointer": "/entry/scheduled_delivery" } ], - "idempotent": true, + "idempotent": false, "open_world": true, "readonly": false, "retry": { diff --git a/openapi.json b/openapi.json index 73867c73..85afea61 100644 --- a/openapi.json +++ b/openapi.json @@ -2061,7 +2061,7 @@ "tags": [ "Calendar Journal" ], - "x-hey-destructive": false, + "x-hey-destructive": true, "x-hey-open-world": false, "x-hey-retry": { "maxAttempts": 2, diff --git a/scripts/generate-behavior-model b/scripts/generate-behavior-model index e9c20f58..0eb5290a 100755 --- a/scripts/generate-behavior-model +++ b/scripts/generate-behavior-model @@ -45,8 +45,12 @@ def condition: value: ( { readonly: (if .value.traits["smithy.api#readonly"] != null then true else false end), - idempotent: (if .value.traits["smithy.api#idempotent"] != null - or .value.traits["hey.traits#heyIdempotent"] != null + # @heyIdempotent counts only for what its natural member says: natural: false + # is an opt-out (UpdateMessage delivers), and a trait carrying only + # keySupported says nothing about resending. + idempotent: (if .value.traits["hey.traits#heyIdempotent"].natural == false then false + elif .value.traits["smithy.api#idempotent"] != null + or .value.traits["hey.traits#heyIdempotent"].natural == true then true else false end) } + # Pagination diff --git a/scripts/test-behavior-traits b/scripts/test-behavior-traits index ca11f703..d64466a7 100755 --- a/scripts/test-behavior-traits +++ b/scripts/test-behavior-traits @@ -104,6 +104,14 @@ reset_model edit_operation UpdateMessage sub 'natural: false' 'natural: true' expect_refusal "a send opted into resends" HeyOpenWorldRetried hey#UpdateMessage +reset_model +edit_operation UpdateMessage drop '@heyIdempotent(' +expect_refusal "a PUT send left to the verb's retries" HeyOpenWorldRetried hey#UpdateMessage + +reset_model +edit_operation CreateBulkReply sub '@heyOpenWorld(true)' '@heyOpenWorld(true)\n@heyDraftWhen([])' +expect_refusal "a send whose draft conditions are empty" TraitValue hey#CreateBulkReply + if [[ $failures -gt 0 ]]; then echo "$failures tripwire check(s) failed" exit 1 diff --git a/spec/hey-traits.smithy b/spec/hey-traits.smithy index c6ea0872..c69f42fe 100644 --- a/spec/hey-traits.smithy +++ b/spec/hey-traits.smithy @@ -34,12 +34,17 @@ metadata validators = [ name: "EmitEachSelector" id: "HeyOpenWorldRetried" severity: "DANGER" - message: "An open-world operation must not be resent: a retry after an ambiguous first attempt can deliver twice." + message: "An open-world operation must not be resent: a retry after an ambiguous first attempt can deliver twice. Declare @heyIdempotent(natural: false) to opt a PUT out of the verb's retries." + // Every generator decides a resend the same way: x-hey-idempotent's natural + // when it is a boolean, otherwise @readonly or @idempotent, otherwise the + // verb (GET, HEAD, PUT, DELETE). So refuse any open-world operation that + // does not say natural: false and would be resent by one of the other two. + // // DELETE is exempt: the deliveries HEY makes on a delete (calendar // cancellations) are keyed to the record it destroys, so a resend finds // nothing and answers 404 rather than notifying again. configuration: { - selector: "operation [trait|hey.traits#heyOpenWorld = true] :not([trait|http|method = DELETE]) :is([trait|idempotent], [trait|hey.traits#heyIdempotent|natural = true])" + selector: "operation [trait|hey.traits#heyOpenWorld = true] :not([trait|http|method = DELETE]) :not([trait|hey.traits#heyIdempotent|natural = false]) :is([trait|idempotent], [trait|hey.traits#heyIdempotent|natural = true], [trait|http|method = GET, HEAD, PUT])" } } ] @@ -47,6 +52,7 @@ metadata validators = [ namespace hey.traits use smithy.api#documentation +use smithy.api#length use smithy.api#trait use smithy.openapi#specificationExtension @@ -227,9 +233,11 @@ boolean heyOpenWorld /// draft instead of delivering. When every condition holds, the call delivers /// nothing; when any fails, treat the call as delivering. The conditions are /// sufficient, not necessary: HEY may also hold a call they do not describe, and a -/// consumer that gates on them errs toward asking. +/// consumer that gates on them errs toward asking. At least one condition: an empty +/// list would hold vacuously and wave every send through as a draft. @trait(selector: "operation [trait|hey.traits#heyOpenWorld = true]") @specificationExtension(as: "x-hey-draft-when") +@length(min: 1) list heyDraftWhen { member: HeyBodyCondition } diff --git a/spec/hey.smithy b/spec/hey.smithy index addf906a..6526f355 100644 --- a/spec/hey.smithy +++ b/spec/hey.smithy @@ -3001,7 +3001,7 @@ structure GetJournalEntryOutput { @http(method: "PATCH", uri: "/calendar/days/{day}/journal_entry") @tags(["Calendar Journal"]) @heyRetry(maxAttempts: 2, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) -@heyDestructive(false) +@heyDestructive(true) @heyOpenWorld(false) @heyUntrustedContent(false) operation UpdateJournalEntry { From 8486588ae9c46a34a2523f4c73e0ace233ddc6ef Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 2 Oct 2026 13:44:25 -0700 Subject: [PATCH 3/3] Declare ListAddressableContacts' untrusted content Main gained ListAddressableContacts (#231) after this branch's HeyUntrustedContentUndeclared validator was written, so the rebased spec failed smithy-validate. Its labels are the display names correspondents declared for themselves, the same content ListContacts and GetContact already mark untrusted. --- behavior-model.json | 5 ++++- openapi.json | 3 ++- spec/hey.smithy | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/behavior-model.json b/behavior-model.json index ef2d0d7b..aecc6452 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -1239,7 +1239,9 @@ "untrusted_content": false }, "ListAddressableContacts": { + "destructive": false, "idempotent": false, + "open_world": false, "readonly": true, "retry": { "backoff": "exponential", @@ -1249,7 +1251,8 @@ 429, 503 ] - } + }, + "untrusted_content": true }, "ListBoxGroups": { "destructive": false, diff --git a/openapi.json b/openapi.json index 85afea61..7ada78db 100644 --- a/openapi.json +++ b/openapi.json @@ -414,7 +414,8 @@ 429, 503 ] - } + }, + "x-hey-untrusted-content": true } }, "/boxes.json": { diff --git a/spec/hey.smithy b/spec/hey.smithy index 6526f355..7488851c 100644 --- a/spec/hey.smithy +++ b/spec/hey.smithy @@ -2094,6 +2094,7 @@ structure GetContactOutput { @http(method: "GET", uri: "/autocompletable/contacts/addressable.json") @tags(["Contacts"]) @heyRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) +@heyUntrustedContent(true) operation ListAddressableContacts { input: ListAddressableContactsInput output: ListAddressableContactsOutput