STAGE 1 / MILESTONE 7

Harness Platform M7: putting durable policy before tool execution

How M7 adds a deterministic multi-round session loop with versioned context, strict tool admission, durable policy-before-effect ordering, hard budgets, cancellation, and timeouts.

01 / MILESTONE CONTRACT

M7 turns one durable request into a loop that can reason, act, observe, and continue.

M6 established the narrow runtime boundary: append events before exposing them, let the consumer control progress, and keep lifecycle outcomes typed. M7 spends those semantics on the first complete agent turn—multiple model rounds joined by strictly admitted, policy-fenced pure-tool calls and immutable observations.

The milestone is deliberately smaller than “an autonomous coding agent.” It proves the control loop itself. A model can request a registered pure function, the runtime can validate and authorize it, its result can enter the next context revision, and the model can finish—all without giving model output a direct path to files, processes, the network, or a live provider.

Stage 1 / Milestone 7 contract (condensed)text · source
M7 — Deterministic minimal session loop

ship
├── one turn with repeated model / pure-tool rounds
├── versioned, immutable message and context snapshots
├── strict model-stream and tool-argument validation
├── tool intent → durable policy → optional permission → execution
├── hard step, cumulative-token, and requested-tool-call budgets
├── per-round model deadlines and cooperative cancellation
└── one terminal-finalization path

preserve
├── append-before-yield and consumer backpressure from M6
├── legacy runAgent()
├── legacy Model.complete() through CompleteModelAdapter
└── additive event decoding

defer
├── operational workspace, file, process, network, or MCP tools
├── live provider evidence
├── multi-turn follow-ups and mid-loop steering
├── context compaction and restart replay
├── exactly-once external effects
└── a CLI or service that self-hosts on this runtime

02 / MULTI-ROUND LOOP

A tool result is not an endpoint; it becomes the next model observation.

The canonical test uses two model requests. The first asks for a pure tool. The runtime records the assistant intention, validates and fences the call, executes only after an allowed decision is durable, stores the result, and appends a typed tool message. The second request receives that observation and produces the final answer.

The canonical M7 path completes a two-round tool loopAt M7 merge 41af384, one admitted turn can cross multiple model rounds. Every user, assistant, and tool message advances immutable state; each model request identifies the exact revision it sees; and the run closes with one ordered terminal pair when both terminal appends succeed.
Canonical successful two-round ordertext · source
agent.started
turn.started
message.completed  role=user                 revision=1
model.request      contextVersion=1            messageRevision=1
model.response     finishReason=tool_calls
message.completed  role=assistant              revision=2
tool.call          durable intent · no execution yet
policy.decision    allow | ask | deny
permission.requested                         # ask only
permission.resolved                          # ask only
tool.result        after allowed pure execution
message.completed  role=tool                 revision=3
model.request      contextVersion=1            messageRevision=3
message.delta      role=assistant · 0..n
model.response     finishReason=stop
message.completed  role=assistant              revision=4
turn.completed
agent.stopped

Zero or more assistant deltas may appear before each completed response. Calls are processed sequentially, not in parallel. agent.started and agent.stopped bookend exactly one turn.started and one turn.completed outcome when terminal appends succeed. A text-only answer is the same structure with the tool segment omitted.

03 / VERSIONED CONTEXT

Every request can name the exact immutable message history it consumed.

M7 introduces explicit version-one message state and model context. A prior context starts at a revision equal to its validated message count. Appending the new user message, assistant intention, tool observation, and final assistant response advances the revision one step at a time. Each model.request records both contextVersion and messageRevision.

Versioned snapshots make every model round explainableAt M7 merge 41af384, message state and model context use version 1 plus a monotonic revision. Each appended user, assistant, or tool message creates a detached immutable snapshot, so every model.request can name the exact context it consumed.
Versioned state and request snapshot contract (condensed TypeScript)typescript · source
type VersionedMessageState = Readonly<{
  version: 1
  revision: number
  messages: readonly ChatMessage[]
}>

type VersionedModelContext = Readonly<{
  version: 1
  messageRevision: number
  messages: readonly ChatMessage[]
  tools: readonly ToolDefinition[]
}>

appendMessage(state, message)
  → clones + validates the current snapshot
  → appends a detached frozen message
  → returns revision + 1

buildModelContext(state, tools)
  → returns a detached frozen request snapshot
  → records the exact messageRevision consumed

These are detached deep snapshots, not frozen wrappers around caller-owned references. The normalizer rejects getters and setters, symbols, cycles, named properties on arrays, non-finite numbers, excessive depth, excessive node counts, and values over their byte bounds. That hardening prevents a model, consumer, permission callback, or tool from mutating the data that a later boundary believes it authorized.

04 / MODEL STREAM INTEGRITY

Provider output must agree with itself before it can become an executable intention.

The model protocol now has three frames: text delta, complete tool call, and completed response. The runtime aggregates text, buffers tool intentions, and cross-checks both against the terminal response. It rejects a missing terminal, text after tool calls, mismatched content, mismatched call arrays, reused IDs, impossible finish reasons, malformed usage, accessor-backed values, and oversized structures. The first response.completed ends model pulling, so a provider frame queued after it is not inspected or described as a duplicate-terminal rejection.

M7 provider-stream admissiontext · source
valid provider stream
├── text.delta*                       nonempty · bounded · before tool calls
├── tool.call*                        complete ordered intentions
└── response.completed                first terminal frame ends model pulling

cross-check before canonical tool.call
├── concatenated deltas === completed content, when deltas exist
├── streamed calls === terminal calls by id, name, and JSON value
├── finishReason agrees with tool presence
├── usage counters are safe, finite, and internally consistent
├── call IDs are unique across the turn
└── objects are bounded ordinary data with no accessors or exotic fields

failure
└── RUNTIME_MODEL_STREAM_INVALID → failed terminal path
                                   · no effect from that malformed round

The existing completion API remains usable. CompleteModelAdapter emits each already-complete tool intention followed by one terminal response; it does not fabricate token deltas. Native adapters keep their real stream. The entire M7 verification lane uses FakeModel or local deterministic adapters—never a production model endpoint.

05 / TOOL ADMISSION

Unknown and invalid calls become observations, never authorization questions.

The canonical tool.call event means “the model requested this intent,” not “the runtime already executed it.” After that intent is durable, the runtime resolves the registered definition and validates a detached argument snapshot. Only a known call with valid input can reach policy.

Unknown and invalid tool branchestext · source
for each requested tool intention
  1. persist tool.call
  2. find the registered tool
  3. validate a detached JSON argument snapshot

unknown name
  → tool.result { ok: false, code: "TOOL_NOT_FOUND" }
  → tool observation enters context
  → no policy derivation · no permission · no execution

invalid input
  → tool.result { ok: false, code: "TOOL_BAD_INPUT" }
  → tool observation enters context
  → no policy derivation · no permission · no execution

A typed failed result is intentionally returned to the conversation. The model may recover in a later round instead of losing the entire turn. Tool exceptions and invalid tool outputs follow the same observation pattern with their own error codes, while an unpersisted intent cannot progress at all.

06 / DURABLE EXECUTION FENCE

The intent and the authorization record exist before the effect begins.

M7 extends M6's append-before-yield discipline across the tool boundary. It stores the requested call, validates input, lets the tool derive its authorization intent, requires the trusted pure marker, and then chooses the built-in or injected decision. It stores policy.decision and any interactive permission resolution before invoking the implementation. If any pre-effect append fails, execution is prevented.

Durable evidence fences every pure-tool executionAt M7 merge 41af384, tool intent is durable before any effect. A registered call must survive strict argument validation and a persisted allow or ask resolution before a trusted in-process pure tool may run; invalid, unknown, denied, over-budget, or unpersisted calls cannot execute.
Policy-before-effect sequencetext · source
durable tool.call
        │
        ▼
validate registered tool + immutable input
        │
        ▼
derive authorization intent
        │
        ▼
require trusted pure boundary · choose allow | ask | deny
        │
        ▼
durable policy.decision
        │
        ├── deny ────────────────→ failed tool.result observation
        │
        └── ask → durable permission.requested
                    → wait for resolver
                    → durable permission.resolved
        │
        ▼ allowed only
execute trusted in-process pure tool
        │
        ▼
durable tool.result → durable tool message → next model context

07 / PERMISSION SEMANTICS

Allow, ask, and deny remain explicit even when the tool is pure.

A valid pure call can be allowed, denied, or paused for a resolver. When no permission controller is supplied, a registry-marked pure tool receives the built-in runtime.m7.pure allow decision. An ask path persists the request before waiting and persists its resolution before executing. A hard deny cannot be overridden. A missing or failed resolver fails closed. Resolver decisions are attributed to the operator; synthetic denials remain attributed to the kernel.

