FIELD NOTE / FIRST REAL MODEL

Harness Platform field note: what the first real model exposed

How a local Qwen model crossed the Harness tool loop—and exposed guarded-array, timeout, policy, token-budget, sandbox, and evidence boundaries before the work became a release.

01 / FIELD TEST

The first real model did not validate the happy path. It found the seams.

A local Qwen model eventually crossed Harness's provider, agent loop, policy, and Docker sandbox boundaries, inspected the repository, and returned the requested five-bullet architecture summary. Getting there exposed three things deterministic fakes had not: a guarded-array compatibility mismatch, a provider deadline too short for local inference, and a token budget that could be crossed inside one response.

This is a field note about dogfooding, not a new milestone announcement. The experiment ran from a dirty local branch based on public M8 merge d14fc13. Its seven-path patch has not been committed, pushed, reviewed, or exercised by hosted CI. Calling it M9 would erase the most important fact in the record: the live run taught us what must be hardened before the behavior becomes a release.

Observed experiment boundarytext
local field test · 2026-09-02

model label     openai-compatible/qwen3.8:27b-mlx
provider        Ollama · http://127.0.0.1:11434/v1
workspace       Harness repository based on M8 d14fc13
task            inspect the repository, then return exactly five concise bullets
tool            sandbox_exec with structured argv
workspace       recursively read-only
sandbox network none
writable binds  zero
final result    completed · 3 model turns · 4 tool calls · 18,014 tokens

status          uncommitted local experiment · not M9 · no PR or hosted CI

02 / TWO COMMUNICATION PLANES

Model inference and tool execution crossed different trust boundaries.

The terminal spoke ACP to Agent Server on one loopback socket. Agent Server sent non-streaming OpenAI-compatible HTTP requests to Ollama on another. When the model asked for sandbox_exec, Harness authorized a structured argument vector and launched one fresh offline container for that command. “Network denied” therefore means the tool container had no network—not that the entire experiment stopped using sockets.

The first real-model smoke test crossed the complete conversational pathIn an uncommitted local worktree based on M8 merge d14fc13, a loopback Ollama endpoint answered a non-streaming OpenAI-compatible request from the Agent Server. ACP carried one turn into the kernel, and the terminal UI rendered the resulting events. This first smoke path completed without a tool call.
Observed local topologytext
terminal UI
  │ ACP · ws://127.0.0.1:8765
  ▼
Agent Server
  ├── HTTP /v1/chat/completions ──► Ollama on loopback
  │                                  configured model label
  │                                  non-streaming request · no retry
  │
  └── sandbox_exec ───────────────► one Docker container per command
                                     repository mounted read-only
                                     --network none
                                     zero writable workspace binds
                                     ephemeral writable /tmp

03 / INFERENCE BEFORE AGENCY

A real response proved provider connectivity, not repository access.

The first smoke returned HARNESS_OLLAMA_OK after one 71-token model request and no tool call. A placeholder task then received the right refusal: there was no task to perform. When asked to summarize the repository without an admitted task or tool, the model again behaved correctly and said it had not been given repository content.

That sequence clarified a frequently blurred boundary. Passing --workspace .identifies and scopes a session; it does not inject files into context and it does not grant filesystem or process authority. Repository-aware behavior begins only when the service admits a task, advertises a reviewed tool, and can execute the approved effect.

A workspace name became useful only after explicit authority was attachedThe experiment separated naming a workspace from granting an effect. The --workspace flag supplied session metadata only. A task-backed session loaded the manifest, while server configuration injected the built-in sandbox_exec tool; permitted calls then crossed policy into a Docker-backed, read-only workspace.
Three no-tool smoke sessionstext
provider smoke
├── model requests       1
├── model tokens        71
├── tool calls           0
└── response             HARNESS_OLLAMA_OK

placeholder task
├── model requests       1
├── model tokens       232
├── tool calls           0
└── result               asked for a real task

repository-summary prompt without an admitted task
├── model requests       1
├── model tokens       274
├── tool calls           0
└── result               declined to invent repository knowledge

04 / TASK CONTRACT

The successful run began with an explicit read-only contract.

