Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b913b8d
M9W4E: record first-run model setup boundary
Ian747-tw Aug 26, 2026
3898a25
M9W4E: add immutable setup catalog and durable authority
Ian747-tw Aug 26, 2026
957721e
M9W4E: add packaged Executor readiness integration
Ian747-tw Aug 26, 2026
3dfd81a
M9W4E: activate setup during next Runtime construction
Ian747-tw Aug 26, 2026
ebf3044
M9W4E: add OpenTUI role-model onboarding
Ian747-tw Aug 26, 2026
9585062
M9W4E: prove restart readiness and launch through real user flow
Ian747-tw Aug 26, 2026
637eb77
M9W4E: harden packaged readiness identity evidence
Ian747-tw Aug 26, 2026
4948ca7
M9W4E: align first-run onboarding evidence
Ian747-tw Aug 26, 2026
055ad03
M9W4E: preserve established setup authority flows
Ian747-tw Aug 26, 2026
26de3cc
M9W4E: preserve historical no-mutation evidence
Ian747-tw Aug 26, 2026
98c617f
M9W4E: record local validation evidence
Ian747-tw Aug 26, 2026
3b86959
M9W4E: preserve first-run setup navigation
Ian747-tw Aug 26, 2026
10ee3ec
M9W4E: retain restart-required setup ownership
Ian747-tw Aug 26, 2026
51f6174
M9W4E: enforce one OpenCode process authority
Ian747-tw Aug 26, 2026
addccb6
M9W4E: gate first-run runtime startup
Ian747-tw Aug 27, 2026
638449f
M9W4E: fence interactive startup on setup authority
Ian747-tw Aug 27, 2026
4809fe9
M9W4E: fail closed on setup inspection errors
Ian747-tw Aug 27, 2026
69e6a08
M9W4E: close interactive and project-root startup bypasses
Ian747-tw Aug 27, 2026
050a61c
M9W4E: validate and recover setup startup inspection
Ian747-tw Aug 27, 2026
e55a5f3
M9W4E: enforce exclusive complete setup authority
Ian747-tw Aug 27, 2026
5fd9b70
M9W4E: reject inconsistent and unavailable setup views
Ian747-tw Aug 27, 2026
c679f70
M9W4E: serialize setup mutation against startup
Ian747-tw Aug 27, 2026
aed3314
M9W4E: reserve startup and setup authority atomically
Ian747-tw Aug 27, 2026
4a0f7d1
M9W4E: serialize readiness inspection with startup
Ian747-tw Aug 27, 2026
c28a6ee
M9W4E: bound readiness observer termination
Ian747-tw Aug 27, 2026
d53ce33
M9W4E: latch readiness termination authority
Ian747-tw Aug 27, 2026
462f640
M9W4E: gate headless resume on model setup
Ian747-tw Aug 27, 2026
0b8b6a6
M9W4E: enforce packaged Executor startup authority
Ian747-tw Aug 27, 2026
47c4305
M9W4E: require model setup before Runtime startup
Ian747-tw Sep 3, 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
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,7 @@ Branch 9W4D adds OpenAI Responses as a Commander conformance-policy-v3
selection. The model-configuration schema and Executor mapping stay v1, and
the compatibility matrix remains descriptive evidence rather than profile,
conformance, mapping, or readiness authority.

Branch 9W4E constructs this unchanged vocabulary from a fixed six-recipe
catalog and persists only credential-free recipe identities and semantic
hashes. It does not introduce a second selection schema.
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ External research MCP remains post-v1.

`resume_supported=false`, `provider_tool_loop_enabled=false`, and
`external_read_execution_enabled=false` remain unchanged.

Branch 9W4E activates persisted setup only during the next RuntimeServer
construction. Executor readiness uses the exact packaged OpenCode executable
selected for launch and remains separate non-authoritative evidence; the
registry and projection hashes remain immutable for the process lifetime.
94 changes: 94 additions & 0 deletions agentcore/adr/ADR-040-first-run-model-setup-and-role-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ADR-040 - First-run model setup and role selection

## Status

Accepted for Branch 9W4E.

## Context

ADR-035 defines credential-free model connections, profiles, and independent
Commander/Executor bindings. ADR-036 activates those snapshots in one immutable
RuntimeServer registry. ADR-037 and ADR-038 add native Gemini and OpenAI
Responses. None provides durable user selection or a first-run product flow.

Model setup must not turn compatibility evidence, OpenCode discovery, or TUI
state into provider authority. It also cannot mutate the active registry or
store connector credentials.

## Decision

1. Add the code-owned `nexusloop_model_setup_catalog_v1`. It offers exactly
three Commander recipes backed by static conformance and three primary
Executor recipes backed by the static provider mapping: Anthropic Claude
Sonnet 4.5, Google Gemini 2.5 Flash, and OpenAI GPT-4.1 mini. Either role may
remain explicitly unconfigured.
2. RuntimeServer reconstructs a complete ADR-035 candidate from exact recipe
IDs. The compatibility matrix, OpenCode catalog/authentication, and caller
provider/model assertions cannot add recipes or conformance.
3. Preview is read-only and returns the current revision plus candidate,
configuration, and role-projection hashes. Confirmation requires the exact
revision, candidate hash, bounded human identity, and
`CONFIRM_MODEL_SETUP`.
4. The sole durable transition is
`runtime_model_setup_committed` under
`nexusloop_model_setup_event_v1`. Its allowlist contains recipe IDs,
contiguous revision linkage, semantic hashes, bounded human identity, and
time. It contains no complete configuration, connector URL, header,
credential reference name/value, environment name, provider object, or
OpenCode authentication state.
5. `EventStore.appendIfLatestKind` provides atomic expected-kind comparison
without allowing unrelated journal traffic to starve setup confirmation.
Exact duplicate confirmation is idempotent; stale, different, malformed,
truncated, duplicate-revision, unknown-version, or hash-invalid authority
fails closed.
6. Startup projects the append-only event stream before RuntimeServer
construction and builds the existing immutable ADR-036 registry. Persisted
setup, explicit registry/provider authority, and legacy Commander
environment authority are mutually exclusive.
7. A commit never replaces the active registry. It is pending for the next
process start. Setup writes are RuntimeServer-owned, acquire or reuse the
run lock, and drain before `runtime_shutdown`.
8. Commander and Executor readiness remain independent evidence. A safe
selection may persist while connector or credential readiness is blocked or
unknown. Neither role falls back to the other.
9. Production Executor readiness invokes the 9W4E0 command `opencode
nexusloop executor-readiness-v1`. Runtime sends only the exact immutable
Executor projection. The packaged OpenCode command checks pinned
catalog/config/plugin/auth semantics, discards unrelated identities and raw
state, and returns only matching tri-state evidence. Runtime owns
timeout, output bounds, concurrency, identity validation, cancellation, and
shutdown drain. The observation cannot select, map, normalize, recommend,
fall back, or authorize Commander.
Production uses the exact validated OpenCode executable configured for
Executor launch and fixes readiness arguments internally. No separate
executable, source module, preload, dependency path, or environment
assertion may replace it; process injection is package-internal test
machinery only.
10. OpenTUI extends the existing initialization/onboarding surface with
keyboard selection, preview, separate explicit confirmation, current and
pending hashes, blocked readiness, cancellation, and restart-required
rendering. Cached UI state is never mutation authority.
11. Executor selection still reaches only the primary tactical OpenCode run as
one exact `--model provider/model` argument. Auxiliary models and OpenCode
global/user configuration are unchanged.
12. The unset TUI runtime-client mode is `auto`: legacy fake behavior is
limited to pre-spec onboarding, while an approved project constructs the
real RuntimeServer client. Explicit fake mode remains non-production
fixture authority and cannot prove a durable setup commit.

## Consequences

First-run and later model selection now have one credential-free, append-only,
restart-only authority path. Connector construction and both role-owned
credential resolvers remain separate. Custom endpoints, arbitrary model
discovery, credentials in TUI state, OpenCode `auth.json` mutation, hot reload,
fallback, retry, streaming, auxiliary model selection, MCP, proposals,
governance, and mutation remain out of scope.

OpenCode's public provider-list response is not Runtime authority and is not
crossed into RuntimeServer. The packaged command performs no provider execution
request, retry, mutation, or catalog refresh. Observation failure,
partial state, timeout, truncation, cancellation, or shutdown remains unknown.

