@parcha/agentrun-kernel 0.0.0-stage → 0.1.0-beta.29

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +52 -2
  5. package/dist/delegate.d.ts +87 -0
  6. package/dist/delegate.js +143 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.js +37 -0
  9. package/dist/host/alive.d.ts +2 -0
  10. package/dist/host/alive.js +28 -0
  11. package/dist/host/box-slot.d.ts +26 -0
  12. package/dist/host/box-slot.js +112 -0
  13. package/dist/host/box.d.ts +104 -0
  14. package/dist/host/box.js +16 -0
  15. package/dist/host/egress-proxy.d.ts +45 -0
  16. package/dist/host/egress-proxy.js +416 -0
  17. package/dist/host/egress-table.d.ts +24 -0
  18. package/dist/host/egress-table.js +165 -0
  19. package/dist/host/index.d.ts +16 -0
  20. package/dist/host/index.js +12 -0
  21. package/dist/host/overhead.d.ts +53 -0
  22. package/dist/host/overhead.js +75 -0
  23. package/dist/host/parent-death.d.ts +15 -0
  24. package/dist/host/parent-death.js +42 -0
  25. package/dist/host/parent-watch.d.ts +1 -0
  26. package/dist/host/parent-watch.js +38 -0
  27. package/dist/host/pinned-binary.d.ts +6 -0
  28. package/dist/host/pinned-binary.js +48 -0
  29. package/dist/host/proxy.d.ts +179 -0
  30. package/dist/host/proxy.js +629 -0
  31. package/dist/host/serial.d.ts +3 -0
  32. package/dist/host/serial.js +10 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +6 -0
  35. package/dist/kernel/blobs.d.ts +16 -0
  36. package/dist/kernel/blobs.js +59 -0
  37. package/dist/kernel/drive.d.ts +35 -0
  38. package/dist/kernel/drive.js +202 -0
  39. package/dist/kernel/errors.d.ts +27 -0
  40. package/dist/kernel/errors.js +42 -0
  41. package/dist/kernel/follow-up.d.ts +67 -0
  42. package/dist/kernel/follow-up.js +146 -0
  43. package/dist/kernel/halt.d.ts +11 -0
  44. package/dist/kernel/halt.js +17 -0
  45. package/dist/kernel/index.d.ts +8 -0
  46. package/dist/kernel/index.js +5 -0
  47. package/dist/kernel/types.d.ts +120 -0
  48. package/dist/kernel/types.js +1 -0
  49. package/dist/process/builtins.d.ts +142 -0
  50. package/dist/process/builtins.js +207 -0
  51. package/dist/process/inbox.d.ts +29 -0
  52. package/dist/process/inbox.js +52 -0
  53. package/dist/process/index.d.ts +9 -0
  54. package/dist/process/index.js +11 -0
  55. package/dist/process/journal.d.ts +111 -0
  56. package/dist/process/journal.js +232 -0
  57. package/dist/process/records.d.ts +143 -0
  58. package/dist/process/records.js +142 -0
  59. package/dist/process/recovery.d.ts +76 -0
  60. package/dist/process/recovery.js +139 -0
  61. package/dist/process/settle.d.ts +11 -0
  62. package/dist/process/settle.js +20 -0
  63. package/dist/process/tail.d.ts +20 -0
  64. package/dist/process/tail.js +53 -0
  65. package/dist/process/tools.d.ts +105 -0
  66. package/dist/process/tools.js +120 -0
  67. package/dist/process/zod.d.ts +32 -0
  68. package/dist/process/zod.js +109 -0
  69. package/dist/runner.d.ts +154 -0
  70. package/dist/runner.js +547 -0
  71. package/dist/schema-check-cases.d.ts +7 -0
  72. package/dist/schema-check-cases.js +73 -0
  73. package/dist/schema-check.d.ts +3 -0
  74. package/dist/schema-check.js +88 -0
  75. package/dist/skill-files.d.ts +23 -0
  76. package/dist/skill-files.js +136 -0
  77. package/dist/testing.d.ts +91 -0
  78. package/dist/testing.js +196 -0
  79. package/dist/types.d.ts +237 -0
  80. package/dist/types.js +10 -0
  81. package/package.json +74 -4
  82. package/vendor/durable-harness/UPSTREAM.json +67 -0