Permission and run-scoped grant behaviortext · source
allow
└── execute only after policy.decision append succeeds

ask
├── persist permission.requested
├── pause without executing
├── resolver allow → persist operator resolution → execute
└── resolver missing, failed, canceled, or deny
    → persist kernel/operator denial when possible → do not execute

run-scoped grant
├── key = action + subject
├── may satisfy a later matching ask in the same run
└── never overrides a later hard deny

no PermissionController
└── a registered pure tool is allowed by runtime.m7.pure

An allowed ask may create a run-scoped grant keyed by action and subject, so a later matching call does not ask twice. The cache is deliberately smaller than a global approval: it ends with the run, and a later hard deny still wins.

08 / HARD BUDGETS

The loop stops at model-round, reported-token, and requested-call boundaries.

Runtime budgets are part of execution control, not advisory telemetry. Steps are checked before a model request; requested tool intentions count even when unknown or invalid; and prompt plus completion usage accumulates across rounds. Warnings are durable and normally appear once per metric after the 50-percent threshold.

Runtime budget semantics versus task authoring budgettext · source
runtime budget                      behavior
──────────────────────────────────  ───────────────────────────────────────────
maxSteps (default 8)                checked before each model request
maxModelTokens (optional)           cumulative prompt + completion usage
maxToolCalls (optional)             counts requested intentions, even invalid

warning threshold                   once per metric at 50% unless forced
over-limit tool intention           tool.call stays durable; no policy/effect
provider token overshoot            response recorded; no next model/tool unit
terminal status                     budget_exceeded

task manifest authoring budget      100,000 model tokens · 200 tool calls
                                    governs development, not runtime defaults

The remaining token allowance is passed to the next request, but the provider reports actual usage only after producing a response. That response can overshoot the remaining allowance; M7 records it and blocks the next model or tool unit. An exactly-at-limit stop response may complete, while an exactly-at-limit tool response cannot start another effect.

09 / CANCELLATION AND DEADLINES

All stop reasons converge; the runtime cannot force non-cooperative work to stop.

Every model round gets its own deadline, defaulting to 60 seconds. Cancellation can arrive through the runtime control, a caller signal, or iterator abandonment. The runtime forwards AbortSignal through model, permission, and tool waits, asks an active iterator to return, and gives cleanup 100 milliseconds before finalization continues.

Every stop reason converges without inventing duplicate terminalsAt M7 merge 41af384, successful completion, budget exhaustion, cooperative cancellation, model timeout, and non-persistence runtime failures converge on one terminal-finalization path. The path attempts turn.completed before agent.stopped and never retries an uncertain durable append.
Cancellation, timeout, cleanup, and terminal publicationtext · source
model wait
├── per-round deadline · default 60,000 ms
├── timeout aborts the request
└── terminal status failed

permission wait / cooperative tool wait
├── cancel, external AbortSignal, or iterator abandonment
└── terminal status canceled

cleanup
├── forward AbortSignal
├── call iterator.return() best-effort
├── wait at most 100 ms for cleanup
└── cannot force code that ignores cancellation

terminal publication
├── turn.completed
└── agent.stopped

append uncertainty
└── never retry a durable append that may already have succeeded

Successful completion, budget exhaustion, cancellation, timeout, and other failures share one finalizer. It attempts turn.completed before agent.stopped. A pre-append event-construction failure may retry once as a failed outcome. A durable append is never retried because the store may already have accepted it. Persistence failure can therefore leave an incomplete terminal pair.

10 / MACHINE-READABLE SCOPE

The development task allowed runtime, event, and model work—and denied the network.

The M7 manifest limits the change to EVENTS.md, the events, kernel, and models packages, plus the task itself. Network and Git push are denied, commands are allowlisted, and the delivery target is a pull request. Its 100,000-model-token and 200-tool-call authoring budget governs the development agent, not applications using the runtime.

M7 task manifest (abridged)yaml · source
id: m7-deterministic-session-loop
goal: deterministic multi-round model and pure-tool execution

