Skip to content

Repository files navigation

gete

Write an agent.yaml, and the agent is deployed to Agent Runtime (Vertex AI Agent Engine), registered with Gemini Enterprise, and wired to per-user authorizations.

People adding an agent do not write Python. Instructions live in Markdown; connections and tools are declared in YAML. Python is only needed for tools whose logic you implement yourself.

The name is "gate", Gemini flavoured: the gate agents walk through into Gemini Enterprise.

Status

0.1 is being assembled. The pieces below exist and are tested; the first release follows once an existing installation runs on them unchanged.

How it fits together

agent.yaml ──validate──▶ archive ──▶ Agent Engine ──register──▶ Gemini Enterprise
    │                      │              │                           │
gete.yaml            gete_entry.py   reasoning engine        authorization per
policies/*.yaml      agent.resolved  (Terraform module)       agent × connection
connections          requirements.txt                        registration → engine
  • Declarationsgete.yaml (project, policies, connections) and one agents/<name>/agent.yaml per agent. JSON Schemas reject unknown keys.
  • Policies — rules every agent gets from the outside: text put in front of the instruction, redaction of tool results, confirmation of writes. gete fixes the shape; the text is yours.
  • Connections — external services read with the user's token, which Gemini Enterprise hands over per authorization. gete's client only sends a token to the hosts the connection declares, and only when it has the connection's shape. That guard covers builtin and MCP tools; a python tool is handed the caller's token to work with, so where the token goes from there is its code's doing — reviewing an agent's src/ is reviewing that.
  • Shared credentials — the opposite trust model, for writes that have no per-user token to ride on: one credential the agent holds, acting for whoever calls it. The tools and their guardrails ship with gete; a declaration can only switch them on.
  • Runtime — builds the ADK agent from agent.resolved.yaml, carries the user's token to the tools (builtin, MCP, OpenAPI, python), redacts what comes back.
  • Delivery — a deterministic archive, a Terraform module, and register for the parts Terraform has no resources for.

Quick start

uv tool install "gete[cli] @ git+https://github.com/pepabo/gete"
gete init mail-triage          # gete.yaml, policies/example.yaml, agents/mail-triage/
$EDITOR agents/mail-triage/instruction.md
gete validate
gete run mail-triage           # talk to it locally
gete terraform                 # one module call per agent, under terraform/
gete register                  # once the engines exist: authorizations and the listing

The rest of the way — the GCP project, the Terraform root the generated module calls need, and the one registration a person makes by hand — is in docs/quickstart.md. That install resolves from whatever index your uv names, PyPI unless you say otherwise; the index it resolves from covers pointing it at a mirror.

Commands

Command What it does Touches GCP
gete init <name> Scaffold an agent (and the project if there is none) no
gete validate [--check-secrets] [--import-check] Schemas and rules; optionally secrets' versions and a deployment-shaped import --check-secrets reads
gete run <name> Local conversation; tokens from GETE_TOKEN_<CONNECTION> calls the model
gete graph [name...] Mermaid diagram of agents, engines, tools, connections no
gete connections [id] Catalog plus your own; with an id, what a person must prepare no
gete archive <dir> [--out file] The tar.gz Agent Engine receives; --external for Terraform no
gete terraform [--out dir] [--check] Generate module calls; --check fails when stale no
gete register [name...] Create/update authorizations, bring registrations in line writes

Exit codes: 0 on success, 1 when a check fails. register exits 0 when steps remain for a person (they are written to registration-notice.md) and 1 only when an agent could not be processed at all.

A project

gete.yaml
policies/
  finance.yaml
agents/
  mail-triage/
    agent.yaml
    instruction.md
  partner-review/
    agent.yaml
    instruction.md
    src/                     python tools, packaged with the agent
    requirements.txt         their extra dependencies
terraform/                   generated by `gete terraform`

See examples/minimal for the smallest working project.

agent.yaml

name: partner-review
display_name: Quarterly partner review
description: Aggregates spend per partner from freee and drafts the review.
model: gemini-2.5-flash
instruction: ./instruction.md

connections: [freee]            # read with the user's own authorization

tools:
  - mcp:
      url: https://mcp.freee.example/mcp
      connection: freee         # the user's token rides along as Authorization
      allow: [get_deals, get_partners]
      effect: read
      does_not: Does not create, update, or approve anything.
  - python:
      ref: partner_review.agent:TOOLS
      effect: read
source: ./src

runtime:
  agent_engine:
    env: {FREEE_COMPANY_ID: "123456"}
    secret_env: {SOME_TOKEN: some-secret-name}   # names only; values stay in Secret Manager

registration:
  gemini_enterprise:
    engine: my-app_1234567890   # from the console URL; omit to deploy without listing

Tools that do not say effect: read count as writes, which is what the has_write_tools policies key on.

allow and effect are declared per mcp: block, so one server's reads and writes are split by naming it twice — the same url and connection, two lists of tool names, two effects. Where a server hands out one grant for both, that is the only place an agent can say it means to read.

Tools from an OpenAPI description

A service that publishes an OpenAPI description but runs no MCP server can be declared without writing Python:

tools:
  - openapi:
      spec: ./specs/helpdesk.yaml         # read at packing time, travels in the archive
      connection: helpdesk
      operations: [ListSearchResults, ShowTicket, ListTicketComments]
      effect: read
      does_not: Results are the caller's own view; not found is not proof of absence.
      params:
        ListSearchResults:
          query: {prefix: "type:ticket "}
          per_page: {value: 25}
      describe:
        ListSearchResults: Search tickets. The kind is fixed to tickets.
  - openapi:
      spec: ./specs/helpdesk.yaml
      connection: helpdesk
      operations: [UpdateTicket]
      effect: write
      only:
        UpdateTicket: [ticket_id, ticket.comment.body]   # all the model may write
      params:
        UpdateTicket:
          ticket.comment.public: {value: false}          # internal note, never mail
  • operations is required, never defaulted. A published description holds far more than an agent means to expose — hundreds of operations is normal — and forgetting to choose must not mean offering everything. Operations are picked by operationId, which also becomes the tool's name.
  • Request URLs are built from the connection's base_url. The description's own servers are never read: a published root may carry variables, a stale default, or another tenant. The client's destination check and token rules hold exactly as for every other request.
  • params keeps what the code it replaces used to enforce. value fixes a parameter and takes it out of what the model sees — its value is declared, so there is nothing left for the model to say. prefix and suffix wrap what the model writes; the declared text comes first, so nothing the model writes can displace it.
  • A dotted name reaches into the JSON body. Services commonly nest what matters: whether a helpdesk comment goes out to the requester is a boolean two levels down. ticket.comment.public: {value: false} pins it there — the leaf disappears from what the model sees, and the declared value is written wherever its parent object is sent, overwriting anything found there and never conjuring the parent up. A name that matches a parameter literally keeps meaning that parameter.
  • only names what the model may write. Published update operations accept the whole record — status, assignee, tags — when an agent is only meant to add a comment. Everything only leaves unlisted is taken out of the declaration and never sent, even smuggled into the arguments; a params value still rides. A name a parameter carries literally stays that parameter's, as with params. Counting up what goes out fails safe as the description grows: a new field stays unexposed until someone declares it.
  • describe replaces the vendor's text. Vendor descriptions are written for developers sitting next to the docs and often cite links a model cannot follow; does_not is appended to every tool, as with mcp:.
  • The description is fixed at packing time. gete archive takes the file into the archive and the runtime reads it from there, so a vendor editing their published description changes nothing until someone re-archives deliberately.
  • The archive carries only what was declared. Keep the vendor's original in the repo; gete archive prunes it to the declared operations, path-level parameters and every referenced component riding along. Cutting a description down by hand breaks quietly — path-level parameters fall away, a flattened $ref takes its arguments with it, and validate cannot tell such a description from one that never declared them — so the cutting is gete's job, and validate now also reports a {placeholder} in a path that no path parameter declares. The packing then holds the declaration against the pruned description, so a reference pruning cannot keep is refused before anything deploys.
  • Writes ride the same rails. PUT, PATCH, and DELETE operations must sit in a block declared effect: write, which the confirmation policies key on, and results pass the same redaction as every other tool. A change that may already have been applied is never resent — for a DELETE, what it removed usually cannot be brought back, so a confirmation policy on write tools is worth having before declaring one.

Connections

gete connections lists what ships: freee, google, github, notion-mcp, zendesk, and slack (retired, with the reason). Add your own or override a catalog entry in gete.yaml:

connections:
  github:
    base_url: https://api.github.example.com     # GitHub Enterprise
  internal-api:
    display_name: Internal API
    hosts: [api.internal.example.com]
    token_prefixes: []
    oauth:
      authorization_url: https://auth.internal.example.com/authorize
      token_url: https://auth.internal.example.com/token
      scopes: {read: Read internal data}

A hosts entry is an exact host name; nothing is matched by suffix. When one host serves unrelated APIs side by side — www.googleapis.com carries Drive and Calendar next to GCP's storage and compute — the entry can be scoped to a path prefix, written host/path/, and requests must stay below that path. A bare entry admits every path on its host, so declaring the same host bare next to a scoped entry — or setting base_url on that host, which lists it bare — is reported: the scoping would silently not happen.

A connection's oauth.scopes go to every agent that declares it, so they stay a read-only minimum. Scopes under oauth.optional_scopes are a menu: an agent gets one only by selecting it in its own declaration, and the selection lands in that agent's own authorization, so consenting to one agent's writes grants nothing to any other. A scope outside the menu is refused by gete validate.

# agent.yaml
connections:
  - freee                       # the defaults only
  - id: google                  # the defaults plus a selection from the menu
    scopes: [https://www.googleapis.com/auth/spreadsheets]

oauth.pkce: true asks Gemini Enterprise to carry a code challenge through the flow. An authorization server that requires PKCE refuses the code exchange without one, and there is no other way to ask for it from a declaration.

token_prefixes: [] says the service does not announce itself: a token is taken as its own once no other connection's prefix matches it. Two such connections cannot be told apart, so an agent may hold only one of them. Declaring a second one in gete.yaml is fine; naming both under one agent's connections is what gete validate refuses.

A service whose root moves with the installation — the tenant in a subdomain, or a deployment you host — writes its URLs around {base_url}, and the installation fills it in:

connections:
  rooted-api:
    display_name: Rooted API
    hosts: []                       # the only host comes from base_url
    base_url: https://acme.example.com
    oauth:
      authorization_url: "{base_url}/oauth/authorizations/new"
      token_url: "{base_url}/oauth/tokens"
      scopes: {read: Read data}

Leave base_url out and nothing the connection declares is an address: no host is added, and gete validate refuses it where an agent names it. That is how a definition reaches the catalog without knowing a tenant. Writing a stand-in host instead would put a name a stranger can register on the list of places a user's token may be sent.

Some of what a connection needs is not gete's to do. The OAuth client is registered by a person, at the provider, once; setup is where a connection says so, and gete connections <id> prints it next to the secret names and the redirect URI that registration asks for at the same moment:

connections:
  internal-api:
    setup: |
      Register an OAuth client in the service's admin console.
      Put the client id and secret in Secret Manager under the names above.
      The consent screen is the service's own; it grants writing as well.

Prose, not a checklist. What matters most about a connection is often not a step — what the consent screen actually grants, which providers hand out no way to delete a client again — and no check can see any of it.

$ gete connections internal-api
internal-api  Internal API
  ...
  client id       ge-oauth-internal-api-client-id
  client secret   ge-oauth-internal-api-client-secret
  redirect uri    https://vertexaisearch.cloud.google.com/oauth-redirect

Before anyone can authorize:
  Register an OAuth client in the service's admin console.
  ...

Adding a connection to the catalog is one YAML file under src/gete/catalog/connections/; the conformance tests check it.

Shared credentials

A connection reads with the caller's token. Some writes have no such token to ride on — Slack posting is one, which is why the slack connection is retired — so gete also ships tools that act with a credential the agent holds. Whoever can call the agent acts through that credential; the tools and their guardrails ship with gete, and a declaration can only name them:

# agent.yaml
shared_credentials: [slack_post]

# gete.yaml — the secret is named once for the project
shared_credentials:
  slack_post:
    token_secret: slack-bot-token    # Secret Manager secret holding the xoxb- token

slack_post previews a post, posts it as the bot once the user approved, and reads a single linked message — never the channel around it. The fence moves with the destination: a public channel takes inviting the bot, a private channel takes its ID in the agent's SLACK_ALLOWED_PRIVATE_CHANNELS env, and direct messages are never posted to. Text the policies' redact patterns would change is refused rather than masked, and who posted where is logged — never what. Declaring the credential counts as has_write_tools, and policies can key on has_shared_credentials.

Delivery wires token_secret into the deployment's secret_env; the agent neither writes nor can change which secret the credential comes from. The Slack app behind the token needs a bot user with chat:write, channels:read, groups:read, channels:history, and groups:history — and not chat:write.public, which would let the bot past the invitation fence. Locally, gete run reads the token from SLACK_BOT_TOKEN.

Development

uv sync --all-extras
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run mypy

The version is derived from git tags (hatch-vcs); it is not written in pyproject.toml. Everything in this repository is written in English.

License

Apache-2.0

About

Declare agents in YAML, deploy them to Vertex AI Agent Engine, and register them with Gemini Enterprise

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages