tacplus¶
Annotated protobuf in. Complete services and clients out.
tacplus is a compiler for Protocol Buffer definitions that carry spec
annotations. From one proto set it generates the validation, state
ownership, authorization, identity, lifecycle and clients around your
handler: full services in Go, Rust and Java, and clients in TypeScript,
Python, Swift and C#. Every user-facing RPC is also an MCP tool, with the same
authorization as the RPC. Open source under Apache-2.0.
The problem¶
Every service in a multi-language platform needs the same infrastructure: typed messages, validated inputs, serialization, authentication, authorization, state, health. The usual answer is hand-rolled validation, shared libraries and conventions, so the contract (which values are valid, who may call what, who owns which state) ends up scattered across code in several languages. A rule changes in one language and not another; a service writes state it does not own; an auth check exists here but not there.
tacplus puts the whole contract in the proto files, where every language already agrees on the shape of the data, and generates everything that follows from it. Your code is the handler. Read the theory.
Write the contract once¶
A proto file with spec annotations is the whole service definition. This is
the service from the quick start, shortened:
syntax = "proto3";
package notesapi;
import "sys/spec.proto";
option (spec.service) = {name: "notes"};
message Note {
string id = 1 [(spec.assert_required) = true, (spec.indexed) = true];
string tenant_namespace = 2 [(spec.assert_required) = true];
string text = 3 [(spec.assert_required) = true, (spec.assert_max_size) = 1024];
}
message CreateNoteRequest {
string tenant_namespace = 1 [(spec.assert_required) = true, (spec.assert_min_size) = 1];
string text = 2 [(spec.assert_required) = true, (spec.assert_max_size) = 1024];
}
message CreateNoteResponse { Note note = 1; }
// Called by authenticated users through the gateway.
service NotesApi {
option (spec.service_c2s) = {auth_entitled_users: true};
// Create a note in the caller's tenant namespace.
rpc CreateNote(CreateNoteRequest) returns (CreateNoteResponse) {
option (spec.rpc) = {principal: RPC_PRINCIPAL_USER, exposure: RPC_EXPOSURE_EXTERNAL,
flags: [RPC_FLAG_AUTH_ENTITLED_USERS], target_namespace_field: "tenant_namespace"};
}
}
// A state root: a partition of distributed state this service owns.
message NotesState {
option (spec.service_state) = {name: "NotesState", label: ["notesapi"], export: true};
repeated Note notes = 1 [(spec.indexed) = true];
}
A tacplus.yaml says which language and package to generate, and nothing
about the service itself:
What comes out¶
For Go, the build above writes a module under model/:
model/
├── s2sm/
│ ├── s2sm.go # ServiceManager: identity, TLS, health, state, clients
│ ├── grpc/servers/notes/notesapi/
│ │ ├── notes_api.go # NotesApiHandler interface + validating, authorizing wrapper
│ │ ├── notes_api_mcp.go # the same rpcs as MCP tools
│ │ └── notes_state_service.go # state service: Get / Set / Watch / Delete
│ └── state/notes/notes_state/ # typed accessors for NotesState
├── types/notes/notesapi/ # protobuf types with validation helpers
├── chassis/ # runtime libraries the generated code calls
├── registry.json # the whole model as data, tool catalogue included
└── AGENT.md # the architectural norms, for agents
You implement the handler interface. The wrapper validates every request and
response against the assert_* annotations, runs authentication and
authorization, builds the caller identity, and only then calls your code.
Four generated surfaces¶
Which clients are generated follows from the proto: a declared dependency produces its client, and nothing is switched on in YAML.
-
C2S: client to service
Native gRPC clients for applications calling a service through the gateway. Tokens are attached for you, and in every language but Swift and C# the request is validated before it leaves the process.
-
W2S: web to service
The browser-friendly surface: unary calls over HTTP/REST, server streaming over SSE, client and bidi streaming over WebSockets.
-
S2S: service to service
Typed gRPC clients between services, carrying service identity tokens. Generated for the server languages only.
-
C2D: client to daemon
gRPC over local IPC (Unix-domain sockets, named pipes) to a daemon on the same machine. The socket's permissions are the auth boundary.
Seven target languages¶
-
Full service: Go, Rust, Java
Types, validation, server wrappers, MCP dispatch, state, the ServiceManager runtime, and S2S, C2S, W2S and C2D clients, at parity across the three.
-
Full client: TypeScript, Python
Every client surface (C2S, W2S, C2D and REST) with client-side validation. Service-side generation is rejected, by design.
-
Lite client: Swift, C#
C2S, W2S and C2D clients. Streaming methods the target cannot carry become compile-time-unavailable stubs, never silently dropped.
-
Compare them
What each language gets, surface by surface.
Where to next¶
- Install: release binaries,
tacplus upgrade, the container image, or from source. - Quick start: generate and run the example: a Go server and a TypeScript client.
- User guide: annotations,
tacplus.yaml, and what gets generated. - Annotation reference: every
(spec.*)option, generated fromspec.proto.