Skip to content

feat(seat): per-agent integration access - #51

Merged
pinotelio merged 5 commits into
mainfrom
feat/seat-integration-access
Oct 30, 2026
Merged

pinotelio merged 5 commits into
mainfrom
feat/seat-integration-access

Conversation

@pinotelio

@pinotelio pinotelio commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

The operator companion of per-agent integration access in Crewlet: crewlet/crewlet#478 (feat/agents-access). A Seat now says, per vendor, whether the agent is on there, what it may do in the vendor's own terms, and where; the engine holds the vendor to it.

Breaking change to v1alpha1. No tag has shipped this surface, so it is a plain reshape with no alias or conversion. Existing objects must be rewritten as below; a field the CRD no longer has is pruned on the next write.

What changes

Seat.spec.integrations gains gitlab, jira, confluence and datadog, and every block takes a required enabled (an absent block is off):

Block Access Scope
datadog role: a Datadog role id none
github permissions: contents, issues, pullRequests, checks, actions, deployments, workflows, each Read or Write (replaces tier and workflows: true) repos
gitlab accessLevel: Guest, Planner, Reporter, Developer, Maintainer, Owner; or memberRoleID projects
jira projectRole: a project role id (none, with no projects, is product access only) projects
confluence spaceRole, or operations where the site has no space roles (neither, with no spaces, is product access only) spaces
mattermost roles: SystemUser, alone or with one of SystemPostAllPublic, SystemPostAll, SystemAdmin channels (replaces channel)
slack none none

CEL holds each fixed vendor set (GitLab roles, Mattermost role sets, GitHub permission keys and levels, workflows as Write alone), accessLevel or memberRoleID, spaceRole or operations, and the access each block needs while the seat is on; live-listed ids (Datadog, Jira, Confluence roles, GitLab custom roles) are strings the engine checks against the vendor. A scope outside the company's reach, and a credential the engine manages written where it may not be, are refused by the engine's dry run and reported as Invalid on the Seat.

Moved off Integration:

  • spec.gitlab.engineProvisioning.accessLevel and accessLevels become each Seat's spec.integrations.gitlab.accessLevel.
  • spec.datadog.role becomes each Seat's spec.integrations.datadog.role.
  • spec.gitlab.engineProvisioning.tokenScopes is removed: a seat's token scopes follow its role.

Credentials:

  • A credential the engine mints (GitLab or Mattermost under provisioning Engine, Datadog, an Atlassian organization) is sealed by the engine under its own name, <VENDOR>_<FIELD>_<HANDLE>, and bound to the vendor's tool server by the engine. Seats no longer name it in spec.mcp; the engine's dry run refuses one there (reported Invalid on the Seat).
  • spec.mcp values come from Secrets only (provisioned is gone there). Seat.spec.integrations.mattermost.botToken is now optional and a Secret only (unset under provisioning Engine).
  • provisioned survives in spec.sandbox.env as {vendor, field} and renders the engine's own name, e.g. ${GITLAB_TOKEN_ANA} for git in the seat's box. secretsync.ProvisionedName now copies the engine's envref.SealedName (fingerprinted in the mirror table); ADR-0004 is rewritten in place for this.
  • External credentials (Slack app, GitHub App, an External Mattermost bot or GitLab account, Jira/Confluence on Data Center) stay Secret-backed positions and must be the seat's alone; the engine refuses a variable shared with another seat or a company block.
  • MCPServer.spec.seatCredentials declares which integration's seat credential a per-seat server reads and under which variable or header (kind, for Atlassian field, key, prefix), replacing the engine's defaults for servers it recognises; refused on a shared server. Crewlet's as is not offered, since it must equal the transport's.
  • The engine's new integrations.slack.app_config_token (the engine creating each agent's Slack app) is listed as not offered: operator Slack seats are apps made outside the engine.

Render: the role blocks with enabled always written, GitHub permissions as GitHub's keys, GitLab access_level as GitLab's integer, Mattermost roles in Mattermost's order; provisioning.role, access_level and access_levels are no longer rendered. Every document names every integration kind in the top-level seat_access_migrated, the engine's gate for acting on a kind's seat blocks, since the operator renders that schema natively and under Enforce the engine's migration cannot write the list; engineConfig may not set the key.

Docs: integrations guide ("An agent's access" and each vendor), credentials concept page and guide, tool-servers guide, troubleshooting, company-document, ADR-0004/0005, samples, generated API reference.

Tests

  • test/crd: every new CEL rule, accepted and refused, plus the moved fields and the reshaped provisioned source.
  • Render: goldens updated (nimbus, github-enterprise, atlassian, secret-positions, optional-keys, slack), credential-position table and its doc page, NoProvisioner per vendor, provisioned names. The samples' per-seat example server is now Linear, since GitHub's and GitLab's servers get each seat's token from the engine.
  • Mirror entries for the engine's fixed sets (GitHubPermissionKeys, GitLabRoles, MattermostRoleSets) and envref.SealedName.

