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.
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 CI02 / 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.
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 /tmp03 / 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.
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 knowledge04 / 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.
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-only05 / 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.
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 effect06 / 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.
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 startupThe 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 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 budgetThis 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.
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 kernelThe 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.
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 total10 / 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.
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 imageThe 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.
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 a613df5a59c59e0ee41f18ad6f2192a978c96190f7e4fb5d182f820acbf9b8b512 / 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.
| Session | Wall time | Tokens | Tools requested / run | Tool time |
|---|---|---|---|---|
| First completed tool loop | 111.174 s | 9,781 | 3 / 3 | 1.679 s |
| Budget-exceeded run | 96.405 s | 19,932 | 17 / 11 | 2.724 s |
| Revised verification | 292.827 s | 14,162 | 4 / 4 | 1.429 s |
| Final user run | 84.329 s | 18,014 | 4 / 4 | 0.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 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 passedThe 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.
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 artifact15 / CURRENT TRUTH
One useful agent run is evidence of integration, not evidence of general reliability.
| Surface | What was observed | What remains unproven |
|---|---|---|
| Provider identity | The 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 grounding | The 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 authority | Every 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 authority | The 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 boundary | Every 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 policy | Two 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. |
| Budgets | The 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 image | The 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 compatibility | A 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. |
| Performance | Tool 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.
Agent loop and model boundary
- packages/kernel/src/run.ts — public M8 bounded-JSON and legacy agent loop baseline; locally changed for guarded arrays.
- packages/models/src/openai-compatible.ts — provider normalization, guarded arrays, non-streaming completion request, usage, and timeout behavior.
Service configuration
- services/agent-server/src/config.ts — public provider-registry baseline; locally changed to validate and forward the optional model timeout.
- services/agent-server/src/cli.ts — CLI environment help updated only in the uncommitted worktree.
Sandbox and policy
- services/sandbox-runner/src/plan.ts — policy compilation, Docker arguments, read-only mounts, and resource limits.
- services/sandbox-runner/src/runner.ts — output limits, Docker client lifecycle, cancellation, and deterministic cleanup.
- services/agent-server/src/sandbox-tool.ts — strict argument-vector tool schema and policy-before-run handoff.
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.
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 request18 / 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.