duto-ai

module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: Apache-2.0

README

duto-ai

duto-ai is a CLI and runtime for bounded, typed AI workflow DAGs. It strictly decodes a trusted runtime configuration and a portable workflow, compiles an immutable effective plan, and executes that plan with ADK Go v2.

The local CLI remains the primary interface. M2 adds an official one-shot GitHub Action adapter in this repository, with its contract frozen in ADR 009. Workspace or Git mutation and remote publication are M3. Durable pause/resume, cross-runner recovery, and asynchronous reply correlation are future hosting work.

Build and inspect a workflow

Build the binary:

mise install
go build -o ./bin/duto-ai ./cmd/duto-ai

Create duto.yaml:

version: 1
providers:
  default:
    type: custom-provider
    config: {}
models:
  light:
    provider: default
    target: example-model

Create workflow.yaml:

version: 1
name: example
model: light
tools: []
limits:
  timeout: 1m
  max_iterations: 2
  max_model_calls: 2
  max_tool_calls: 0
  max_concurrency: 1
  max_parallel_calls: 1
  max_artifact_bytes: 0
steps:
  - id: report
    needs: []
    instruction: Return a typed report.
    tools: []
    workspaces: []
    input:
      type: object
      properties: {}
      required: []
    with: {}
    output:
      type: object
      properties:
        outcome: {type: string, enum: [completed]}
        report: {type: string, max_length: 1024}
      required: [outcome, report]
result: {step: report}

Validate and inspect the effective plan:

./bin/duto-ai validate --config duto.yaml workflow.yaml
./bin/duto-ai plan --format json --config duto.yaml workflow.yaml > plan.json

The placeholder provider binding is enough for validate and plan, which do not construct a provider. Before run, replace it with a trusted configuration supported by the binary. Provider credentials, endpoints, and concrete model targets belong in duto.yaml, never in portable workflow YAML.

Run a workflow that declares no runtime inputs:

./bin/duto-ai run --format json --config duto.yaml workflow.yaml > result.json

CLI reference

duto-ai validate [--config FILE] [--format text|json] WORKFLOW|-
duto-ai plan     [--config FILE] [--format text|json] WORKFLOW|-
duto-ai run      [--config FILE] [--format text|json] [--inputs FILE] [--evidence-directory DIR] WORKFLOW|-
duto-ai version

--config defaults to duto.yaml. --format defaults to text. A workflow path of - reads one YAML document from stdin.

Each operation writes exactly one payload followed by a newline to stdout. Diagnostics go to stderr.

  • validate emits valid or {"version":1,"valid":true}.
  • plan emits the complete effective plan as pretty or compact JSON. The plan includes frozen instruction and skill content, so protect it according to the source material.
  • run emits a pretty or compact typed result object.

Exit codes are stable:

Code Meaning
0 Success
1 Unexpected internal error
2 Command usage error
3 Configuration, workflow, or admission error
4 Execution failure or incomplete execution
130 Cancellation
Runtime inputs

run accepts --inputs FILE for one strict UTF-8 JSON object. --inputs - is invalid because stdin remains reserved for WORKFLOW=-. When a workflow declares top-level inputs, --inputs is required and is validated before provider construction.

run also accepts --evidence-directory DIR as a trusted run-only override for the runtime evidence bundle path.

Use the one-shot GitHub Action (M2)

M2 wraps the same admitted one-shot runtime path used by the local CLI. Caller workflows own checkout and permission ceilings.

Minimal caller workflow (pinned actions)
name: duto

on:
  workflow_dispatch:
  schedule:
    - cron: "0 4 * * *"
  push:
  pull_request:
  issues:
  issue_comment:

jobs:
  run-duto:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      # Add only when the admitted workflow uses GitHub read tools:
      # pull-requests: read
      # issues: read
      # checks: read
    steps:
      - if: github.event_name == 'pull_request'
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
        with:
          ref: ${{ github.event.pull_request.base.sha }}
          persist-credentials: false

      - if: github.event_name != 'pull_request'
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
        with:
          ref: ${{ github.sha }}
          persist-credentials: false

      - name: Run duto-ai
        uses: PedroKlein/duto-ai@432887efbfac9e4cec0bd6bedadb85999772740f
        with:
          workflow: .github/ai-workflows/scenarios/template-variables.yaml
          config: .github/ai-workflows/config-m2.yaml
          version: v0.2.2
          evidence-retention-days: "7"

Pin both actions/checkout and PedroKlein/duto-ai to full 40-character commit SHAs. Update those pins through your normal dependency-review process.

