Agent platform (MCP)
Jentrix exposes its entire task surface as an MCP server, so AI agents operate boards with the same operations, authorization, and audit trail as the UI. This page is the platform reference: tokens, scopes, the tool catalog, and the contracts (errors, idempotency, concurrency) agents rely on. For client-by-client connection instructions, see Agent setup variants.
Endpoint and authentication
| Endpoint | Use |
|---|---|
https://tm.jentrix.ai/api/mcp | Flat Streamable HTTP server: the full tool surface is listed immediately. |
https://tm.jentrix.ai/api/mcp-dynamic | Session-based Streamable HTTP server: starts with discovery tools and loads only the tools the client selects. |
Both endpoints use Authorization: Bearer <token>. The dynamic endpoint also
returns an Mcp-Session-Id; send it on later requests and DELETE the session
when finished.
Two bearer types are accepted:
- Personal access tokens (
tm_…) — minted at/account/tokens. The right choice for your own scripts, cron agents, and CLIs. - OAuth 2.1 access tokens (
tmo_…) — issued by the built-in authorization server for third-party clients (see OAuth).
CLI access
Every tool on this page is also reachable from the shell via the jentrix CLI — a thin client over this same MCP endpoint, so it inherits scopes, workspace pinning, rate limits, idempotency, and freshness unchanged. It is a peer of connecting an MCP client directly: jentrix task list --board X --json | jq …, an escape hatch (jentrix tool <name> --args '<json>') for any tool, and frozen exit codes for scripting. See the CLI reference.
Tokens, scopes, and workspace pinning
Create tokens at /account/tokens. Each token has:
-
Scopes — exact-match classes, no hierarchy:
Scope Allows readAll list_*/get_*/ search tools and MCP resourceswriteCreating and updating tasks, comments, subtasks, contacts, sidecars, links, labels, columns, boards adminStructural/destructive and outward-facing tools: convert_board_kind, webhooks, automationsA tool call outside the token's scopes returns a structured
FORBIDDENerror before anything runs. (Tokens created before scoping existed have full access until rotated — the tokens page nags about them.) -
Workspace pinning (recommended) — restrict the token to a single workspace. Every authorization check rejects anything outside it, and
list_workspacesonly returns the pinned one. -
Agent identity — an optional display name and emoji. Every mutation the token makes is badged in the UI as
via <emoji> <name>(activity feeds, comment threads, the home digest). The identity is snapshotted into each activity at write time, so the audit trail survives token renames and deletion. -
Usage tracking — the tokens page shows per-token daily requests/mutations for the last 7 days plus the token's recent actions.
Tokens are personal credentials — only their owner manages them at /account/tokens. For governance, workspace admins additionally see every token pinned to their workspace (any member's) in workspace settings — owner, scopes, last used — and can revoke them there. Unpinned tokens aren't listed; removing the member disables those, since every call re-checks the owner's role.
Least-privilege examples: a read-only digest bot needs just read; a triage bot needs read + write; only a bot that manages webhooks or automation rules needs admin. Pin everything to one workspace unless it genuinely needs more.
Rate limits
60 requests per minute per token. Over the limit, calls still authenticate but every tool returns a RATE_LIMITED error envelope with retryAfterSeconds — agents should back off and retry, not treat it as a dead token.
Bulk export (GET /api/export)
The workspace/board export at GET /api/export?workspaceId=…[&boardId=…]&format=csv|json accepts the same bearer tokens as the MCP surface, so an agent can pull a full board dump without a browser session. JSON nests boards → columns → tasks; CSV is one flat task row per line; archived boards, columns, and tasks are excluded from both.
The endpoint reads the Authorization header first, and the bearer scheme takes strict precedence:
- Any syntactically present
Bearer <credential>(case-insensitive scheme, any prefix) is authenticated exclusively on the bearer path. There is notm_/tmo_pre-filter — prefix legality is the token verifier's job. - A verification failure — empty, malformed, unknown, revoked, or expired credential — returns 401 and never falls through to session auth, even if a valid session cookie is also present.
- A valid token needs the
readscope (a grandfathered empty-scope token has full access); anything less is 403. A workspace-pinned token exporting any other workspace is 403. - When the token is a governed worker's credential (an agent-credential-linked token, or a run-scoped bearer), the export is subject to that worker's compiled permission revision exactly as every MCP tool call is: the compiled
readscope-class check and the compiled workspace authz boundary both apply, so a worker whose published/pinned permissions refuse the read — or bind it to another workspace — gets 403 even though its owner is a member. The export is never a way around the post-claim permission boundary. - The project boundary is enforced by failing closed, not by narrowing.
GET /api/exportauthorizes at workspace grain and has no project axis — it names a workspace and optionally a board, never project ids — so a governed worker whose compiled revision binds it to a non-empty set of projects is 403, even for its own workspace. The endpoint refuses rather than invent a board/task mapping for a project-scoped restriction (which would be a false narrowing), following the permission core's rule that a restriction which cannot be applied must refuse, never silently widen. A worker with an empty project boundary means "the whole workspace" and exports unchanged; honoring a project boundary on export would be a separate change that gives the endpoint a project axis. Both this refusal and the workspace boundary above are resolved from the same binding — the revision the credential is actively governed by for the requested workspace (the run it is executing, via that run's claim-time pin; otherwise its workspace-pinned or template-head revision) — so widening a worker's template head mid-run cannot re-open the export to a run whose claim pinned a project boundary. - Each successful bearer read is counted in the token's existing daily usage rollup — no separate export counter.
Only a request with no Authorization header, or a non-Bearer scheme, uses the operator session (unchanged): a signed-in workspace member exports, everyone else gets 401. Either way the workspace-role check and the export payload are identical.
Error envelope
Failed tool calls return isError: true with a machine-readable JSON body:
{ "error": { "code": "CONFLICT", "message": "…", "hint": "…" } }
| Code | Meaning |
|---|---|
FORBIDDEN | Missing scope, wrong workspace, or insufficient role |
NOT_FOUND | Entity doesn't exist or isn't visible to you |
INVALID_INPUT | Failed validation (the message says which field) |
RATE_LIMITED | Over 60 req/min — carries retryAfterSeconds |
CONFLICT | Idempotency-key reuse with different args, or a stale write (see below) |
INTERNAL | Unexpected server error |
The hint is written for agents — follow it instead of retrying blindly.
Concurrency: expectedUpdatedAt
Every update_* tool and move_task accept an optional expectedUpdatedAt (the entity's updatedAt you last read). If someone changed the entity since, the call fails with CONFLICT — and the envelope embeds the entity's current state under error.current, so you can merge and retry without an extra read. Omit the field to last-write-wins.
Idempotency: idempotencyKey
Every create_* and bulk_* tool accepts an optional idempotencyKey (24-hour memory, scoped per token):
- Same key + same arguments → the stored response is replayed; nothing is created twice.
- Same key + different arguments →
CONFLICT.
Derive keys from your trigger (e.g. a webhook delivery id) so retries are naturally safe.
create_workspace is the one exception where the key is required: provisioning a workspace mints a workspace, its OWNER membership, and the default governance pack, so a retried CI step must never mint a second one. A key-less create_workspace call is refused with INVALID_INPUT at the schema boundary, before the handler runs. Every other create tool — invite_member included — keeps the key optional.
Membership governance
Workspace membership changes are governed in the ops core, so the gate holds on every surface (there is no self-serve membership MCP tool yet — the enforcement lives in the core, not an MCP wrapper). Three frozen action keys are evaluated through the Policy Engine before the write:
| Action key | Governs |
|---|---|
workspace.member.invite | Inviting a member (and minting an owner invite) |
workspace.member.remove | Removing a member |
workspace.member.set_role | Changing a member's role |
Every new workspace is seeded with a default pack — a REQUIRE_APPROVAL rule (overridable, so an admin can approve) for each of the three keys, one rule per key. An admin resolves a held change exactly as for any other policy evaluation (resolve_approval). Seeding is idempotent and only runs on the create path, so pre-existing workspaces are untouched.
Concurrency here is value-CAS, not expectedUpdatedAt — a workspace and a membership row carry no updatedAt. Pass expectedName when renaming a workspace, or expectedRole when removing a member / changing a role; a stale value fails with CONFLICT carrying the current one.
Response sizes: response_format
list_tasks, get_task, list_contacts, and the harness reads (list_harnesses, get_harness, list_harness_stages, list_harness_findings) take response_format: "concise" | "detailed". Lists default to concise (≤400 chars per task, ≤20k chars per page — a frozen contract); get_task and get_harness default to detailed. Paginated lists return totalCount and a notice when truncated. Every task in any response includes its number and human key (STK-123), and get_task accepts { workspaceId, number } as an alternative to the task id.
Dynamic tool loading
Connect tool-count-sensitive clients to /api/mcp-dynamic. A new session
exposes only search_tools, describe_tools, and load_tools; tools loaded in
one request become callable on the next request and remain selected in the
session's Postgres-backed state across application instances. Sessions expire
after 30 minutes. Their loaded-tool manifest is capped at 48 KiB, evicting the
oldest selections when necessary; after expiry, create a new session and reload.
The original /api/mcp endpoint remains the compatibility surface for clients
that expect every tool in tools/list.
Long-running MCP Tasks
run_eval and request_harness_stage_run advertise optional MCP Tasks support.
Task-capable clients can create a task, poll tasks/get, cancel it with
tasks/cancel, and collect the final value through tasks/result. Other clients
receive the normal synchronous-looking tool result while the SDK polls for them.
Task handles expire after 10 minutes and currently live in process memory. A serverless cold start can therefore lose an outstanding handle; the durable eval, harness, job, and run records remain the source of truth for recovery.
Connector authorization elicitation
authorize_connector uses URL-mode MCP elicitation to send an admin to Jentrix'
connector authorization flow. Secrets never travel in MCP arguments or results;
the successful result contains only a brokered credentialRef. The tool requires
admin scope plus the workspace ADMIN role, and clients without elicitation
support receive INVALID_INPUT instead of a partially configured connector.
Tool catalog (242 tools)
The surface is frozen by contract tests — names, annotations, and output schemas only change deliberately. The curated tables below cover the core task-platform families; the M13 quality-plane families (evals, immutable releases, experiments) are documented in Quality plane, the M14 Skill Registry family (list_skills / get_skill / convert_playbook_to_skill) in Skill registry, and the harness family in Harness. The complete, machine-generated list of every tool is the full generated catalog at the end of this section — pinned to the live surface by the registry drift test and reachable at runtime via search_tools.
Orientation and reads (read scope)
| Tool | What it does |
|---|---|
list_workspaces | Workspaces visible to the token |
get_board_snapshot | Board + columns + concise tasks in one call — start here |
search_tasks | Workspace-wide search by exact task key or full text (titles, descriptions, comments) |
find_similar_tasks | Semantic similarity — by example task (taskId) or free-text query (workspaceId + query). Embeddings refresh on a ~5-minute cron, so very recent edits may not match yet. Returns an empty result with a notice (never an error) when semantic search isn't configured (no embeddings provider key) |
search_tools | Discover this server's own tools: capability manifests (scope, safety annotations, parameters) filtered by query / scope / task profile, plus the derived task profiles (include_profiles). Reads the CI-generated, drift-pinned registry — see the generated catalog and task profiles below |
list_activity | Activity feed by task or board, filterable by time, type, and source (ui / mcp / automation) — separates humans from agents |
list_boards / list_columns / list_labels / list_members | Workspace structure (list_boards returns active and archived boards with archivedAt, so archived ids can be passed to unarchive_boards) |
list_tasks / get_task | Tasks (filters, pagination, concise/detailed) |
list_comments / list_attachments | Task discussion + files (attachments via short-lived signed URLs) |
list_contacts / list_initiatives / list_task_links | Directory, roadmap, and link-graph reads |
list_github_links | Tasks linked to GitHub issues (url, state, last-synced) — see GitHub sync |
list_delivery_items | The delivery graph's pull-request links: PR state, link state (MATCHED/ORPHAN), owning task key, aggregate CI signal. Filter by board, linkState (ORPHAN = adoptable PRs), or PR state — see GitHub sync |
get_pull_request | One PR link in full: review decision, mergeable state, head/base branch, CI runs with check counts, redacted CI-evidence count, GitHub deep link |
list_ci_runs / get_ci_run | CI runs (filter by PR or task) with check roll-up counts; the single-run read adds the individual child checks |
list_projects / get_project | Project containers with owner, links, milestones, risks, and counts — see Projects & decisions |
list_decisions / get_decision | The Decision Register: status, options, dissent, supersession lineage, and revision history |
list_work_orders / get_work_order | Agent-executable briefs with versions, status, and the latest pinned launch |
get_context_pack | A Work Order version's versioned, checksummed source/tool/policy manifest + token-budget estimate |
list_agent_jobs / get_agent_job | Plan → review → execute → review workflow jobs: phase, turn, runs, findings, and approval gates — see Agent workflows. Harness-bound jobs also carry harnessRun (run id, repo, base branch) for reviewer grounding. On a harness stage execution job get_agent_job additionally carries a harness block (run/stage ids, branch/repo/base + strategy/template, commit policy, acceptance criteria) so the harness-aware runner detects and drives the stage |
list_agents / get_agent | The Agent Registry: teammate identity (role, provider, runtime, trust level, status), scopes/skills, eval & failure caches, linked credentials (no secret material), and a computed launch-eligibility breakdown |
list_control_tower_items | The Control Tower: OPEN, actionable operational items ranked highest-severity-then-oldest. Agent workflow, delivery, harness, SLA, eval, policy, and credential queues are live. Governed memory review lives on the dedicated Memory page; the legacy MEMORY_CANDIDATES Tower adapter has no MemoryItem source. Each item carries a queue, stable sourceKey, an "explain why this is here" reason, severity, optional project/agent, and valid actions |
list_policies / get_policy_evaluation / list_policy_evaluations | The Policy Engine: layered rules and the append-only evaluation feed — see Policies |
list_leases / get_lease | Conflict control: active/historical work leases (target, holder, mode, conflict policy, expiry, takeover/handoff lineage) |
list_specs / get_spec | Executable specs + their immutable versions (user story / constraints / non-goals / test plan / rollback / evidence / definition of done) with weak-spec quality warnings and a 0-100 score |
list_artifacts | First-class outputs (PR/diff/log/screenshot/eval report/run summary/…) linked to task/project/run/Work Order, filterable by type/creator. Internal R2 objects expose hasObject; redacted artifacts are tombstones |
list_outcomes | Outcome metrics (cycle time, reopen rate, human edits, approval burden, …) split by agent-assisted vs human-only cohort; substrateGated kinds await the run-ledger/eval substrate |
list_sources | Governed knowledge sources (trust/freshness/privacy/owner/client/supersession); superseded excluded unless includeSuperseded |
list_incidents / get_incident | Postmortems: timeline, root cause, affected entities, rollback log, policy/eval gaps, linked decisions/policies/credentials/artifacts, and follow-up tasks — see Later hardening |
list_simulations / get_simulation | Dry-run plans + their projectedEffects (diffs, projected notifications/activities/webhooks/automations/cost/credentials/approvals/lease-conflicts) computed before any side effect |
list_playbooks | Promoted reusable workflows with trigger, required policy/eval coverage, and enabled state |
list_communication_sources | Governed Slack/email/calendar/doc connectors (extend a source record; outboundEnabled gates drafts) |
list_linked_messages | Stakeholder messages/meetings linked to a task/project/contact/decision/Work Order, with meeting commitments |
list_harnesses / get_harness | Harness Automation runs (the governed spec→plan→stage→commit build loop): status, current stage, freeze state, and the LIVE registry projection — per-stage status/gate/branch/commit/evidence + done/total — generated from the stage records, not a stored markdown blob. The get_harness output is a frozen projection; read pending gates, active/stuck runs, and operability blockers through the companion calls (list_harness_stages, list_agent_jobs / get_agent_job, list_control_tower_items) rather than expecting them here. See Harness Automation |
list_harness_stages | A run's stages in order: status, the human gate it parks at (plan_review/execution_review/commit_gate), branch, next build action, open-finding + evidence counts, launchable (whether the stage has a backing Work Order — false for a draft stage; it means "not a draft", not "launchable right now", since serial ordering + run/stage status still apply at launch), and (detailed) Work Order/lease ids + acceptance criteria |
get_harness_artifact | One harness artifact by its link id (from get_harness): kind, version, checksum, supersession lineage, producing agent/model, plus a short-lived signed download pointer — the body is never inlined and the title is redacted |
list_harness_findings | A run's review findings (severity / category / status / target stage), filterable by stage, status, or severity |
Harness writes (write scope)
The Phase-2 tools that let an external runner drive the harness build loop. No gate-approval, merge, or push tool exists, and an agent has no path to unfreeze its own run — those human boundaries stay UI-first. (Freeze/resume are exposed as admin tools below for operators, never for a run's own agent token.) See Harness Automation for the full system and Agent workflows for the loop engine it reuses.
| Tool | What it does |
|---|---|
create_harness | Create a harness run over a task/project/repo scope (idempotencyKey); returns its id + slug |
submit_harness_artifact | Attach a generated artifact (PRD/plan/prompt/review/summary/diff/test-evidence) — capture new redacted markdown or classify an existing visible Artifact (idempotencyKey); returns the link or null when the artifact store is unconfigured |
submit_harness_review | Reviewer verdict (APPROVED / CHANGES_REQUESTED) + structured findings on a stage/plan job — the SAME M7 review contract; the findings normalize into the harness finding table so the validation gate + re-review continuity see them (idempotencyKey) |
update_harness_finding | Resolve a finding (ACCEPTED / FIXED / REJECTED / VERIFIED / SUPERSEDED, or OPEN to reopen a fixed/verified finding on a regression; rejection/supersession need a note) with an expectedUpdatedAt optimistic-concurrency guard (stale → CONFLICT embedding the finding's current status/resolution) |
request_harness_stage_run | Launch the run's first undone stage (execution is serial, so it is the only one in line): acquires the stage lease + opens a fresh M7 AgentJob (idempotencyKey). The stage must have executable backing — a first-undone draft stage (no Work Order, e.g. a create_harness_stage draftOnly placeholder) is rejected as a draft, never launched. Optional stageId asserts which stage you expect (a mismatch is rejected, never launches the wrong one) |
submit_harness_stage_result | Record a stage's structured execution output (changed files, commands, tests run/not-run, risks, and the branch/commit + diffLines it landed on) as a redacted execution-summary + link diff/test evidence (idempotencyKey); returns the validation gate (validatable + missing evidence + blocking-finding count). When a commit is reported, pass your claimed jobId — it binds the commit report to your attempt so a superseded attempt's late report is dropped (no commit-on-red), and it is what the M9.4 commit boundary later pushes/PRs per policy |
Harness admin (admin scope)
The run-level operator controls — freeze is one of the two controls the builder keeps (the other is the gate). Freeze is enforced at every Jentrix boundary immediately: claim_* rejects, submit_* rejects/parks, heartbeat_* returns a frozen signal so the runner pauses its subprocess on its next tick, and brokered repo:* / deploy:* / cloud credential requests fail closed. Resume restores the run's exact recorded prior status and is fully attributed. These controls require the admin scope and the admin role — an agent's own run token can never reach them.
| Tool | What it does |
|---|---|
freeze_harness | Freeze a run (the human STOP control): rejects new claims, parks submissions, signals frozen on heartbeat, and fails brokered credentials closed. Records the freezing actor + reason (redacted) + the prior status to restore. expectedUpdatedAt guard |
unfreeze_harness | Resume a frozen run — restores its exact prior status and clears the freeze. An operator control (admin scope + role): no agent self-resume. Records the resuming actor + reason. expectedUpdatedAt guard |
cancel_harness | Cancel a run while preserving its audit history (artifacts/findings/decisions/activity stay queryable) — a terminal state move, not an archive/delete |
set_harness_policy | Change a run's loop policy: iteration and auto-retry budgets, ordering, reviewer/human-gate requirements, auto-advance (including the first decomposition loop after spec acceptance), and branch strategy. expectedUpdatedAt guard |
set_harness_agents | Set the run's active PLANNER + REVIEWER agents so it can launch execution jobs — pass an AgentProfile id per role (plannerAgentId/reviewerAgentId); each must be ACTIVE, usable in the workspace, with exactly one usable linked PAT. The token is resolved server-side from the agent, never accepted from input. Supersedes prior assignments (archived, not deleted). Idempotent (re-applying the same agents is a no-op) with an expectedUpdatedAt guard so two admins can't silently clobber each other (stale → CONFLICT embedding the run's current planner/reviewer/ready state) |
start_harness_authoring | Move a DRAFT run into the authoring loop: DRAFT → AUTHORING, seed a run-owned draft Spec, and open the run-bound authoring agent job (the planner/reviewer run the review loop toward the run's spec), emitting the normal agent.handoff. The run-owned spec is a scaffold seed pinned on the run, so two harnesses on one anchor task never share a draft (wiring the planner's reviewed output into that spec's versions is the harness-aware runner's job, H10.6). Requires an anchor task, a repository on the run, and active planner+reviewer agents (set via set_harness_agents). Approving the resulting plan-approval gate (UI / Control Tower — no MCP self-approval) accepts the run-owned spec, pins its SpecVersion, and advances the run AUTHORING → PLANNING. A BLOCKED authoring job can be superseded in place by calling the tool again; the blocked job is archived and a fresh attempt is opened. expectedUpdatedAt guard. Returns the run's new status + the opened job id |
start_harness_decomposition | Move a PLANNING run (spec validated + pinned) into the decomposition loop: open the run-bound decomposition agent job, emitting the normal agent.handoff. The planner proposes ordered stages via submit_plan's harnessStages (each tracing the accepted spec criteria it delivers — keyed to the submitting job and persisted as the proposal the accept gate mints); the reviewer validates the plan version the same submit freezes. Approving the resulting plan-approval gate (UI / Control Tower — no MCP self-approval) validates that every accepted spec criterion is covered (before the gate is consumed and again inside the mint tx, against that job's persisted proposal), then mints the ordered stages + 1:1 Work Orders, snapshots the registry, and advances the run PLANNING → READY — an uncovered plan is rejected (naming the orphaned criteria) and the gate stays pending. Requires an anchor task, a repository, and active planner+reviewer agents; at most one live decomposition job per run. A BLOCKED decomposition job can be superseded in place by calling the tool again. The run stays PLANNING until the gate mints the stages. expectedUpdatedAt guard. The generic request_agent_workflow cannot start decomposition; this harness-bound tool is the only way in. Returns the run's status + the opened job id |
create_harness_stage | Hand-author a stage directly on a run — the fallback to the coverage-enforced spec→plan decomposition path (start_harness_decomposition). admin scope + ADMIN role — authoring a stage changes the run's executable plan scope. Appends it after the existing stages (next 1-based ordinal) carrying the same full plan a decomposition proposal does — title, acceptance criteria, traced spec criteria, expected branch behavior, suggested tests, risk notes, branch, commit policy (idempotencyKey); it still bypasses decomposition's coverage enforcement. A non-draft stage gets executable backing: it auto-mints a 1:1 Work Order when workOrderId is omitted, so it can be passed to request_harness_stage_run (the run must be task-anchored and non-terminal; a supplied workOrderId must back the run's anchor task; and the stage must carry at least one acceptance criterion + one suggested test). Pass draftOnly: true for a non-executable placeholder (no Work Order minted; request_harness_stage_run rejects it as a draft, and list_harness_stages/the UI mark it launchable: false). Returns the new stage's id + number |
authorize_harness_local_commit | The external runner's pre-commit repo:commit permission check (keyed on the stage). Authorization only: honors freeze / leases / repo:commit policy but mints no credential and approves no gate — the runner performs the local git commit itself, then reports the SHA via submit_harness_stage_result. Pass the resolved branch + the changedPaths / diffLines the commit will touch so branch-scoped leases and the high_risk_change diff-size policy evaluate the actual target before the commit (the not-yet-pinned-branch case otherwise bypasses them). Only a VALIDATED/COMMIT_GATE stage on a non-frozen run can authorize; otherwise CONFLICT/FORBIDDEN. admin scope + ADMIN role — a run's own agent token can never reach it. Returns { authorized, harnessRunId, branch }. The push/PR remains the single-use brokered repo:push / repo:pr_create path; merge stays the separate always-human MERGE gate |
Task writes (write scope)
| Tool | What it does |
|---|---|
create_task / update_task / move_task / archive_task | The core lifecycle (archive = complete) |
set_task_labels / set_task_assignees | Replace-style setters |
bulk_create_tasks | Up to a column's worth in one all-or-nothing transaction |
bulk_update_tasks / bulk_move_tasks | Per-item validation; valid writes apply in one transaction with per-item results |
create_comment / update_comment / delete_comment | Comments (mentions param notifies members; edits are author-only) |
create_subtask / toggle_subtask / delete_subtask | Checklists |
add_task_link / remove_task_link | Dependency graph (BLOCKS / DEPENDS_ON / RELATES_TO / DUPLICATES; cycles rejected with the path) |
link_task_to_issue | Link an existing task to a GitHub issue by number (idempotent) — see GitHub sync |
link_task_to_pr | Adopt an ORPHAN pull-request link onto a task (idempotent; expectedUpdatedAt guard). The task must be on the board linked to the PR's repo (repo ↔ board is 1:1) — see GitHub sync |
acquire_lease | Claim a work lease before starting work; returns a CONFLICT (with the active lease in error.current) when the target is exclusively/blocking-leased and no policy-approved takeover is supplied |
release_lease | Release a lease you hold (idempotent; re-release no-ops) |
attach_artifact | Attach a first-class artifact by external URL or internal R2 object, linked to a task/project/run/Work Order |
create_project / create_decision | Create a project container / propose a Decision Register entry (graduated in M6) |
create_work_order / version_work_order / request_run | Create a Work Order, freeze an immutable version + Context Pack, and request an agent run (PENDING until a runner claims it) |
create_simulation | Dry-run a staged plan; returns diffs + projected side effects and never commits (approve + execute stay on the human surface — no execute_simulation tool in v1) |
create_incident | Escalate a failed run / bad action into a structured incident |
Kind sidecars and contacts (write scope)
| Tool | What it does |
|---|---|
update_deal / set_deal_contacts | CRM deal fields + linked contacts |
update_ticket / link_contact | Support ticket fields + its contact |
update_bug_report | Bug severity, repro, versions, resolution |
update_initiative | Roadmap quarter, confidence, effort, RICE |
create_contact / update_contact / archive_contact | The People directory |
Structure (write scope)
| Tool | What it does |
|---|---|
create_board / rename_board | Boards (kind chosen at creation) |
manage_columns / manage_labels | Consolidated action-param tools (create / rename / move / archive…) |
Agent workflows (write / admin)
The plan → review → execute → review loop (full reference). The agent-facing protocol is write; the runner's lifecycle tools are runner-only admin.
| Tool | Class | What it does |
|---|---|---|
request_agent_workflow | write | Start a two-agent workflow on a task (planner + reviewer agents, max iterations) |
submit_plan | write | Planner turn: freeze the plan as a Work Order version; optional finding resolutions. For a harness decomposition job, pass harnessStages — the proposed ordered stages the accept gate mints (rejected for any other workflow) |
submit_review | write | Reviewer turn: verdict (APPROVED / CHANGES_REQUESTED) + findings |
submit_execution_result | write | Executor turn: execution summary + artifact ids |
claim_agent_job | admin | Runner-only: claim the current turn → opens a run, returns its runId |
heartbeat_agent_job | admin | Runner-only: keep the active run alive (10-minute TTL). For a harness stage job, the result carries frozen: true when the run has been frozen — the runner must pause/interrupt its subprocess on that tick |
fail_agent_job | admin | Runner-only: hard-fail the active run → the job goes BLOCKED |
Connected sessions (M20.1)
Project-scoped interactive Claude Code / Codex sessions (CLI guide). One AgentSession anchor; ordered events live in redacted R2 TRACE artifacts, closure stores a deterministic RUN_SUMMARY. Session heartbeat and trace-part ingestion are authenticated REST boundaries (/api/agent-sessions/*), deliberately not tools.
| Tool | Class | What it does |
|---|---|---|
resolve_projects_for_repo | read | Cross-visible-workspace project candidates for a normalized owner/name; refused for workspace-pinned credentials (PROJECT_DISCOVERY_REQUIRES_UNPINNED_LOGIN) |
create_agent_session | write | Create a STARTING session before provider launch (project confirmed, repo link checked fail-closed). Requires idempotencyKey |
attach_agent_session | write | Bind a TRUSTED current provider thread — late-bind a started session (CAS) or attach-create; repeats converge, competitors fail SESSION_ALREADY_BOUND. Requires idempotencyKey |
get_agent_session / list_agent_sessions | read | Session metadata, capture integrity (orthogonal to lifecycle), the stable usage object (provider receipts or null — never estimated), and — on the detail read — the append-only task-alignment interval history (alignments[], task-performance-monitoring PRD AC1.5) |
resume_agent_session | write | Reopen an eligible INTERRUPTED session (same project/repo/operator). Requires expectedUpdatedAt |
complete_agent_session | write | Close with an honest outcome; the server verifies the trace-part manifest and stores the RUN_SUMMARY. Requires expectedUpdatedAt |
get_work_order_version | read | The FULL frozen body of one immutable plan version (+ Context Pack reference) |
Session correlation header. The runner sends X-Stacks-Session-Id next to the bearer: the bearer authorizes, the header only correlates (a session id grants nothing). Verification applies syntax checks only; safe() validates the claim lazily once per request and caches the verdict, so requests without the header pay zero session queries, and artifact/activity writers stamp the validated session id automatically — no tool takes a sessionId argument. This header is UNRELATED to the dynamic mount's Mcp-Session-Id (an MCP transport session): X-Stacks-Session-Id names a durable AgentSession row. MCP resources deliberately stay uncorrelated.
Admin (admin scope)
| Tool | What it does |
|---|---|
convert_board_kind | Convert a board between kinds (backfills sidecars) |
unarchive_boards | Atomically restore 1–50 boards in one workspace; already-active boards are idempotent no-ops |
create_webhook / list_webhooks / delete_webhook / list_webhook_deliveries | Outbound webhooks |
list_automations / create_automation / set_automation_enabled | Automation rules |
list_github_connections / link_github_repo / unlink_github_repo | GitHub sync — connection + repo↔board mapping |
resolve_approval | Resolve a REQUIRE_APPROVAL policy evaluation by minting a scoped, time-bounded approval binding (Policies) |
create_agent_release / clone_agent_release / mark_release_eligible / set_active_release | Immutable-release lifecycle: cut or safely clone a CANDIDATE, record the green gating verdict (CANDIDATE → ELIGIBLE), move the active pointer (Quality plane) |
seed_starter_eval_dataset | Seed grader smoke plus the explicit real-provider managed release gate; returns the managed dataset head to pin (Quality plane) |
request_credential_grant | Mint a short-lived, scoped, action-bound credential grant; material is minted only at the injection boundary (Credential broker) |
freeze_harness / unfreeze_harness / cancel_harness / set_harness_policy / set_harness_agents / start_harness_authoring / start_harness_decomposition / authorize_harness_local_commit | Run-level harness operator + runner controls (see Harness admin above) |
All tools carry proper MCP spec annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint: false) and typed output schemas, so clients can reason about safety before calling.
Provisioning primitives (M19.1)
The Foundry Provisioning Plane exposes ~25 already-shipped operations over MCP (plus four tightenings) so a headless agent can provision and run a workspace end to end. Every tool wraps the same ops core the UI uses — no new engine, and the harness five add no transition legality. All are admin scope except create_workspace (write) and get_token_context (read).
| Tool | What it does |
|---|---|
create_workspace / update_workspace | Create a workspace (idempotencyKey required — a key-less call is refused before the handler runs; refused for a workspace-pinned token) and rename it (slug/taskPrefix stay write-once; concurrency is VALUE-CAS via expectedName, not expectedUpdatedAt). Delete stays UI-only |
invite_member / revoke_invitation / list_invitations / set_member_role / remove_member | Staff a workspace under the default membership-governance pack (invite/remove/set-role park behind REQUIRE_APPROVAL); membership concurrency is VALUE-CAS via expectedRole. See Membership governance |
archive_board / delete_automation / stop_experiment / merge_contacts | Structure & cleanup closure (the destructive inverses of the shipped create paths) |
update_harness / archive_harness / transition_harness_stage / link_harness_decision / record_harness_disposition | Harness lifecycle closure — the ops keep refusing illegal §10.6 actions (Harness) |
create_agent_profile / update_agent_profile / link_agent_credential / save_worker_draft | Agent authoring: profile → credential link → worker draft |
create_model_profile / discover_model_profiles / set_model_profile_enabled / archive_model_profile / set_provider_connection_enabled / archive_provider_connection / create_credential_record | Provider/model/credential authoring (credential records carry references only — no secret enters via MCP) |
create_eval_dataset / add_eval_cases | Cut a gating dataset — each append cuts a new immutable version (Quality plane) |
import_contacts / import_tasks / import_trello_board | Bulk data in (idempotent by externalId/email; chunked; Trello lists→columns) |
list_installation_repos / set_repo_config / start_repo_import / map_github_user | GitHub repo wiring after the one human install (GitHub sync) |
get_token_context | Report THIS token's scopes (incl. grandfathered-empty), workspace pinning, identity, and rate-limit window — no bearer material. jentrix whoami renders it |
retire_agent_release | Terminal release retirement (CONFLICT while the pointer holds it; idempotent replay on RETIRED) (Quality plane) |
Adjacent to these — authorised OUTSIDE the M19.1 R-list (AGE-898) but load-bearing
for the remediation contract below — is publish_permission_revision (admin;
governed worker.permission.publish, seeded REQUIRE_APPROVAL): the atomic compile +
GRANT boundary that writes a worker's CURRENT permission revision. It clears the
permission_revision_unset and compiled-artifact staleness blockers a launch
preview reports when the launch inherits that current revision — but it writes
the profile side, which an explicit per-assignment pin overrides, so a PINNED
revision is corrected in the template draft instead (below) (Harness).
Every launch-preview blocker names a remediation (C14 / M19-AC16). Each
blockers[]/warnings[] finding from preview_harness_template_launch carries
a remediation — an ORDERED, NON-EMPTY sequence of steps [{ kind: "tool" | "ceremony", ref }, …] applied in order — next to its verbatim message. Each
step's ref is either an MCP tool name present in this catalog or an H1–H5
ceremony id from the provisioning ledger (prds/foundry-provisioning-plane-prd.md
§1). A live-state fix that takes effect immediately is one step (a worker with no
active release → [set_active_release]); a fix that edits the template
DEFINITION is two, because the preview resolves against the IMMUTABLE PUBLISHED
version — the draft edit is inert until published — so it is
[update_harness_template_draft, publish_harness_template_version]. The
compiled-permission blockers split by PROVENANCE, not by code: an assignment
that PINS an unusable revision resolves to the two-step draft-then-publish
sequence (correct or clear the pin so it falls through to the worker's current
revision), while an inherited or absent one resolves to
[publish_permission_revision] (republishing writes the profile side the launch
reads directly). The remediation is REQUIRED and never empty — a blocker with no
reachable path is the prose-only dead end AC16 forbids — and the remediation-map
test proves each sequence both names real refs AND actually clears its blocker, by
triggering it through the real preview_harness_template_launch, applying every
step in order, and re-running the preview (operator ruling #4 on
cmsbltbg800ww04jsj3xu5uhj, 2026-08-03: a single ref could not express the
draft-then-publish case truthfully).
Full generated catalog
Every registered tool, its required token scope, and its safety flags — generated from the live MCP surface by pnpm gen:tool-registry and pinned to the registry (src/lib/mcp/tool-registry.generated.json) by a drift test, so a hand edit that diverges from the surface is refused in CI. search_tools serves the same manifests at runtime.
242 tools, generated from the live MCP surface and pinned to the registry by tests/mcp/tool-registry-docs.test.ts. Reachable via search_tools.
| Tool | Scope | Read-only | Destructive | Idempotent |
|---|---|---|---|---|
acquire_lease | write | – | – | – |
add_eval_cases | admin | – | – | – |
add_project_link | write | – | – | ✓ |
add_task_link | write | – | – | ✓ |
align_agent_session | write | – | – | ✓ |
archive_board | admin | – | ✓ | – |
archive_contact | write | – | ✓ | – |
archive_harness | admin | – | ✓ | – |
archive_model_profile | admin | – | ✓ | – |
archive_provider_connection | admin | – | ✓ | – |
archive_task | write | – | ✓ | – |
attach_agent_session | write | – | – | – |
attach_artifact | write | – | – | – |
authorize_connector | admin | – | – | – |
authorize_harness_local_commit | admin | – | – | – |
bulk_create_tasks | write | – | – | – |
bulk_move_tasks | write | – | – | – |
bulk_update_tasks | write | – | – | ✓ |
cancel_harness | admin | – | ✓ | – |
claim_agent_job | admin | – | – | – |
clone_agent_release | admin | – | – | – |
clone_harness_template | admin | – | – | – |
compare_harness_template_versions | read | ✓ | – | – |
complete_agent_session | write | – | – | ✓ |
configure_software_company | admin | – | – | ✓ |
convert_board_kind | admin | – | ✓ | ✓ |
convert_playbook_to_skill | admin | – | – | ✓ |
create_agent_profile | admin | – | – | – |
create_agent_release | admin | – | – | – |
create_agent_session | write | – | – | – |
create_automation | admin | – | – | – |
create_board | write | – | – | – |
create_comment | write | – | – | – |
create_contact | write | – | – | – |
create_credential_record | admin | – | – | – |
create_decision | write | – | – | – |
create_eval_dataset | admin | – | – | – |
create_harness | write | – | – | – |
create_harness_stage | admin | – | – | – |
create_harness_template | admin | – | – | – |
create_incident | write | – | – | – |
create_model_profile | admin | – | – | – |
create_operations_request | write | – | – | – |
create_project | write | – | – | – |
create_provider_connection | admin | – | – | – |
create_simulation | write | – | – | – |
create_subtask | write | – | – | – |
create_task | write | – | – | – |
create_webhook | admin | – | – | – |
create_work_order | write | – | – | – |
create_workspace | write | – | – | – |
delete_automation | admin | – | ✓ | – |
delete_comment | write | – | ✓ | – |
delete_subtask | write | – | ✓ | – |
delete_webhook | admin | – | ✓ | – |
discover_model_profiles | admin | – | – | ✓ |
export_harness_template | read | ✓ | – | – |
fail_agent_job | admin | – | – | – |
find_similar_tasks | read | ✓ | – | – |
freeze_harness | admin | – | – | – |
get_agent | read | ✓ | – | – |
get_agent_job | read | ✓ | – | – |
get_agent_release | read | ✓ | – | – |
get_agent_session | read | ✓ | – | – |
get_artifact | read | ✓ | – | – |
get_board_snapshot | read | ✓ | – | – |
get_ci_run | read | ✓ | – | – |
get_company_pulse | read | ✓ | – | – |
get_context_pack | read | ✓ | – | – |
get_decision | read | ✓ | – | – |
get_eval_case | read | ✓ | – | – |
get_eval_dataset | read | ✓ | – | – |
get_eval_run | read | ✓ | – | – |
get_experiment | read | ✓ | – | – |
get_harness | read | ✓ | – | – |
get_harness_artifact | read | ✓ | – | – |
get_harness_template | read | ✓ | – | – |
get_incident | read | ✓ | – | – |
get_lease | read | ✓ | – | – |
get_memory | read | ✓ | – | – |
get_policy_evaluation | read | ✓ | – | – |
get_project | read | ✓ | – | – |
get_pull_request | read | ✓ | – | – |
get_simulation | read | ✓ | – | – |
get_skill | read | ✓ | – | – |
get_software_company | read | ✓ | – | – |
get_spec | read | ✓ | – | – |
get_task | read | ✓ | – | – |
get_token_context | read | ✓ | – | – |
get_work_order | read | ✓ | – | – |
get_work_order_version | read | ✓ | – | – |
heartbeat_agent_job | admin | – | – | ✓ |
import_contacts | admin | – | – | ✓ |
import_harness_template | admin | – | – | – |
import_tasks | admin | – | – | ✓ |
import_trello_board | admin | – | – | – |
invite_member | admin | – | – | – |
launch_harness_template | admin | – | – | – |
link_agent_credential | admin | – | – | – |
link_contact | write | – | – | ✓ |
link_github_repo | admin | – | – | – |
link_harness_decision | admin | – | – | ✓ |
link_task_to_issue | write | – | – | ✓ |
link_task_to_pr | write | – | – | ✓ |
list_activity | read | ✓ | – | – |
list_agent_jobs | read | ✓ | – | – |
list_agent_releases | read | ✓ | – | – |
list_agent_sessions | read | ✓ | – | – |
list_agents | read | ✓ | – | – |
list_artifacts | read | ✓ | – | – |
list_attachments | read | ✓ | – | – |
list_automations | admin | ✓ | – | – |
list_boards | read | ✓ | – | – |
list_ci_runs | read | ✓ | – | – |
list_columns | read | ✓ | – | – |
list_comments | read | ✓ | – | – |
list_communication_sources | read | ✓ | – | – |
list_contacts | read | ✓ | – | – |
list_control_tower_items | read | ✓ | – | – |
list_decisions | read | ✓ | – | – |
list_delivery_items | read | ✓ | – | – |
list_eval_cases | read | ✓ | – | – |
list_eval_datasets | read | ✓ | – | – |
list_eval_runs | read | ✓ | – | – |
list_experiments | read | ✓ | – | – |
list_github_connections | admin | ✓ | – | – |
list_github_links | read | ✓ | – | – |
list_harness_findings | read | ✓ | – | – |
list_harness_stages | read | ✓ | – | – |
list_harness_templates | read | ✓ | – | – |
list_harnesses | read | ✓ | – | – |
list_incidents | read | ✓ | – | – |
list_initiatives | read | ✓ | – | – |
list_installation_repos | admin | ✓ | – | – |
list_invitations | admin | ✓ | – | – |
list_labels | read | ✓ | – | – |
list_leases | read | ✓ | – | – |
list_linked_messages | read | ✓ | – | – |
list_members | read | ✓ | – | – |
list_memory | read | ✓ | – | – |
list_outcomes | read | ✓ | – | – |
list_playbooks | read | ✓ | – | – |
list_policies | read | ✓ | – | – |
list_policy_evaluations | read | ✓ | – | – |
list_projects | read | ✓ | – | – |
list_provider_connections | read | ✓ | – | – |
list_release_transitions | read | ✓ | – | – |
list_simulations | read | ✓ | – | – |
list_skills | read | ✓ | – | – |
list_sources | read | ✓ | – | – |
list_specs | read | ✓ | – | – |
list_task_links | read | ✓ | – | – |
list_tasks | read | ✓ | – | – |
list_webhook_deliveries | admin | ✓ | – | – |
list_webhooks | admin | ✓ | – | – |
list_work_orders | read | ✓ | – | – |
list_workers | read | ✓ | – | – |
list_workspaces | read | ✓ | – | – |
manage_columns | write | – | ✓ | – |
manage_labels | write | – | ✓ | – |
map_github_user | admin | – | – | ✓ |
mark_release_eligible | admin | – | – | – |
merge_contacts | admin | – | ✓ | – |
move_task | write | – | – | – |
preview_harness_template_launch | read | ✓ | – | – |
promote_experiment | admin | – | – | – |
promote_harness_template_from_run | admin | – | – | – |
publish_harness_template_version | admin | – | – | – |
publish_permission_revision | admin | – | – | – |
reconcile_software_company | admin | – | – | ✓ |
record_harness_acceptance_evidence | admin | – | – | – |
record_harness_disposition | admin | – | – | – |
record_run_usage | admin | – | – | ✓ |
release_lease | write | – | – | ✓ |
remove_member | admin | – | ✓ | – |
remove_project_link | write | – | – | ✓ |
remove_task_link | write | – | – | ✓ |
rename_board | write | – | – | ✓ |
request_agent_workflow | write | – | – | – |
request_credential_grant | admin | – | – | – |
request_delivery_workflow | write | – | – | – |
request_harness_stage_run | write | – | – | – |
request_run | write | – | – | – |
resolve_approval | admin | – | – | ✓ |
resolve_projects_for_repo | read | ✓ | – | – |
resume_agent_session | write | – | – | ✓ |
retire_agent_release | admin | – | ✓ | – |
retry_harness_stage | admin | – | – | ✓ |
revoke_invitation | admin | – | ✓ | – |
run_eval | write | – | – | – |
save_worker_draft | admin | – | – | ✓ |
search_artifacts | read | ✓ | – | – |
search_tasks | read | ✓ | – | – |
search_tools | read | ✓ | – | – |
seed_starter_eval_dataset | admin | – | – | ✓ |
send_communication | admin | – | – | – |
set_active_release | admin | – | – | – |
set_automation_enabled | admin | – | – | ✓ |
set_deal_contacts | write | – | – | ✓ |
set_harness_agents | admin | – | – | ✓ |
set_harness_policy | admin | – | – | ✓ |
set_harness_template_archived | admin | – | ✓ | – |
set_member_role | admin | – | – | ✓ |
set_model_profile_enabled | admin | – | – | ✓ |
set_provider_connection_enabled | admin | – | – | ✓ |
set_repo_config | admin | – | – | ✓ |
set_software_company_enabled | admin | – | – | ✓ |
set_task_assignees | write | – | – | ✓ |
set_task_labels | write | – | – | ✓ |
set_worker_enabled | admin | – | – | ✓ |
start_experiment | admin | – | – | – |
start_harness_authoring | admin | – | – | – |
start_harness_decomposition | admin | – | – | – |
start_repo_import | admin | – | – | ✓ |
stop_experiment | admin | – | – | – |
submit_execution_result | write | – | – | – |
submit_harness_artifact | write | – | – | – |
submit_harness_review | write | – | – | – |
submit_harness_stage_result | write | – | – | – |
submit_plan | write | – | – | – |
submit_review | write | – | – | – |
toggle_subtask | write | – | – | ✓ |
transition_harness_stage | admin | – | – | ✓ |
unarchive_boards | admin | – | – | ✓ |
unfreeze_harness | admin | – | – | – |
unlink_github_repo | admin | – | ✓ | – |
update_agent_profile | admin | – | – | ✓ |
update_bug_report | write | – | – | ✓ |
update_comment | write | – | – | ✓ |
update_contact | write | – | – | ✓ |
update_deal | write | – | – | ✓ |
update_harness | admin | – | – | ✓ |
update_harness_finding | write | – | – | ✓ |
update_harness_template_draft | admin | – | – | ✓ |
update_initiative | write | – | – | ✓ |
update_project | write | – | – | ✓ |
update_task | write | – | – | ✓ |
update_ticket | write | – | – | ✓ |
update_workspace | admin | – | – | ✓ |
validate_harness_template | read | ✓ | – | – |
verify_provider_connection | admin | – | – | ✓ |
version_work_order | write | – | – | – |
Task profiles
Task profiles are recommended tool bundles per agent workflow, derived from the registry + tool-span telemetry (which tools co-occur within a task's mcp.tool_call spans) — never hand-curated. They are generated alongside the catalog and served by search_tools (include_profiles: true, or profile: "<key>" to filter the catalog to one bundle).
Task profiles are DERIVED from the registry + tool-span telemetry (co-occurrence of mcp.tool_call tool names within a task session), never hand-curated. Fetch them via search_tools with include_profiles.
| Profile | Task | Scopes | Tools |
|---|---|---|---|
daily_standup | Daily standup | read | get_board_snapshot, list_activity, list_task_links |
pipeline_review | Pipeline review | read | get_task, list_boards, list_tasks |
sla_watch | SLA watch | read, write | create_comment, get_task, list_tasks |
triage_bugs | Triage bugs | read, write | add_task_link, create_comment, get_board_snapshot, get_task, move_task, search_tasks, update_bug_report |
Resources
Read-only context documents (require read scope; honor workspace pinning):
| URI | Contents |
|---|---|
stacks://workspace/{id} | Workspace overview: boards, members, counts (the template lists your workspaces) |
stacks://board/{id} | Concise board snapshot |
stacks://task/{key} | Full task detail by human key, e.g. stacks://task/STK-42 |
stacks://lease/{id} | One work lease (target, holder, mode, conflict policy, status, expiry, lineage) |
stacks://artifact/{id} | One artifact + a short-lived signed download URL (internal) or the external URL; redacted ⇒ tombstone |
Prompts
Canned workflows over the tool surface, ready to invoke from MCP clients that support prompts:
triage_bugs— work the Triage column of a bugs boarddaily_standup— summarize the last 24h of activity, humans vs. agentspipeline_review— CRM pipeline health passsla_watch— find tickets at or near SLA breach
OAuth 2.1 (for third-party clients)
For clients you don't mint a PAT for (e.g. hosted agent products), Jentrix ships a complete OAuth 2.1 authorization server:
- Discovery: RFC 9728 protected-resource metadata at
/.well-known/oauth-protected-resource; RFC 8414 AS metadata at/.well-known/oauth-authorization-server. - Flow: authorization code + PKCE (S256, required).
GET /oauth/authorizeshows a consent screen where you pick the scopes and workspace the client gets;POST /oauth/tokenexchanges codes and rotates refresh tokens (single-use; the paired access token is revoked on rotation). - Client registration, two supported ways, no manual step in either: CIMD, where the
client_idis an HTTPS URL serving the client's metadata document (preferred — anyone can re-fetch it and check who the client claims to be), and RFC 7591 dynamic registration atPOST /oauth/registerfor clients that implement only that (Codex), which issues an opaquetmc_client id. Registration mints an identifier, never an authorization: public clients only (no client secret), PKCE still required, and the consent screen marks a self-registered client as unverified so its chosen name isn't mistaken for a checked one. - Tokens: access tokens (
tmo_) live 1 hour; refresh tokens (tmr_) are never valid as bearers. OAuth grants get the same scope/workspace model as PATs, and the client's name automatically becomes the agent identity badge. - Token lists stay clean: because each refresh mints a fresh access token, expired and rotated-away
tmo_rows are hidden from the tokens page and workspace governance list — only the live one shows. (Revoked PATs remain visible for audit.)
Attribution and auditability
Everything an agent does is tagged source: "mcp" with the token's id, name, and identity snapshotted into the activity payload. In the UI this renders as the agent badge; over the API, list_activity can filter by source — so "what did the bots do yesterday?" is one call. The bundled standup agent is exactly that.
Agent Registry and Control Tower
Agents are teammates; tokens are credentials. An AgentProfile is a durable, routed identity — name/emoji, role, provider, runtime, trust level, scopes, approved skills, routing boundaries, cost profile, eval/failure caches, escalation path, and status (ACTIVE / PAUSED / DISABLED / DEPRECATED). Tokens stay credentials; an AgentCredentialLink is the only bridge between them and never stores secret material — it references existing token rows. Personal agents live at /account/agents; workspace-governed agents at /[workspace]/settings/agents (registering, trust changes, status, and credential links are admin-gated). An admin binds a worker by choosing its model — in the registry row itself, and on the agent's own page. That one choice is the whole binding: a model profile already belongs to a provider connection and a runtime adapter, so both are derived from it rather than asked for as prior steps. Its parameters, prompt and fallback sit behind the same control, and only the generation controls the resolved adapter can actually enforce are editable — the rest render disabled, carrying the adapter's own reason. Existing displayName tokens are backfilled into default agent profiles so historical attribution badges keep resolving.
get_agent returns a launch-eligibility breakdown — one pure, explainable check per dimension (active status, trust floor, workspace/project routing, credential-bound scope coverage — capability is bounded by the agent's active linked credentials, not just the profile's declared scopes — skill coverage, eval health where a null score is "unknown, not blocking", and a recent-failure ceiling). The legacy recentRuns and latestEvals arrays in this response remain empty; current run and quality state is available through the Agent Job, trace, eval, and release surfaces. Disabling an agent flips it to ineligible and blocks new credential links, but never deletes its runs, activity, or links (revoked credential links are kept for audit).
The Control Tower (/[workspace]/control-tower, workspace-first with a personal rollup) is the operational inbox across twenty-five queues. Agent-workflow, delivery, harness, SLA, eval-regression, policy-event, and credential-event sources are live. Governed memory candidates instead use the dedicated /[workspace]/memory review queue; the legacy MEMORY_CANDIDATES Tower adapter is not connected to MemoryItem and reports no source. Harness queues are de-duplicated from their generic agent-run counterparts, and human approvals are highlighted consistently in workspace and personal views. Items rank highest-severity then oldest, explain why they appeared, deep-link to their source, and record audited resolutions. list_control_tower_items exposes the read side; resolution actions remain UI-only. The sweep notifies owners when items age past their threshold.
The execution lifecycle view
Above the queues, the tower shows every execution across the seven lifecycle phases — PRD → Plan → Code → Test → Deploy → Monitor → Improve — with its Work Item, current phase and step, health, elapsed time, runtime-reported cost, last event, and blocker. Monitor is built from records the platform already writes (deployment receipts, incidents, provider-connection verification state, stale runner nodes, and attempt failure counts); there is no telemetry or SLO ingestion, and a signal kind that does not map to an existing model is dropped rather than absorbed.
The view is a derived, query-time projection over authoritative records — the same rule as the queue aggregator. Nothing is stored, so nothing can go stale: opening the page recomputes it. If a cost cache is ever added, the cron sweep converges it; ops code never hand-maintains projection state.
Every piece of evidence is labelled with who owns its truth —
provider_owned (GitHub checks, deployment receipts), runner_attested (test
evidence the runner executed and bound to the reviewed diff),
platform_recorded, human_recorded, or agent_asserted. An agent's claim
about CI, a deployment, or a monitoring result is recorded and displayed but is
never authoritative: an agent cannot mark its own CI, deploy, or monitoring
result successful, and unattested "test evidence" is shown as the agent
assertion it is.
Active and historical executions both project. Completed, cancelled, and archived runs resolve to their terminal phase, stay filterable on every dimension whose value they hold, and remain directly addressable — the workspace scope, not the soft-delete flag, is what authorizes the read.
Live work reads first; stopped work folds away. The list opens on the executions that are still going and groups everything finished, cancelled, frozen, or archived into one collapsed section that states its count. The fold is a presentation grouping over the same projected rows — not an eighth view and not a second query — so an active primary view still narrows the whole set and the fold then applies to whatever it returned. Collapse state is a local preference and stays out of shared links.
A stopped execution is never described as live work. A run's status answers
whether it stopped; its phases do not, because cancelling freezes the pipeline
wherever it stood and routinely leaves the last step blocked or mid-attempt. So a
cancelled run reads cancelled (not blocked), reports no blocker, and offers
no next action. It stays in Failed, where it belongs. A completed run reads
complete even when Deploy and Monitor never ran, which is the normal shape of a
docs-only execution.
Stopping is not the same as being done with it. A stopped run's step gates
are moot — nobody will approve the commit gate of a step on a cancelled
execution — so they stop calling for a human. Its acceptance verdict is a
different question: acceptanceState rides alongside status, so COMPLETED
means "all steps committed", never "accepted". A finished execution still waiting
on an operator's acceptance decision therefore stays in Needs attention and
stays out of the fold, because hiding it in the one view meant to surface it is
how outstanding work goes missing. The distinction is only about what calls for
attention: the approval filter still records what each record holds, so a run
that died at its commit gate remains findable by that gate forever.
Each row shows step progress (how many of the run's steps are done), the current step, and the newest attempt behind it — whether something is running right now, its outcome if not, and the provider and model that executed it. It carries navigation to the Work Item, PRD, current step, active attempt, evidence, pull request, CI run, deployment, incident, and improvement work — each destination carrying the same two identifiers (Work Item ID for business-level correlation, Execution ID for the instantiated lifecycle). Destinations that do not exist yet are counted rather than narrated: a run with no delivery graph shows "N not correlated" with each reason on hover, instead of one line of prose per absent record. The reasons themselves are unchanged and the execution detail page still spells them out.
Acting on a row
The human actions for an execution are available on its tower row, not only on its detail page: launch the current step, freeze or resume, cancel, archive a finished run, and record the §10.6 next action — including the terminal disposition a failed or cancelled execution owes, which the row flags as owed.
Every control is the same one the execution detail page mounts, and legality is
the server's answer: the actions offered come from the same rule that decides
what the ops core will accept, resolved for the whole page in one batched read.
Admin-gated operations stay admin-gated and are re-checked server-side, and each
mutation still carries the run's expectedUpdatedAt, so a row that has moved
since the page was read is refused rather than silently overwritten. Hiding a
control is never the enforcement — the ops core refuses an illegal action
regardless of what was rendered.
Filters and views
Thirteen filter dimensions narrow the list: lifecycle phase, project, Work Item, harness template/version, AI worker and release, runtime/provider and model, environment, status, approval state, incident severity, time window, cost range, and human owner. Multiple values within one dimension are OR; distinct dimensions are AND; an absent dimension narrows nothing. A row with no value for an actively-filtered dimension is excluded, and an empty list names which filter excluded the rows.
Filter state is URL state only — no saved view, no per-user default, no server-side filter cache — and the parser is total: a hand-edited URL with a malformed date, a non-numeric cost bound, or an unknown key drops exactly that dimension and the rest still apply, so a bad link degrades to a wider result set rather than a blank tower. The cost range compares reported cost only: a run whose runtime reported nothing is excluded from a cost filter rather than being read as $0 (there is no pricing engine).
Seven primary views preset those filters — Needs attention, Active, Scheduled, Completed, Failed, Paused, and Archived. A view is a named preset over the same rows, not a second query path; Needs attention keeps the exception queues as its source adapters, and a view with no representable source today (Scheduled has no scheduler record) says so instead of showing a placeholder row.
Filters search the whole workspace, not a page of it. Search, every filter dimension, the view, and the sort are applied to every execution the workspace holds — active and historical, including archived ones — and only then is the first page cut. There is no scan bound between the read and the filter, so a match is found however far back it sits: the Archived view answers with real history rather than with whatever happened to be recent, and a Work Item, time window, status, or owner that was last touched thousands of executions ago is still reachable. The choices each filter offers are collected the same way, before any narrowing, so selecting one value never removes the others from the menu and a second value can still be OR-ed in. Changing a filter re-runs the read and the control shows that it is working for the whole round trip.
Only the workspace, the project scope, and a direct execution link narrow the underlying query; every one of the thirteen dimensions is evaluated over the projected rows. That keeps the filter and the database from holding two different opinions about what a dimension means, and it is why the facet menus can be honest about what the workspace contains. The cost is that a request projects the workspace's executions rather than a slice of them — the honest price of a query-time projection with no cache to converge.
Execution detail: Pipeline and Attempts
Opening a row goes to the execution's own page (/[workspace]/harnesses/[slug]),
which gains two tabs alongside Overview, Stages, Artifacts, Findings, Runs,
Delivery, Decisions, and Settings:
- Pipeline — the seven phases with the evidence that justifies each, its ownership label, and whether that evidence is authoritative, plus the full correlated navigation set.
- Attempts — per-attempt telemetry: runtime adapter and whether it is registered, provider connection, model, pinned release, permission revision, tokens and cost (runtime-reported — an attempt whose runtime reported nothing shows "not reported", never $0), duration (an open attempt reads "in flight"), retry ordinal and the stage's retry count, policy decisions with the deciding revision, and the attempt's trace id (a diagnostic, not a correlation key).
Both tabs render the same query-time projection the tower list is derived from — one read layer, one execution-detail surface.
Managed execution backend and reference software company
Managed eval workloads run on a real sandbox-backed backend (M16), gated on a
live worker — not a constant. list_workers shows the deployment-global,
platform-owned worker registry (liveness, driver class, revision, current work);
set_worker_enabled (admin) disables/re-enables a worker. The managed-eval gate
is green only while an enabled, live worker advertises an approved production
driver class (local-docker; in-process/host-process are excluded
permanently, so simulated evidence can never satisfy managed gating). A
managed-mode gating run with no approved worker is refused, never silently
demoted to the simulator.
Reference-software-company enablement is fail-closed and operator-driven.
configure_software_company binds boards/agents/repo; set_software_company_enabled
(enable/pause/resume) re-runs the fail-closed preflight on enable and
resume — enablement is refused unless the managed-eval gate is green and every
board/agent/release binding resolves. reconcile_software_company (the same op
the 5-minute sweep runs) starts no new work for a disabled or paused profile,
and auto-pauses on binding drift (a binding/release that goes invalid pauses
new launches and opens one Control Tower exception, leaving reads + active work
intact; repair + resume clears it). Operational kill switches — profile pause,
harness freeze, set_worker_enabled disable, and provider/workload-token
revocation — each independently stop new managed work.
Provider connections and runtime adapters (Operator Control Plane)
A provider connection is what a runtime bills through and where it sends
requests (Operator Control Plane PRD §3 Layer 2, §11 R1). Every connection is
workspace-scoped and owned by a registered runtime adapter — the runtime the
platform launches to reach a provider, never a provider Jentrix calls itself. The
registered set is claude (Claude Agent SDK) and codex (OpenAI Codex SDK);
runtimeAdapterKey is an open string validated in code (src/server/runtime-adapters),
not a Postgres enum. A vendor without a registered adapter — Google Gemini
today — is refused with an explanation at connection time, never silently
accepted; adding one is a separate adapter project with its own sandboxing and
capability review. Connection kind is likewise an open string (no per-vendor
enum migration), and connections are created from operator-supplied
configuration plus a credential reference — no environment-file editing.
Connection types are adapter-dependent (src/server/providers/connections.ts):
api_key, subscription, and custom_openai_compatible. Claude supports an
Anthropic API key or a Claude subscription; Codex supports an OpenAI API key, a
ChatGPT subscription (codex login), or a public-HTTPS custom OpenAI-compatible
endpoint (OpenRouter, Kimi/Moonshot). A custom endpoint is validated through the
existing webhook SSRF guard (src/lib/webhooks/security.ts): https-only, no
embedded credentials, no localhost/loopback/link-local/private destinations —
a private runner-network endpoint is refused, never reached.
Verification is per-adapter (src/server/providers/verification.ts). API-key
and custom connections are checked server-side through the SSRF guard (a
model-list / auth probe). A subscription authenticates node-locally on a
runner host, so it cannot be pinged server-side; it is proved by a
runner-reported probe that yields a time-bounded, per-runner attestation
(src/server/providers/attestations.ts) surfaced as "verified by runner name
at time." A bare { runnerToken, connectionId } is not sufficient: the
runner must submit signed, validated evidence that it actually ran the
node-local probe and it succeeded — an ok flag plus a distinguished success
outcome mapped through the same taxonomy, carrying the tested adapter and runtime
evidence, HMAC-signed by the runner identity and bound to the connection
(signAttestationEvidence) and freshness-checked server-side
(verifyAttestationEvidence). A forged signature, an adapter mismatch, a stale
probe, or a probe the runner reports as failed (invalid credentials, model
access denied, provider unavailable, …) mints no attestation and never marks
the connection healthy — the failed outcome is surfaced instead. A stale
attestation degrades that node to ineligible; when
no live attestation remains the connection degrades to "configured but
unverified," never a false "healthy." A workspace-level "verified" summary
never substitutes for node eligibility, and a claim using a subscription
connection is eligible only on a node with a live attestation for it.
Verification distinguishes eight outcomes — invalid credentials, unsupported
endpoint, network failure, rate limiting, model access denied, provider
temporarily unavailable, success-with-no-models, and plain success — and never
collapses them into a bare "failed."
Raw secrets are accepted only through the protected credential-entry flow and
are never returned. A raw API key enters exactly once, through
enterProviderSecret, which seals it into an AES-256-GCM envelope (ProviderSecret;
the key is derived from the existing app secret, so no environment-file editing)
and returns only a non-secret last-4 fingerprint. A connection stores only the
opaque providerSecretId reference; subscription connections carry no
server-side secret at all. The sealed secret is recovered server-side only to
run the /v1/models verification probe (a listing, never a model invocation).
Redaction is asserted at the ops-core serialization boundary
(src/server/providers/redaction.ts): every external-facing payload — browser
responses, MCP results, exports, logs, and audit rows — is assembled from a fixed
field allowlist and run through the M4 secret redactor, so a secret pasted into a
free-text field is scrubbed and a record that smuggles a raw secret field is
refused at the boundary. The same discipline runs at the write boundary for
every operator/runner-supplied metadata bag, so raw material never reaches
database JSON through any route but enterProviderSecret:
- Runner-reported probe metadata is reduced to a non-secret allowlist
(
normalizeAttestationEvidence) and scrubbed (redactAttestationProbeForPersist) before it is persisted, so a runner cannot routeapiKey/authorization/session material intoProviderConnectionAttestation.probeJSON through the attest endpoint. - A connection's
configbag carries no operator free-form metadata:sanitizeConnectionConfigenforces empty-refusal in the ops core before the row is written, and the create schema is a.strict(), empty object — both accept only an absent or empty ({})config. An ADMIN through the UI or MCP could otherwise submit raw credential material under a benign key (config.note = "sk-ant-…",config.region = "<opaque token>") outside the credential-entry flow; the boundary refuses a secret-bearing key (config_secret_field, at any depth) and refuses any other key (config_not_allowed) — never silently dropped or stored. An allowlist of "benign" string keys would still admit an unpatterned secret sitting in a stored value that a read-boundary redactor cannot recognize, so the boundary admits no free-form value at all. A genuine future need for non-secret routing metadata must arrive as a typed, secret-incapable field, never a JSON bag; raw secrets are accepted only throughenterProviderSecret.
The shipped surfaces. The ops core (src/server/providers/operations.ts)
composes those pure helpers into a persisted, authenticated feature over four
models — ProviderConnection, ProviderConnectionAttestation, ProviderSecret,
and the node-scoped WorkerClaimCredential. Workspace ADMINs create, verify,
enable/disable, and archive connections from the Providers page
(/{workspace}/settings/providers) or the create_provider_connection,
verify_provider_connection, and list_provider_connections MCP tools. A runner
node proves a subscription login by POSTing signed, validated probe evidence
to /api/providers/attest (authenticated by its heartbeat identity secret);
it then obtains a single-use,
node-scoped claim credential from /api/providers/claim-credential, which is
issued only when that node holds a live attestation. That credential is then
consumed at the scheduling boundary: claim_agent_job
(src/server/workflows/operations.ts) takes the single-use workerClaimToken and,
inside the claim transaction, resolves it to its runner node, re-checks that
node's live attestation for the selected subscription connection (a mint-time
proof can lapse before the claim lands), binds the opened run to the node
(AgentRun.claimedByRunnerNodeId / subscriptionConnectionId), and burns the
credential single-use — all atomic with the run, so a rolled-back claim leaves the
token unconsumed. When a workspace bills a runtime adapter only through a
subscription connection, a tokenless claim is refused fail-closed. This closes the
loop on the mechanical enforcement that a claim using a subscription connection
is eligible only on a node with a live attestation.
The browser connection form presents the supported combinations directly:
Anthropic API key or Claude subscription through claude, and OpenAI API key or
ChatGPT subscription through codex (plus the existing custom OpenAI-compatible
Codex path). A new API key can be entered inline, but still passes through the
same one-way enterProviderSecret boundary before the connection stores its
opaque reference. The create flow immediately runs the redaction-safe
/v1/models verification probe and surfaces the distinguished failure instead
of leaving the operator to notice an unverified row later. Selecting a
subscription does not create a permanently unverified browser-side record.
Instead, the page shows one workspace-level Copy setup command action (also
shown when old subscription rows lack a live attestation). The copied fail-fast
command installs the matching current CLI and runner packages, runs an
admin-scoped jentrix login, and then runs workspace-scoped jentrix runner setup
with --runtime claude, --runtime codex, or --runtime all according to the
Claude Code and Codex checkboxes. Existing unverified rows are selected and
reused by default. That preview-first setup checks only the selected locally
authenticated runtime or runtimes, reuses or creates their subscription
connections and selected model profiles only after confirmation, enrolls the
existing runner identity, and posts signed node-local evidence without making a
model call. It never installs or signs in to a provider CLI.
The packaged local onboarding path composes these same boundaries. The
admin-authenticated POST /api/runners/setup accepts only the runner's
normalized, non-secret probe envelope and returns a preview or one-time apply
grants; it creates no alternate runtime identity. Apply uses
/api/workers/enroll, /api/workers/heartbeat, and signed
/api/providers/attest; the running worker mints each subscription claim token
through /api/providers/claim-credential. Workspace-pinned tokens remain
restricted at setup. Setup may save worker drafts but never publishes or
activates a release or launches work; immutable release routing remains
authoritative at claim.
Model profiles and capability-gated pre-launch validation (PRD §11 R2/R4)
persist which models and settings are valid for a runtime + connection pair. A
ModelProfile is owned by a provider connection (so its runtime adapter is
authoritative, never operator-picked), records the provider model identifier,
advisory capability metadata, and an optional free-text pricing note —
there is no pricing engine and no numeric rate column (PRD §6): costs come
from runtime-reported usage. Verifying an API-key connection imports that
provider's catalog as model profiles (the same idempotent discover_model_profiles
operation, upserting on [providerConnectionId, modelIdentifier], so re-verifying
preserves operator edits); ids a runtime adapter can never drive a worker turn with
— embedding, speech, image, moderation, rerank endpoints — are not persisted
(isWorkerModelId, a deny-list, so an unrecognized id is always imported and a
newly-released chat model is never hidden). Subscription profiles come from the
selected model in the runner's read-only local probe. Model choice is not a
provider-page decision and has no UI there: which model a worker runs on, and
with which parameters, is per-agent and lives on the agent's own page — a models
section on the provider page asked the operator to configure a model with no
worker in view. That agent surface renders every normalized generation control,
editable only where the owning adapter can enforce it and otherwise disabled
carrying the adapter's own reason, so an unenforceable control (fast latency on
either shipped adapter, maxTurns on Codex) is explained rather than absent.
Provider model-list APIs do not publish an
authoritative generation-parameter schema, so agent configuration renders
parameters from the registered runtime adapter capability schema and refuses
unsupported controls at save/launch rather than inventing provider metadata.
Capability schemas are keyed by runtimeAdapterKey and are authoritative;
discovered metadata is advisory. Before a Work Order launch records a
WorkOrderLaunch, validateGenerationParameters
(src/server/providers/capability-gating.ts, wired into launchWorkOrder)
refuses an unsupported reasoningMode/latencyMode, a raw runtime-native
param (e.g. temperature), or maxTurns for Codex — which manages its own loop
budget and has no turn cap until a host-side limit exists — each with a readable
reason; only the normalized, enforceable controls are pinned on the launch.
Permission templates (PRD §11 R5) are authoring sugar over the shipped
enforcement planes, never a second policy engine. Creating an AI worker
automatically creates a worker-owned, least-privilege PermissionTemplate
from its role preset (src/server/permissions/templates.ts) — no preset grants
the admin scope class, and every preset starts at deny-all egress, no shell, no
deployment authority, and drafts instead of sends. Publishing a revision
compiles that snapshot into the artifacts that already enforce
(src/server/permissions/compilation.ts): M4 policy rows (hashed with the
existing policyContentHash), P2.1 read/write/admin scope classes, M4
broker credential classes, claim/heartbeat budgets, the authz boundary, and the
selected adapter's runtime sandbox configuration. There are no per-tool MCP
allow/deny lists in v1.
Compilation is atomic and fail-closed. A refusal produces no artifacts; a
revision is unpublishable unless every required artifact is present and
content-hashed; and the claim/launch preflight
(assertClaimReleasePreflight → assertPermissionArtifactsForLaunch) refuses a
worker whose compiled bundle is absent, incomplete, or stale — compiled from
different content, or by an older compiler revision. The policy engine's
default-allow floor never stands in for a restriction that failed to compile:
a declared restriction with no expressible enforcement path (e.g. an approval
gate on an action no shipped policy template can gate) is refused at compile
time, naming the field.
The compiled bundle is applied at run time, not merely stored. A claim
pins the revision the launch gate just verified on AgentRun.permissionRevisionId,
and every runtime trust boundary resolves that pin — never the worker's mutable
template head — through src/server/permissions/enforcement.ts, so publishing a
revision mid-run can neither widen nor narrow an in-flight turn and "which
permissions did this run execute under?" has exactly one answer. Every one of the
six required artifact kinds is read by a production boundary (pinned by
templates.test.ts, so a seventh kind cannot land without a consumer):
evaluatePolicy folds the compiled policy rows in beside the
workspace's hand-authored ones (same layerApplies + resolvePolicy, same
persisted PolicyEvaluation). Those rows are agent-scoped, so the acting
worker must be named in the evaluation: verifyToken resolves it from the
credential's agent link, the MCP request context carries it, and
policyActorFor folds it into every PolicyContext — and because an ordinary
tool call names no run, the binding maps the acting role token through the M5
turn guard to its open runs and enforces those pins (the template head governs
only a worker holding no open turn). Beyond policy: issueRunCapability narrows the run's
authorization envelope before the STS mints, so a capability can never carry a
scope the bundle withheld or an action class it denied; the runner-bootstrap role
tokens are rotated with the compiled scope classes instead of a fixed
read+write, and the compiled scope class is re-checked on every governed
tool call — a mint-time narrowing binds only what it mints, and a role token
lives an hour, a brokered token outlives its grant, and an agent-linked PAT never
rotates, so without the per-call check a token issued while the revision allowed
write would keep writing after it narrowed to read (the token's own
scopeViolation stays the credential's upper bound; the compiled class is the
worker's); the M4 credential broker refuses an action class the worker's
compiled credential classes do not authorize (and intersects a minted Jentrix
token's scopes with them), so a permissive CredentialRecord cannot widen what
the revision granted — at every grant boundary, not only the one that mints
material: request_credential_grant is an MCP tool, so a governed worker can
name its own run, and gating only redemption would leave it holding an ISSUED
CredentialGrant for a class its revision never authorized (the row the Control
Tower and an auditor read as the platform's authorization record); the claim
response carries the compiled sandbox
snapshot on workerExecution.permissions, so the selected adapter configures
its shell, writable roots, and network egress from the compiled artifact rather
than its own defaults; the compiled authz boundary binds both at the claim
and for the rest of the turn — a worker cannot open a run on work outside the
workspace (or, when its template names them, the projects) its revision froze,
and it cannot reach outside that boundary afterwards either: every MCP tool call
by a worker credential installs a compiled-boundary guard on the request context and
requireWorkspaceRole / requireProjectAccess / requireWorkOrderAccess — the
choke point every authz helper funnels through — evaluate it with the same
claimWithinAuthzBoundary the claim ran, resolved from the run's pin (UI/session
callers never carry the guard, so human access is unchanged); and the heartbeat applies the
compiled budgets, refusing to keep a run alive past its wall-clock or spend
ceiling from the control plane's own facts (AgentRun.startedAt / costUsd), so
a runner that ignores the budgets in its sandbox snapshot still cannot exceed
them.
Because the same compiled scope classes decide both, the claim gate also checks
that a worker's revision can sustain the turn it is about to claim: every run
role ends its turn through a write-class submit tool (submit_plan,
submit_execution_result / submit_harness_stage_result, submit_review /
submit_harness_review), so a worker whose revision compiles read-only is
refused before a run opens — with the refusal recorded on the preflight-refusal
surface — instead of claiming a turn whose submit the MCP boundary would refuse
and leaving the job open until the sweep expired it. Widen the worker's
permission template (add the write scope class) and republish it, or bind a
worker whose revision already grants it.
The self-hosted runner has a second issuance path, because one shared role
token cannot represent per-run authority: it is a single row used by every run of
that role, so the only revision it can be narrowed by is the worker's mutable
head — which would re-scope an in-flight turn the moment an operator publishes,
and could not honor two overlapping runs pinned to different revisions. So the
M11.1 claim endpoint (POST /api/agents/claim/{grantId}) accepts an optional
runId: with it the shared token is left alone and the plane mints a
separate run-scoped bearer carrying role base ∩ THAT RUN's pinned compiled classes (AgentRun.permissionRevisionId, never the head), stamped with
ApiToken.mintedForRunId. That column binds the bearer at both ends — the M5
turn guard admits it for exactly the job whose activeRunId it names (run A's
bearer can never submit for run B), and the MCP boundary resolves that run's
pinned revision from it, which is what keeps it governed even though a run bearer
is deliberately not agent-credential-linked (linking it would break
resolveAgentTokenId's exactly-one-usable-link invariant). Its TTL is the same
ROLE_TOKEN_TTL_MS (60 min) the shared role token uses — by construction never
shorter-lived than the bearer it replaces, so the supported 30-minute turn
envelope cannot regress — and run close revokes it, so a run's authority dies
with the run instead of lingering for the rest of the TTL. "Run close" means
every path that closes a run, through one shared revocation helper: a
submit_*/fail_agent_job terminal move, a claim that supersedes an expired
turn, the provider-claim rollback, cancel_agent_job, cancel_harness (a bulk
close of every one of the harness run's jobs), and the heartbeat-expiry sweep —
the one close path a hung runner reaches. Revoking matters even though the turn
guard already refuses the bearer for the next run: until it is revoked it is a
live credential whose every ordinary tool call resolves the now-closed run's pin
and is admitted by it. A refused pin
(absent/incomplete/stale) or an empty intersection mints nothing, and a claim
naming a run that is not this grant's open turn (superseded, closed, or another
role's) is refused. A claim WITHOUT a runId is byte-identical to the pre-M18.2
rotation, so a runner that predates this keeps working — and is still governed,
because the per-call check above resolves its open turn's pin either way;
adopting run bearers narrows the credential's blast radius, it does not add
authority the shared token escaped. Order of operations: the control plane ships
this first — a runner may only start sending runId once the deployment it talks
to carries the column.
Each boundary fails closed: a governed worker whose
bundle is absent, incomplete, or stale is denied — verified through the same
assertCompiledArtifactsForLaunch gate the claim runs, so a bundle refused at
claim can never be honored mid-run — and an intersection that authorizes no scope
mints nothing, because an empty ApiToken.scopes reads as grandfathered full
access. A worker with no template at all predates the plane
(PERMISSION_PLANE_ENFORCEMENT) and is skipped with the reason surfaced, never
silently.
Every permission field displays its enforcement class — server-enforced,
runtime-configured, or advisory — wherever it appears (the worker page's
Effective permissions panel). Token scopes, credential classes, budgets,
workspace/project boundaries, deployment and send rights, and approval gates are
server-enforced. Shell, repository access, path patterns, and network egress
destinations are runtime-configured: honored by the runtime sandbox, and on a
self-hosted runner node with no live attestation only advisory —
attested, not guaranteed. Egress stays runtime-configured until the M16.5 egress
plane (AGE-387) lands, and a template that asks for the managed engine
(AGE-384) or server-enforced egress is refused rather than compiled into a
promise nothing keeps.
Effective permissions are the intersection of system restrictions, workspace
policy, the worker's permission revision, a harness-step narrowing, the
credential-grant scope, and runtime sandbox capabilities. No layer can increase
authority a stricter layer withheld — a broader grant is ignored and reported
(src/server/permissions/effective-preview.ts), never silently applied. Set
fields intersect, caps take the minimum, ordered levels take the least
authoritative, booleans AND, and approval gates union (one more gate is
stricter, not broader); the single-valued workspace boundary intersects to
none when two layers name different workspaces, because neither may re-point
a boundary the other withheld. Every field reaches the panel as a value, so a
restriction the runtime enforces is never displayed as a blank. The
preview itself is produced by the same evaluation path that enforces at run
time: it calls layerApplies + resolvePolicy from @/server/policies/engine,
the two functions evaluatePolicy calls, and holds no matcher, ordering, or
outcome resolution of its own.
Conflict control, specs, artifacts, outcomes, and source governance
Leases (WorkLease) add intent on top of optimistic concurrency. A holder (human/agent/automation/system) claims a target — task, project, repo path, GitHub issue/PR, contact/deal/ticket, Work Order, or agent job — for a bounded window with a conflict policy (WARN / REQUIRE_APPROVAL / BLOCK / ALLOW_PARALLEL) and a mode (ADVISORY / EXCLUSIVE / REVIEW / HANDOFF). Before expensive execution, acquire_lease runs duplicate-run detection: an EXCLUSIVE/BLOCK active lease returns a CONFLICT envelope whose error.current is the live lease, so an agent can wait, take over (admin + a policy-approved policyEvaluationId for EXCLUSIVE), hand off, or run in parallel — before burning a run. Objective-similarity detection reuses semantic search. Leases carry a required expiry for agent holders and are auto-expired by the 5-minute sweep (expired = visible but non-blocking). Takeover/handoff preserve Work Order, run, and artifact lineage via the takenOverBy chain. Lease badges render on the task panel and Control Tower; the second-run conflict dialog appears before launch.
Specs (Spec / SpecVersion) turn a rough task into an executable, versioned brief — user story, constraints, non-goals, test plan, rollback plan, evidence required, definition of done, open questions. Weak-spec detection flags a vague objective, missing acceptance criteria, a missing/trivial test plan, or conflicting constraints, and scores 0-100. Accepting a spec versions and links it, then seeds an M2 Work Order whose launch preview surfaces those quality warnings.
Artifacts (Artifact) make agent output first-class: PRs, diffs, logs, screenshots, eval/policy reports, run summaries, decision memos. Each links to a task/project/run/decision/Work Order/approval/memory-candidate, stores metadata (source/checksum/retention/visibility), and supports external-by-URL or internal-by-R2 (short-lived signed URLs on read). Visibility is enforced (workspace / admins / private). Redaction never hard-deletes — it clears the content, marks the row redacted + archived, and writes an ARTIFACT_REDACTED audit row, leaving a tombstone so links still resolve. Redaction is admin-gated.
Outcomes (OutcomeMetric) measure whether agent work helped. A daily cron rolls up Jentrix-native and delivery signals — cycle time, reopen rate, human edits, approval burden, DORA, and agent-native delivery metrics — split into agent-assisted vs human-only cohorts where applicable. Legacy project cards still mark PR-acceptance, cost-per-task, and eval-rate slots as substrate-gated rather than deriving them from the newer run/eval registries. Cards show on project pages and agent profiles.
Source governance (SourceRecord) classifies knowledge sources by trust, freshness, privacy, owner, project/client, and supersession. The Work Order launch preview warns on stale/low-trust sources and blocks a client-private source from entering an unauthorized cross-client context. Superseded sources stay visible (toggle) but are excluded by default in list_sources and context assembly. Management lives at /[workspace]/settings/sources (admin-gated).
All five are workspace-scoped subjects: their audit rows are WorkspaceActivity, and when a lease/artifact targets a board task the board also gets a task.updated so card badges refresh. Lease acquire/release and artifact attach are the only Phase-2 MCP writes; takeover and redaction stay UI/admin-only for now.