The local ollama-repo-summary task asked the model to inspect the repository before answering, denied writes and sandbox network, and allowed a small family of direct read commands. The final hard limits were 30,000 model tokens and 30 requested tool calls. “Exactly five bullets” and “at most five useful calls” remained prompt instructions; only the manifest limits were machine-enforced.

Local task manifest (condensed)yaml
goal
  inspect this repository through the policy-enforced read-only sandbox
  and return exactly five concise architecture bullets grounded in read files

permissions
  fs.read       allow
  fs.write      deny
  process.exec  selected direct-argv prefixes allow; everything else deny
  network       deny
  git.push      deny

budget
  max_model_tokens  30,000
  max_tool_calls        30

important
  allowed_paths constrains authoring scope;
  flat fs.read: allow exposed the whole repository read-only

05 / FIRST FAILURE

The model produced a tool call, then a defensive array guard met a stricter boundary.

Ollama's first repository-aware request did reach tool intention, but the kernel stopped it with MODEL_INVALID_RESPONSE: the arguments were not accepted as bounded JSON. The OpenAI-compatible adapter deliberately put an own, non-enumerabletoJSON: undefined property on decoded arrays to shadow any inheritedArray.prototype.toJSON hook. The older normalizer rejected every named array property, including that guard.

The local fix is intentionally narrow. It recognizes only that inert shape, maintains the guard while serializing cloned arrays, and removes it before returning provider-neutral JSON. Executable, accessor-backed, or enumerable variants still fail before a tool can run.

The first tool loop emerged through three bounded correctionsThe real provider exposed three integration boundaries in order: guarded argv arrays failed compatibility normalization, a narrow non-enumerable undefined toJSON shim restored that boundary, the default 60-second model deadline then expired, and a validated 180-second override finally allowed a complete tool loop.
Guarded-array compatibility rule (local, uncommitted)text
provider tool arguments
└── argv array has own non-enumerable toJSON: undefined
    └── shadows a hostile inherited Array.prototype.toJSON hook

normalizeToolJson
├── accept only an own data property named toJSON
├── require value === undefined
├── require enumerable === false
├── reject accessors, functions, and enumerable variants
├── clone the array and shadow inherited toJSON while serializing
└── remove the temporary guard before returning plain JSON

regressions
├── guarded argv crosses one model tool call
└── executable or enumerable toJSON is rejected before effect

06 / SECOND FAILURE

Two runs hit the same 60-second wall before any tool executed.

With array normalization repaired, the next two sessions ended after 60.011 and 60.009 seconds with MODEL_TIMEOUT. The provider adapter already supported a constructor-level deadline, but Agent Server exposed no configuration for it. The local patch adds HARNESS_MODEL_TIMEOUT_MS at the service boundary and keeps the existing 60-second default when it is absent.

Provider timeout admission (local, uncommitted)text
HARNESS_MODEL_TIMEOUT_MS
├── absent                provider default remains 60,000 ms
├── experiment value      180,000 ms
├── accepted syntax       positive decimal integer, no whitespace
├── maximum               2,147,483,647
├── requires              model ID + base URL
└── invalid configuration fail closed at service startup

The experiment used 180 seconds. That is not a recommendation for every model; it is a validated escape hatch for a local model whose observed responses sometimes took longer than one minute.

07 / FIRST COMPLETE TOOL LOOP

Three read operations finally closed the reason–act–observe loop.

After both compatibility corrections, the model read the first parts ofREADME.md and ARCHITECTURE.md, then listedpackages. Each request crossed the model-facing tool schema, the durable policy fence, and the sandbox planner before Docker saw it. Each ran in a separate container and returned an observation for the next model turn.

First successful repository-aware runtext
first completed repository-aware session
├── sed -n 1,180p README.md
├── sed -n 1,200p ARCHITECTURE.md
└── ls -la packages

model response usage
  422 + 2,825 + 6,534 = 9,781 cumulative tokens

observed result
├── 111.174 seconds wall time
├── 3 requested / 3 executed tool calls
├── 3 fresh containers · all exited 0 and were removed
├── network none · 0 writable workspace mounts
├── no truncated tool output
└── budget warning at 82% of the original 12,000-token budget

This 9,781-token session is the first evidence that the local model could use the repository tool loop, not merely answer through the provider bridge. It is still one observed session, not repeatability proof.

08 / USEFUL FAILURE

The next run showed policy holding while the budget failed late.

A less-directed rerun requested 17 tool calls. Harness denied two shell-formsh -c requests and four slash-suffixed ls forms before Docker started, while 11 direct commands were executed. The model recovered from those denials by trying allowed argument vectors. That is stronger evidence than a prompt-only “do not use a shell” instruction: the disallowed effect did not run.

12,000-token budget pressure runtext
cumulative token progression
  502 → 1,083 → 2,782 → 5,274 → 8,701 → 19,932
                                               ▲
                                  12,000 budget crossed

requested calls   17
executed calls    11
denied calls       6
  ├── 2 × sh -c ...
  └── 4 × slash-suffixed ls forms outside the original prefixes

terminal result   budget_exceeded
final response    received from the provider, then discarded by the kernel

The runtime checks cumulative usage after a provider response arrives. The last response moved the run from 8,701 to 19,932 tokens, so the kernel recordedbudget_exceeded and withheld that answer. The budget is an enforcement boundary, but it is not a reservation that prevents provider work from crossing the line within one turn.

09 / FINAL OBSERVED RESULT

The tuned run completed with four reads and five visible bullets.

After raising the hard limits and allowing the needed direct command forms, the user's final run completed in three model turns. Four commands inspected the two architecture documents and repository roots. All four containers exited successfully, produced untruncated observations, and were removed.

Three runs separate conversational success from safe tool-use successThree local runs established progressively stronger evidence: a 9,781-token tool loop completed under a 12k hard budget; a 19,932-token run exceeded that budget after 17 requested tools, 11 executions, and six denials; and an 18,014-token run completed four of four tools under a 30k hard budget. These are worktree observations, not reliability, security, or performance proof.
Final run and corrected token accountingtext
final user-run session
├── cat README.md
├── cat ARCHITECTURE.md
├── ls -la
└── ls packages services apps

model response usage
  466 + 8,471 + 9,077 = 18,014 cumulative tokens

result
├── completed in 84.329 seconds
├── 3 model turns · 4 requested / 4 executed calls
├── 4 containers exited 0, returned full output, and were removed
├── budget.warning emitted at the first observed crossing: 60% of 30,000
└── exactly five bullets observed in the terminal

separate verification run correction
  594 + 6,488 + 7,080 = 14,162 cumulative tokens
  7,080 was the final response only, not the run total

10 / SANDBOX BOUNDARY

The observed tool effects were disposable, offline, and workspace-read-only.

The existing sandbox path translated each admitted argument vector into a bounded Docker plan. The container did not receive a shell, host credentials, network, Linux capabilities, or a writable repository mount. Its root filesystem was read-only and its deliberately writable /tmp was ephemeral and constrained.

Observed Docker plan (condensed)text
docker run
├── --pull never · --rm · --init · --read-only
├── --network none · --cap-drop ALL
├── --security-opt no-new-privileges=true
├── numeric non-root user
├── 128 PIDs · 512 MiB memory · 1 CPU
├── /tmp = 64 MiB noexec,nosuid,nodev tmpfs
├── /workspace = repository bind, recursively read-only
├── proxy environment values blanked
└── argv entrypoint selected directly; no shell inserted

development exception
└── HARNESS_SANDBOX_TRUST_LOCAL_IMAGE=true
    harness-sandbox:local was a mutable trusted tag, not a pinned release image

The test used harness-sandbox:local with the explicit local-image trust override. The inspected image had a content identity at that moment, but the task selected a mutable tag rather than enforcing a reviewed digest. Container hardening and cleanup are useful evidence; they are not a production isolation certificate.

11 / DURABLE RECORD

The event log proves effects and budgets better than it proves the final prose.

Two local SQLite stores survive the interaction. The smaller smoke record contains three closed no-tool sessions. The task database passed SQLite integrity checking and records seven closed sessions, 412 canonical events, 28 tool intentions, 22 executed sandboxes, six denials, and cleanup for every started container. Full local paths, identifiers, and stdout remain unpublished because the database also contains machine-specific detail.