Action reference
  • Exact inputs: workflow, config (default duto.yaml), version (vMAJOR.MINOR.PATCH), evidence-retention-days (default 7)
  • Exact outputs: status, outcome, run-id, result-path, evidence-path, failed-step, clarification-required
  • Supported events: workflow_dispatch, schedule, push, pull_request, issues, issue_comment
  • Checkout contract: caller-owned checkout with persist-credentials: false; use pull_request.base.sha for pull_request, otherwise github.sha
  • Permissions contract: baseline contents: read; add only pull-requests: read, issues: read, and checks: read when needed by admitted GitHub read tools
  • Failure handling: if runtime emits a typed result, JSON mode writes exactly one newline-terminated payload before exit 0, 4, or 130; pre-result usage/admission/internal failures keep stdout empty
  • Evidence and retention: full typed result and runtime evidence remain runner-local; uploaded artifact is the redacted Action bundle with configured retention days (subject to repository policy)
  • Process boundary: shell.run and runner execution are not a sandbox
  • Exclusions: no writes, no SafeOutputs application, no durable state, no pause/resume, no cross-runner recovery, and no async replies

Trusted configuration reference

The trusted configuration is a strict version: 1 YAML document. Its root fields are:

Field Required Value
version yes Integer 1
providers yes Map of provider alias to {type, config}
models yes Map of model alias to {provider, target}
workspaces no Map of symbolic name to {root, access}; M1 accepts access: read
tool_profiles no Map of profile name to a flat selector list
tools no Trusted tool ceiling as a selector list; omission means an empty ceiling
tool_limits no Exact tool-name map of hard limits
tool_config no Closed trusted bindings for selected tool families
evidence no {directory} for an optional one-shot evidence bundle

A tool_limits entry accepts max_calls, timeout, max_request_bytes, and max_result_bytes. Every selected tool must have a positive trusted limit. Portable workflow and child limits may narrow this record but cannot widen it.

tool_config has these closed family records:

  • files: workspace
  • git: workspace, refs, allow_working_tree, max_log_count
  • github: base_url, optional token, owner, repository, subject, ref, max_pages, max_results
  • web: allowed_domains, max_redirects
  • shell: absolute executable, fixed args, workspace, environment, max_stdout_bytes, max_stderr_bytes

Trusted provider, workspace, evidence, GitHub, web, and shell scalar values may expand environment variables after structural decoding. Expanded secret values are not copied into plan output or diagnostics. Portable workflows never expand environment variables.

Portable workflow reference

A workflow is a strict version: 1 YAML document. Required root fields are version, name, model, limits, steps, and result. Optional root fields are description, inputs, model_config, tool_profiles, tools, tool_limits, skills, and agents.

Unknown fields, duplicate keys, aliases, anchors, merge keys, explicit nulls, unsupported tags, invalid UTF-8, scalar coercion, numeric overflow, and multiple documents are rejected. Diagnostics include the file, line, column, field path, and stable error code.

Types and graph

Supported schema types are object, array, string, integer, number, and boolean. Objects are closed. Arrays require items and max_items; unconstrained strings require max_length. Every step or agent output is an object with a required finite outcome enum.

An inline step accepts:

  • required id, instruction, input, with, and output;
  • optional needs, wait, when, model, model_config, tools, tool_limits, skills, workspaces, retry, and limits.

A named-agent step accepts id, optional needs, wait, and when, plus required agent and with. It cannot override the selected agent.

needs defines a static acyclic graph. Fan-in uses wait: all_succeeded and builds inputs in source order. A with binding is exactly one of a workflow input, an ancestor output property path, or a scalar literal. There is no expression language, implicit predecessor map, or public ADK state key.

result is either {step: terminal-step} or an exhaustive list of routes keyed by a terminal step and one of its declared outcomes. awaiting_input is an ordinary successful domain outcome; it does not pause or resume the run.

Limits and retry

The workflow limits record contains:

  • timeout
  • max_iterations
  • max_model_calls
  • max_tool_calls
  • max_concurrency
  • max_parallel_calls
  • max_artifact_bytes

All required workflow values must be finite; model, tool, step, and agent scopes can only narrow inherited ceilings. Retry uses max_attempts, initial_delay, and max_delay. Process-capable steps cannot use automatic retry, and unsafe work cannot overlap another graph branch.

Tools and authority

M1 has this catalog:

  • Files: files.read, files.find, files.grep
  • Git: git.read.log, git.read.blame, git.read.show, git.read.diff
  • GitHub: github.read.issue, github.read.pr, github.read.diff, github.read.changed-files, github.read.comments, github.read.reviews, github.read.checks, github.read.search-issues
  • Network: web.fetch
  • Process: shell.run

Portable selectors may use an exact name or one of the terminal namespace wildcards files.*, git.read.*, and github.read.*. Broader or global wildcards reject.

A tool scope is either an array or a closed expression with from, add_profiles, add, remove_profiles, and remove. Profiles are flat selector lists. Trusted and workflow profile names must not collide. Expansion is deterministic and final names use catalog byte order.

