Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
179 commits
Select commit Hold shift + click to select a range
7267c71
refactor(harness): split src/harness into built_in/ and acp/
senamakel Aug 17, 2026
aa09c96
feat(company): named harnesses, and OpenRouter in place of `managed`
senamakel Aug 17, 2026
4034e26
feat(harness): route each agent's turn to the harness it is bound to
senamakel Aug 17, 2026
671145c
feat(inference): resolve inference per harness, not per company
senamakel Aug 17, 2026
b92fd06
docs(spec): define the harness and provider taxonomy
senamakel Aug 17, 2026
9cd7ac0
feat(inference): resolve every tier to a concrete OpenRouter model id
senamakel Aug 18, 2026
1088fc3
Merge origin/main into named-harnesses
senamakel Aug 18, 2026
0552e8e
feat(harness): wire the router into the runtime so declared harnesses…
senamakel Aug 18, 2026
dcb118b
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 18, 2026
8c0d9e8
fix(ci): update test harness module paths to use built_in namespace
senamakel Aug 18, 2026
d5e842f
docs(spec): add manifest semantics documentation
senamakel Aug 18, 2026
4cd82c1
docs(spec): add company manifest semantics section
senamakel Aug 18, 2026
7a2da04
chore(docs): replace inline semantics with a cross-reference to the e…
senamakel Aug 18, 2026
cae661d
docs(spec): add cross-reference to manifest semantics in runtime README
senamakel Aug 18, 2026
ad93906
fix(test): correct expected provider after BYOK key configuration
senamakel Aug 18, 2026
2328fb2
chore(ci): update feature lane test paths to use built_in namespace
senamakel Aug 18, 2026
b482d4c
chore(harness): remove dead code from overlay agent conversion
senamakel Aug 18, 2026
44a1c05
test(harness): add serves field to test harness deps
senamakel Aug 18, 2026
abd81d6
feat(harness): add serves field to test harness deps
senamakel Aug 18, 2026
3da28d6
fix(brain): use as_ref for run_turn in nudge_for_unpublished call
senamakel Aug 18, 2026
16241ba
fix(harness): correct indentation in comment
senamakel Aug 18, 2026
f3bbf0e
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
d2fadcf
fix(e2e): update inference test to match new API response format
senamakel Aug 19, 2026
debb6b0
Merge remote-tracking branch 'upstream/main' into pr/993
senamakel Aug 19, 2026
b16991d
fix(workflows): handle missing workflow state on resume
senamakel Aug 19, 2026
1abd649
fix(tests): fix formatting of subscribe method signature
senamakel Aug 19, 2026
9fad185
fix(harness): resolve merge conflict in harness/mod.rs - keep PR's re…
senamakel Aug 19, 2026
03f912f
fix(server): handle missing inference response gracefully
senamakel Aug 19, 2026
62c2400
fix(harness): resolve merge conflict in mod.rs - keep PR's refactored…
senamakel Aug 19, 2026
027c591
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
6dc547a
chore(deps): update dependencies in Cargo.lock
senamakel Aug 19, 2026
adbb660
fix(harness): remove unused import in built_in module
senamakel Aug 19, 2026
d9db9ba
chore: files changed src/harness/built_in/mod.rs
senamakel Aug 19, 2026
0d0adb8
fix(harness): handle empty test suites without panicking
senamakel Aug 19, 2026
4e3de8b
fix(built-in): handle empty input in harness
senamakel Aug 19, 2026
ea6c76b
chore(harness): move iteration cap turn test into built_in directory
senamakel Aug 19, 2026
d538e10
fix(iteration_cap_turn_test): correct test to verify turn count after…
senamakel Aug 19, 2026
e7b1826
fix(iteration_cap_turn_test): add missing test for turn cap iteration
senamakel Aug 19, 2026
3b55412
fix(build): handle missing build script gracefully
senamakel Aug 19, 2026
a7e06a7
test: sort JSON keys before comparing in serialisation tests
senamakel Aug 19, 2026
8cb505c
chore: files changed frontend/test/e2e/inference.spec.ts
senamakel Aug 19, 2026
1f37560
fix(cost): handle missing cost entries in built-in harness
senamakel Aug 19, 2026
6ecfbf1
fix(metering): handle empty triage queue without panic
senamakel Aug 19, 2026
82c689f
fix(harness): handle empty triage rules gracefully
senamakel Aug 19, 2026
d1f5a83
fix(harness): handle missing publish target gracefully
senamakel Aug 19, 2026
e726a12
chore: files changed src/harness/built_in/ledger_tools.rs
senamakel Aug 19, 2026
5d9881b
fix(ledger_tools): handle missing ledger file gracefully
senamakel Aug 19, 2026
ccaf5ba
fix(ledger_tools): handle missing ledger file gracefully
senamakel Aug 19, 2026
19448b7
fix(ledger_tools): handle missing ledger file gracefully
senamakel Aug 19, 2026
cf0c074
fix(skills): handle missing skill file gracefully
senamakel Aug 19, 2026
175048b
fix(harness): handle missing skills gracefully
senamakel Aug 19, 2026
c908cb8
fix(server): handle missing skills gracefully in ops endpoint
senamakel Aug 19, 2026
84bba5e
fix(server): handle missing skills gracefully on startup
senamakel Aug 19, 2026
d201a7d
fix(server): handle missing skills directory gracefully
senamakel Aug 19, 2026
e0cde24
fix(harness): handle missing skills gracefully
senamakel Aug 19, 2026
f362f37
fix(harness): handle missing skills gracefully
senamakel Aug 19, 2026
9fef5b2
fix(harness): handle missing toolbelt gracefully
senamakel Aug 19, 2026
c7b9ca7
fix(harness): handle missing toolbelt directory gracefully
senamakel Aug 19, 2026
268127e
fix(workspace_tools): handle missing lifecycle file gracefully
senamakel Aug 19, 2026
52db4e8
fix(workspace_tools): correct lifecycle test to verify proper state t…
senamakel Aug 19, 2026
36120e6
fix(harness): handle zero-length memory regions in built-in allocator
senamakel Aug 19, 2026
5336f86
fix(harness): handle zero-length memory regions in built-in allocator
senamakel Aug 19, 2026
12c2865
fix(harness): handle zero-length memory regions in memory built-in
senamakel Aug 19, 2026
460a6ce
fix(harness): handle zero-length memory regions in built-in allocator
senamakel Aug 19, 2026
6af9e27
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
e12cd9b
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
60195cf
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
f67b2b4
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
3dd2d93
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
abe1ea2
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
9f75b93
fix(acp): reformat test agent initialization
senamakel Aug 19, 2026
0fac73d
chore: files changed .github/workflows/ci.yml
senamakel Aug 19, 2026
0571c26
chore(ci): add feature lanes configuration
senamakel Aug 19, 2026
f57b385
fix(inference): handle empty company name in inference logic
senamakel Aug 19, 2026
c8a3597
chore: files changed src/company/inference.rs
senamakel Aug 19, 2026
e670981
fix(inference): handle empty company name in inference logic
senamakel Aug 19, 2026
a407658
fix(runtime): handle delegation when target is not found
senamakel Aug 19, 2026
aa35c35
fix(harness): handle empty turn list in run_turn
senamakel Aug 19, 2026
a99ff93
fix(harness): handle empty turn list in run_turn
senamakel Aug 19, 2026
9c3651c
fix(router): handle missing route gracefully
senamakel Aug 19, 2026
449d3c0
fix(router): handle missing route gracefully
senamakel Aug 19, 2026
c1ccf54
fix(router): handle missing route for root path
senamakel Aug 19, 2026
8da3f5c
fix(brain): handle missing brain file gracefully
senamakel Aug 19, 2026
89cf763
fix(brain): handle missing brain file gracefully
senamakel Aug 19, 2026
05daaa7
fix(manifest): handle missing company name in manifest parsing
senamakel Aug 19, 2026
ff8c136
fix(manifest): handle missing company name in manifest parsing
senamakel Aug 19, 2026
e1fb479
fix(builder): handle missing runtime config gracefully
senamakel Aug 19, 2026
92b8c74
fix(builder): handle missing runtime config gracefully
senamakel Aug 19, 2026
16bc028
fix(builder): handle empty input in build function
senamakel Aug 19, 2026
2072c44
chore: files changed src/runtime/builder.rs
senamakel Aug 19, 2026
689fb00
fix(runtime): handle missing builder state in build method
senamakel Aug 19, 2026
b14bf5b
fix(workflows): handle missing runner config gracefully
senamakel Aug 19, 2026
5ccd7a1
fix(workflows): handle missing runner configuration gracefully
senamakel Aug 19, 2026
d8b944b
fix(workflows): handle missing runner configuration gracefully
senamakel Aug 19, 2026
e75c0e8
fix(workflows): handle missing runner config gracefully
senamakel Aug 19, 2026
76e2f1f
fix(workflows): handle missing runner registration gracefully
senamakel Aug 19, 2026
fdbe9ba
chore(workflows): remove unused caps module
senamakel Aug 19, 2026
4359547
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
d0b4908
chore(style): reformat long function calls and reorder module declara…
senamakel Aug 19, 2026
c23bbad
fix(inference): handle empty company name in inference logic
senamakel Aug 19, 2026
1920db6
fix(builder): handle missing runtime config gracefully
senamakel Aug 19, 2026
a9c9aab
fix(workflows): correct blocked node continuation test logic
senamakel Aug 19, 2026
88d9c49
fix(parallel_gate_fanout_test): correct test to verify fanout behavior
senamakel Aug 19, 2026
98340a6
fix(harness): remove unused import in built_in module
senamakel Aug 19, 2026
0aa0b5a
fix(built-in): handle missing harness module gracefully
senamakel Aug 19, 2026
c42aa9b
fix(workflows): handle missing caps gracefully
senamakel Aug 19, 2026
0e0c01e
fix(workflows): handle missing capability definitions gracefully
senamakel Aug 19, 2026
94ffde2
fix(workflows): correct caps module to handle missing capabilities gr…
senamakel Aug 19, 2026
b100bbf
fix(caps): handle empty capabilities list in workflow
senamakel Aug 19, 2026
f335a84
chore(workflows): remove unused caps module
senamakel Aug 19, 2026
f7418ce
fix(caps): handle empty capabilities list in workflow validation
senamakel Aug 19, 2026
51355a6
chore(workflows): remove unused caps module
senamakel Aug 19, 2026
72bcf7d
feat(workflows): add capability to handle dynamic workflow scopes
senamakel Aug 19, 2026
a814b99
fix(caps): handle missing capabilities gracefully
senamakel Aug 19, 2026
63b05a7
fix(caps): handle missing capabilities gracefully
senamakel Aug 19, 2026
0059c94
fix(caps): handle empty capabilities list in workflow validation
senamakel Aug 19, 2026
a841324
chore(workflows): remove unused caps module
senamakel Aug 19, 2026
946d455
refactor(caps): shorten fully-qualified RunTurn paths
senamakel Aug 19, 2026
7885bc0
fix(workflows/caps): handle missing capability sets gracefully
senamakel Aug 19, 2026
36fa38b
fix(caps): handle missing capabilities gracefully
senamakel Aug 19, 2026
cd8bc57
fix(workflows): handle missing runner config gracefully
senamakel Aug 19, 2026
0441cf9
fix(workflows): handle missing runner config gracefully
senamakel Aug 19, 2026
310bd88
refactor(workflows): collapse and reformat harness turn construction
senamakel Aug 19, 2026
0d0ba80
fix(router): handle missing route in harness router
senamakel Aug 19, 2026
3df6044
fix(router): handle missing route for root path
senamakel Aug 19, 2026
ecd1a50
fix(router): handle missing route in match
senamakel Aug 19, 2026
ad81abd
fix(router): handle missing route gracefully
senamakel Aug 19, 2026
c40b004
fix(router): handle missing route gracefully when no match found
senamakel Aug 19, 2026
a367c35
fix(router): handle missing route for root path
senamakel Aug 19, 2026
d6e2324
fix(router): handle missing route gracefully
senamakel Aug 19, 2026
bb92e46
chore: files changed src/harness/router.rs
senamakel Aug 19, 2026
aea4e8c
fix(docs): correct provider specification for runtime environment
senamakel Aug 19, 2026
803bae1
docs(spec): clarify manifest semantics for runtime behaviour
senamakel Aug 19, 2026
0a6ca4f
docs(spec): clarify manifest semantics for runtime
senamakel Aug 19, 2026
d7e30fb
chore: files changed docs/spec/runtime/manifest-semantics.md
senamakel Aug 19, 2026
5d720e8
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
9fc316c
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
ecd8c6c
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
48dc258
fix(acp): handle empty turn list in run_turn
senamakel Aug 19, 2026
a4576d1
fix(builder): handle missing runtime config gracefully
senamakel Aug 19, 2026
e5f389f
fix(metering): handle empty triage queue without panic
senamakel Aug 19, 2026
4baba56
fix(server): handle empty inference response gracefully
senamakel Aug 19, 2026
daaf7e8
fix(test): update test to reflect new write behavior
senamakel Aug 19, 2026
3ddfb09
fix(harness): handle zero-length memory regions in built-in allocator
senamakel Aug 19, 2026
5e63b0c
fix(memory_loop): handle zero-length reads without blocking
senamakel Aug 19, 2026
e75340b
fix(harness): handle empty search results gracefully
senamakel Aug 19, 2026
8cfc2cc
fix(harness): handle zero-length reads in memory loop
senamakel Aug 19, 2026
096a569
chore: reformat multi-line function calls for consistency
senamakel Aug 19, 2026
6b716df
fix(server): handle empty inference response gracefully
senamakel Aug 19, 2026
40bb840
fix(server): handle empty inference response gracefully
senamakel Aug 19, 2026
8822314
fix(server): handle empty inference request gracefully
senamakel Aug 19, 2026
a5eb575
fix(server): handle empty inference response gracefully
senamakel Aug 19, 2026
d285382
refactor(server): flatten chained method call in test helper
senamakel Aug 19, 2026
90ff219
fix(server): handle missing inference result gracefully
senamakel Aug 19, 2026
9019f1d
fix(server): handle empty inference response gracefully
senamakel Aug 19, 2026
e192ec0
merge main into named-harnesses
senamakel Aug 19, 2026
7bdabd9
fix(lanes): handle empty lane set in lane selection
senamakel Aug 19, 2026
5ef1e98
test(lanes): add unit tests for overlay agent lane assignment
senamakel Aug 19, 2026
b5de89a
fix(lanes): handle empty lane set in lane selection
senamakel Aug 19, 2026
674d69e
fix(inference): handle missing company name in inference response
senamakel Aug 19, 2026
a358249
fix(brain): handle missing brain file gracefully
senamakel Aug 19, 2026
9525e56
fix(server): handle missing skills in skill list endpoint
senamakel Aug 19, 2026
80a7f89
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
9d9ace6
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
d3dc791
Merge upstream/main into pr/993 (resolve harness split conflict)
senamakel Aug 19, 2026
87a127f
fix(server): handle missing skills gracefully on startup
senamakel Aug 19, 2026
1d3b0ec
fix(standing-grant): correct test to verify grant is not reissued whe…
senamakel Aug 19, 2026
d856865
fix(brain): handle missing brain file gracefully
senamakel Aug 19, 2026
8c72d08
fix(router): handle missing route gracefully when no match is found
senamakel Aug 19, 2026
9a568f3
fix(spend_halt_turn_test): correct test to verify halt on spend durin…
senamakel Aug 19, 2026
321a54f
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
6b4c2cc
fix(spend_halt_turn_test): correct test to verify halt on spend durin…
senamakel Aug 19, 2026
991829c
fix(spend_halt_turn_test): correct test to verify halt on spend durin…
senamakel Aug 19, 2026
8be8f78
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
cf96399
chore(skills): reformat test code for readability
senamakel Aug 19, 2026
9ca96f5
fix(spend_halt_turn_test): correct test to verify halt on spend durin…
senamakel Aug 19, 2026
6fe0c00
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/993
senamakel Aug 19, 2026
ab2770b
fix(harness): correct delivery status filter and add lane routing test
senamakel Aug 19, 2026
917e492
Merge remote-tracking branch 'upstream/main' into pr-993-merge
senamakel Aug 20, 2026
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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ jobs:
- name: Test the ACP surface
run: |
scripts/ci/run-scoped-suite.sh "acp server" acp,runner,tinymemory server::acp
scripts/ci/run-scoped-suite.sh "acp run turn" acp,runner,tinymemory harness::acp_run_turn
scripts/ci/run-scoped-suite.sh "acp run turn" acp,runner,tinymemory harness::acp::run_turn

# Issue #788: Chargebee billing — the REST layer (`chargebee::`) and the
# toolbelt bridge (`harness::chargebee`) in one filter, which selects both.
Expand Down Expand Up @@ -1013,13 +1013,13 @@ jobs:
# entry that decides whether an agent can reach them at all. An unrun
# namespace pin is the wrong thing to have never executed.
- name: Test tool-belt contract (openhuman, mcp, telegram, media)
run: scripts/ci/run-scoped-suite.sh "tool-belt contract" openhuman,mcp,telegram,media harness::build::tests
run: scripts/ci/run-scoped-suite.sh "tool-belt contract" openhuman,mcp,telegram,media harness::built_in::build::tests

# The `media` toolbelt's other half: the three tool names and their single
# `media` namespace. Same feature set as the step above, so this costs a
# filtered re-run of an already-compiled tree and no fourth resolution.
- name: Test the media toolbelt
run: scripts/ci/run-scoped-suite.sh "media toolbelt" openhuman,mcp,telegram,media harness::toolbelt
run: scripts/ci/run-scoped-suite.sh "media toolbelt" openhuman,mcp,telegram,media harness::built_in::toolbelt

# The `mcp` OAuth-flow state, which the belt filter above never reached.
# `app::types` holds two gated tests — a parked flow is SINGLE-USE (a
Expand Down Expand Up @@ -1079,10 +1079,10 @@ jobs:
# the ACP step above states: the first bare argument is the filter and a
# second would narrow the selection to nothing.
- name: Test Composio tenant isolation
run: scripts/ci/run-scoped-suite.sh "composio isolation" openhuman,tinycortex,chargebee,paypal,composio harness::composio::isolation_tests
run: scripts/ci/run-scoped-suite.sh "composio isolation" openhuman,tinycortex,chargebee,paypal,composio harness::built_in::composio::isolation_tests

- name: Test the Composio ops helpers
run: scripts/ci/run-scoped-suite.sh "composio ops helpers" openhuman,tinycortex,chargebee,paypal,composio harness::composio::ops_helper_tests
run: scripts/ci/run-scoped-suite.sh "composio ops helpers" openhuman,tinycortex,chargebee,paypal,composio harness::built_in::composio::ops_helper_tests

# Issue #820 — which connected account an agent acts as. A third narrow
# filter for the same reason the two above are narrow, and the same reason
Expand All @@ -1093,7 +1093,7 @@ jobs:
# the negative half is the one protecting every existing single-account
# company from having its account resolution changed.
- name: Test which Composio account an execute acts as
run: scripts/ci/run-scoped-suite.sh "composio account choice" openhuman,tinycortex,chargebee,paypal,composio harness::composio::live::live_tests
run: scripts/ci/run-scoped-suite.sh "composio account choice" openhuman,tinycortex,chargebee,paypal,composio harness::built_in::composio::live::live_tests

# Issue #820 — a fourth narrow filter, and the first outside `harness::`.
# The console-plane half of the same decision: the grouping the choice is
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ L0 Substrate api.tinyhumans.ai, openhuman-core, tiny.place, filesystem
| [runtime/ports-runs.md](runtime/ports-runs.md) | `RunStore`: attempts and their traces |
| [runtime/events.md](runtime/events.md) | `CompanyEvent` vocabulary + journal correlation rules |
| [runtime/manifest.md](runtime/manifest.md) | `company.toml` schema, `agents.toml` compatibility |
| [runtime/harnesses.md](runtime/harnesses.md) | Named execution engines: `built_in` vs `acp`, transports, per-agent binding |
| [runtime/providers.md](runtime/providers.md) | Inference providers, dual-mode OpenRouter, per-harness credentials |
| [runtime/globals.md](runtime/globals.md) | The global baseline every company gets: agents, workflows, skills, the starting tool belt, and `[globals].disable` |
| [runtime/lifecycle.md](runtime/lifecycle.md) | Company state machine and durability |
| [runtime/planning.md](runtime/planning.md) | The Planning station: pass contract, prerequisite verdicts, boot sweep |
Expand Down
3 changes: 2 additions & 1 deletion docs/spec/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Supporting docs:
- [artifacts.md](artifacts.md) — what makes something a deliverable: the
explicit-publish rule, `(task, source)` identity, body caps and reference
bodies, and the single follow-up nudge
- [manifest.md](manifest.md) — `company.toml` schema
- [manifest.md](manifest.md) — `company.toml` schema,
with [manifest-semantics.md](manifest-semantics.md) for each key's behaviour
- [globals.md](globals.md) — the global baseline: the agents, workflows, skills
and starting tool belt every company gets whichever vertical it started from,
how a company supersedes or disables one, and why provenance is persisted
Expand Down
16 changes: 16 additions & 0 deletions docs/spec/runtime/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ layout first.
role = "Copywriter" # required
description = "Write ads and campaign copy."
tier = "reasoning" # cognition hint; never selects a model
harness = "deep" # which [[harness]] runs this agent's
# turns — see harnesses.md. Omitted
# means the company's default harness.
tools = ["docs.*", "mcp:notion"] # grant globs — see tools.md
delegates_to = ["creative"] # desks this agent may hand work to
budget_usd_daily = 5.0 # per-agent daily cap
Expand All @@ -90,6 +93,19 @@ ledgers = [ # per-agent ledger access (omit for
can_declare_ledgers = false # may this agent `define_ledger`? default true
```

### `tier` versus `harness`

They answer different questions and are deliberately separate fields.

`tier` names a **workload** (`reasoning`, `vision`, …) and is resolved against
whatever provider the agent's harness turns out to use. `harness` names the
**engine and the credential**. So an agent keeps its tier when it moves between
harnesses, and two agents sharing a tier on different harnesses run on different
models — which is the point of naming more than one.

Naming a harness the company does not declare is a validation error, reported
against both the agent and the id. Naming none is not: every roster written
before `[[harness]]` existed binds nobody, and all of them keep working.
### `context` write access

A bare string in `context` is read-only — routed into the prompt, nothing
Expand Down
16 changes: 14 additions & 2 deletions docs/spec/runtime/credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,20 @@ one account into a broken toolkit: when the console revokes an account, and when
whatever credential the company's *declared provider* wants — an OpenRouter
`sk-or-…`, a raw BYOK token, an `openai_compatible` key. It is provider-scoped,
not an identity, and handing it to the TinyHumans backend would present one
vendor's credential to another. The two coincide only when the declared provider
is `managed`, and even then they are the same value for different reasons.
vendor's credential to another.

Since `managed`'s removal the two never coincide, which makes the separation
cleaner rather than looser. A company holding **no** inference key rides the
subscription on the platform's own credential — resolved from this host's
identity, not from `inference/key` — and a company that sets one is naming an
OpenRouter account that has nothing to do with TinyHumans. See
[providers.md](providers.md).

There is one such slot **per harness**: the default harness keeps the flat
`inference/key`, and every named one uses `harness/<id>/inference/key`. The
asymmetry is deliberate — the `SecretStore` has no rename, so namespacing the
default too would orphan the stored credential of every company already
running.

## What this does not cover

Expand Down
227 changes: 227 additions & 0 deletions docs/spec/runtime/harnesses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Harnesses

*What actually runs an agent's turn, and how a company picks.*

Terms: [glossary](../glossary.md). The models a harness talks to are
[providers.md](providers.md); the roster it runs is [agents.md](agents.md).

---

## What a harness is

A **harness** is one answer to "what runs this agent's turn". A company declares
a named set of them and binds each teammate to one, so a single roster can span
a cheap model, an expensive one, and the operator's own coding CLI.

Two kinds ship:

| kind | what runs the turn | credential |
|---|---|---|
| `built_in` | the embedded OpenHuman/tinyagents loop, in this process | its own `[harness.inference]` |
| `acp` | an external agent over the Agent Client Protocol | the agent's own |

`built_in` is the default and the only kind that consults
[providers.md](providers.md). An ACP agent already holds a credential — that is
the point of it — so it needs nothing from us.

### The case this exists for

A desktop company with **no key at all**. The operator has Claude Code installed
and signed in; OpenCompany drives it over ACP against their existing
subscription. Nothing to configure on first run, which is a materially different
product from one that opens on a credential form.

The same seam serves two more things at no extra cost: reverse dispatch (a cloud
host hands work to a runner on someone's machine, which is an ACP agent as far
as this is concerned) and any other harness that speaks the protocol.

---

## Declaring harnesses

```toml
[[harness]]
id = "embedded"
kind = "built_in"
default = true

[harness.inference] # attaches to the entry above
provider = "openrouter"

[[harness]]
id = "deep"
kind = "built_in"

[harness.inference]
provider = "openrouter"
api_key_secret = "harness/deep/inference/key"
models = { "reasoning-v1" = "<openrouter-slug>" }

[[harness]]
id = "my_laptop"
kind = "acp"

[harness.acp]
transport = "local"
agent = "claude"
```

`[harness.inference]` and `[harness.acp]` attach to the **most recently
declared** `[[harness]]`. That is ordinary TOML array-of-tables sub-table
syntax, but it is easy to misread as a company-level section, so it is worth
reading twice.

### Binding an agent

```toml
# agents/researcher.toml
role = "Researcher"
harness = "deep"
```

Inline `[[agent]]` entries take the same field. An agent naming no harness runs
on the one marked `default = true`.

### The implicit harness

A company with **no `[[harness]]` block** gets one implicit `built_in` harness,
marked default, inheriting the company-level `[inference]`. Every bundle under
`companies/` and every existing tenant lands here, so named harnesses are purely
additive: nothing has to be rewritten to keep working.

Read harnesses through `CompanyManifest::effective_harnesses`, never the bare
`harnesses` field. A company that declares none still runs on a harness, and a
caller reading the raw field would see an empty list and conclude it has no
engine, which is never true.

---

## Validation

`CompanyManifest::validate` rejects, in prosumer language:

- a duplicate, empty, or non-snake_case `id`
- zero or more than one `default = true`, naming the candidates either way
- an agent naming a harness nothing declares, naming what *is* declared
- `[harness.inference]` on an `acp` kind, or `[harness.acp]` on a `built_in` one
- `transport = "local"` with no `agent`, or naming a `runner`; and the reverse
for `transport = "runner"`

A section on the wrong kind is an **error, not an ignored key**. This is the
same rule [agents.md](agents.md) applies to a bundle carrying both roster forms,
and for the same reason: a silently discarded declaration stays invisible until
the thing it configured misbehaves, and "my model setting does nothing" is an
expensive way to discover that `[harness.inference]` needs `kind = "built_in"`.

---

## ACP transports

```toml
[harness.acp]
transport = "local" # spawn an agent on this machine
agent = "claude" # claude | codex | goose

[harness.acp]
transport = "runner" # reach one that dialed in
runner = "stevens_laptop"
```

**A remote runner is a transport, not a third kind.**
`src/runner/dispatch.rs::RunnerDispatch` already implements the same `AcpAgent`
port the local subprocess does, so the only thing that differs is how bytes
reach the agent. Modelling it as a third kind would add a resolution path that
resolves to the same place.

The transports differ in where they live, which is why `AcpAgent` is a **port**
rather than an ACP client in the host crate: a subprocess over stdio belongs to
the desktop shell, a WebSocket to the runner lane. The same inversion the
storage ports use.

### Readiness

For `transport = "local"`, the desktop probes four states rather than two:

| state | what to do |
|---|---|
| `NotInstalled` | install it |
| `NotSignedIn` | sign in |
| `Ready` | — |
| `SpawnFailed` | read the reason |

**Installed but not signed in** is the most common state on a fresh machine, and
it looks identical to "not installed" if all you check is `which`. The fixes are
completely different, so collapsing them tells someone to do the wrong thing.

Sign-in is probed by looking for the harness's credential file, not by running
it: asking a harness whether it is logged in means starting it, which is slow on
a list refreshed whenever a settings pane opens, and for some prompts
interactively. The probe can be wrong in one direction — a stale credential
reads as signed in — and that is the acceptable direction, because the failure
then surfaces on first use with the harness's own message, which is more
accurate than anything guessed.

---

## Routing

`HarnessRouter` (`src/harness/router.rs`) holds one `RunTurn` per declared
harness and forwards each call to the one its agent names. `RunTurn` already
carried `agent_id` on all three of its methods, so the dispatch point always
existed — nothing had ever varied on it.

The lanes are built at runtime-build time by `harness::lanes::build`, and
`HarnessBrain` routes through them. **A company declaring one harness (or none)
builds no router at all** — `run_turn()` hands back the single lane directly, so
the overwhelmingly common path is byte-identical to what it was.

Each `built_in` lane gets its own `HarnessPool` and its own `HarnessDeps`,
differing in exactly two fields: the provider (scoped to that harness's config
and credential slots) and `serves`, which narrows the pool to the agents bound
to it. That narrowing is what makes one-pool-per-harness affordable — without
it, a ten-agent roster across three harnesses would stand up thirty live agents
to use ten.

All three methods route. A method forwarding to a fixed engine would send
*dispatched card* turns to the wrong model while operator chat looked correct.

### A harness with no engine fails the turn

A harness can be declared, valid, and still have no engine. Today that is every
`acp` harness on a server build: the transports live in the desktop shell (a
stdio subprocess) and the runner lane (a socket), and neither is wired into the
server. Those turns fail, naming the harness and the fix.

They MUST NOT fall back to another harness's engine. That is the worst outcome
available: the turn would succeed, on a model and a credential nobody chose, and
the only evidence would be a billing line.

---

## What a harness does not decide

- **`[brain].mode`** (`hosted` | `sidecar`) is a separate axis. It selects the
cognition seam *within* the built-in harness.
- **Tools, policy, budgets, desks.** All company- or agent-scoped, and unchanged
by which engine runs the turn. An ACP agent is still subject to the company's
approval policy.
- **Which model an agent's `tier` means.** A tier names a workload and is
resolved against whatever provider its harness turns out to use, so an agent
keeps its tier when it moves between harnesses. See
[providers.md](providers.md).

---

## Implementation map

| concern | where |
|---|---|
| manifest types, kind/transport vocabularies | `src/company/types.rs` |
| validation, `effective_harnesses`, `harness_for` | `src/company/manifest.rs` |
| per-agent dispatch | `src/harness/router.rs` |
| building the lanes at boot | `src/harness/lanes.rs` |
| the built-in engine | `src/harness/built_in/` |
| the ACP `RunTurn` and its port | `src/harness/acp/run_turn.rs` |
| local transport: discovery, spawn, codec | `src-tauri/src/acp/` |
| runner transport | `src/runner/dispatch.rs` |
| per-harness roster narrowing | `HarnessDeps::serves` |
Loading
Loading