Redacted local event-store ledgertext
early smoke database
├── 3 closed sessions · 15 events
├── 71 / 232 / 274 tokens · no tool calls
└── sha256 d759894c4379e7676b4c25cb1991f491081e27584ce40308ee9ab819bbdf2964

task-session database
├── SQLite integrity_check                         ok
├── closed sessions                                 7
├── canonical events                              412
├── tool.call events                               28
├── executed sandboxes                             22
├── policy-denied calls                             6
├── removed containers                             22
├── truncated tool results                          0
└── sha256 a613df5a59c59e0ee41f18ad6f2192a978c96190f7e4fb5d182f820acbf9b8b5

12 / PERFORMANCE OBSERVATION

Inference dominated the recorded wall time, but four runs are not a benchmark.

Docker tool time was under three seconds in each listed session while end-to-end wall time ranged from 84 to 293 seconds. That makes local inference the likely dominant component in these observations. It does not isolate hardware, prompt growth, scheduling, model warm state, or other causes well enough to compare systems.

SessionWall timeTokensTools requested / runTool time
First completed tool loop111.174 s9,7813 / 31.679 s
Budget-exceeded run96.405 s19,93217 / 112.724 s
Revised verification292.827 s14,1624 / 41.429 s
Final user run84.329 s18,0144 / 40.917 s

13 / LOCAL VERIFICATION

The changed worktree passes locally, with a clear split between retained and rerun proof.

The task's local exit gate produced an ignored report after one earlier gate correctly rejected a runtime SQLite file inside the repository. The passed report records 669 tests across 42 files and zero scope violations. It does not contain a typecheck result. For this publication, the current worktree separately reproduced all 669 tests, strict TypeScript, and manifest validation.

Local verification ledgertext
local exit gate · uncommitted and ignored report
├── status                         passed
├── test files                    42 / 42
├── tests                        669 / 669
├── recorded test command       42.178 seconds
├── changed paths                  7
├── path violations                0
├── head / base                    d14fc13 / d14fc13
└── report sha256                  24be1c40db5605ee223da051052aac8759661f548bcde14e8fde24bc20df82d6

publication audit of current worktree
├── 669 / 669 tests                 passed
├── strict TypeScript               passed
└── task-manifest validation        passed

The experiment server ran under Node 24.18.0 while the project and hosted workflows target Node 22. That difference is disclosed rather than treated as cross-version proof.

14 / DELIVERY TRUTH

The public commit is the baseline, not the experiment.

Public main, local main, and the local experiment branch all point to M8 merge d14fc13. The guarded-array, timeout, regression, and task-manifest changes sit only in the working tree. The newest public PR, CI, and CodeQL records all belong to M8, so they can verify the baseline but not this experiment patch.

Publication boundary at audit timetext
public repository
├── main / origin/main     d14fc13e299a6718d9e8a98ba9e028b320cd5f53 · M8 merge
├── latest public PR       #9 · M8
├── latest exact-merge CI  33646021258 · M8
└── latest CodeQL          33646020469 · M8

local experiment branch
├── tasks/ollama-repo-summary
├── 6 modified tracked files + 1 untracked task manifest
├── final snapshot +195 / -1 across 7 paths
├── no remote branch
├── no pull request
├── no hosted CI or CodeQL
└── no published run artifact

15 / CURRENT TRUTH

One useful agent run is evidence of integration, not evidence of general reliability.

SurfaceWhat was observedWhat remains unproven
Provider identityThe server reached a local endpoint configured with the label qwen3.8:27b-mlx.A configured label is not cryptographic proof of the exact weights or model provenance.
Repository groundingThe final session read two documents and listed repository roots before returning five bullets.The event database retains tool observations but not the final text, and no evaluator scored claim quality.
Read authorityEvery executed tool container received the repository as a recursively read-only bind.Flat fs.read: allow exposed the whole repository; allowed_paths did not restrict reads to seven paths.
Write authorityThe workspace had zero writable bind mounts and fs.write was denied.The run did not exercise a write attempt, and the container intentionally retained an ephemeral writable /tmp.
Network boundaryEvery tool container ran with Docker network set to none.The host Agent Server still used loopback HTTP to reach Ollama; this was not whole-process network isolation.
Process policyTwo shell-form calls and four unmatched ls forms were denied before a container started.Prefix globs such as ls** are broader and less legible than an executable-aware rule language.
BudgetsThe runtime stopped the 19,932-token session instead of delivering an over-budget answer.The check happens after a provider response, so provider work can overshoot the configured limit within one turn.
Sandbox imageThe observed containers used the hardened local Docker plan and were removed.The experiment trusted a mutable local image tag; it did not use a production digest-enforcement path.
Runtime compatibilityA narrow guarded-array patch and validated 180-second timeout enabled the real-model loop locally.Those changes remain uncommitted and have no remote review, hosted CI, or CodeQL evidence.
PerformanceTool execution was a small fraction of the four recorded session wall times.One user, unknown hardware and warm state, no concurrency, and non-streaming calls make this diagnostic—not a benchmark.

The precise claim is one local model completed a repository-grounded, policy-gated, read-only Docker tool loop after two narrow compatibility changes. This is not a deterministic quality result, load test, security certification, model provenance proof, production deployment, or released Harness capability.

16 / FILE GUIDE

The public baseline is inspectable; the experiment delta is described, not linked as release code.

Tests and task

  • packages/kernel/test/run-agent.test.ts — public loop tests; two guarded-array regressions exist only in the local delta.
  • tasks/ollama-repo-summary.yaml — untracked task contract used for the final experiment; there is no honest public source link yet.

17 / WHAT IS NEXT

Turn a successful field test into a reviewable, repeatable capability.

The next work is less glamorous than the first five-bullet answer and more valuable: narrow the process rule language, close budget overshoot, retain a privacy-reviewed final output record, evaluate grounding, measure streaming behavior, and publish the actual compatibility delta through review.

Field-test follow-up sequencetext
1  replace broad command-prefix globs with executable-aware rules
2  make the prompt's five-call ceiling a machine-enforced task budget
3  reserve token budget before a turn so one response cannot overshoot dramatically
4  persist a privacy-reviewed final-output artifact or structured digest
5  add an evaluator for five-bullet shape and repository grounding
6  add true provider streaming and time-to-first-token measurement
7  repeat controlled warm/cold and concurrent trials
8  publish the compatibility work through a reviewed pull request
  1. 01

    Make policy intent machine-legible

    Replace broad prefixes such as ls** with executable-aware rules and move the five-call expectation from prose into an enforced budget.

  2. 02

    Make the answer part of the evidence story

    Persist a privacy-reviewed output artifact or digest and add a structured evaluator for shape, citations, and support from observed files.

  3. 03

    Measure the provider instead of narrating four timings

    Add streaming, time-to-first-token, controlled warm/cold trials, and concurrent sessions before making latency or capacity decisions.

  4. 04

    Ship the patch before naming the capability

    Commit the seven-path delta, open a pull request, retain the gate artifact, and let hosted CI, CodeQL, and review establish a public evidence pin.

18 / EVIDENCE LEDGER

Public baseline, local runtime evidence, and observed terminal output remain distinct.

  • Harness M8 baseline d14fc13 — exact public source tree underneath the local experiment, not a commit containing its changes.
  • M8 pull request #9, exact-merge CI, and exact-merge CodeQL — public evidence for the baseline only.
  • Local worktree audit — seven changed paths, guarded-array and provider-timeout diffs, task-manifest validation, 669/669 tests, and strict TypeScript; no commit, remote branch, or hosted check exists for this snapshot.
  • Redacted local SQLite audit — integrity check, event chronology, model usage, policy decisions, sandbox lifecycle, output truncation, and cleanup counts. The source database is withheld because it includes local paths, identifiers, and command output.
  • Owner-supplied shared build conversation and terminal record — chronology, operator intent, and manual confirmation that the final response contained five bullets. It is not used as a substitute for implementation, CI, or durable-output evidence.

CONTINUE EXPLORING

Inspect the public baseline—and the exact work still needed to ship the field test.

The public link is the M8 baseline. The guarded-array, timeout, task, and live-run evidence described here remain an uncommitted local experiment until they pass review and hosted checks.