Omitted tools, tools: [], and an empty expression expose no direct tools. Inheritance requires from: parent. The trusted configuration is the outer ceiling, workflow tools are the parent for top-level steps and agents, and each child must be a subset of its parent. Registration never grants authority.

Selected families also require their trusted tool_config binding. File, Git, GitHub, network, and process handlers repeat resource and byte checks at the I/O boundary. shell.run takes no model-selected command: it executes the exact absolute executable and arguments from trusted configuration with a closed environment, workspace, deadline, call count, and output bounds. It is not a sandbox.

M1 has no file or Git mutation, GitHub mutation or publication, arbitrary-method web request, tool plugin registry, or portable provider registry.

Instructions, templates, and skills

instruction accepts exactly one of:

  • a scalar or {text: string} literal;
  • {file: {workspace, path, max_bytes}};
  • {template: {text, max_output_bytes}};
  • {template: {file: {workspace, path, max_bytes}, max_output_bytes}}.

Files must be regular UTF-8 files beneath an admitted read workspace. Admission freezes file and template bytes into the effective plan. Traversal, symlink escape, invalid templates, unavailable data, and source or rendered-size overflow fail closed.

Templates use bounded Go text/template with a fixed data object: .Workflow, .Step, .Predecessors, and .Runtime. They cannot read environment variables, secrets, host events, arbitrary files, clocks, or session state.

Top-level skills maps names to {workspace, path}. An agent or inline step selects exact names. Each skill must have a matching SKILL.md; only bounded regular UTF-8 files under references, assets, and scripts are exposed through ADK's native skill toolset. Skill metadata cannot widen tool authority.

Named agents and subagents

Named agents use single_turn, task, or chat mode with fixed model, instruction, schemas, tools, workspaces, skills, context, limits, and declared subagents. Context is either fresh or a bounded snapshot of declared workflow inputs and files.

The current native ADK integration has an important placement limit: a subagent tree is executable only from a chat named agent used by the workflow's sole root and terminal step. A task agent can be a declared child but not a static workflow step. A single_turn named agent can be a static step only when it has no children. Snapshot references to ancestor outputs are decoded but rejected by admission for the root-chat path because no ancestor exists there.

Each child remains inside its parent's model, tool, workspace, and limit envelope. The model sees one native tool per declared child and cannot choose a different child configuration. There is no aggregate delegation tool, nested runner, persistent child conversation, or model-created graph.

Results and evidence

A successful run result contains version, an opaque one-shot run_id, workflow, execution status, domain outcome, timestamps, ordered step results, terminal output, optional reported usage, and normalized error kinds. Text output is pretty JSON; JSON output is compact JSON. Missing usage stays absent.

Every invocation uses fresh in-memory ADK session and artifact services. The runner consumes the full event stream. Raw model reasoning, prompts, provider targets, credentials, and raw tool arguments or results are not written to the evidence event stream.

When evidence.directory is non-empty, run atomically creates a new directory containing:

events.jsonl
result.json
summary.md
manifest.json

The manifest is written last and includes the plan digest plus file sizes and SHA-256 digests. The target directory must not already exist. This bundle records one execution; it is not a durable session, checkpoint, or replay store.

Delivery boundaries

Milestone Scope
M1, shipped Local validate, plan, and one-shot run; strict v1 documents; bounded typed DAGs; read/process tools; native finite subagents; typed results and evidence
M2 (contract frozen, implementation in progress) Official one-shot GitHub Action mapping trusted host inputs to the same CLI contract, then projecting summaries, outputs, and artifacts
M3 Admitted workspace and Git mutation, staged safe outputs, and trusted publication
Future durable hosting Persistent pause/resume, encrypted host state, cross-runner recovery, lifecycle reconciliation, and asynchronous replies

Development

mise install
mise run check
mise run integration
mise run scenarios

See docs/DEVELOPMENT.md for contributor commands, docs/ARCHITECTURE.md for runtime design, and docs/adr/006-workflow-v1-contract.md for the full workflow contract.

License

Licensed under the Apache License 2.0.

Directories

Path Synopsis
cmd
duto-ai command
internal
tool/files
Package files provides file system tools (read, find, grep) for AI workflows.
Package files provides file system tools (read, find, grep) for AI workflows.
tool/git
Package git provides git tools (log, blame, show, diff) for AI workflows.
Package git provides git tools (log, blame, show, diff) for AI workflows.
tool/shell
Package shell provides trusted, bounded process execution for AI workflows.
Package shell provides trusted, bounded process execution for AI workflows.
tool/web
Package web provides a bounded HTTPS read tool for AI workflows.
Package web provides a bounded HTTPS read tool for AI workflows.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL