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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ new version heading in the same commit.

## [Unreleased]

## [0.283.0] — 2026-07-31
### Fixed
- **A Codex run silently inherited a Claude model from the workspace default and died.** Found on the
first live attachable Codex session: the fleet default is `model: "opus"`, `codex-scout` pins no model
of its own, so the launcher passed `opus` to Codex, which answered *"The 'opus' model is not supported
when using Codex with a ChatGPT account."* Two holes, both now closed:
- The cross-runtime model guard only matched full ids (`^claude`), so bare **aliases** — `opus`,
`sonnet`, `haiku`, `fable` — sailed through. The patterns are now anchored on a word boundary and
cover aliases both ways (`gpt`/`o<n>`/`codex`/`glm`/`kimi`/`deepseek` are refused for Claude Code).
- Nothing validated **inheritance**. `PUT /api/agents/:id/config` rejects a foreign model, but the
workspace default spans every runtime and therefore *cannot* be right for all of them at once — an
agent with no model of its own picked one up silently. `resolveRuntimeTuning` now takes the runtime
and **drops** a model that runtime can't run, so the session falls back to the CLI's own default (a
working run) instead of failing outright.
7 new conformance fixtures pin the inheritance matrix; suite is 137/137.

## [0.282.0] — 2026-07-31
### Added
- **Cockpit `action` tier now *executes* — a governed operator carries it out (auto-router Phase 3).**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.282.0",
"version": "0.283.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
14 changes: 13 additions & 1 deletion scripts/governance-conformance.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ if (newestSrc > newestDist) {

const { enrichArgs, autoClearsApproval } = require(path.join(ROOT, 'dist/governance/enricher'));
const { JsonPolicyEngine } = require(path.join(ROOT, 'dist/governance/policy'));
const { resolveRuntimeTuning } = require(path.join(ROOT, 'dist/types.js'));
const { fileGovernanceDecision } = require(path.join(ROOT, 'dist/governance/file-guard.js'));
const { hostGovernanceDecision, stricterDecision } = require(path.join(ROOT, 'dist/governance/host-match'));

Expand Down Expand Up @@ -131,7 +132,18 @@ for (const c of fixture.context) {

const riskyChecks = fixture.decisions.filter((c) => typeof c.expectRisky === 'boolean').length;
const factChecks = fixture.decisions.reduce((n, c) => n + (c.expectFacts ? Object.keys(c.expectFacts).length : 0), 0);
const total = fixture.decisions.length + fixture.context.length + riskyChecks + factChecks;
// Runtime tuning: a model belonging to another runtime must never reach a CLI. The per-agent route
// rejects one outright; INHERITANCE from the workspace default (which spans every runtime and so can't
// be right for all of them) silently drops it instead, so the run falls back to the CLI default rather
// than dying. Regression-guards a live failure where a Codex run inherited `opus`.
for (const c of fixture.tuning || []) {
const got = resolveRuntimeTuning(c.agent || {}, c.defaults || {}, c.override, c.runtime);
const want = c.expectModel === null ? undefined : c.expectModel;
if (got.model === want) pass++;
else failures.push(`tuning ✗ ${c.name}\n expected model=${want}, got ${got.model}`);
}

const total = fixture.decisions.length + fixture.context.length + riskyChecks + factChecks + (fixture.tuning || []).length;
if (failures.length) {
console.error(`\nGOVERNANCE CONFORMANCE: ${pass}/${total} passed, ${failures.length} FAILED\n`);
for (const f of failures) console.error(' ' + f);
Expand Down
2 changes: 1 addition & 1 deletion src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,7 @@ export class TerminalManager {
// we don't wrap the shell in Seatbelt/bubblewrap. Real OS containment is the Linux uid-isolation path.
// Per-agent model / effort / permission-mode fall back to the workspace default; the launcher maps
// them onto `--model`/`--effort`/`--permission-mode` (permission-mode on the interactive lane only).
const tuning = resolveRuntimeTuning(manifest, this.os.settings.runtimeDefaults(), o.tuning);
const tuning = resolveRuntimeTuning(manifest, this.os.settings.runtimeDefaults(), o.tuning, runtime);
if (runtime === 'codex') {
// Codex reads model/effort from the config.toml the launcher generates; it has no
// --permission-mode (Agent OS is the sole authority there via approval_policy = "never"), so
Expand Down
27 changes: 23 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ export const CODING_RUNTIMES: Readonly<Record<CodingRuntimeId, CodingRuntimeSpec
gateHook: 'gate-hook.sh',
credentialEnv: { configDirVar: 'CLAUDE_CONFIG_DIR', apiKeyVar: 'ANTHROPIC_API_KEY', tokenVar: 'CLAUDE_CODE_OAUTH_TOKEN' },
suggestedModels: ['claude-opus-4-8', 'claude-sonnet-4-8', 'claude-haiku-4-5'],
foreignModel: /^(gpt|o[0-9]|codex)/i,
foreignModel: /^(gpt|o[0-9]|codex|glm|kimi|deepseek)\b/i,
capabilities: {
pinnedSessionId: true, resume: true, fork: true, attachableUnattended: true,
residentChat: true, transcript: true, nativeSkills: true, nativeSubagents: true,
Expand All @@ -1155,7 +1155,11 @@ export const CODING_RUNTIMES: Readonly<Record<CodingRuntimeId, CodingRuntimeSpec
// OPENAI_API_KEY is the usage-billed alternative the launcher already accepts.
credentialEnv: { configDirVar: 'AOS_REAL_CODEX_HOME', apiKeyVar: 'OPENAI_API_KEY' },
suggestedModels: ['gpt-5-codex', 'gpt-5.6-sol'],
foreignModel: /^claude/i,
// Bare ALIASES matter as much as full ids: the workspace default is often just `opus`,
// and Codex answers `The 'opus' model is not supported` — seen live. Anchor on a word
// boundary so `opus` and `claude-opus-4-8` are both caught but a hypothetical
// `opusgpt-x` custom id is not.
foreignModel: /^(claude|opus|sonnet|haiku|fable)\b/i,
capabilities: {
// Codex mints its own rollout id (`codex exec resume <id>` / `codex fork <id>`), so the id is
// captured after launch rather than pinned. The unattended lane is `codex exec`, which exits at
Expand Down Expand Up @@ -1543,9 +1547,24 @@ export function sanitizeSvgIcon(input: string): string | undefined {
* own model/effort), else the agent's own value, else the workspace default, else undefined (CLI
* default) — except `permissionMode`, whose floor is `auto` (the interactive lane always runs with a
* mode; the built-in default is `auto`, not the CLI's own). Pure — used by the terminal launcher. */
export function resolveRuntimeTuning(agent: RuntimeTuning, defaults: RuntimeTuning, override?: RuntimeTuning): RuntimeTuning {
export function resolveRuntimeTuning(
agent: RuntimeTuning,
defaults: RuntimeTuning,
override?: RuntimeTuning,
/** The runtime this tuning will actually run under. When given, a resolved model that belongs to a
* DIFFERENT runtime is dropped rather than passed to the CLI. */
runtime?: RuntimeId,
): RuntimeTuning {
let model = override?.model ?? agent.model ?? defaults.model;
// The workspace default spans every runtime, so it CANNOT be correct for all of them at once: a
// fleet default of `opus` is right for Claude Code and fatal for Codex, which answers
// `The 'opus' model is not supported`. The per-agent config route already rejects a cross-family
// model, but nothing validated INHERITANCE — an agent with no model of its own silently picked up a
// foreign one. Drop it here instead, so the run falls back to the CLI's own default (a working
// session on a sensible model) rather than failing outright. Found live on a Codex run.
if (model && runtime && validateModelForRuntime(runtime, model)) model = undefined;
return {
model: override?.model ?? agent.model ?? defaults.model,
model,
effort: override?.effort ?? agent.effort ?? defaults.effort,
permissionMode: override?.permissionMode ?? agent.permissionMode ?? defaults.permissionMode ?? 'auto',
};
Expand Down
83 changes: 83 additions & 0 deletions test/governance/conformance.json
Original file line number Diff line number Diff line change
Expand Up @@ -1630,5 +1630,88 @@
},
"expectAutoClear": false
}
],
"tuning": [
{
"name": "tuning: codex does not inherit a Claude workspace model",
"runtime": "codex",
"agent": {},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"expectModel": null
},
{
"name": "tuning: claude-code does inherit the workspace model",
"runtime": "claude-code",
"agent": {},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"expectModel": "opus"
},
{
"name": "tuning: codex keeps its own valid model",
"runtime": "codex",
"agent": {
"model": "gpt-5-codex"
},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"expectModel": "gpt-5-codex"
},
{
"name": "tuning: a stale per-agent Claude model is dropped for codex",
"runtime": "codex",
"agent": {
"model": "claude-opus-4-8"
},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"expectModel": null
},
{
"name": "tuning: claude-code drops an inherited codex model",
"runtime": "claude-code",
"agent": {},
"defaults": {
"model": "gpt-5-codex"
},
"expectModel": null
},
{
"name": "tuning: a per-launch override still wins when valid",
"runtime": "codex",
"agent": {},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"override": {
"model": "gpt-5.6-sol"
},
"expectModel": "gpt-5.6-sol"
},
{
"name": "tuning: no runtime given leaves the model untouched (back-compat)",
"agent": {},
"defaults": {
"model": "opus",
"effort": "high",
"permissionMode": "auto"
},
"expectModel": "opus"
}
]
}
Loading