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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion docs/mcp-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"work_agreement_resolve",
"work_agreement_revise",
"work_agreement_submit",
"workflow_budget_resume",
"workflow_launch_approve",
"workflow_launch_reject",
"workflow_retry_status"
Expand Down Expand Up @@ -1656,7 +1657,7 @@
"behavior": {
"access": "Main endpoint capability: operator or caretaker routine. Specialists and temporary agents are refused. The handler has no narrower caller-identity check.",
"effects": "None.",
"notes": "group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking.",
"notes": "group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking. A workflow_rail signal's resume_run args carry run and the exact pause id; a run with no known pause id offers only open_workflows. Its raised_at is null when the pause time is unknown.",
"result": "groups array, each with group and signals. Signals contain subject, kind, group, urgency, headline, detail, raised_at and resolving actions (label, op, args).",
"summary": "Read the fleet attention ranking grouped by need."
},
Expand Down Expand Up @@ -6277,6 +6278,101 @@
],
"name": "work_agreement_submit"
},
{
"behavior": {
"access": "Main endpoint, verified authenticated human operator only. The tool checks the verified identity before calling shared Operator.Actions, which enforces human authority again. Caretakers, routines, subagents and absent identity refuse without mutations; no default MCP caller identity.",
"effects": "Exact run and pause lookup, rail raise and resume admission in the shared operation. The tool call does not invoke a model; queued nodes may execute asynchronously.",
"notes": "Input run_id (1 to 1024 characters) and pause_id (1 to 160 characters) are both required; pause_id is the pause arg of the workflow_rail signal's resume_run action in list_attention. The latest pause is never substituted for a missing or different one. Repeating the exact pause returns the original receipt, even after the run pauses again, completes or fails; it never raises the rail twice and may only recover a still-pending initial dispatch. A pause that is no longer current refuses stale_pause; a run that is not parked refuses not_paused. Refusal codes are human_identity_required, pause_required, no_such_run, stale_pause, not_paused, outer_transaction_unsupported and admission_failed. Errors omit private paths, raw exceptions and secrets.",
"result": "custode.workflow_budget_resume.v1 receipt: schema_version, run_id, pause_id, decision resumed, previous_budget_usd and budget_usd (null when unbounded) and status running. The receipt describes the run at admission. Admission is not execution, completion or subprocess settlement.",
"summary": "Raise the rail of one exact workflow budget pause and admit its run to continue."
},
"category": "operator",
"definition": {
"annotations": {},
"description": "Raise the budget rail of one exact workflow budget pause and resume its run as the verified human. Success is admission; work may continue asynchronously and completion is not guaranteed. Repeating the same pause returns the original receipt and never raises the rail twice.",
"inputSchema": {
"additionalProperties": false,
"properties": {
"pause_id": {
"maxLength": 160,
"minLength": 1,
"type": "string"
},
"run_id": {
"maxLength": 1024,
"minLength": 1,
"type": "string"
}
},
"required": [
"pause_id",
"run_id"
],
"type": "object"
},
"name": "workflow_budget_resume",
"outputSchema": {
"additionalProperties": false,
"properties": {
"budget_usd": {
"type": [
"number",
"null"
]
},
"decision": {
"enum": [
"resumed"
],
"type": "string"
},
"pause_id": {
"maxLength": 160,
"minLength": 1,
"type": "string"
},
"previous_budget_usd": {
"type": [
"number",
"null"
]
},
"run_id": {
"maxLength": 1024,
"minLength": 1,
"type": "string"
},
"schema_version": {
"enum": [
"custode.workflow_budget_resume.v1"
],
"type": "string"
},
"status": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"budget_usd",
"decision",
"pause_id",
"previous_budget_usd",
"run_id",
"schema_version",
"status"
],
"type": "object"
},
"title": "workflow_budget_resume"
},
"endpoints": [
"/mcp"
],
"name": "workflow_budget_resume"
},
{
"behavior": {
"access": "Main endpoint, verified authenticated human operator only. Shared Operator.Actions enforces human authority. Caretakers, routines, subagents and absent identity refuse without mutations; no default MCP caller identity.",
Expand Down
84 changes: 82 additions & 2 deletions docs/mcp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Routine instructions, prompt files, the `prompt_agent` tool, and an agent's stru

| Endpoint | Server | Tools | Resources | Templates | Prompts |
| --- | --- | ---: | ---: | ---: | ---: |
| `/mcp` | custode 0.3.0 | 106 | 4 | 13 | 0 |
| `/mcp` | custode 0.3.0 | 107 | 4 | 13 | 0 |
| `/mcp/memory` | memory 0.3.0 | 7 | 0 | 0 | 0 |

**`/mcp`**: protocol versions 2026-07-28, 2025-11-25, 2025-06-18; capabilities `{"resources":{},"tools":{}}`.
Expand Down Expand Up @@ -208,6 +208,7 @@ Categories are descriptive policy metadata, not an authorization guarantee.
| [work_agreement_resolve](#tool-work_agreement_resolve) | operator |
| [work_agreement_revise](#tool-work_agreement_revise) | operator |
| [work_agreement_submit](#tool-work_agreement_submit) | self_write |
| [workflow_budget_resume](#tool-workflow_budget_resume) | operator |
| [workflow_launch_approve](#tool-workflow_launch_approve) | operator |
| [workflow_launch_reject](#tool-workflow_launch_reject) | operator |
| [workflow_retry_status](#tool-workflow_retry_status) | read |
Expand Down Expand Up @@ -789,7 +790,7 @@ Read the fleet attention ranking grouped by need.

**Access:** Main endpoint capability: operator or caretaker routine. Specialists and temporary agents are refused. The handler has no narrower caller-identity check.

**Behavior, defaults and errors:** group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking.
**Behavior, defaults and errors:** group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking. A workflow_rail signal's resume_run args carry run and the exact pause id; a run with no known pause id offers only open_workflows. Its raised_at is null when the pause time is unknown.

### Tool: list_disowned

Expand Down Expand Up @@ -2410,6 +2411,85 @@ Retain a revision-bound result and its evidence limits.

**Behavior, defaults and errors:** agreement_id, agreement_revision, matching assignment_id, summary, nonempty criterion_evidence and verification_limits are required. Each evidence entry has criterion_id, references (possibly empty) and note; criterion IDs must name unique criteria in that retained revision. outputs may be empty for useful negative findings and defaults to empty. Late results for a historical revision remain visible but cannot complete current intent or inherit acceptance. All mutations require a stable request_id for one logical operation. An authorized exact retry returns the original receipt with duplicate=true before stale-revision checks; different semantic payloads with the same caller/request_id conflict. Intent revision and record sequence are distinct. Unknown fields and duplicate atom/string keys are refused. IDs are nonblank strings up to 160 characters; prose up to 2000; reference values up to 2048 and labels up to 200; arrays up to 20 entries; total mutation payload up to 65536 bytes. References are opaque attributed links, never fetched, verified or used as access grants. No operation starts, resumes, cancels, schedules or redispatches work.

### Tool: workflow_budget_resume

Raise the rail of one exact workflow budget pause and admit its run to continue.

**Endpoints:** /mcp. **Category:** operator.

| Argument | Type | Schema required | Description | Other schema constraints |
| --- | --- | --- | --- | --- |
| pause_id | string | yes | | {"maxLength":160,"minLength":1} |
| run_id | string | yes | | {"maxLength":1024,"minLength":1} |

**Result:** custode.workflow_budget_resume.v1 receipt: schema_version, run_id, pause_id, decision resumed, previous_budget_usd and budget_usd (null when unbounded) and status running. The receipt describes the run at admission. Admission is not execution, completion or subprocess settlement.

**Side effects:** Exact run and pause lookup, rail raise and resume admission in the shared operation. The tool call does not invoke a model; queued nodes may execute asynchronously.

**Access:** Main endpoint, verified authenticated human operator only. The tool checks the verified identity before calling shared Operator.Actions, which enforces human authority again. Caretakers, routines, subagents and absent identity refuse without mutations; no default MCP caller identity.

**Behavior, defaults and errors:** Input run_id (1 to 1024 characters) and pause_id (1 to 160 characters) are both required; pause_id is the pause arg of the workflow_rail signal's resume_run action in list_attention. The latest pause is never substituted for a missing or different one. Repeating the exact pause returns the original receipt, even after the run pauses again, completes or fails; it never raises the rail twice and may only recover a still-pending initial dispatch. A pause that is no longer current refuses stale_pause; a run that is not parked refuses not_paused. Refusal codes are human_identity_required, pause_required, no_such_run, stale_pause, not_paused, outer_transaction_unsupported and admission_failed. Errors omit private paths, raw exceptions and secrets.

**Output schema:**

```json
{
"additionalProperties": false,
"properties": {
"budget_usd": {
"type": [
"number",
"null"
]
},
"decision": {
"enum": [
"resumed"
],
"type": "string"
},
"pause_id": {
"maxLength": 160,
"minLength": 1,
"type": "string"
},
"previous_budget_usd": {
"type": [
"number",
"null"
]
},
"run_id": {
"maxLength": 1024,
"minLength": 1,
"type": "string"
},
"schema_version": {
"enum": [
"custode.workflow_budget_resume.v1"
],
"type": "string"
},
"status": {
"enum": [
"running"
],
"type": "string"
}
},
"required": [
"budget_usd",
"decision",
"pause_id",
"previous_budget_usd",
"run_id",
"schema_version",
"status"
],
"type": "object"
}
```

### Tool: workflow_launch_approve

Approve an existing workflow proposal and admit its retained run.
Expand Down
9 changes: 8 additions & 1 deletion docs/mcp/behavior.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
"list_attention": {
"access": "Main endpoint capability: operator or caretaker routine. Specialists and temporary agents are refused. The handler has no narrower caller-identity check.",
"effects": "None.",
"notes": "group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking.",
"notes": "group optionally filters needs_you, watching, working, scheduled or quiet. Unknown group values return no matching groups rather than a validation error. Signal order is the fleet's current attention ranking. A workflow_rail signal's resume_run args carry run and the exact pause id; a run with no known pause id offers only open_workflows. Its raised_at is null when the pause time is unknown.",
"result": "groups array, each with group and signals. Signals contain subject, kind, group, urgency, headline, detail, raised_at and resolving actions (label, op, args).",
"summary": "Read the fleet attention ranking grouped by need."
},
Expand Down Expand Up @@ -848,6 +848,13 @@
"effects": "Exact proposal lookup and rejection event commit in one immediate SQLite transaction. Postcommit notification is best effort. No run or model call.",
"result": "Bounded structured proposal_id and recorded decision rejected. The retained reason is not echoed.",
"notes": "Input proposal_id is required, 1 to 160 characters; reason is optional, at most 2000 characters. An omitted or blank reason records rejected via mcp. Unknown or week-old pending proposals refuse. Authorization never uses bounded display history. Rejection retry retains the original decision and reason; an existing approval refuses. Precommit failure rolls back the decision and leaves the proposal pending. Outer transactions are unsupported. Errors omit private paths, raw exceptions and secrets."
},
"workflow_budget_resume": {
"summary": "Raise the rail of one exact workflow budget pause and admit its run to continue.",
"access": "Main endpoint, verified authenticated human operator only. The tool checks the verified identity before calling shared Operator.Actions, which enforces human authority again. Caretakers, routines, subagents and absent identity refuse without mutations; no default MCP caller identity.",
"effects": "Exact run and pause lookup, rail raise and resume admission in the shared operation. The tool call does not invoke a model; queued nodes may execute asynchronously.",
"result": "custode.workflow_budget_resume.v1 receipt: schema_version, run_id, pause_id, decision resumed, previous_budget_usd and budget_usd (null when unbounded) and status running. The receipt describes the run at admission. Admission is not execution, completion or subprocess settlement.",
"notes": "Input run_id (1 to 1024 characters) and pause_id (1 to 160 characters) are both required; pause_id is the pause arg of the workflow_rail signal's resume_run action in list_attention. The latest pause is never substituted for a missing or different one. Repeating the exact pause returns the original receipt, even after the run pauses again, completes or fails; it never raises the rail twice and may only recover a still-pending initial dispatch. A pause that is no longer current refuses stale_pause; a run that is not parked refuses not_paused. Refusal codes are human_identity_required, pause_required, no_such_run, stale_pause, not_paused, outer_transaction_unsupported and admission_failed. Errors omit private paths, raw exceptions and secrets."
}
}
}
27 changes: 19 additions & 8 deletions lib/custode/attention.ex
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,10 @@ defmodule Custode.Attention do

* `:launches` -- each as `%{id:, workflow:, repo:, summary:, why:,
proposed_at:}`.
* `:paused_runs` -- each as `%{run_id:, workflow:, repo:, reason:,
paused_at:}`, the reason being the run's own note of what it spent and
which nodes it did not run.
* `:paused_runs` -- each as `%{run_id:, pause_id:, workflow:, repo:,
reason:, paused_at:}`, the reason being the run's own note of what it
spent and which nodes it did not run. `pause_id` is the exact pause a
resume must name; `paused_at` is nil when the pause time is unknown.

The subject is the workflow and the repository, which is how the operator
tells two of them apart; the ops carry the proposal or run id.
Expand Down Expand Up @@ -305,8 +306,21 @@ defmodule Custode.Attention do

# "Raise the rail and resume" rather than "Resume": resuming onto the same
# ceiling parks the run again on its next advance, so the one useful act is
# the raise. `Custode.Workflow.Launch.raise_and_resume/1` owns by how much.
# the raise. `Custode.Workflow.BudgetResume` owns by how much.
#
# The op quotes the exact pause it was raised for (#846). A run with no
# usable pause id offers only the workflows page: resuming "whatever pause
# is current" is the guess the pause id exists to prevent.
defp workflow_rail(run) do
resume =
case run[:pause_id] do
pause when is_binary(pause) and pause != "" ->
[op("Raise the rail and resume", :resume_run, %{run: run.run_id, pause: pause})]

_unknown ->
[]
end

%Signal{
subject: workflow_subject(run),
kind: :workflow_rail,
Expand All @@ -316,10 +330,7 @@ defmodule Custode.Attention do
detail: run[:reason],
item: {:run, run.run_id},
raised_at: run[:paused_at],
resolving: [
op("Raise the rail and resume", :resume_run, %{run: run.run_id}),
op("Open workflows", :open_workflows, %{})
]
resolving: resume ++ [op("Open workflows", :open_workflows, %{})]
}
end

Expand Down
41 changes: 15 additions & 26 deletions lib/custode/attention/fleet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,33 +135,22 @@ defmodule Custode.Attention.Fleet do
}
end

# The run row records that it is parked and why, not when: the pause is a
# status change on a row with no timestamp for it. The `workflow_budget_paused`
# feed entry is the record of when, and it is only read when a run is
# actually parked, which is rare.
# The run row names its current pause and when it was taken (#846). The pause
# id is what a resume must quote, so a click on a row the operator saw cannot
# resume a later pause. A legacy pause has an id but no known time: nil, not
# a guess from bounded feed history.
defp paused_runs do
case Run.list(status: "budget_paused") do
[] ->
[]

runs ->
paused_at =
"workflow_budget_paused"
|> Custode.Feed.recent_by_event(limit: 50)
|> Enum.reverse()
|> Map.new(&{&1["run"], parse_at(&1["at"])})

for run <- runs do
%{
run_id: run.run_id,
workflow: run.workflow,
repo: run.repo,
# the pause note says what was spent AND which nodes did not run;
# `error` is the same sentence without the second half
reason: List.last(run.notes) || run.error,
paused_at: Map.get(paused_at, run.run_id)
}
end
for run <- Run.list(status: "budget_paused") do
%{
run_id: run.run_id,
pause_id: run.budget_pause_id,
workflow: run.workflow,
repo: run.repo,
# the pause note says what was spent AND which nodes did not run;
# `error` is the same sentence without the second half
reason: List.last(run.notes) || run.error,
paused_at: run.budget_paused_at
}
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/custode/mcp/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule Custode.MCP.Server do
Custode.MCP.WorkflowRetryTools.Read,
Custode.MCP.WorkflowDecisionTools.Approve,
Custode.MCP.WorkflowDecisionTools.Reject,
Custode.MCP.WorkflowBudgetTools.Resume,
Custode.MCP.ReadTools.ExecutingTurns,
Custode.MCP.ProjectProgressTools.Read,
Custode.MCP.WorkAgreementTools.Create,
Expand Down
Loading
Loading