Verified locally against the engine at feat/agents-access ba705774db (make crewlet CREWLET_VERSION=ba705774db), which carries every part of per-seat access merged, the seat credential binding and product-only access: the render and contract suites (crewlet validate on every golden, the samples and getting-started), the CRD and docs suites, secretsync and the controller suite, golangci-lint on both modules. The chart suite was not run locally (the helm target's checksum step needs GNU sha256sum); CI runs it.

To re-run once it lands on feat/agents-access: the narrowed SeatCredentialMode, under which operator-rendered Slack and GitHub seats are external.

CI runs against Crewlet's main, so it stays red until crewlet/crewlet#478 merges; land that one first.

Crewlet now stores, per agent seat and per vendor, whether the seat is on
there, what it may do in the vendor's own terms and where, and holds the
vendor to it (crewlet/crewlet feat/agents-access). Seat.spec.integrations
follows it: every block takes a required enabled, and gitlab, jira,
confluence and datadog join slack, github and mattermost.

- datadog: role, a Datadog role id.
- github: permissions (contents, issues, pullRequests, checks, actions,
  workflows), each Read or Write, in place of tier and workflows.
- gitlab: accessLevel (Guest to Owner, rendered as GitLab's integer) or
  memberRoleID, and projects.
- jira: projectRole and projects; confluence: spaceRole or operations,
  and spaces.
- mattermost: roles, one of Mattermost's sets for a bot, and channels in
  place of channel.

CEL holds each vendor's fixed set and the access a block needs while the
seat is on; live-listed ids are the engine's to check against the vendor.

Moved off Integration: GitLab's engineProvisioning.accessLevel and
accessLevels and Datadog's role go to each Seat; GitLab's tokenScopes go,
since a seat's token follows its role.

Credentials: the engine seals a credential it mints for a seat under its
own name, <VENDOR>_<FIELD>_<HANDLE>, and binds it to the vendor's tool
server itself, so spec.mcp takes Secrets only and a Mattermost bot token
is a Secret only, for a bot made outside the engine. provisioned remains
in spec.sandbox.env as {vendor, field} and renders the engine's name,
which secretsync.ProvisionedName now copies from envref.SealedName under a
mirror fingerprint. ADR-0004 is rewritten for it.

This reshapes v1alpha1 with no alias or conversion: no tag has shipped
the surface. Objects that set the removed fields must be rewritten.

Signed-off-by: pinotelio <ahmadreza.mollapour@gmail.com>
Crewlet's access pass acts on a kind's seat blocks only where the
document names the kind in seat_access_migrated, its guard against
reading a document from before per-seat access as every seat off. The
operator renders that schema natively, and under the Enforce policy the
engine's own migration cannot add the list, so every document now names
every kind: atlassian, datadog, github, gitlab, mattermost and slack.
The list is held to the engine's seataccess.Kinds in the mirror table,
and Company.spec.engineConfig may not set the key.

The merged engine also offers Deployments among a seat's GitHub App
permissions, which Seat.spec.integrations.github.permissions now takes.

ADR-0004's heading takes a colon in place of the dash.

Signed-off-by: pinotelio <ahmadreza.mollapour@gmail.com>
Crewlet binds each seat's own credential to a per-seat tool server by
the variable or header the server reads, which it knows for the servers
it recognises by their command or address and otherwise takes from the
server's seat_credentials. MCPServer.spec.seatCredentials declares them:
the integration, for Atlassian the account's Token or Email, the key, and
an optional prefix such as "Bearer ". A declaration replaces what the
engine would assume, and a shared server takes none. CEL holds the
integrations, the Atlassian-only field, one header per key, and no
${NAME} in a prefix; the lists are held to the engine's in the mirror
table. as is not offered: Crewlet takes the transport's, the only value
it accepts.

A seat on Jira or Confluence that lists no projects or spaces now holds
the product's access alone, so projectRole, and spaceRole or operations,
are required only where it does.

The engine's new integrations.slack.app_config_token, with which it
creates each agent's Slack app itself, is listed as not offered: an
operator's Slack seats are apps made outside the engine whose
credentials each Seat hands in.

Signed-off-by: pinotelio <ahmadreza.mollapour@gmail.com>
Review follow-ups to per-agent integration access.

- A seat turned off on Jira or Confluence keeps its account and its
  product access: the engine takes it out of its project roles or its
  space access and narrows its token. The enabled docs said product
  access went, which is not what the engine does.
- Under Enforce, "Keep the vendor's setting" and "Keep off" become
  changes to the Seat; "Turn back on" acts at the vendor and still runs.
- ProvisionedName's example pair is nova-1 and nova1: a handle holds no
  '.'.
- An empty list reads as no list, as Crewlet reads it: a Jira seat with
  projects: [] needs no projectRole, and a Confluence spaceRole beside
  operations: [] is one grant. The CEL rules check size() and a CRD case
  sends the empty lists.

Signed-off-by: pinotelio <ahmadreza.mollapour@gmail.com>
Crewlet's main merged Chatlet (crewlet#142), the engine's own chat, as a
ninth state log every data node runs whatever its chat backend. Its 1 GiB
floor raised what a fresh node reserves on its event stream to 8120172544
bytes for the logs and 10826896725 with the nats object bucket, so a data
pod on the smallest volume the API admitted (12Gi, a 9 GiB stream) refused
to boot with "the broker refused to reserve the chat log's ceiling", and so
did every engine the suites start on a 9 GiB stream.

- EngineClass: data.storage.size admits no less than 14Gi with the files
  in the stream's bucket and 11Gi in S3, the least whose three-quarter
  share holds the engine's floor; TestContractStreamFloor holds both to
  `crewlet validate`. The getting-started guide, the e2e classes and the
  engine the suites run move to the new floor.
- The data pod's startup window counts the ninth log: the join reads one
  more trim floor listing, one more row in each and one more log's stats,
  and the admission one more maintenance window, so the boot's own bounds
  are 1866 seconds (2006 with S3), and the API's floors on startupSeconds
  follow. The window is now computed from dataPodStateLogs, which
  TestDataPodBootCountsTheEnginesStateLogs holds to the replication loops a
  running data node reports in /fleet (engine.FleetNode.StateLogs), so the
  next log the register gains fails a test instead of shortening the
  window.
- Unit channel: the engine now refuses a channel that is not a bare name
  (lowercase letters, digits, - and _, up to 64 characters), which failed
  every document with "#platform". The API takes the engine's pattern,
  TestUnitChannelFollowsTheEngine holds it to `crewlet validate` both ways,
  and the sample and fixtures name bare channels.
- Chatlet is the default chat of a company with no Mattermost or Slack
  Integration. The getting-started unit names a channel, so a new company
  gets a team room whose members follow the org chart and the engine has
  nothing to say about the guide (and the README's copy of it); the
  engine now warns of a Human seat
  with no chat contact only on a vendor chat, which the integrations guide
  says.
- Mirrors read again at abd826b859: the launch's decisions moved into
  (*agentLauncher).launch, the inbox relay became the live relay, and the
  boot's handshake replaced its call to admit, each now mirrored; every
  other moved declaration copies what it copied.

Signed-off-by: pinotelio <ahmadreza.mollapour@gmail.com>
@pinotelio
pinotelio marked this pull request as ready for review October 30, 2026 18:45
@pinotelio
pinotelio merged commit 2545570 into main Oct 30, 2026
12 of 13 checks passed
@pinotelio
pinotelio deleted the feat/seat-integration-access branch October 30, 2026 18:45
aliyousefiaan added a commit that referenced this pull request Oct 30, 2026
Unit.channel's doc says what the field holds and how Crewlet spells it,
and since crewlet#142 that on Chatlet each channel is a room whose
members follow the org chart. It did not say that such a room belongs to
one unit: Crewlet refuses a second unit naming the same channel, so a
child unit that should share its parent's room names none, which is the
one way to share it.

The rule over the field itself, and its test against the engine
(TestUnitChannelFollowsTheEngine), came with #51.

Signed-off-by: Ali Yousefian <aliyousefiaan@gmail.com>
aliyousefiaan added a commit that referenced this pull request Oct 30, 2026
Crewlet now runs a chat of its own, Chatlet: a company that declares no
Slack or Mattermost (no integrations block for either, and no seat's
Slack app) derives chat.backend chatlet, which is every company the
operator renders without such an Integration. Two of its advisories
moved with it. The warning of a Human seat with no chat contact is given
only on a vendor surface, judged per surface: a seat with no account on
the Slack or Mattermost the company talks on, a GitHub, GitLab or
Atlassian id not counting. And a company on Chatlet whose units declare
no channel is advised at units[0].channel that no team has a room the
chart keeps.

#51 already expects what the engine says of the guide's company and of
the optional-keys case, which names no channel and is advised at
units[0].channel, and the guide names its unit's channel.

That left nothing holding the narrowed contact advisory, which the Seat
API and two guides describe, so a new contract case,
TestContractContactAdvisoryFollowsTheChatSurface, holds it on the Slack
case: a Human seat with a GitHub login alone is warned at its contact of
having no account on Slack, and one with a Slack member id is not. It
goes red with either expectation flipped. The Seat contact doc,
integrations.md and people.md now say the warning is given only on Slack
or Mattermost, which accounts count, and that on Chatlet a person reads
the rooms on the dashboard and nothing is warned; the troubleshooting
page's list of what the engine says names both advisories, and two test
comments stop calling a GitHub login a chat contact.

Signed-off-by: Ali Yousefian <aliyousefiaan@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant