@odla-ai/harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/chunk-ATKV6VTU.js +168 -0
- package/dist/chunk-ATKV6VTU.js.map +1 -0
- package/dist/chunk-GE6CCN7W.js +93 -0
- package/dist/chunk-GE6CCN7W.js.map +1 -0
- package/dist/chunk-GMVZ4LZH.js +1769 -0
- package/dist/chunk-GMVZ4LZH.js.map +1 -0
- package/dist/chunk-PHXQH4YM.js +550 -0
- package/dist/chunk-PHXQH4YM.js.map +1 -0
- package/dist/chunk-PTXZVYD4.js +81 -0
- package/dist/chunk-PTXZVYD4.js.map +1 -0
- package/dist/chunk-QTUEF2HZ.js +9 -0
- package/dist/chunk-QTUEF2HZ.js.map +1 -0
- package/dist/cli.cjs +795 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +98 -0
- package/dist/cli.js.map +1 -0
- package/dist/code-runtime-cli.cjs +2341 -0
- package/dist/code-runtime-cli.cjs.map +1 -0
- package/dist/code-runtime-cli.d.cts +1 -0
- package/dist/code-runtime-cli.d.ts +1 -0
- package/dist/code-runtime-cli.js +133 -0
- package/dist/code-runtime-cli.js.map +1 -0
- package/dist/index.cjs +228 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +2580 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +544 -0
- package/dist/node.d.ts +544 -0
- package/dist/node.js +71 -0
- package/dist/node.js.map +1 -0
- package/dist/testing.cjs +106 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +25 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.js +79 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-D12vK3K9.d.cts +249 -0
- package/dist/types-D12vK3K9.d.ts +249 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 odla ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @odla-ai/harness
|
|
2
|
+
|
|
3
|
+
@odla-ai/harness is the experimental coding-task boundary shared by odla
|
|
4
|
+
Studio, a trusted Mac/Linux runner, and an untrusted coding-agent container.
|
|
5
|
+
It owns the protocol and safe runner defaults; agent implementations such as Pi
|
|
6
|
+
remain replaceable adapters.
|
|
7
|
+
|
|
8
|
+
Harness can stage a bounded, secret-filtered copy of a local checkout. The
|
|
9
|
+
brokered Pi image receives no workspace mount at all: reads and mutations cross
|
|
10
|
+
the typed tool channel into that disposable snapshot. The original checkout,
|
|
11
|
+
terminal credential, provider credentials, container-engine socket, and
|
|
12
|
+
network are never available to Pi. Inference requests cross the same JSONL
|
|
13
|
+
channel and are fulfilled by the control plane through @odla-ai/ai.
|
|
14
|
+
|
|
15
|
+
## Personal Code terminal
|
|
16
|
+
|
|
17
|
+
Open **Studio → Code → Terminal**, copy the command shown for the selected app
|
|
18
|
+
and environment, and run it from that app's Git checkout. For example:
|
|
19
|
+
|
|
20
|
+
~~~sh
|
|
21
|
+
export ODLA_USER_EMAIL=owner@example.com
|
|
22
|
+
npx @odla-ai/cli@0.16.4 code connect \
|
|
23
|
+
--platform https://odla.ai \
|
|
24
|
+
--app-id my-app \
|
|
25
|
+
--env dev
|
|
26
|
+
~~~
|
|
27
|
+
|
|
28
|
+
The terminal verifies the checkout and isolation engine, builds the Pi adapter
|
|
29
|
+
bundled with the exact CLI release into a content-addressed local image, prepares
|
|
30
|
+
each digest-pinned recipe image, and sends the connection request outbound to odla. The signed-in app
|
|
31
|
+
owner approves it; no bearer is copied from the browser. The process stays in
|
|
32
|
+
the foreground, opens no listener, and keeps its collected host credential in
|
|
33
|
+
memory only.
|
|
34
|
+
|
|
35
|
+
At connection time it freezes tracked and non-ignored untracked source into a
|
|
36
|
+
staged snapshot, omitting `.git`, `.odla`, `.wrangler`, `node_modules`, build
|
|
37
|
+
output, `.env` files, `.dev.vars`, credential JSON, unsafe paths, and symlinks.
|
|
38
|
+
Each session binds an explicitly selected primary repository/ref plus any named
|
|
39
|
+
read-only repository/ref references to exact commits and tree digests. A
|
|
40
|
+
matching local snapshot remains an optimization with explicit provenance;
|
|
41
|
+
otherwise Harness receives the immutable sources from the Registry. References
|
|
42
|
+
are materialized under `.odla-references/<alias>` and the broker rejects every
|
|
43
|
+
attempt to patch them. A checkpoint patch is computed against the selected
|
|
44
|
+
primary Git base, so local developer changes remain explicit provenance
|
|
45
|
+
alongside Pi's changes.
|
|
46
|
+
|
|
47
|
+
On Apple Silicon with macOS 26, install Apple's lightweight runtime once with
|
|
48
|
+
`brew install container`; `code connect` selects it and starts its user service.
|
|
49
|
+
On Linux, auto requires rootless Podman. Podman Machine is the macOS fallback;
|
|
50
|
+
Docker is an explicit compatibility adapter only. Recipe images must be
|
|
51
|
+
digest-pinned; the Pi adapter's local tag contains the SHA-256 of its CLI bundle
|
|
52
|
+
and requires no registry credential. All adapters run Pi networkless and
|
|
53
|
+
non-root with bounded CPU/memory; Pi has no
|
|
54
|
+
checkout or engine-socket mount.
|
|
55
|
+
|
|
56
|
+
## Experimental task runner
|
|
57
|
+
|
|
58
|
+
The older generic task queue is separate from the personal Pi terminal flow.
|
|
59
|
+
It lives under **Code → Task runners · advanced**. To exercise it, register a
|
|
60
|
+
workspace and keep its runner token outside shell history:
|
|
61
|
+
|
|
62
|
+
~~~sh
|
|
63
|
+
export ODLA_HARNESS_TOKEN=odla_hrn_...
|
|
64
|
+
npx @odla-ai/harness runner \
|
|
65
|
+
--endpoint https://odla.ai \
|
|
66
|
+
--workspace my-repo=/absolute/path/to/repo \
|
|
67
|
+
--image registry.example/coding-agent@sha256:<digest> \
|
|
68
|
+
--engine auto
|
|
69
|
+
~~~
|
|
70
|
+
|
|
71
|
+
The runner adds network=none, a read-only root, dropped capabilities, non-root
|
|
72
|
+
uid/gid, and CPU/memory limits across adapters. Docker and Podman additionally
|
|
73
|
+
receive no-new-privileges plus PID and bounded-tmpfs limits. A legacy image's
|
|
74
|
+
only writable mount is the disposable workspace copy; a broker-only image has
|
|
75
|
+
no source mount.
|
|
76
|
+
|
|
77
|
+
The runner skips .git, .odla, .wrangler, node_modules, build output, .env files,
|
|
78
|
+
.dev.vars, credential JSON, and symlinks while staging. Completed attempts
|
|
79
|
+
return a bounded binary-capable Git patch to Studio.
|
|
80
|
+
|
|
81
|
+
Studio can branch the attempt ledger from an inspected event checkpoint. In
|
|
82
|
+
this first release the branch records immutable lineage and starts from a fresh
|
|
83
|
+
copy of the registered workspace; it does not replay the parent patch into the
|
|
84
|
+
new copy.
|
|
85
|
+
|
|
86
|
+
## Build recipes
|
|
87
|
+
|
|
88
|
+
The Code terminal advertises a closed recipe catalog. The content-addressed,
|
|
89
|
+
CLI-embedded Pi image and every registry-digest-pinned recipe are prepared
|
|
90
|
+
before approval:
|
|
91
|
+
|
|
92
|
+
~~~json
|
|
93
|
+
{
|
|
94
|
+
"recipeAuthorization": "registered_recipe",
|
|
95
|
+
"recipes": [{
|
|
96
|
+
"id": "test",
|
|
97
|
+
"image": "registry.example/app-build@sha256:<digest>",
|
|
98
|
+
"command": ["npm", "test"],
|
|
99
|
+
"timeoutMs": 600000,
|
|
100
|
+
"maxOutputBytes": 1048576
|
|
101
|
+
}]
|
|
102
|
+
}
|
|
103
|
+
~~~
|
|
104
|
+
|
|
105
|
+
The runtime validates HTTPS, never places its credential in a request body,
|
|
106
|
+
never opens a listener, and rejects malformed or cross-host binding responses.
|
|
107
|
+
Pi reads, patches, and runs only registered build recipes through the typed
|
|
108
|
+
CaMeL broker. Checkpoint-stop captures a bounded combined patch; resume verifies
|
|
109
|
+
it against the same selected Git base and exact local source descriptor.
|
|
110
|
+
|
|
111
|
+
`--once` is diagnostic-only and refuses pending commands so a one-heartbeat
|
|
112
|
+
probe cannot acknowledge a session as running and then immediately tear it
|
|
113
|
+
down.
|
|
114
|
+
|
|
115
|
+
## Agent protocol
|
|
116
|
+
|
|
117
|
+
The image reads one JSON object per line on stdin and writes protocol messages
|
|
118
|
+
on stdout. Version 1 supports task.start, typed event messages,
|
|
119
|
+
inference.request/inference.response, tool.request/tool.response,
|
|
120
|
+
attempt.cancel, and attempt.complete.
|
|
121
|
+
|
|
122
|
+
## CaMeL tool and build boundary
|
|
123
|
+
|
|
124
|
+
`createCodeToolBroker` is the trusted implementation for the three Pi tools:
|
|
125
|
+
registered staged-file reads, confined unified patches, and app-registered
|
|
126
|
+
build recipes. Each request is labelled and evaluated by @odla-ai/camel before
|
|
127
|
+
an effect. Patch data remains Prompt-Unsafe payload; file and recipe selectors
|
|
128
|
+
must pass closed conversions; recipe execution is either explicitly
|
|
129
|
+
pre-authorized by app policy or bound to an exact approval digest.
|
|
130
|
+
|
|
131
|
+
Recipes contain a fixed command and digest-pinned image. They run without a
|
|
132
|
+
shell in a second networkless, non-root container over a newly staged copy.
|
|
133
|
+
Build output and mutations are discarded after the bounded result returns. The
|
|
134
|
+
decision binds a digest of the exact source copy, and the broker refuses to run
|
|
135
|
+
if that copy changes after authorization. No repository code executes in the
|
|
136
|
+
host runner process.
|
|
137
|
+
|
|
138
|
+
`verifyCodeCandidate` is the independent landing verifier. It reconstructs the
|
|
139
|
+
candidate from the registered trusted-base digest and patch, gives each fixed
|
|
140
|
+
recipe a fresh source copy, and emits a digest-authenticated closed receipt.
|
|
141
|
+
Raw stdout, stderr, and changed-test paths remain separate quarantined evidence
|
|
142
|
+
for coding, review, and owner display. Only the prose-free receipt of registered
|
|
143
|
+
recipe outcomes, digests, and changed-test counts can be promoted to planning
|
|
144
|
+
Safe through `createTrustedCodeVerificationReceipt`; changed tests always set a
|
|
145
|
+
separate review requirement.
|
|
146
|
+
|
|
147
|
+
## Portable checkpoint and resume
|
|
148
|
+
|
|
149
|
+
`createCodeWorkspaceCheckpoint` captures the staged candidate as a bounded,
|
|
150
|
+
validated text patch plus closed resume state: plan and conversation cursors,
|
|
151
|
+
build-policy and dependency-layer digests, completed effect receipts,
|
|
152
|
+
unresolved approvals, and trust status. Patch, state, and total checkpoint have
|
|
153
|
+
independent SHA-256 digests. The Registry verifies the complete payload before
|
|
154
|
+
storing it and keeps only its digest in the command ledger.
|
|
155
|
+
|
|
156
|
+
`restoreCodeWorkspaceCheckpoint` verifies every digest, requires the exact base
|
|
157
|
+
commit, and applies the patch to a fresh staged copy on the currently bound
|
|
158
|
+
host. `isCheckpointEffectCompleted` skips an exact prior effect and rejects an
|
|
159
|
+
effect ID rebound to another action digest, preventing resume from replaying a
|
|
160
|
+
database write, publication, or deployment.
|
|
161
|
+
|
|
162
|
+
Use @odla-ai/harness for protocol types and validation,
|
|
163
|
+
@odla-ai/harness/node for runner/container primitives, and
|
|
164
|
+
@odla-ai/harness/testing for deterministic test doubles.
|
|
165
|
+
|
|
166
|
+
## Local fixture image
|
|
167
|
+
|
|
168
|
+
The included fixture proves the transport without running an autonomous coding
|
|
169
|
+
agent:
|
|
170
|
+
|
|
171
|
+
~~~sh
|
|
172
|
+
docker build -t odla-harness-fixture:local packages/harness/fixtures
|
|
173
|
+
ODLA_HARNESS_CONTAINER_TEST=1 npm test -w @odla-ai/harness
|
|
174
|
+
~~~
|
|
175
|
+
|
|
176
|
+
Unpinned images are accepted only when both the explicit test flag and
|
|
177
|
+
ODLA_HARNESS_UNSAFE_TESTING=1 are present. That escape hatch is not intended
|
|
178
|
+
for a connected Studio runner.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runContainerAttempt,
|
|
3
|
+
stageWorkspace
|
|
4
|
+
} from "./chunk-PHXQH4YM.js";
|
|
5
|
+
import {
|
|
6
|
+
makeHarnessEvent
|
|
7
|
+
} from "./chunk-GE6CCN7W.js";
|
|
8
|
+
import {
|
|
9
|
+
HARNESS_PROTOCOL_VERSION
|
|
10
|
+
} from "./chunk-QTUEF2HZ.js";
|
|
11
|
+
|
|
12
|
+
// src/runner.ts
|
|
13
|
+
import { setTimeout as delay } from "timers/promises";
|
|
14
|
+
async function append(control, lease, kind, actor, payload) {
|
|
15
|
+
await control.appendEvents(lease.task.attemptId, lease.leaseId, [makeHarnessEvent(kind, actor, payload)]);
|
|
16
|
+
}
|
|
17
|
+
async function runLeasedAttempt(lease, options) {
|
|
18
|
+
const source = options.workspaces[lease.task.workspace];
|
|
19
|
+
if (!source) {
|
|
20
|
+
await options.control.complete(lease.task.attemptId, lease.leaseId, {
|
|
21
|
+
status: "failed",
|
|
22
|
+
error: `runner does not expose workspace "${lease.task.workspace}"`
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let staged = null;
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const cancelFromParent = () => controller.abort(options.signal?.reason);
|
|
29
|
+
options.signal?.addEventListener("abort", cancelFromParent, { once: true });
|
|
30
|
+
if (options.signal?.aborted) controller.abort(options.signal.reason);
|
|
31
|
+
let heartbeat;
|
|
32
|
+
let heartbeatInFlight = false;
|
|
33
|
+
const pulse = async () => {
|
|
34
|
+
if (heartbeatInFlight || controller.signal.aborted) return;
|
|
35
|
+
heartbeatInFlight = true;
|
|
36
|
+
try {
|
|
37
|
+
const value = await options.control.heartbeat(lease.task.attemptId, lease.leaseId);
|
|
38
|
+
if (value.cancelRequested) controller.abort("cancel_requested");
|
|
39
|
+
} catch {
|
|
40
|
+
controller.abort("heartbeat_failed");
|
|
41
|
+
} finally {
|
|
42
|
+
heartbeatInFlight = false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
await pulse();
|
|
47
|
+
heartbeat = setInterval(() => {
|
|
48
|
+
void pulse();
|
|
49
|
+
}, options.heartbeatMs ?? 15e3);
|
|
50
|
+
if (controller.signal.aborted) throw new Error("lease was cancelled before workspace staging");
|
|
51
|
+
staged = await stageWorkspace(source);
|
|
52
|
+
if (controller.signal.aborted) throw new Error("lease was cancelled during workspace staging");
|
|
53
|
+
const stagedWorkspace = staged;
|
|
54
|
+
await append(options.control, lease, "runner.workspace_staged", "runner", {
|
|
55
|
+
workspace: lease.task.workspace,
|
|
56
|
+
files: stagedWorkspace.fileCount,
|
|
57
|
+
bytes: stagedWorkspace.byteCount
|
|
58
|
+
});
|
|
59
|
+
const result = await runContainerAttempt({
|
|
60
|
+
engine: options.engine,
|
|
61
|
+
image: options.image,
|
|
62
|
+
workspaceDir: stagedWorkspace.workspaceDir,
|
|
63
|
+
task: lease.task,
|
|
64
|
+
limits: options.limits,
|
|
65
|
+
allowUnpinnedImage: options.allowUnpinnedImage,
|
|
66
|
+
workspaceAccess: options.workspaceAccess ?? (options.toolBroker ? "none" : "read-write"),
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
onStderr: async (text) => {
|
|
69
|
+
await append(options.control, lease, "agent.stderr", "agent", { text: text.slice(0, 64 * 1024) });
|
|
70
|
+
},
|
|
71
|
+
onMessage: async (message) => {
|
|
72
|
+
if (message.type === "event") {
|
|
73
|
+
await append(options.control, lease, message.kind, "agent", message.payload ?? null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (message.type === "attempt.complete") {
|
|
77
|
+
await append(options.control, lease, "agent.completed", "agent", {
|
|
78
|
+
status: message.status,
|
|
79
|
+
result: message.result ?? null
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (message.type === "tool.request") {
|
|
84
|
+
await append(options.control, lease, "tool.requested", "agent", {
|
|
85
|
+
requestId: message.requestId,
|
|
86
|
+
tool: message.tool
|
|
87
|
+
});
|
|
88
|
+
const response2 = options.toolBroker ? await options.toolBroker.execute({
|
|
89
|
+
lease,
|
|
90
|
+
workspaceDir: stagedWorkspace.workspaceDir,
|
|
91
|
+
signal: controller.signal
|
|
92
|
+
}, message) : { requestId: message.requestId, ok: false, content: "tool denied: no trusted broker configured" };
|
|
93
|
+
await append(options.control, lease, "tool.responded", "system", {
|
|
94
|
+
requestId: message.requestId,
|
|
95
|
+
tool: message.tool,
|
|
96
|
+
ok: response2.ok,
|
|
97
|
+
contentBytes: Buffer.byteLength(response2.content)
|
|
98
|
+
});
|
|
99
|
+
return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
|
|
100
|
+
}
|
|
101
|
+
await append(options.control, lease, "model.requested", "agent", {
|
|
102
|
+
requestId: message.requestId,
|
|
103
|
+
messages: message.call.messages.length,
|
|
104
|
+
maxTokens: message.call.maxTokens
|
|
105
|
+
});
|
|
106
|
+
const response = await options.control.infer(lease.task.attemptId, lease.leaseId, {
|
|
107
|
+
requestId: message.requestId,
|
|
108
|
+
call: message.call
|
|
109
|
+
});
|
|
110
|
+
await append(options.control, lease, "model.responded", "model", {
|
|
111
|
+
requestId: message.requestId,
|
|
112
|
+
response: response.response,
|
|
113
|
+
receipt: response.receipt
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
117
|
+
type: "inference.response",
|
|
118
|
+
requestId: message.requestId,
|
|
119
|
+
response: response.response
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const patch = await stagedWorkspace.patch(lease.task.policy.maxPatchBytes);
|
|
124
|
+
await options.control.complete(lease.task.attemptId, lease.leaseId, {
|
|
125
|
+
status: result.status,
|
|
126
|
+
result: result.result,
|
|
127
|
+
patch,
|
|
128
|
+
...result.status === "failed" ? { error: result.stderr.slice(0, 4e3) || "container attempt failed" } : {}
|
|
129
|
+
});
|
|
130
|
+
} catch (reason) {
|
|
131
|
+
const message = reason instanceof Error ? reason.message : "runner failed";
|
|
132
|
+
try {
|
|
133
|
+
await append(options.control, lease, "runner.failed", "runner", { message });
|
|
134
|
+
await options.control.complete(lease.task.attemptId, lease.leaseId, {
|
|
135
|
+
status: controller.signal.aborted ? "cancelled" : "failed",
|
|
136
|
+
error: message
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
142
|
+
options.signal?.removeEventListener("abort", cancelFromParent);
|
|
143
|
+
if (staged && !options.preserveWorkspace) await staged.cleanup();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function runHarnessRunner(options) {
|
|
147
|
+
const workspaceNames = Object.keys(options.workspaces).sort();
|
|
148
|
+
if (!workspaceNames.length) throw new TypeError("at least one workspace mapping is required");
|
|
149
|
+
do {
|
|
150
|
+
if (options.signal?.aborted) return;
|
|
151
|
+
const lease = await options.control.lease(workspaceNames);
|
|
152
|
+
if (lease) {
|
|
153
|
+
options.log?.(`leased ${lease.task.taskId}/${lease.task.attemptId}`);
|
|
154
|
+
await runLeasedAttempt(lease, options);
|
|
155
|
+
if (options.once) return;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (options.once) return;
|
|
159
|
+
await delay(options.pollMs ?? 2e3, void 0, { signal: options.signal }).catch(() => {
|
|
160
|
+
});
|
|
161
|
+
} while (!options.signal?.aborted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
runLeasedAttempt,
|
|
166
|
+
runHarnessRunner
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=chunk-ATKV6VTU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runner.ts"],"sourcesContent":["import { setTimeout as delay } from \"node:timers/promises\";\nimport { runContainerAttempt, type ContainerEngine, type ContainerLimits } from \"./container\";\nimport { makeHarnessEvent } from \"./protocol\";\nimport { stageWorkspace, type StagedWorkspace } from \"./workspace\";\nimport {\n HARNESS_PROTOCOL_VERSION,\n type HarnessAgentInput,\n type HarnessAgentOutput,\n type HarnessControlPlane,\n type HarnessLease,\n type HarnessToolBroker,\n} from \"./types\";\n\n/** Control-plane, workspace, isolation, polling, and lifecycle settings for a runner. */\nexport interface HarnessRunnerOptions {\n control: HarnessControlPlane;\n workspaces: Readonly<Record<string, string>>;\n engine: ContainerEngine;\n image: string;\n limits?: ContainerLimits;\n heartbeatMs?: number;\n pollMs?: number;\n once?: boolean;\n preserveWorkspace?: boolean;\n allowUnpinnedImage?: boolean;\n workspaceAccess?: \"read-write\" | \"read-only\" | \"none\";\n signal?: AbortSignal;\n log?: (message: string) => void;\n toolBroker?: HarnessToolBroker;\n}\n\nasync function append(\n control: HarnessControlPlane,\n lease: HarnessLease,\n kind: string,\n actor: \"runner\" | \"agent\" | \"model\" | \"system\",\n payload: unknown,\n): Promise<void> {\n await control.appendEvents(lease.task.attemptId, lease.leaseId, [makeHarnessEvent(kind, actor, payload)]);\n}\n\n/** Execute one leased task. The registered workspace is copied before the\n * container starts; neither the checkout nor the runner credential is mounted. */\nexport async function runLeasedAttempt(lease: HarnessLease, options: HarnessRunnerOptions): Promise<void> {\n const source = options.workspaces[lease.task.workspace];\n if (!source) {\n await options.control.complete(lease.task.attemptId, lease.leaseId, {\n status: \"failed\",\n error: `runner does not expose workspace \"${lease.task.workspace}\"`,\n });\n return;\n }\n\n let staged: StagedWorkspace | null = null;\n const controller = new AbortController();\n const cancelFromParent = () => controller.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", cancelFromParent, { once: true });\n if (options.signal?.aborted) controller.abort(options.signal.reason);\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let heartbeatInFlight = false;\n const pulse = async () => {\n if (heartbeatInFlight || controller.signal.aborted) return;\n heartbeatInFlight = true;\n try {\n const value = await options.control.heartbeat(lease.task.attemptId, lease.leaseId);\n if (value.cancelRequested) controller.abort(\"cancel_requested\");\n } catch {\n controller.abort(\"heartbeat_failed\");\n } finally {\n heartbeatInFlight = false;\n }\n };\n try {\n await pulse();\n heartbeat = setInterval(() => { void pulse(); }, options.heartbeatMs ?? 15_000);\n if (controller.signal.aborted) throw new Error(\"lease was cancelled before workspace staging\");\n staged = await stageWorkspace(source);\n if (controller.signal.aborted) throw new Error(\"lease was cancelled during workspace staging\");\n const stagedWorkspace = staged;\n await append(options.control, lease, \"runner.workspace_staged\", \"runner\", {\n workspace: lease.task.workspace,\n files: stagedWorkspace.fileCount,\n bytes: stagedWorkspace.byteCount,\n });\n\n const result = await runContainerAttempt({\n engine: options.engine,\n image: options.image,\n workspaceDir: stagedWorkspace.workspaceDir,\n task: lease.task,\n limits: options.limits,\n allowUnpinnedImage: options.allowUnpinnedImage,\n workspaceAccess: options.workspaceAccess ?? (options.toolBroker ? \"none\" : \"read-write\"),\n signal: controller.signal,\n onStderr: async (text) => {\n await append(options.control, lease, \"agent.stderr\", \"agent\", { text: text.slice(0, 64 * 1024) });\n },\n onMessage: async (message: HarnessAgentOutput): Promise<HarnessAgentInput | void> => {\n if (message.type === \"event\") {\n await append(options.control, lease, message.kind, \"agent\", message.payload ?? null);\n return;\n }\n if (message.type === \"attempt.complete\") {\n await append(options.control, lease, \"agent.completed\", \"agent\", {\n status: message.status,\n result: message.result ?? null,\n });\n return;\n }\n if (message.type === \"tool.request\") {\n await append(options.control, lease, \"tool.requested\", \"agent\", {\n requestId: message.requestId, tool: message.tool,\n });\n const response = options.toolBroker\n ? await options.toolBroker.execute({\n lease, workspaceDir: stagedWorkspace.workspaceDir, signal: controller.signal,\n }, message)\n : { requestId: message.requestId, ok: false, content: \"tool denied: no trusted broker configured\" };\n await append(options.control, lease, \"tool.responded\", \"system\", {\n requestId: message.requestId, tool: message.tool, ok: response.ok,\n contentBytes: Buffer.byteLength(response.content),\n });\n return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: \"tool.response\", ...response };\n }\n await append(options.control, lease, \"model.requested\", \"agent\", {\n requestId: message.requestId,\n messages: message.call.messages.length,\n maxTokens: message.call.maxTokens,\n });\n const response = await options.control.infer(lease.task.attemptId, lease.leaseId, {\n requestId: message.requestId,\n call: message.call,\n });\n await append(options.control, lease, \"model.responded\", \"model\", {\n requestId: message.requestId,\n response: response.response,\n receipt: response.receipt,\n });\n return {\n protocolVersion: HARNESS_PROTOCOL_VERSION,\n type: \"inference.response\",\n requestId: message.requestId,\n response: response.response,\n };\n },\n });\n\n const patch = await stagedWorkspace.patch(lease.task.policy.maxPatchBytes);\n await options.control.complete(lease.task.attemptId, lease.leaseId, {\n status: result.status,\n result: result.result,\n patch,\n ...(result.status === \"failed\" ? { error: result.stderr.slice(0, 4_000) || \"container attempt failed\" } : {}),\n });\n } catch (reason) {\n const message = reason instanceof Error ? reason.message : \"runner failed\";\n try {\n await append(options.control, lease, \"runner.failed\", \"runner\", { message });\n await options.control.complete(lease.task.attemptId, lease.leaseId, {\n status: controller.signal.aborted ? \"cancelled\" : \"failed\",\n error: message,\n });\n } catch {\n // A lost or revoked lease can also make the terminal report unavailable.\n }\n } finally {\n if (heartbeat) clearInterval(heartbeat);\n options.signal?.removeEventListener(\"abort\", cancelFromParent);\n if (staged && !options.preserveWorkspace) await staged.cleanup();\n }\n}\n\n/** One runner executes one attempt at a time. Concurrency comes from multiple\n * independently credentialed runner processes. */\nexport async function runHarnessRunner(options: HarnessRunnerOptions): Promise<void> {\n const workspaceNames = Object.keys(options.workspaces).sort();\n if (!workspaceNames.length) throw new TypeError(\"at least one workspace mapping is required\");\n do {\n if (options.signal?.aborted) return;\n const lease = await options.control.lease(workspaceNames);\n if (lease) {\n options.log?.(`leased ${lease.task.taskId}/${lease.task.attemptId}`);\n await runLeasedAttempt(lease, options);\n if (options.once) return;\n continue;\n }\n if (options.once) return;\n await delay(options.pollMs ?? 2_000, undefined, { signal: options.signal }).catch(() => {});\n } while (!options.signal?.aborted);\n}\n"],"mappings":"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","names":["response"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HARNESS_PROTOCOL_VERSION
|
|
3
|
+
} from "./chunk-QTUEF2HZ.js";
|
|
4
|
+
|
|
5
|
+
// src/protocol.ts
|
|
6
|
+
var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
7
|
+
var HarnessProtocolError = class extends Error {
|
|
8
|
+
name = "HarnessProtocolError";
|
|
9
|
+
};
|
|
10
|
+
function record(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function boundedText(value, label, max) {
|
|
14
|
+
if (typeof value !== "string" || !value || value.length > max || CONTROL.test(value)) {
|
|
15
|
+
throw new HarnessProtocolError(`${label} must be a non-empty string of at most ${max} characters`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function parseAgentOutput(line) {
|
|
20
|
+
if (Buffer.byteLength(line, "utf8") > 1e6) throw new HarnessProtocolError("agent message exceeds 1 MB");
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(line);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new HarnessProtocolError("agent emitted invalid JSON");
|
|
26
|
+
}
|
|
27
|
+
const message = record(value);
|
|
28
|
+
if (!message || message.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
|
|
29
|
+
throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
|
|
30
|
+
}
|
|
31
|
+
if (message.type === "event") {
|
|
32
|
+
return {
|
|
33
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
34
|
+
type: "event",
|
|
35
|
+
kind: boundedText(message.kind, "event.kind", 120),
|
|
36
|
+
...message.payload === void 0 ? {} : { payload: message.payload }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (message.type === "inference.request") {
|
|
40
|
+
const call = record(message.call);
|
|
41
|
+
if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
|
|
42
|
+
throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
46
|
+
type: "inference.request",
|
|
47
|
+
requestId: boundedText(message.requestId, "requestId", 180),
|
|
48
|
+
call
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (message.type === "tool.request") {
|
|
52
|
+
const input = record(message.input);
|
|
53
|
+
const tool = String(message.tool);
|
|
54
|
+
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
55
|
+
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
59
|
+
type: "tool.request",
|
|
60
|
+
requestId: boundedText(message.requestId, "requestId", 180),
|
|
61
|
+
tool,
|
|
62
|
+
input
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (message.type === "attempt.complete") {
|
|
66
|
+
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message.status))) {
|
|
67
|
+
throw new HarnessProtocolError("attempt.complete.status is invalid");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
71
|
+
type: "attempt.complete",
|
|
72
|
+
status: message.status,
|
|
73
|
+
...message.result === void 0 ? {} : { result: message.result }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
throw new HarnessProtocolError("agent message type is unsupported");
|
|
77
|
+
}
|
|
78
|
+
function encodeAgentInput(message) {
|
|
79
|
+
return `${JSON.stringify(message)}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
function makeHarnessEvent(kind, actor, payload, now = Date.now(), id = crypto.randomUUID()) {
|
|
83
|
+
boundedText(kind, "event.kind", 120);
|
|
84
|
+
return { eventId: id, kind, actor, payload, createdAt: now };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
HarnessProtocolError,
|
|
89
|
+
parseAgentOutput,
|
|
90
|
+
encodeAgentInput,
|
|
91
|
+
makeHarnessEvent
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=chunk-GE6CCN7W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["import type { ChatInput } from \"@odla-ai/ai\";\nimport {\n HARNESS_PROTOCOL_VERSION,\n type HarnessAgentInput,\n type HarnessAgentOutput,\n type HarnessEventInput,\n} from \"./types\";\n\nconst CONTROL = /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/;\n\n/** Error raised when an agent emits malformed, oversized, or unsupported protocol data. */\nexport class HarnessProtocolError extends Error {\n override readonly name = \"HarnessProtocolError\";\n}\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value !== null && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : null;\n}\n\nfunction boundedText(value: unknown, label: string, max: number): string {\n if (typeof value !== \"string\" || !value || value.length > max || CONTROL.test(value)) {\n throw new HarnessProtocolError(`${label} must be a non-empty string of at most ${max} characters`);\n }\n return value;\n}\n\n/** Parse and validate one newline-delimited message emitted by an agent container. */\nexport function parseAgentOutput(line: string): HarnessAgentOutput {\n if (Buffer.byteLength(line, \"utf8\") > 1_000_000) throw new HarnessProtocolError(\"agent message exceeds 1 MB\");\n let value: unknown;\n try { value = JSON.parse(line); } catch { throw new HarnessProtocolError(\"agent emitted invalid JSON\"); }\n const message = record(value);\n if (!message || message.protocolVersion !== HARNESS_PROTOCOL_VERSION) {\n throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);\n }\n if (message.type === \"event\") {\n return {\n protocolVersion: HARNESS_PROTOCOL_VERSION,\n type: \"event\",\n kind: boundedText(message.kind, \"event.kind\", 120),\n ...(message.payload === undefined ? {} : { payload: message.payload }),\n };\n }\n if (message.type === \"inference.request\") {\n const call = record(message.call);\n if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {\n throw new HarnessProtocolError(\"inference.request.call requires messages and maxTokens\");\n }\n return {\n protocolVersion: HARNESS_PROTOCOL_VERSION,\n type: \"inference.request\",\n requestId: boundedText(message.requestId, \"requestId\", 180),\n call: call as unknown as ChatInput,\n };\n }\n if (message.type === \"tool.request\") {\n const input = record(message.input);\n const tool = String(message.tool);\n if (!input || ![\"sandbox.read\", \"sandbox.apply_patch\", \"sandbox.run_recipe\"].includes(tool)) {\n throw new HarnessProtocolError(\"tool.request requires a registered tool and object input\");\n }\n return {\n protocolVersion: HARNESS_PROTOCOL_VERSION,\n type: \"tool.request\",\n requestId: boundedText(message.requestId, \"requestId\", 180),\n tool: tool as \"sandbox.read\" | \"sandbox.apply_patch\" | \"sandbox.run_recipe\",\n input,\n };\n }\n if (message.type === \"attempt.complete\") {\n if (!new Set([\"completed\", \"failed\", \"cancelled\"]).has(String(message.status))) {\n throw new HarnessProtocolError(\"attempt.complete.status is invalid\");\n }\n return {\n protocolVersion: HARNESS_PROTOCOL_VERSION,\n type: \"attempt.complete\",\n status: message.status as \"completed\" | \"failed\" | \"cancelled\",\n ...(message.result === undefined ? {} : { result: message.result }),\n };\n }\n throw new HarnessProtocolError(\"agent message type is unsupported\");\n}\n\n/** Serialize one trusted runner message as a newline-terminated JSONL record. */\nexport function encodeAgentInput(message: HarnessAgentInput): string {\n return `${JSON.stringify(message)}\\n`;\n}\n\n/** Create a timestamped, uniquely identified event for control-plane submission. */\nexport function makeHarnessEvent(\n kind: string,\n actor: HarnessEventInput[\"actor\"],\n payload: unknown,\n now = Date.now(),\n id = crypto.randomUUID(),\n): HarnessEventInput {\n boundedText(kind, \"event.kind\", 120);\n return { eventId: id, kind, actor, payload, createdAt: now };\n}\n"],"mappings":"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","names":[]}
|