@@ -0,0 +1,237 @@
1
+ import type { WorkflowDeps } from "@parcha/agentrun-dsl";
2
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ /** What a node runner is asked, with the step it runs as under recovery and the node's own turn limit (#1061). */
6
+ export type NodeRequest = Parameters<NonNullable<WorkflowDeps["runNode"]>>[0] & {
7
+ step?: {
8
+ sessionId: string;
9
+ attempt: number;
10
+ earlierSessionIds: readonly string[];
11
+ };
12
+ /** An `agent` node's `max_turns`; applied within the runner's own cap. */
13
+ maxTurns?: number;
14
+ };
15
+ /** An image a tool answers with: its media type (`image/png`) and its bytes, base64. */
16
+ export interface ToolImage {
17
+ mimeType: string;
18
+ data: string;
19
+ }
20
+ /**
21
+ * The most characters a host tool's result reaches a harness with: its text as the model reads it (`text.length`). The
22
+ * kernel's tool bridge, which every host tool's result passes on its way to a harness, cuts every result past it: its
23
+ * start, cut where a character ends, then a line saying where it was cut (`[cut at N of M characters]`), the call's end
24
+ * flagged `truncated`; the call's record keeps it whole. Each harness tells its CLI to pass a result this long to the
25
+ * model whole: the Claude Agent CLI otherwise saves one past 50,000 characters to a file and tells the model to read it
26
+ * (which a confined session cannot), and Codex cuts the middle of one past its truncation policy. The Claude Agent CLI
27
+ * takes at most 500,000 here.
28
+ */
29
+ export declare const HOST_RESULT_MAX_CHARS = 120000;
30
+ /** A tool the host gives a session. */
31
+ export interface HostTool {
32
+ name: string;
33
+ description: string;
34
+ inputSchema: Record<string, unknown>;
35
+ /** `safe`: may run again after a crash. `never`: at most once; a call a crash cut off is reported, never repeated. */
36
+ replay: "safe" | "never";
37
+ /** The tool's answer: its text (cut at HOST_RESULT_MAX_CHARS on its way to the harness), and any images it shows (a
38
+ * screenshot), which a harness that can show them sends the model with the text. `sessionDir` is the runner's own,
39
+ * when it has one: a tool that runs a node of its own (the delegate) gives its child a private directory the same
40
+ * way, by the child's key. */
41
+ call(args: unknown, context: {
42
+ callId: string;
43
+ signal: AbortSignal;
44
+ sessionDir?: (key: string) => string;
45
+ }): Promise<{
46
+ text: string;
47
+ isError?: boolean;
48
+ images?: readonly ToolImage[];
49
+ }>;
50
+ }
51
+ /** One durable record of a session, in the order it was appended. */
52
+ export interface SessionRecord {
53
+ kind: string;
54
+ data: JsonValue;
55
+ }
56
+ /** Where a session's records live: the run's store, written through whoever owns it. A key is one session for its
57
+ * whole life, across processes. Records are written only through a writer, and one writer holds a session at a time:
58
+ * `open` takes the session, and from the moment it resolves every writer opened earlier for that key is fenced, whether
59
+ * or not the new one has written anything. A process whose lease lapsed while it was still alive therefore cannot write
60
+ * into a session a newer process holds. */
61
+ export interface SessionStore {
62
+ read(key: string): Promise<SessionRecord[]>;
63
+ /** Take the session. Resolves once the new holder is durable: every earlier writer of `key` is fenced from then on. */
64
+ open(key: string): Promise<SessionWriter>;
65
+ }
66
+ /** The one holder that may write a session. */
67
+ export interface SessionWriter {
68
+ readonly key: string;
69
+ read(): Promise<SessionRecord[]>;
70
+ /** Resolves once the records are durable, in order. Rejects with `SessionFenced`, writing none of them, once a newer
71
+ * `open` of the key has resolved. */
72
+ append(records: readonly SessionRecord[]): Promise<void>;
73
+ }
74
+ /** What one session is. */
75
+ export interface SessionSpec {
76
+ /** The session's durable identity: a node step's `sessionId`, or a key of the runner's own for a call with no step. */
77
+ key: string;
78
+ label: string;
79
+ executionPath?: string;
80
+ system: string[];
81
+ /** Exactly these tools; the harness's own stay off. */
82
+ tools: HostTool[];
83
+ context: "fresh" | "fork";
84
+ tier?: NodeRequest["tier"];
85
+ effort?: NodeRequest["effort"];
86
+ thinking?: NodeRequest["thinking"];
87
+ }
88
+ /** A paid model request, as the session is about to send it. */
89
+ export interface ModelRequest {
90
+ model: string;
91
+ /** The most input tokens the request can be billed for (the model proxy's `inputTokensMaxOf`: the body's UTF-8 bytes
92
+ * plus what the provider adds), undefined when it was not measured. */
93
+ inputTokensMax?: number;
94
+ /** The most output tokens the request asks for, when its body says (the proxy's policy reads it). */
95
+ outputTokensAsked?: number;
96
+ /** The least output it may be limited to and still answer: its answer's 1,024 tokens, plus the least thinking its
97
+ * provider takes when it thinks (the proxy's policy says). Absent: 1,024. */
98
+ outputTokensLeast?: number;
99
+ }
100
+ /** What a paid request cost, once it completed. */
101
+ export interface ModelUsage {
102
+ input: number;
103
+ output: number;
104
+ cacheRead?: number;
105
+ cacheWrite?: number;
106
+ /** Null when the price is unknown; `estimated` when it is not the provider's own figure (the host's price table's). */
107
+ usd: number | null;
108
+ estimated?: boolean;
109
+ /** Beside the provider's own figure, what the host's price table makes of the same usage: kept to audit the table. */
110
+ tableUsd?: number | null;
111
+ /** A request a crash cut off: it left, and no answer came back to this run. Its tokens are unknown (0 here) and its
112
+ * price is null; the provider may have billed it. */
113
+ cut?: boolean;
114
+ /** The ids the provider's response gave the request, by header name (the provider's `request-id`, a gateway's call
115
+ * id such as LiteLLM's `x-litellm-call-id`): what joins this booking to the provider's own log. A cut request has
116
+ * them only if its response had begun. */
117
+ providerIds?: Record<string, string>;
118
+ /** A response its client stopped part way, its cost unknown, with the bytes of it the client was sent (the model
119
+ * proxy's `stoppedAfterBytes`): the host counts it by what streamed (`Admission.stoppedUsd`), never more than its hold. */
120
+ stoppedAfterBytes?: number;
121
+ }
122
+ /** The session's model requests, through the runner: every paid request is announced before it is sent, and may be
123
+ * refused there (a turn cap, the host's budget); its usage is reported when it completes. */
124
+ export interface ModelPort {
125
+ /** Admits a paid request, or throws to refuse it. It may return the request's ticket, which names the turn it was admitted as. */
126
+ beforeRequest(request: ModelRequest): Promise<ModelTicket | void>;
127
+ /** Books a completed request. `ticket` is what its own `beforeRequest` returned: requests may be in flight together, so
128
+ * an answer is booked under the turn it was admitted as, not the latest one. */
129
+ afterRequest(usage: ModelUsage, ticket?: ModelTicket): Promise<void>;
130
+ /** Records the ids a paid request's response gave it, as its headers arrive and before its answer is read. */
131
+ responded?(ids: Record<string, string>, ticket?: ModelTicket): Promise<void>;
132
+ }
133
+ /** A paid request's admission: the turn it was admitted as, which its answer is booked under. */
134
+ export interface ModelTicket {
135
+ turn: number;
136
+ /** The most output tokens, thinking included, the request may be sent with: what is left of its host's spend cap can
137
+ * buy. The session's model proxy writes it into the body; absent, the request goes as asked. */
138
+ outputTokensMax?: number;
139
+ }
140
+ /** What the host lends a session. */
141
+ export interface SessionIO {
142
+ /** The session's writer, opened by the runner for this attempt. The adapter writes through it and never opens the
143
+ * session itself, so a drive it restarts cannot take the session back from a newer process. */
144
+ writer: SessionWriter;
145
+ models: ModelPort;
146
+ signal: AbortSignal;
147
+ /** The session's private directory, when the host gives one: its HOME and the harness's own files. */
148
+ dir?: string;
149
+ /** The session's working directory, when the host gives one: the workspace its file tools confine to, absolute. A
150
+ * harness runs there, so a path the model builds from its working directory is one the host's tools take. Without it,
151
+ * a harness works in an empty directory of its own. */
152
+ cwd?: string;
153
+ onEvent(event: AdapterEvent): void;
154
+ }
155
+ /** An event of a session, as the adapter reports it. The runner adds the node it belongs to. A call's `tool_end` is sent
156
+ * only once its outcome is durable in the session's records, so a host never shows a result its store does not have;
157
+ * `result` is the text recorded, when the adapter reports it. A call stopped before its outcome was recorded gets no
158
+ * `tool_end`: its turn's `turn_end` closes it. */
159
+ export type AdapterEvent = {
160
+ type: "text";
161
+ delta: string;
162
+ } | {
163
+ type: "tool_start";
164
+ callId: string;
165
+ name: string;
166
+ args: unknown;
167
+ parent?: string;
168
+ } | {
169
+ type: "tool_end";
170
+ callId: string;
171
+ name: string;
172
+ isError: boolean;
173
+ result?: string;
174
+ truncated?: true;
175
+ } | {
176
+ type: "turn_end";
177
+ status: TurnStatus;
178
+ };
179
+ /** An event as a host receives it: the adapter's, the runner's own, and the node each belongs to. */
180
+ export type HarnessEvent = (AdapterEvent | {
181
+ type: "request_start";
182
+ turn: number;
183
+ model: string;
184
+ } | {
185
+ type: "request_end";
186
+ turn: number;
187
+ usage: ModelUsage;
188
+ }) & {
189
+ key: string;
190
+ label: string;
191
+ executionPath?: string;
192
+ };
193
+ export type TurnStatus = "completed" | "interrupted" | "failed";
194
+ /** A session as `open` found it: new, cut off inside a turn (the adapter owes a recovery turn), or between turns. */
195
+ export type SessionState = "new" | "open_turn" | "idle";
196
+ export interface AdapterSession {
197
+ readonly state: SessionState;
198
+ /** Place the input, then send it: a new turn. */
199
+ send(text: string): Promise<void>;
200
+ /** Resume the turn a crash cut off, from the session's records. Only when `state` is `open_turn`. */
201
+ recover(): Promise<void>;
202
+ /** Record the open turn as ended, interrupted, and send nothing: the turn is not to be resumed (a person stopped it, and
203
+ * an interrupt writes no end in the session). Only when `state` is `open_turn`; the session is `idle` after, here and
204
+ * when a later process opens it, and the next `send` is a turn of its own. A follow-up closes a cut turn the same way
205
+ * (kernel/follow-up.ts, FollowUpTarget.settleOpenTurn). */
206
+ settleOpenTurn(): Promise<void>;
207
+ /** Resolves when the turn in flight ends. */
208
+ turnEnd(): Promise<{
209
+ status: TurnStatus;
210
+ error?: string;
211
+ }>;
212
+ /** Stop the turn in flight; calls it left open end interrupted. With `drainMs`, a pause: no new turn is sent, and the
213
+ * calls in flight run on until their outcomes are recorded, for at most `drainMs`, before the turn stops; the host
214
+ * refuses new calls and paid requests meanwhile (the runner's `stop` does). An adapter that cannot drain stops at
215
+ * once, as with no `drainMs`. It only stops the turn: the turn stays open in the session's records, and an adapter
216
+ * whose session can tell reads `open_turn` after it while it does, so a host that is done with the turn (a person's
217
+ * stop) settles it with `settleOpenTurn()`, and one that is not (a pause) leaves it for the next process. */
218
+ interrupt(options?: {
219
+ drainMs?: number;
220
+ }): Promise<void>;
221
+ /** Stop the harness's processes and let go of the session. */
222
+ close(): Promise<void>;
223
+ }
224
+ export interface AdapterCapabilities {
225
+ /** Whether the adapter can start a session from another's transcript. */
226
+ fork: boolean;
227
+ }
228
+ export interface AgentAdapter {
229
+ readonly id: string;
230
+ /** The harness version the adapter is pinned to. */
231
+ readonly version: string;
232
+ capabilities(): AdapterCapabilities;
233
+ /** Problems with a session before anything is spent; an empty list opens it. */
234
+ check(spec: SessionSpec): string[];
235
+ /** Opens the session, restoring it from `io.writer` when its key has records. */
236
+ open(spec: SessionSpec, io: SessionIO): Promise<AdapterSession>;
237
+ }
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The most characters a host tool's result reaches a harness with: its text as the model reads it (`text.length`). The
3
+ * kernel's tool bridge, which every host tool's result passes on its way to a harness, cuts every result past it: its
4
+ * start, cut where a character ends, then a line saying where it was cut (`[cut at N of M characters]`), the call's end
5
+ * flagged `truncated`; the call's record keeps it whole. Each harness tells its CLI to pass a result this long to the
6
+ * model whole: the Claude Agent CLI otherwise saves one past 50,000 characters to a file and tells the model to read it
7
+ * (which a confined session cannot), and Codex cuts the middle of one past its truncation policy. The Claude Agent CLI
8
+ * takes at most 500,000 here.
9
+ */
10
+ export const HOST_RESULT_MAX_CHARS = 120_000;
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@parcha/agentrun-kernel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.29",
4
+ "description": "Agent harnesses other than Pi, starting with Codex, behind one seam: a DSL node's runNode and an AgentRun run on a harness adapter, durable in AgentRun's own store.",
5
+ "keywords": [
6
+ "agentrun",
7
+ "agent",
8
+ "harness",
9
+ "durable",
10
+ "workflow"
11
+ ],
12
+ "type": "module",
13
+ "license": "Apache-2.0",
14
+ "author": "Parcha Labs, Inc.",
15
+ "engines": {
16
+ "node": ">=22.19.0"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./testing": {
26
+ "types": "./dist/testing.d.ts",
27
+ "default": "./dist/testing.js"
28
+ },
29
+ "./kernel": {
30
+ "types": "./dist/kernel/index.d.ts",
31
+ "default": "./dist/kernel/index.js"
32
+ },
33
+ "./process": {
34
+ "types": "./dist/process/index.d.ts",
35
+ "default": "./dist/process/index.js"
36
+ },
37
+ "./host": {
38
+ "types": "./dist/host/index.d.ts",
39
+ "default": "./dist/host/index.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "CHANGELOG.md",
47
+ "LICENSE",
48
+ "NOTICE",
49
+ "vendor/durable-harness/UPSTREAM.json"
50
+ ],
51
+ "sideEffects": false,
52
+ "scripts": {
53
+ "build": "rm -rf dist && tsc -p tsconfig.json",
54
+ "prepack": "npm run build",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "node --test --test-concurrency=8 'test/*.test.mjs'"
57
+ },
58
+ "dependencies": {
59
+ "@parcha/agentrun-dsl": "0.1.0-beta.29",
60
+ "ajv": "8.18.0",
61
+ "typebox": "1.3.7"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/Parcha-ai/agentrun.git",
66
+ "directory": "packages/run-adapters"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/Parcha-ai/agentrun/issues"
70
+ },
71
+ "homepage": "https://agentrun.ai",
72
+ "publishConfig": {
73
+ "access": "public",
74
+ "tag": "beta"
75
+ }
76
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "repo": "github.com/vercel-labs/durable-harness",
3
+ "commit": "ea8aa4060cf4fb8cd39737ce8ab21c4e0f40c741",
4
+ "license": "Apache-2.0",
5
+ "copyright": "Copyright 2026 Vercel Inc.",
6
+ "notice": null,
7
+ "files": {
8
+ "src/process/records.ts": {
9
+ "upstream": "packages/harness-process/src/records.ts",
10
+ "blob": "8ce81fe33c7bc8a563030f021e5f2bcdcc2b9597",
11
+ "sha256": "53b1ed7e60756835e112e7387e483d3baf92385c50b1906d69f5c3e55f5879bc"
12
+ },
13
+ "src/process/recovery.ts": {
14
+ "upstream": "packages/harness-process/src/recovery.ts",
15
+ "blob": "57e495656f63dde58d11c7775601132dd99d09e6",
16
+ "sha256": "16fcd724a3646b1218833ba8aad4141b757ae992cb55ac523aa2690b34e00640"
17
+ },
18
+ "src/process/journal.ts": {
19
+ "upstream": "packages/harness-process/src/journal.ts",
20
+ "blob": "a86848a6ce7962f1325b2b73fed47772ca16e6a3",
21
+ "sha256": "73e0f451d008d82692462e66719f51686bf92898b9d0088e8658450fffdfd182"
22
+ },
23
+ "src/process/tools.ts": {
24
+ "upstream": "packages/harness-process/src/tools.ts",
25
+ "blob": "f0dab38d2440513c84e442dfed8d6a8a5d654616",
26
+ "sha256": "e8f811beffb7eab60fc245f28cd6082228bbe11d1d0260d3f1946c67c1fbfe14"
27
+ },
28
+ "src/process/builtins.ts": {
29
+ "upstream": "packages/harness-process/src/builtins.ts",
30
+ "blob": "602487d768314e68eca215fd582e9d2e4cd37482",
31
+ "sha256": "62750338eb34974b22ee8de74783fe2b373adfdcad57f882b032cc8941e722dd"
32
+ },
33
+ "src/process/inbox.ts": {
34
+ "upstream": "packages/harness-process/src/inbox.ts",
35
+ "blob": "0f588d2d9f9d5e35882a09b6a921a72a80058ad7",
36
+ "sha256": "59d2a96779b065dfcb90868c4a7216bbdc98d90663fd45b9953a48c0b4c9ceae"
37
+ },
38
+ "src/process/tail.ts": {
39
+ "upstream": "packages/harness-process/src/tail.ts",
40
+ "blob": "0c9f118023f7e7122621c84c4766b478fe8a73ca",
41
+ "sha256": "8814035a33a55916306f2e91f9fb582389556da858a94085449f0a66aba4846d"
42
+ },
43
+ "src/kernel/types.ts": {
44
+ "upstream": "packages/harness-world/src/types.ts",
45
+ "blob": "d12a050e7395a6a4d5319acf88ba3d728b783443",
46
+ "sha256": "775a12d0c7737dad58ac8b5e8a9695c559762a8196b7cd78b48fa284803a9a3f",
47
+ "kind": "interfaces matched",
48
+ "declarations": [
49
+ "EffectRecord"
50
+ ]
51
+ },
52
+ "src/kernel/errors.ts": {
53
+ "upstream": "packages/harness-world/src/errors.ts",
54
+ "blob": "f778e8b800281227c9b005865a02e1026cc1fefd",
55
+ "sha256": "672cd4afb4caa648737f74b0ea71c0fc4bba53fb518de7c3e6303648de2c8d2f",
56
+ "kind": "interfaces matched",
57
+ "declarations": [
58
+ "EffectFailure"
59
+ ]
60
+ },
61
+ "src/process/zod.ts": {
62
+ "upstream": "packages/harness-process/src/zod.ts",
63
+ "blob": "67c50b6ede18d73a8edd7006a301e22d10580242",
64
+ "sha256": "3a1cac6ac89608ae63cbd8ef5ceeccd46cf6aff184c62011a4d9966f85b4b92f"
65
+ }
66
+ }
67
+ }