`resume_supported=false`, `provider_tool_loop_enabled=false`, and
`external_read_execution_enabled=false` remain unchanged.
2 changes: 2 additions & 0 deletions agentcore/runtime/src/authority/command-authority-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const profiles = {
apply: profile(["tests/e2e_user/scenarios/test_commander_cycle_tui.py"]),
externalApi: profile(["tests/e2e_user/scenarios/test_reasoning_provider_tui.py"]),
commanderRecovery: profile(["tests/e2e_user/scenarios/test_commander_recovery_operator_controls_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_commander_continuity_packet_tui.py"]),
modelSetup: profile(["tests/e2e_user/scenarios/test_model_setup_executor_readiness_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py"]),
}

type BaseRecord = Omit<CommandAuthorityRecord, "authority_id" | "aliases" | "creates_external_process" | "calls_provider" | "requires_run_lock" | "blocked_by_default" | "expected_event_kinds" | "recommended_reads" | "notes" | "out_of_scope">
Expand Down Expand Up @@ -178,6 +179,7 @@ function write(args: {
}

export const COMMAND_AUTHORITY_REGISTRY: CommandAuthorityRecord[] = [
record({ slash_command: "/model-setup", runtime_command: "runtime.confirm_model_setup", risk: "medium_risk_write", gate: "model_setup_runtime", owner: "model_setup", mutates_events: true, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: true, requires_approval: true, approval_surface: "/model-setup", expected_event_kinds: ["runtime_model_setup_committed"], blocked_by_default: true, current_phase_status: "implemented", validation_profile: profiles.modelSetup, notes: ["Explicitly confirms one credential-free model setup candidate and appends one setup record for restart-only activation. Confirmation itself starts no external process; subsequent status and launch-readiness reads may invoke the bounded packaged OpenCode observer."], out_of_scope: ["credential storage", "provider calls", "OpenCode launch", "hot reload", "automatic restart", "provider discovery", "role fallback"] }),
read("/authority", "runtime.command_authority_summary", "runtime_status", "none", profiles.authority, ["/command-authority", "/command-map"]),
read("/authority-summary", "runtime.command_authority_summary", "runtime_status", "none", profiles.authority),
read("/authority-list", "runtime.command_authority_list", "runtime_status", "none", profiles.authority),
Expand Down
2 changes: 2 additions & 0 deletions agentcore/runtime/src/authority/command-authority-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type CommandAuthorityGate =
| "opencode_runtime"
| "external_api_runtime"
| "commander_recovery_runtime"
| "model_setup_runtime"
| "unknown"

export type CommandAuthorityOwner =
Expand Down Expand Up @@ -52,6 +53,7 @@ export type CommandAuthorityOwner =
| "playbook"
| "commander_apply"
| "commander_recovery"
| "model_setup"
| "unknown"

export type CommandPhaseStatus =
Expand Down
40 changes: 40 additions & 0 deletions agentcore/runtime/src/events/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,46 @@ export class EventStore {
return operation.finally(() => { this.pendingAppends -= 1 })
}

async appendIfLatestKind(
event: JsonlEvent,
kind: string,
expectedLatestEventId: string | null,
operational: { before_write?: () => void } = {},
): Promise<string> {
if (event.kind !== kind) throw new Error("event kind does not match append authority")
this.appendGeneration += 1
this.pendingAppends += 1
const operation = this.appendQueue.then(async () => {
await mkdir(dirname(this.eventsPath), { recursive: true })
const events = await this.readAllSnapshot()
let latest: JsonlEvent | undefined
for (let index = events.length - 1; index >= 0; index -= 1) {
if (events[index]?.kind === kind) {
latest = events[index]
break
}
}
const latestEventId = latest?.event_id ? String(latest.event_id) : null
if (latestEventId !== expectedLatestEventId) throw new Error("event kind changed before append")
const safeEvent = redactValue({
...event,
event_id: event.event_id ?? makeEventId(),
timestamp: event.timestamp ?? new Date().toISOString(),
})
const handle = await open(this.eventsPath, "a")
try {
operational.before_write?.()
await handle.write(JSON.stringify(safeEvent) + "\n")
await handle.sync()
} finally {
await handle.close()
}
return String(safeEvent.event_id)
})
this.appendQueue = operation.catch(() => undefined)
return operation.finally(() => { this.pendingAppends -= 1 })
}

async readAll(): Promise<JsonlEvent[]> {
while (true) {
const generationBefore = this.appendGeneration
Expand Down
3 changes: 3 additions & 0 deletions agentcore/runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { RuntimeServer } from "./server"
export type { RuntimeResearchDbProjection, RuntimeResearchDbReader, RuntimeServerOptions } from "./server"
export { locateProjectRoot } from "./project/project-root"
export { createRuntimeServerFromLaunchConfig, readRuntimeServerLaunchOptionsFromEnv, readWakeSchedulerBootstrapConfigFromEnv } from "./launch-config"
export type { RuntimeServerLaunchConfig } from "./launch-config"
export { EventStore } from "./events/event-store"
Expand All @@ -13,6 +14,8 @@ export { adaptLegacyCommanderModelAuthority } from "./model-configuration/model-
export * from "./model-configuration/model-profile-runtime-registry-types"
export { providerCompatibilityMatrix, PROVIDER_COMPATIBILITY_MATRIX_POLICY_VERSION, PROVIDER_COMPATIBILITY_MATRIX_SCHEMA_VERSION } from "./model-configuration/provider-compatibility-matrix"
export type { CommanderProviderCompatibilityEvidence, CommanderProviderCompatibilityMatrix } from "./model-configuration/provider-compatibility-matrix"
export { ModelSetupService, buildModelSetupCandidate, modelSetupCatalog, projectModelSetupEvents, readPersistedModelSetupAuthority } from "./model-configuration/model-setup"
export type { ModelSetupCandidate, ModelSetupCatalog, ModelSetupChoices, ModelSetupCommitInput, ModelSetupCommitResult, ModelSetupPreview, ModelSetupProjection, ModelSetupRecipe, PersistedModelSetupAuthority } from "./model-configuration/model-setup"
export type { WakeSchedulerBootstrapConfig, WakeSchedulerBootstrapStatus, WakeSchedulerStaleRunInfo } from "./schedules/wake-scheduler-bootstrap-types"
export type { WakeSchedulerRecovery, WakeSchedulerRecoveryAcknowledgeInput, WakeSchedulerRecoveryCommand, WakeSchedulerRecoveryPreview, WakeSchedulerRecoveryRecord, WakeSchedulerRecoveryStatus } from "./schedules/wake-scheduler-recovery-types"
export type { WakeSchedulerRecoveryWorkflow, WakeSchedulerRecoveryWorkflowCancelInput, WakeSchedulerRecoveryWorkflowInput, WakeSchedulerRecoveryWorkflowPreview, WakeSchedulerRecoveryWorkflowRecord, WakeSchedulerRecoveryWorkflowStep, WakeSchedulerRecoveryWorkflowStepRecordInput, WakeSchedulerRecoveryWorkflowVerification } from "./schedules/wake-scheduler-recovery-workflow-types"
Expand Down
67 changes: 64 additions & 3 deletions agentcore/runtime/src/launch-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { readExternalApiConnectorsFromEnv } from "./external-api/api-connector-r
import { readReasoningProviderConfigFromEnv } from "./reasoning/reasoning-provider-config"
import { readCommanderInvestigationProviderConfigFromEnv } from "./commander-agent"
import type { WakeSchedulerBootstrapConfig } from "./schedules/wake-scheduler-bootstrap-types"
import { locateProjectRoot } from "./project/project-root"
import { readPersistedModelSetupAuthority } from "./model-configuration/model-setup"
import { createPackagedOpenCodeExecutorReadinessResolver, snapshotPackagedOpenCodeAdapterConfig } from "./model-configuration/opencode-executor-readiness-resolver"

export interface RuntimeServerLaunchConfig extends RuntimeServerOptions {
export interface RuntimeServerLaunchConfig extends Omit<RuntimeServerOptions, "executorModelReadinessResolver"> {
env?: Record<string, string | undefined>
}

export function readRuntimeServerLaunchOptionsFromEnv(
env: Record<string, string | undefined>,
baseOptions: RuntimeServerOptions = {},
): RuntimeServerOptions {
rejectExecutorObserverEnvironment(env)
const options: RuntimeServerOptions = { ...baseOptions }
if (options.modelProfileRuntimeRegistry && hasLegacyCommanderEnvironmentAuthority(env)) {
throw new Error("explicit model-profile registry cannot be combined with legacy Commander environment authority")
Expand Down Expand Up @@ -56,8 +60,65 @@ function hasLegacyCommanderEnvironmentAuthority(env: Record<string, string | und
}

export function createRuntimeServerFromLaunchConfig(config: RuntimeServerLaunchConfig = {}): RuntimeServer {
const { env, ...baseOptions } = config
return new RuntimeServer(env ? readRuntimeServerLaunchOptionsFromEnv(env, baseOptions) : baseOptions)
if (Object.prototype.hasOwnProperty.call(config, "executorModelReadinessResolver")) {
throw new Error("custom Executor readiness resolver is not supported by production launch configuration")
}
const { env, ...providedOptions } = config
if (env) rejectExecutorObserverEnvironment(env)
const projectDir = locateProjectRoot(providedOptions.projectDir)
const persisted = readPersistedModelSetupAuthorityBeforeLock(projectDir)
if (persisted && providedOptions.modelProfileRuntimeRegistry) {
throw new Error("persisted model setup cannot be combined with an explicit model-profile registry")
}
if (persisted && providedOptions.commanderInvestigationProviderConfig) {
throw new Error("persisted model setup cannot be combined with explicit Commander provider authority")
}
if (persisted && env && hasLegacyCommanderEnvironmentAuthority(env)) {
throw new Error("persisted model setup cannot be combined with legacy Commander environment authority")
}
const baseOptions: RuntimeServerOptions = persisted
? {
...providedOptions,
projectDir,
revalidatePersistedModelSetupOnStart: true,
modelProfileRuntimeRegistry: persisted.registry,
modelSetupActiveHash: persisted.setup_hash,
modelSetupActiveCandidate: persisted.candidate,
...(persisted.commander_provider_config ? { commanderInvestigationProviderConfig: persisted.commander_provider_config } : {}),
}
: { ...providedOptions, projectDir, revalidatePersistedModelSetupOnStart: true }
const options = env ? readRuntimeServerLaunchOptionsFromEnv(env, baseOptions) : baseOptions
if (options.modelProfileRuntimeRegistry?.executorSelection()) {
if (options.adapter || options.opencodeLaunchAdapter || options.opencodeLaunchSpawn || options.openCodeAdapterFactoryOptions?.spawn) {
throw new Error("Executor model-profile selection requires the same packaged OpenCode execution target for readiness and launch")
}
if (options.openCodeAdapterConfig?.kind !== "process") {
throw new Error("Executor model-profile selection requires a packaged process OpenCode execution target for readiness and launch")
}
const executionTarget = snapshotPackagedOpenCodeAdapterConfig(options.openCodeAdapterConfig)
options.openCodeAdapterConfig = executionTarget
options.executorModelReadinessResolver = createPackagedOpenCodeExecutorReadinessResolver({
projectDir,
openCodeAdapterConfig: executionTarget,
})
}
return new RuntimeServer(options)
}

function readPersistedModelSetupAuthorityBeforeLock(projectDir: string): ReturnType<typeof readPersistedModelSetupAuthority> {
try {
return readPersistedModelSetupAuthority(projectDir)
} catch (error) {
if (error instanceof Error && error.message === "model setup journal is malformed") return undefined
throw error
}
}

function rejectExecutorObserverEnvironment(env: Record<string, string | undefined>): void {
if (env.NXL_OPENCODE_EXECUTOR_READINESS_COMMAND !== undefined
|| env.NXL_OPENCODE_EXECUTOR_READINESS_ARGS_JSON !== undefined) {
throw new Error("custom Executor readiness observer environment configuration is not supported")
}
}

export function readWakeSchedulerBootstrapConfigFromEnv(env: Record<string, string | undefined>): WakeSchedulerBootstrapConfig | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export type ExecutorModelReadinessObservation = Readonly<{
}>

export interface ExecutorModelReadinessResolver {
start?(): Promise<void> | void
observe(selection: ExecutorModelSelectionProjection): Promise<unknown> | unknown
shutdown?(): Promise<void> | void
}

export type CommanderModelReadinessInput = CommanderInvestigationProviderReadiness
Loading
Loading