allowed_paths:
  - EVENTS.md
  - packages/events/**
  - packages/kernel/**
  - packages/models/**
  - tasks/m7-deterministic-session-loop.yaml

permissions:
  network: deny
  git.push: deny

delivery:
  type: pull_request

The retained PR-head report checks the path boundary both before and after tests. All 15 changed paths remained inside the manifest and the report records zero violations.

11 / DELIVERY CHRONOLOGY

One implementation commit merged quickly, then its automated evidence finished.

Pull request #6 was created at 19:44:06 UTC on September 1, 2026 and merged 27 seconds later as 41af384. The displayed PR CI and CodeQL runs had started but had not finished when the merge happened. Both later passed on feature head 8e6bf73, and separate push workflows passed on the exact merge.

Authoritative M7 release boundarytext · source
base          98924a66628bc66a88093ec6bee05f426f0fea9d
feature head  8e6bf735a8685f1f1deab63fe691f8df6c434166
merge         41af384b6d990c53aefe81e826e59cc33f00c47c

pull request #6
├── 15 changed files
├── 4,465 insertions
├── 277 deletions
├── 1 implementation commit
└── no dependency or lockfile change
15changed files
4,465insertions
277deletions
1implementation commit

12 / VERIFIED RESULT

Adversarial tests exercise the loop’s boundaries, not its capacity.

PR-head CI subsequently passed 623 tests across 40 files, strict TypeScript, one golden scenario, and the two-sided path gate. The pull-request description identifies 59 M7 runtime cases. They cover multiple rounds, unknown and invalid tools, execution failure, malformed streams, mutable and accessor-backed values, permission and model waits, cancellation races, deadlines, budgets, append failures, and single-terminal behavior.

The development chronology also records two material corrections before that release. An early patch was moved from a stale pre-M6 checkout onto an isolated tree based on exact M6 merge 98924a6. Adversarial review then found a nested accessor-backed JSON gap and a terminal-event construction edge; the final source and tests harden both boundaries.

M7 development and adversarial-hardening recordtext
development correction
└── move the M7 patch from a stale pre-M6 checkout
    onto an isolated tree based on exact M6 merge 98924a6

adversarial finding 1
├── nested accessor-backed JSON could cross the first normalizer
└── inspect descriptors once; reject accessors, symbols, and exotic arrays

adversarial finding 2
├── terminal event construction could fail before producing a terminal pair
└── centralize finalization; retry only pre-append construction as failed

race hardening
└── exercise cancellation, timeout, iterator cleanup, mutation, and append failure

For this publication I created a clean detached checkout of exact merge 41af384 under Node 26.5.0. With loopback sockets available, it reproduced 623/623 tests, strict TypeScript, and 1/1 golden scenario. This confirms the article pin; it does not add a new release, provider run, or operating benchmark. The command output is an unretained local publication record, not a public Harness artifact.

M7 verification ledgertext
public PR-head evidence · GitHub Actions 33551227339
├── strict TypeScript                         passed
├── test files                               40 / 40
├── workspace tests                          623 / 623
├── M7 runtime cases                          59 / 59
├── golden scenarios                           1 / 1
├── changed paths checked before / after      15 / 15
├── path-policy violations                     0
└── run-report/v2                             passed

post-merge evidence · exact merge 41af384
├── CI 33551269520                            passed
└── CodeQL workflow 33551268282               passed

unretained local publication audit · exact merge · Node 26.5.0
├── clean-checkout tests                      623 / 623
├── strict TypeScript                         passed
└── golden scenarios                            1 / 1
623 / 623workspace tests
40 / 40test files
59 / 59M7 runtime cases
0path violations

13 / EVIDENCE ARTIFACT

The retained gate artifact attests the feature head and has a finite lifetime.

Workflow 33551227339 uploaded gate-evidence-33551227339. Its JSON report pins head 8e6bf73, base 98924a6, 623 passing tests, both 15-path decisions, zero violations, and run-report/v2 status passed. The artifact is retention-bound rather than version-controlled evidence.

The report has seven serialized run events. Its accompanying SQLite file contains five events and still marks the session active; the final delivered and run-recorded events exist only in the JSON report. This note therefore does not call that database a closed terminal session log. Exact-merge CI, the public source pin, and the unretained local checkout are separate evidence lanes.

Both CodeQL analysis jobs completed successfully. GitHub's separate PR differential result was nevertheless inconclusive because one default-setup configuration was missing, so this article makes no “zero vulnerabilities” claim.

14 / CURRENT TRUTH

M7 proves local control semantics while leaving operational authority closed.

SurfaceWhat the evidence supportsWhat remains open
Session scopeOne admitted turn may make multiple sequential model requests and execute multiple registered pure-tool intentions.There is no general multi-turn follow-up manager, mid-loop steering channel, or production CLI/service path on MinimalAgentRuntime.
StateEvery user, assistant, and tool message advances a version-1 immutable snapshot; each request names the revision it consumed.M7 does not compact context, load a prior durable checkpoint, or resume a half-finished turn after restart.
Model boundaryFakeModel and local adapters prove strict text/tool stream validation, aggregation, ordering, deadlines, and failure semantics offline.No live model provider, provider retry policy, billing-grade token counter, or provider cancellation compliance was exercised.
Tool boundaryUnknown and invalid calls cannot reach authorization or execution; valid calls require durable intent and authorization first.Only WeakMap-registered pure tools are in scope. No file, process, workspace, network, secret, MCP, or remote tool is exposed.
PurityThe registry makes the executable set explicit and testable, and M7 passes an AbortSignal into each trusted implementation.The pure marker is a trusted in-process capability—not a sandbox, proof of no side effects, or defense against malicious tool code.
DurabilityIntent, policy, and any permission resolution append before execution; observable events append before yield.EventStore remains injected and tested in memory. A tool.result append can fail after an effect, so M7 does not promise exactly-once execution.
BudgetsSteps, cumulative reported tokens, and requested tool calls have explicit stopping boundaries and durable warnings.A provider response can overshoot the remaining token allowance before reporting usage; the runtime prevents the next unit of work.
CancellationModel, permission, and cooperative tool waits receive cancellation; cleanup is bounded and terminal publication is centralized.External code that ignores AbortSignal cannot be force-killed, and a failed terminal append can leave no complete terminal pair.
VerificationPublic head CI and a local exact-merge publication checkout passed 623 tests, strict types, and one deterministic golden scenario.Those are correctness checks—not load, throughput, latency, capacity, live-provider, or production-durability evidence.

The precise claim is a deterministic, append-fenced, single-turn multi-round loop proven offline with FakeModel and registered pure tools. It is not a production tool runner, isolated executor, durable resume engine, live model integration, multi-user service, or self-hosted Harness.

15 / FILE GUIDE

The loop, state, model protocol, evidence, and release contract are all inspectable.

Events and tool boundary

  • packages/events/src/schemas.ts — additive runtime identities, state revisions, tool messages, cumulative usage, and step-budget warnings.
  • EVENTS.md — canonical model/tool loop and permission ordering; its summary table retains the post-merge identity-field mismatch described above.
  • packages/tools/src/tool.ts — the pre-existing trusted registration marker that bounds M7 to pure tools.

16 / WHAT IS NEXT

The current roadmap turns this local loop into a governed self-hosting platform.

A documentation-only follow-up first described M8 through M12. The current public roadmap has since decomposed that work through M76. That later planning does not enlarge what M7 shipped; it makes the remaining boundaries smaller and reviewable.

Current post-M7 roadmap (condensed)text · source
M7       deterministic single-turn session loop              complete
M8–M11  workspace boundary, Local/Docker adapters, five tools planned
M12–M15 steering, compaction, durable replay, restart safety planned
M16–M18 offline runner, authorship attestation, live doctor  planned

later lanes
├── M19–M31  effects, policy, durable sessions, SDK
├── M32–M42  MCP and ACP
├── M43–M58  remote execution, Docker, Kubernetes control plane
├── M59–M66  governed self-release
├── M67–M71  Canvas
└── M72–M76  automation ingress and rehearsal
  1. 01

    M8–M11 — bound the workspace and its first five tools

    Define an enforced Workspace contract, add trusted local and disposable Docker adapters, then expose only the five bounded development tools in the plan.

  2. 02

    M12–M15 — make interaction and continuity explicit

    Generalize steering and follow-up turns, implement context accounting and compaction, wire durable replay, and prove restart-safe continuation.

  3. 03

    M16–M18 — earn self-hosting

    Integrate the native kernel offline, attest authorship and evidence, then prove a live cutover rather than declaring self-hosting from unit tests.

  4. 04

    M19–M76 — expand effects and operations in governed lanes

    Policy, durable sessions, MCP/ACP, remote execution, container control planes, Kubernetes, self-release, Canvas, and automation each retain their own gates.

17 / EVIDENCE LEDGER

Repository and delivery claims resolve to public evidence; local reproduction is labeled.

CONTINUE EXPLORING

Inspect the session loop—and the operational authority it still leaves closed.

The pinned public merge contains the deterministic multi-round loop, versioned immutable context, strict tool admission, policy-before-effect fence, budgets, cancellation, and offline proof described here.