Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/smithy-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 31 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,38 @@ 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 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 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
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:

Expand Down Expand Up @@ -375,7 +404,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,
Expand Down
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand All @@ -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
#------------------------------------------------------------------------------
Expand Down Expand Up @@ -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 \
Expand Down
Loading
Loading