@ricsam/r5d-worker 0.0.137 → 0.0.139

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 (236) hide show
  1. package/README.md +4 -94
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/mjs/main.mjs +46 -7543
  4. package/dist/mjs/package.json +1 -1
  5. package/dist/mjs/personal/action-scheduler.mjs +31 -0
  6. package/dist/mjs/personal/client.mjs +215 -0
  7. package/dist/mjs/personal/runtime.mjs +359 -0
  8. package/dist/mjs/personal/tcp.mjs +217 -0
  9. package/dist/mjs/runtime/adapter.mjs +42 -0
  10. package/dist/mjs/runtime/client.mjs +95 -0
  11. package/dist/mjs/runtime/daemon.mjs +131 -0
  12. package/dist/mjs/runtime/executor.mjs +436 -0
  13. package/dist/mjs/runtime/index.mjs +15 -0
  14. package/dist/mjs/runtime/protocol.mjs +139 -0
  15. package/dist/mjs/runtime/pty-host.mjs +156 -0
  16. package/dist/mjs/runtime/pty.mjs +127 -0
  17. package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
  18. package/dist/mjs/runtime/releases/grants.mjs +93 -0
  19. package/dist/mjs/runtime/releases/main.mjs +101 -0
  20. package/dist/mjs/runtime/releases/manager.mjs +541 -0
  21. package/dist/mjs/runtime/releases/process.mjs +88 -0
  22. package/dist/mjs/runtime/releases/retirement.mjs +18 -0
  23. package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
  24. package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
  25. package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
  26. package/dist/mjs/runtime/storage.mjs +156 -0
  27. package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
  28. package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
  29. package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
  30. package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
  31. package/dist/mjs/runtime/workspace/files.mjs +308 -0
  32. package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
  33. package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
  34. package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
  35. package/dist/types/main.d.ts +0 -1167
  36. package/dist/types/personal/action-scheduler.d.ts +23 -0
  37. package/dist/types/personal/client.d.ts +14 -0
  38. package/dist/types/personal/runtime.d.ts +121 -0
  39. package/dist/types/personal/tcp.d.ts +14 -0
  40. package/dist/types/runtime/client.d.ts +3 -1
  41. package/dist/types/runtime/executor.d.ts +6 -1
  42. package/dist/types/runtime/protocol.d.ts +176 -4
  43. package/dist/types/runtime/pty.d.ts +1 -0
  44. package/dist/types/runtime/releases/artifacts.d.ts +51 -0
  45. package/dist/types/runtime/releases/grants.d.ts +16 -0
  46. package/dist/types/runtime/releases/manager.d.ts +145 -0
  47. package/dist/types/runtime/releases/process.d.ts +14 -0
  48. package/dist/types/runtime/releases/retirement.d.ts +15 -0
  49. package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
  50. package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
  51. package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
  52. package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
  53. package/dist/types/runtime/workspace/authority.d.ts +338 -0
  54. package/dist/types/runtime/workspace/contracts.d.ts +86 -0
  55. package/dist/types/runtime/workspace/file-write.d.ts +14 -0
  56. package/dist/types/runtime/workspace/files.d.ts +21 -0
  57. package/dist/types/runtime/workspace/http-body.d.ts +9 -0
  58. package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
  59. package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
  60. package/package.json +8 -6
  61. package/dist/cjs/atomic-rename.cjs +0 -303
  62. package/dist/cjs/browser-upload.cjs +0 -73
  63. package/dist/cjs/cli-update.cjs +0 -108
  64. package/dist/cjs/command-launcher.cjs +0 -423
  65. package/dist/cjs/control-command-policy.cjs +0 -177
  66. package/dist/cjs/git-blob-hash.cjs +0 -41
  67. package/dist/cjs/git-identity.cjs +0 -39
  68. package/dist/cjs/git-process-environment.cjs +0 -133
  69. package/dist/cjs/inline-output.cjs +0 -62
  70. package/dist/cjs/main.cjs +0 -7517
  71. package/dist/cjs/managed-paths.cjs +0 -215
  72. package/dist/cjs/plan-lint.cjs +0 -165
  73. package/dist/cjs/plan-parser.cjs +0 -127
  74. package/dist/cjs/plan-paths.cjs +0 -53
  75. package/dist/cjs/port-forward-client.cjs +0 -206
  76. package/dist/cjs/process-tree.cjs +0 -117
  77. package/dist/cjs/project-checkout-garbage.cjs +0 -219
  78. package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
  79. package/dist/cjs/project-mirror-refs-token.cjs +0 -85
  80. package/dist/cjs/project-workspace-state.cjs +0 -972
  81. package/dist/cjs/project-worktrees.cjs +0 -1713
  82. package/dist/cjs/pty-output-coalescer.cjs +0 -87
  83. package/dist/cjs/recovery-journal-protocol.cjs +0 -56
  84. package/dist/cjs/recovery-journal-runtime.cjs +0 -133
  85. package/dist/cjs/recovery-journal-thread.cjs +0 -9
  86. package/dist/cjs/recovery-journal.cjs +0 -742
  87. package/dist/cjs/recovery-output-window.cjs +0 -77
  88. package/dist/cjs/recovery-protocol.cjs +0 -31
  89. package/dist/cjs/recovery-store.cjs +0 -252
  90. package/dist/cjs/registry-auth.cjs +0 -310
  91. package/dist/cjs/repository-transition-policy.cjs +0 -49
  92. package/dist/cjs/runtime-version.cjs +0 -88
  93. package/dist/cjs/session-file-mutations.cjs +0 -61
  94. package/dist/cjs/supervisor.cjs +0 -147
  95. package/dist/cjs/three-way-merge.cjs +0 -346
  96. package/dist/cjs/working-tree-mirror.cjs +0 -1872
  97. package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
  98. package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
  99. package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
  100. package/dist/cjs/workspace-command-targets.cjs +0 -63
  101. package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
  102. package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
  103. package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
  104. package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
  105. package/dist/cjs/workspace-git-sync.cjs +0 -3595
  106. package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
  107. package/dist/cjs/workspace-hydration-merge.cjs +0 -448
  108. package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
  109. package/dist/cjs/workspace-incident-state.cjs +0 -37
  110. package/dist/cjs/workspace-merge-projection.cjs +0 -628
  111. package/dist/cjs/workspace-mount-boundary.cjs +0 -71
  112. package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
  113. package/dist/cjs/workspace-mutation-gate.cjs +0 -127
  114. package/dist/cjs/workspace-path-move.cjs +0 -195
  115. package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
  116. package/dist/cjs/workspace-project-config-policy.cjs +0 -53
  117. package/dist/cjs/workspace-projection-ledger.cjs +0 -81
  118. package/dist/cjs/workspace-publication-evidence.cjs +0 -46
  119. package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
  120. package/dist/mjs/atomic-rename.mjs +0 -261
  121. package/dist/mjs/browser-upload.mjs +0 -42
  122. package/dist/mjs/command-launcher.mjs +0 -393
  123. package/dist/mjs/control-command-policy.mjs +0 -146
  124. package/dist/mjs/git-blob-hash.mjs +0 -16
  125. package/dist/mjs/git-identity.mjs +0 -15
  126. package/dist/mjs/git-process-environment.mjs +0 -105
  127. package/dist/mjs/inline-output.mjs +0 -37
  128. package/dist/mjs/managed-paths.mjs +0 -174
  129. package/dist/mjs/plan-lint.mjs +0 -129
  130. package/dist/mjs/plan-parser.mjs +0 -94
  131. package/dist/mjs/plan-paths.mjs +0 -25
  132. package/dist/mjs/port-forward-client.mjs +0 -172
  133. package/dist/mjs/project-checkout-garbage.mjs +0 -178
  134. package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
  135. package/dist/mjs/project-mirror-refs-token.mjs +0 -53
  136. package/dist/mjs/project-workspace-state.mjs +0 -937
  137. package/dist/mjs/project-worktrees.mjs +0 -1657
  138. package/dist/mjs/pty-output-coalescer.mjs +0 -61
  139. package/dist/mjs/recovery-journal-protocol.mjs +0 -30
  140. package/dist/mjs/recovery-journal-runtime.mjs +0 -112
  141. package/dist/mjs/recovery-journal-thread.mjs +0 -8
  142. package/dist/mjs/recovery-journal.mjs +0 -694
  143. package/dist/mjs/recovery-output-window.mjs +0 -53
  144. package/dist/mjs/recovery-protocol.mjs +0 -6
  145. package/dist/mjs/recovery-store.mjs +0 -217
  146. package/dist/mjs/registry-auth.mjs +0 -269
  147. package/dist/mjs/repository-transition-policy.mjs +0 -22
  148. package/dist/mjs/runtime-version.mjs +0 -54
  149. package/dist/mjs/session-file-mutations.mjs +0 -37
  150. package/dist/mjs/supervisor.mjs +0 -114
  151. package/dist/mjs/three-way-merge.mjs +0 -318
  152. package/dist/mjs/working-tree-mirror.mjs +0 -1817
  153. package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
  154. package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
  155. package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
  156. package/dist/mjs/workspace-command-targets.mjs +0 -37
  157. package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
  158. package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
  159. package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
  160. package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
  161. package/dist/mjs/workspace-git-sync.mjs +0 -3553
  162. package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
  163. package/dist/mjs/workspace-hydration-merge.mjs +0 -414
  164. package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
  165. package/dist/mjs/workspace-incident-state.mjs +0 -12
  166. package/dist/mjs/workspace-merge-projection.mjs +0 -594
  167. package/dist/mjs/workspace-mount-boundary.mjs +0 -37
  168. package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
  169. package/dist/mjs/workspace-mutation-gate.mjs +0 -103
  170. package/dist/mjs/workspace-path-move.mjs +0 -160
  171. package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
  172. package/dist/mjs/workspace-project-config-policy.mjs +0 -27
  173. package/dist/mjs/workspace-projection-ledger.mjs +0 -57
  174. package/dist/mjs/workspace-publication-evidence.mjs +0 -21
  175. package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
  176. package/dist/types/atomic-rename.d.ts +0 -78
  177. package/dist/types/browser-upload.d.ts +0 -7
  178. package/dist/types/command-launcher.d.ts +0 -104
  179. package/dist/types/control-command-policy.d.ts +0 -53
  180. package/dist/types/git-blob-hash.d.ts +0 -10
  181. package/dist/types/git-identity.d.ts +0 -9
  182. package/dist/types/git-process-environment.d.ts +0 -9
  183. package/dist/types/inline-output.d.ts +0 -3
  184. package/dist/types/managed-paths.d.ts +0 -19
  185. package/dist/types/plan-lint.d.ts +0 -16
  186. package/dist/types/plan-parser.d.ts +0 -38
  187. package/dist/types/plan-paths.d.ts +0 -36
  188. package/dist/types/port-forward-client.d.ts +0 -8
  189. package/dist/types/project-checkout-garbage.d.ts +0 -86
  190. package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
  191. package/dist/types/project-mirror-refs-token.d.ts +0 -53
  192. package/dist/types/project-workspace-state.d.ts +0 -188
  193. package/dist/types/project-worktrees.d.ts +0 -397
  194. package/dist/types/pty-output-coalescer.d.ts +0 -20
  195. package/dist/types/recovery-journal-protocol.d.ts +0 -35
  196. package/dist/types/recovery-journal-runtime.d.ts +0 -12
  197. package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
  198. package/dist/types/recovery-journal-thread.d.ts +0 -1
  199. package/dist/types/recovery-journal.d.ts +0 -248
  200. package/dist/types/recovery-output-window.d.ts +0 -19
  201. package/dist/types/recovery-protocol.d.ts +0 -59
  202. package/dist/types/recovery-store.d.ts +0 -52
  203. package/dist/types/registry-auth.d.ts +0 -47
  204. package/dist/types/repository-transition-policy.d.ts +0 -26
  205. package/dist/types/runtime-version.d.ts +0 -1
  206. package/dist/types/session-file-mutations.d.ts +0 -22
  207. package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
  208. package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
  209. package/dist/types/supervisor-signal-fixture.d.ts +0 -1
  210. package/dist/types/supervisor.d.ts +0 -23
  211. package/dist/types/three-way-merge.d.ts +0 -77
  212. package/dist/types/working-tree-mirror.d.ts +0 -424
  213. package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
  214. package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
  215. package/dist/types/workspace-command-sync-policy.d.ts +0 -57
  216. package/dist/types/workspace-command-targets.d.ts +0 -37
  217. package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
  218. package/dist/types/workspace-filesystem-executor.d.ts +0 -123
  219. package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
  220. package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
  221. package/dist/types/workspace-git-sync.d.ts +0 -514
  222. package/dist/types/workspace-hydration-ledger.d.ts +0 -76
  223. package/dist/types/workspace-hydration-merge.d.ts +0 -103
  224. package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
  225. package/dist/types/workspace-incident-state.d.ts +0 -19
  226. package/dist/types/workspace-merge-projection.d.ts +0 -69
  227. package/dist/types/workspace-mount-boundary.d.ts +0 -10
  228. package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
  229. package/dist/types/workspace-mutation-gate.d.ts +0 -42
  230. package/dist/types/workspace-path-move.d.ts +0 -21
  231. package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
  232. package/dist/types/workspace-project-config-policy.d.ts +0 -48
  233. package/dist/types/workspace-projection-ledger.d.ts +0 -59
  234. package/dist/types/workspace-publication-evidence.d.ts +0 -17
  235. package/dist/types/workspace-sync-coalescer.d.ts +0 -28
  236. /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
@@ -0,0 +1,184 @@
1
+ import { createConnection } from "node:net";
2
+ import { lstatSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import { dirname } from "node:path";
5
+ import { z } from "zod";
6
+ import {
7
+ ExecutorError,
8
+ isStartNonAdmissionCode,
9
+ MAX_FRAME_BYTES,
10
+ ExecutorCommand
11
+ } from "../protocol.mjs";
12
+ import { canonicalDirectory } from "./artifacts.mjs";
13
+ import { digest } from "./grants.mjs";
14
+ import { DeliveryReceipt, ManagerRpcClientConfig } from "./rpc-protocol.mjs";
15
+ class ManagerRpcClient {
16
+ config;
17
+ constructor(config) {
18
+ this.config = ManagerRpcClientConfig.parse(config);
19
+ }
20
+ request(command) {
21
+ const c = this.config;
22
+ canonicalDirectory(dirname(c.socketPath));
23
+ const st = lstatSync(c.socketPath), requestId = randomUUID();
24
+ if (!st.isSocket() || st.uid !== process.getuid?.() || st.mode & 63)
25
+ return Promise.reject(new ExecutorError("insecure_manager_socket"));
26
+ const frame = JSON.stringify({
27
+ protocol: 1,
28
+ requestId,
29
+ installationId: c.installationId,
30
+ workerId: c.workerId,
31
+ auth: { principalId: c.principalId, token: c.token },
32
+ command
33
+ }) + "\n";
34
+ if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) return Promise.reject(new ExecutorError("request_too_large"));
35
+ return new Promise((resolve, reject) => {
36
+ const socket = createConnection(c.socketPath);
37
+ let bytes = Buffer.alloc(0), done = false;
38
+ const finish = (error, result) => {
39
+ if (done) return;
40
+ done = true;
41
+ socket.destroy();
42
+ if (error) reject(error);
43
+ else resolve(result);
44
+ };
45
+ socket.setTimeout(c.timeoutMs, () => finish(new ExecutorError("manager_transport_unknown")));
46
+ socket.once("error", () => finish(new ExecutorError("manager_transport_unknown")));
47
+ socket.once("close", () => finish(new ExecutorError("manager_transport_unknown")));
48
+ socket.once("connect", () => socket.write(frame));
49
+ socket.on("data", (chunk) => {
50
+ bytes = Buffer.concat([bytes, chunk]);
51
+ if (bytes.length > MAX_FRAME_BYTES) {
52
+ finish(new ExecutorError("manager_response_invalid"));
53
+ return;
54
+ }
55
+ if (!bytes.includes(10)) return;
56
+ try {
57
+ const response = z.discriminatedUnion("ok", [
58
+ z.object({ requestId: z.literal(requestId), ok: z.literal(true), result: z.unknown() }).strict(),
59
+ z.object({
60
+ requestId: z.literal(requestId),
61
+ ok: z.literal(false),
62
+ error: z.object({ code: z.string(), rejectedBeforeAdmission: z.boolean() }).strict()
63
+ }).strict()
64
+ ]).parse(JSON.parse(bytes.toString("utf8")));
65
+ if (response.ok === true && Object.hasOwn(response, "result")) finish(void 0, response.result);
66
+ else
67
+ finish(
68
+ new ExecutorError(
69
+ response.ok === false ? response.error.code : "manager_response_invalid",
70
+ void 0,
71
+ response.ok === false && response.error.rejectedBeforeAdmission && command.method === "execute" && command.command.method === "start" && [
72
+ "candidate_not_ready",
73
+ "manager_delivery_busy",
74
+ "delivery_retention_full",
75
+ "release_manager_closed",
76
+ "manager_ipc_busy"
77
+ ].includes(response.error.code)
78
+ )
79
+ );
80
+ } catch {
81
+ finish(new ExecutorError("manager_response_invalid"));
82
+ }
83
+ });
84
+ });
85
+ }
86
+ probe() {
87
+ return this.request({ method: "probe", adapterId: this.config.adapterId, hello: this.config.hello });
88
+ }
89
+ async execute(callId, command) {
90
+ return DeliveryReceipt.parse(
91
+ await this.request({
92
+ method: "execute",
93
+ callId,
94
+ adapterId: this.config.adapterId,
95
+ hello: this.config.hello,
96
+ command: ExecutorCommand.parse(command)
97
+ })
98
+ );
99
+ }
100
+ async observe(command) {
101
+ return DeliveryReceipt.parse(
102
+ await this.request({
103
+ method: "observe",
104
+ adapterId: this.config.adapterId,
105
+ hello: this.config.hello,
106
+ command: ExecutorCommand.parse(command)
107
+ })
108
+ );
109
+ }
110
+ async receipt(callId) {
111
+ return DeliveryReceipt.parse(await this.request({ method: "receipt", callId }));
112
+ }
113
+ async acknowledge(callId, responseDigest, ackToken) {
114
+ return z.object({ callId: z.literal(callId), responseDigest: z.literal(responseDigest), acknowledged: z.literal(true) }).strict().parse(await this.request({ method: "acknowledge", callId, responseDigest, ackToken }));
115
+ }
116
+ }
117
+ class ManagedExecutorClient {
118
+ constructor(config, receiptSink, onAcknowledged) {
119
+ this.receiptSink = receiptSink;
120
+ this.onAcknowledged = onAcknowledged;
121
+ this.rpc = new ManagerRpcClient(config);
122
+ }
123
+ receiptSink;
124
+ onAcknowledged;
125
+ rpc;
126
+ get config() {
127
+ return this.rpc.config;
128
+ }
129
+ status() {
130
+ return this.rpc.probe();
131
+ }
132
+ async request(command) {
133
+ if (command.method === "status") return this.status();
134
+ if (!["start", "poll", "write", "resize", "cancel"].includes(command.method)) throw new ExecutorError("forbidden");
135
+ command = ExecutorCommand.parse(command);
136
+ const callId = command.method === "poll" ? null : `delivery-${digest(command)}`;
137
+ let receipt;
138
+ try {
139
+ receipt = callId === null ? await this.rpc.observe(command) : await this.rpc.execute(callId, command);
140
+ } catch (error2) {
141
+ if (command.method === "start" && error2 instanceof ExecutorError && error2.rejectedBeforeAdmission)
142
+ throw new ExecutorError("capacity_busy", void 0, true);
143
+ throw error2;
144
+ }
145
+ const identity = command.method === "start" ? command.operation : "run" in command ? command.run : null;
146
+ if ((callId !== null ? receipt.callId !== callId || receipt.deliveryKind !== "mutation" : !/^observation-[1-9][0-9]*$/.test(receipt.callId) || receipt.deliveryKind !== "observation") || receipt.adapterId !== this.config.adapterId || receipt.requesterId !== this.config.principalId || receipt.userId !== identity?.userId || receipt.sessionId !== identity?.sessionId || receipt.operationId !== identity?.operationId || receipt.commandDigest !== digest(command) || receipt.installationId !== this.config.installationId || receipt.workerId !== this.config.workerId || receipt.responseDigest !== digest({ callId: receipt.callId, commandDigest: receipt.commandDigest, state: receipt.state, outcome: receipt.outcome }))
147
+ throw new ExecutorError("manager_response_invalid");
148
+ if (this.receiptSink) await this.receiptSink(receipt);
149
+ if (receipt.state !== "completed" || receipt.outcome === null) throw new ExecutorError("manager_delivery_unknown");
150
+ if (this.receiptSink) {
151
+ try {
152
+ await this.rpc.acknowledge(receipt.callId, receipt.responseDigest, receipt.ackToken);
153
+ await this.onAcknowledged?.(receipt);
154
+ } catch {
155
+ }
156
+ }
157
+ if (receipt.outcome.ok) return receipt.outcome.result;
158
+ const error = receipt.outcome.error;
159
+ throw new ExecutorError(
160
+ error.code,
161
+ void 0,
162
+ command.method === "start" && isStartNonAdmissionCode(error.code) && error.rejectedBeforeAdmission
163
+ );
164
+ }
165
+ start(operation) {
166
+ return this.request({ method: "start", operation });
167
+ }
168
+ poll(run, stdoutOffset = 0, stderrOffset = 0, maxBytes = 32768) {
169
+ return this.request({ method: "poll", run, stdoutOffset, stderrOffset, maxBytes });
170
+ }
171
+ write(run, actionId, data, eof = false) {
172
+ return this.request({ method: "write", run, actionId, data, eof });
173
+ }
174
+ resize(run, actionId, cols, rows) {
175
+ return this.request({ method: "resize", run, actionId, cols, rows });
176
+ }
177
+ cancel(run, actionId) {
178
+ return this.request({ method: "cancel", run, actionId });
179
+ }
180
+ }
181
+ export {
182
+ ManagedExecutorClient,
183
+ ManagerRpcClient
184
+ };
@@ -0,0 +1,213 @@
1
+ import { chmodSync, constants, closeSync, fstatSync, lstatSync, openSync, readSync, unlinkSync } from "node:fs";
2
+ import { timingSafeEqual } from "node:crypto";
3
+ import { createServer } from "node:net";
4
+ import { dirname, join } from "node:path";
5
+ import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
6
+ import { ExecutorError, MAX_FRAME_BYTES } from "../protocol.mjs";
7
+ import { WorkerAdapterReleases } from "./manager.mjs";
8
+ import { bytesHash, canonicalDirectory } from "./artifacts.mjs";
9
+ import { ManagerRequest, ManagerRpcConfig } from "./rpc-protocol.mjs";
10
+ function readManagerPrivateJson(path) {
11
+ canonicalDirectory(dirname(path));
12
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
13
+ try {
14
+ const st = fstatSync(fd);
15
+ if (!st.isFile() || st.nlink !== 1 || st.uid !== process.getuid?.() || st.mode & 63 || st.size > MAX_FRAME_BYTES)
16
+ throw new ExecutorError("insecure_manager_config");
17
+ const bytes = Buffer.alloc(st.size + 1);
18
+ let offset = 0;
19
+ while (offset < bytes.length) {
20
+ const n = readSync(fd, bytes, offset, bytes.length - offset, null);
21
+ if (!n) break;
22
+ offset += n;
23
+ }
24
+ const after = fstatSync(fd);
25
+ if (offset !== st.size || after.size !== st.size || after.mtimeMs !== st.mtimeMs || after.ctimeMs !== st.ctimeMs)
26
+ throw new ExecutorError("manager_config_changed");
27
+ return JSON.parse(bytes.subarray(0, offset).toString("utf8"));
28
+ } finally {
29
+ closeSync(fd);
30
+ }
31
+ }
32
+ async function startManagerRpc(input) {
33
+ const config = ManagerRpcConfig.parse(input), manager = new WorkerAdapterReleases(config.manager);
34
+ const socketPath = join(manager.config.stateDir, "manager.sock"), sockets = /* @__PURE__ */ new Set();
35
+ let pending = 0;
36
+ const server = createServer((socket) => {
37
+ if (sockets.size >= 48) {
38
+ socket.destroy();
39
+ return;
40
+ }
41
+ sockets.add(socket);
42
+ socket.once("close", () => sockets.delete(socket));
43
+ socket.on("error", () => {
44
+ });
45
+ socket.setTimeout(15e3, () => socket.destroy());
46
+ let bytes = Buffer.alloc(0), received = false;
47
+ socket.on("data", (chunk) => {
48
+ if (received) {
49
+ socket.destroy();
50
+ return;
51
+ }
52
+ if (bytes.length + chunk.length > MAX_FRAME_BYTES) {
53
+ socket.destroy();
54
+ return;
55
+ }
56
+ bytes = Buffer.concat([bytes, chunk]);
57
+ const end = bytes.indexOf(10);
58
+ if (end < 0) return;
59
+ received = true;
60
+ if (end !== bytes.length - 1) {
61
+ socket.destroy();
62
+ return;
63
+ }
64
+ let requestId = null;
65
+ const execute = async () => {
66
+ const raw = JSON.parse(bytes.subarray(0, end).toString("utf8"));
67
+ requestId = RuntimeId.parse(raw?.requestId);
68
+ const request = ManagerRequest.parse(raw);
69
+ const principal = config.principals.find((p) => p.id === request.auth.principalId);
70
+ if (!timingSafeEqual(
71
+ Buffer.from(bytesHash(Buffer.from(request.auth.token)), "hex"),
72
+ Buffer.from(principal?.tokenHash ?? "0".repeat(64), "hex")
73
+ ) || !principal)
74
+ throw new ExecutorError("unauthenticated");
75
+ if (request.installationId !== manager.config.installationId || request.workerId !== manager.config.workerId)
76
+ throw new ExecutorError("wrong_installation");
77
+ const command = request.command;
78
+ const sessionAllowed = (userId, sessionId) => principal.sessions.some((scope) => scope.userId === userId && scope.sessionId === sessionId) || principal.role === "workspace" && principal.userIds?.includes(userId) === true;
79
+ if (command.method === "probe") return manager.probe(command.adapterId, command.hello);
80
+ if (command.method === "execute" || command.method === "observe") {
81
+ if (principal.role !== "workspace") throw new ExecutorError("forbidden");
82
+ const operation = command.command.method === "start" ? command.command.operation : "run" in command.command ? command.command.run : null;
83
+ if (!operation?.sessionId || !sessionAllowed(operation.userId, operation.sessionId)) throw new ExecutorError("forbidden");
84
+ if (pending > (command.command.method === "cancel" ? 44 : 40))
85
+ throw new ExecutorError("manager_ipc_busy", void 0, command.command.method === "start");
86
+ return command.method === "observe" ? manager.observe(command.adapterId, command.hello, principal.id, command.command) : manager.deliver(command.callId, command.adapterId, command.hello, principal.id, command.command);
87
+ }
88
+ if (command.method === "receipt" || command.method === "acknowledge") {
89
+ if (principal.role !== "workspace") throw new ExecutorError("forbidden");
90
+ const receipt = manager.delivery(command.callId);
91
+ if (receipt && (receipt.requesterId !== principal.id || !sessionAllowed(receipt.userId, receipt.sessionId)))
92
+ throw new ExecutorError("delivery_not_found");
93
+ if (command.method === "receipt") {
94
+ if (!receipt) throw new ExecutorError("delivery_not_found");
95
+ return receipt;
96
+ }
97
+ return manager.acknowledgeDelivery(command.callId, command.responseDigest, command.ackToken, principal.id);
98
+ }
99
+ if (principal.role !== "admin") throw new ExecutorError("forbidden");
100
+ switch (command.method) {
101
+ case "stage":
102
+ return manager.stage(command);
103
+ case "launch":
104
+ return manager.launch(command.adapterId);
105
+ case "stop-adapter":
106
+ return manager.stopAdapter(command.adapterId);
107
+ case "promote":
108
+ return manager.promote(command.adapterId, command.fence, command.validUntil);
109
+ case "retire":
110
+ return manager.retire(command.adapterId, command.retirementIntentDigest);
111
+ case "settle-unknown":
112
+ return manager.settleUnknownDelivery(command.callId, command.evidenceDigest);
113
+ case "reconcile":
114
+ return manager.reconcile();
115
+ case "status":
116
+ return {
117
+ managerInvocation: manager.invocation,
118
+ releases: manager.list().map((r) => ({
119
+ principalId: r.principalId,
120
+ phase: r.phase,
121
+ retirementCompletion: r.retirementCompletion,
122
+ hello: r.manifest.hello,
123
+ artifactDigest: r.artifactDigest,
124
+ drain: manager.deliveryDrain(r.principalId)
125
+ })),
126
+ transitions: manager.transitions()
127
+ };
128
+ }
129
+ };
130
+ pending++;
131
+ void execute().then(
132
+ (result) => {
133
+ const frame = JSON.stringify({ requestId, ok: true, result: result ?? null }) + "\n";
134
+ if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) socket.destroy();
135
+ else socket.end(frame);
136
+ },
137
+ (error) => socket.end(
138
+ JSON.stringify({
139
+ requestId,
140
+ ok: false,
141
+ error: {
142
+ code: error instanceof ExecutorError ? error.code : "invalid_request",
143
+ rejectedBeforeAdmission: error instanceof ExecutorError && error.rejectedBeforeAdmission
144
+ }
145
+ }) + "\n"
146
+ )
147
+ ).finally(() => pending--);
148
+ });
149
+ });
150
+ try {
151
+ manager.bindRpcPrincipals(config.principals);
152
+ canonicalDirectory(manager.config.stateDir);
153
+ try {
154
+ const st = lstatSync(socketPath);
155
+ if (!st.isSocket() || st.uid !== process.getuid?.()) throw new ExecutorError("unsafe_manager_socket");
156
+ unlinkSync(socketPath);
157
+ } catch (error) {
158
+ if (error.code !== "ENOENT") throw error;
159
+ }
160
+ await new Promise((resolve, reject) => {
161
+ server.once("error", reject);
162
+ server.listen(socketPath, () => {
163
+ server.off("error", reject);
164
+ resolve();
165
+ });
166
+ });
167
+ chmodSync(socketPath, 384);
168
+ } catch (error) {
169
+ server.close();
170
+ await manager.close();
171
+ throw error;
172
+ }
173
+ let closing;
174
+ return {
175
+ manager,
176
+ socketPath,
177
+ get pending() {
178
+ return pending;
179
+ },
180
+ close() {
181
+ return closing ??= (async () => {
182
+ for (const socket of sockets) socket.destroy();
183
+ await new Promise((resolve) => server.close(() => resolve()));
184
+ await manager.close();
185
+ })();
186
+ }
187
+ };
188
+ }
189
+ if (import.meta.main) {
190
+ try {
191
+ if (process.argv.length !== 3) throw new ExecutorError("private_config_required");
192
+ const service = await startManagerRpc(ManagerRpcConfig.parse(readManagerPrivateJson(process.argv[2])));
193
+ console.log(JSON.stringify({ ready: true, pid: process.pid, protocol: 1 }));
194
+ let stopping = false;
195
+ const stop = () => {
196
+ if (stopping) return;
197
+ stopping = true;
198
+ void service.close().then(
199
+ () => process.exit(0),
200
+ () => process.exit(1)
201
+ );
202
+ };
203
+ process.once("SIGTERM", stop);
204
+ process.once("SIGINT", stop);
205
+ } catch {
206
+ console.error("Private adapter manager startup failed");
207
+ process.exitCode = 1;
208
+ }
209
+ }
210
+ export {
211
+ readManagerPrivateJson,
212
+ startManagerRpc
213
+ };
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ import { RuntimeHello, RuntimeId, OwnershipFence } from "@ricsam/r5d-api/runtime-protocol";
3
+ import { AdapterConfig, AdapterGrant, ExecutorCommand } from "../protocol.mjs";
4
+ import { AdapterManifest } from "./artifacts.mjs";
5
+ import { ReleaseManagerConfig } from "./manager.mjs";
6
+ const Absolute = z.string().startsWith("/").max(4096);
7
+ const ManagerRpcConfig = z.object({
8
+ manager: ReleaseManagerConfig,
9
+ principals: z.array(
10
+ z.object({
11
+ id: RuntimeId,
12
+ tokenHash: z.string().regex(/^[a-f0-9]{64}$/),
13
+ role: z.enum(["admin", "probe", "workspace"]),
14
+ sessions: z.array(z.object({ userId: RuntimeId, sessionId: RuntimeId }).strict()).max(100),
15
+ // Only the trusted catalog-authorizing workspace keeper receives these grants.
16
+ userIds: z.array(RuntimeId).max(1e3).optional()
17
+ }).strict()
18
+ ).min(1).max(100)
19
+ }).strict().refine(
20
+ (v) => new Set(v.principals.map((p) => p.id)).size === v.principals.length && new Set(v.principals.map((p) => p.tokenHash)).size === v.principals.length,
21
+ "Duplicate RPC principal/credential"
22
+ );
23
+ const ManagerRpcClientConfig = z.object({
24
+ socketPath: Absolute,
25
+ installationId: RuntimeId,
26
+ workerId: RuntimeId,
27
+ principalId: RuntimeId,
28
+ token: z.string().min(32).max(512),
29
+ adapterId: RuntimeId,
30
+ hello: RuntimeHello,
31
+ timeoutMs: z.number().int().min(100).max(6e4).default(1e4)
32
+ }).strict();
33
+ const ManagerCommand = z.discriminatedUnion("method", [
34
+ z.object({ method: z.literal("probe"), adapterId: RuntimeId, hello: RuntimeHello }).strict(),
35
+ z.object({ method: z.literal("execute"), adapterId: RuntimeId, hello: RuntimeHello, callId: RuntimeId, command: ExecutorCommand }).strict(),
36
+ z.object({ method: z.literal("observe"), adapterId: RuntimeId, hello: RuntimeHello, command: ExecutorCommand }).strict(),
37
+ z.object({ method: z.literal("receipt"), callId: RuntimeId }).strict(),
38
+ z.object({
39
+ method: z.literal("acknowledge"),
40
+ callId: RuntimeId,
41
+ responseDigest: z.string().regex(/^[a-f0-9]{64}$/),
42
+ ackToken: z.string().regex(/^[a-f0-9]{64}$/)
43
+ }).strict(),
44
+ z.object({
45
+ method: z.literal("stage"),
46
+ source: Absolute,
47
+ manifest: AdapterManifest,
48
+ approvalDigest: z.string(),
49
+ grant: AdapterGrant,
50
+ adapter: AdapterConfig
51
+ }).strict(),
52
+ z.object({ method: z.literal("launch"), adapterId: RuntimeId }).strict(),
53
+ z.object({ method: z.literal("stop-adapter"), adapterId: RuntimeId }).strict(),
54
+ z.object({ method: z.literal("promote"), adapterId: RuntimeId, fence: OwnershipFence, validUntil: z.number().int().positive() }).strict(),
55
+ z.object({
56
+ method: z.literal("retire"),
57
+ adapterId: RuntimeId,
58
+ retirementIntentDigest: z.string().regex(/^[a-f0-9]{64}$/).optional()
59
+ }).strict(),
60
+ z.object({ method: z.literal("settle-unknown"), callId: RuntimeId, evidenceDigest: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
61
+ z.object({ method: z.literal("status") }).strict(),
62
+ z.object({ method: z.literal("reconcile") }).strict()
63
+ ]);
64
+ const ManagerRequest = z.object({
65
+ protocol: z.literal(1),
66
+ requestId: RuntimeId,
67
+ installationId: RuntimeId,
68
+ workerId: RuntimeId,
69
+ auth: z.object({ principalId: RuntimeId, token: z.string().min(32).max(512) }).strict(),
70
+ command: ManagerCommand
71
+ }).strict();
72
+ const DeliveryOutcome = z.discriminatedUnion("ok", [
73
+ z.object({ ok: z.literal(true), result: z.unknown() }).strict(),
74
+ z.object({ ok: z.literal(false), error: z.object({ code: z.string(), rejectedBeforeAdmission: z.boolean() }).strict() }).strict()
75
+ ]);
76
+ const DeliveryReceipt = z.object({
77
+ callId: RuntimeId,
78
+ deliveryKind: z.enum(["mutation", "observation"]),
79
+ ackToken: z.string().regex(/^[a-f0-9]{64}$/),
80
+ installationId: RuntimeId,
81
+ workerId: RuntimeId,
82
+ adapterId: RuntimeId,
83
+ requesterId: RuntimeId,
84
+ userId: RuntimeId,
85
+ sessionId: RuntimeId,
86
+ operationId: RuntimeId,
87
+ commandDigest: z.string().regex(/^[a-f0-9]{64}$/),
88
+ state: z.enum(["pending", "completed", "unknown"]),
89
+ outcome: DeliveryOutcome.nullable(),
90
+ responseDigest: z.string().regex(/^[a-f0-9]{64}$/),
91
+ acknowledged: z.boolean()
92
+ }).strict().superRefine((receipt, ctx) => {
93
+ if (receipt.state === "completed" !== (receipt.outcome !== null) || receipt.outcome?.ok === true && !Object.hasOwn(receipt.outcome, "result"))
94
+ ctx.addIssue({ code: "custom", message: "Delivery state/payload mismatch" });
95
+ });
96
+ export {
97
+ DeliveryOutcome,
98
+ DeliveryReceipt,
99
+ ManagerCommand,
100
+ ManagerRequest,
101
+ ManagerRpcClientConfig,
102
+ ManagerRpcConfig
103
+ };
@@ -0,0 +1,156 @@
1
+ import { Database } from "bun:sqlite";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ readSync,
12
+ rmSync,
13
+ writeSync
14
+ } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { ExecutorError } from "./protocol.mjs";
17
+ function privateDirectory(path) {
18
+ mkdirSync(path, { mode: 448, recursive: true });
19
+ const stat = lstatSync(path);
20
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
21
+ throw new ExecutorError("insecure_directory");
22
+ }
23
+ function readPrivateJson(path) {
24
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
25
+ try {
26
+ const stat = fstatSync(fd);
27
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 63 || stat.size > 1024 * 1024)
28
+ throw new ExecutorError("insecure_config");
29
+ return JSON.parse(readFileSync(fd, "utf8"));
30
+ } finally {
31
+ closeSync(fd);
32
+ }
33
+ }
34
+ function privateDatabase(path) {
35
+ try {
36
+ closeSync(openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384));
37
+ } catch (error) {
38
+ if (error.code !== "EEXIST") throw error;
39
+ }
40
+ for (const file of [path, `${path}-journal`, `${path}-wal`, `${path}-shm`]) {
41
+ try {
42
+ const stat = lstatSync(file);
43
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.mode & 63)
44
+ throw new ExecutorError("insecure_store_file");
45
+ } catch (error) {
46
+ if (error.code !== "ENOENT") throw error;
47
+ }
48
+ }
49
+ return new Database(path);
50
+ }
51
+ class ExecutorStore {
52
+ constructor(root, installationId, workerId) {
53
+ this.root = root;
54
+ privateDirectory(root);
55
+ this.lock = privateDatabase(join(root, "authority.lock.sqlite"));
56
+ try {
57
+ this.lock.exec(
58
+ "PRAGMA busy_timeout=0; PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS keeper_lock (id INTEGER)"
59
+ );
60
+ } catch {
61
+ this.lock.close();
62
+ throw new ExecutorError("executor_already_running");
63
+ }
64
+ try {
65
+ this.db = privateDatabase(join(root, "receipts.sqlite"));
66
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON;
67
+ CREATE TABLE IF NOT EXISTS identity (id INTEGER PRIMARY KEY CHECK (id=1), installation TEXT NOT NULL, worker TEXT NOT NULL, version INTEGER NOT NULL);
68
+ CREATE TABLE IF NOT EXISTS authority (id INTEGER PRIMARY KEY CHECK (id=1), fence TEXT NOT NULL, valid_until INTEGER NOT NULL);
69
+ CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, receipt TEXT NOT NULL);
70
+ CREATE TABLE IF NOT EXISTS actions (run_id TEXT NOT NULL REFERENCES runs(id), id TEXT NOT NULL, hash TEXT NOT NULL, state TEXT NOT NULL, PRIMARY KEY(run_id,id));`);
71
+ const identity = this.db.query("SELECT * FROM identity WHERE id=1").get();
72
+ if (identity && (identity.installation !== installationId || identity.worker !== workerId || identity.version !== 1))
73
+ throw new ExecutorError("wrong_store_identity");
74
+ if (!identity) this.db.query("INSERT INTO identity VALUES (1,?,?,1)").run(installationId, workerId);
75
+ privateDirectory(join(root, "output"));
76
+ privateDirectory(join(root, "secrets"));
77
+ for (const receipt of this.all()) {
78
+ if (receipt.state === "accepted" || receipt.state === "running") {
79
+ receipt.state = "unknown";
80
+ receipt.reason = "executor_restarted_reconciliation_required";
81
+ this.put(receipt);
82
+ }
83
+ }
84
+ this.db.exec("UPDATE actions SET state='unknown' WHERE state='accepted'; UPDATE authority SET valid_until=0");
85
+ rmSync(join(root, "secrets"), { recursive: true });
86
+ privateDirectory(join(root, "secrets"));
87
+ } catch (error) {
88
+ this.db?.close();
89
+ this.lock.close();
90
+ throw error;
91
+ }
92
+ }
93
+ root;
94
+ db;
95
+ lock;
96
+ all() {
97
+ return this.db.query("SELECT receipt FROM runs").all().map((row) => JSON.parse(row.receipt));
98
+ }
99
+ get(id) {
100
+ const row = this.db.query("SELECT receipt FROM runs WHERE id=?").get(id);
101
+ return row ? JSON.parse(row.receipt) : null;
102
+ }
103
+ put(receipt) {
104
+ this.db.query("INSERT INTO runs VALUES (?,?) ON CONFLICT(id) DO UPDATE SET receipt=excluded.receipt").run(receipt.operationId, JSON.stringify(receipt));
105
+ }
106
+ outputPath(id, stream) {
107
+ return join(this.root, "output", `${id}.${stream}`);
108
+ }
109
+ append(id, stream, data) {
110
+ const fd = openSync(
111
+ this.outputPath(id, stream),
112
+ constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW,
113
+ 384
114
+ );
115
+ try {
116
+ let offset = 0;
117
+ while (offset < data.length) offset += writeSync(fd, data, offset);
118
+ fsyncSync(fd);
119
+ const directory = openSync(join(this.root, "output"), constants.O_RDONLY | constants.O_DIRECTORY);
120
+ try {
121
+ fsyncSync(directory);
122
+ } finally {
123
+ closeSync(directory);
124
+ }
125
+ } finally {
126
+ closeSync(fd);
127
+ }
128
+ }
129
+ read(id, stream, offset, end, max) {
130
+ if (offset > end) throw new ExecutorError("invalid_offset");
131
+ const buffer = Buffer.alloc(Math.min(end - offset, max));
132
+ if (buffer.length) {
133
+ const fd = openSync(this.outputPath(id, stream), constants.O_RDONLY | constants.O_NOFOLLOW);
134
+ try {
135
+ let n = 0;
136
+ while (n < buffer.length) {
137
+ const read = readSync(fd, buffer, n, buffer.length - n, offset + n);
138
+ if (!read) throw new ExecutorError("output_truncated");
139
+ n += read;
140
+ }
141
+ } finally {
142
+ closeSync(fd);
143
+ }
144
+ }
145
+ return { base64: buffer.toString("base64"), nextOffset: offset + buffer.length };
146
+ }
147
+ close() {
148
+ this.db.close();
149
+ this.lock.close();
150
+ }
151
+ }
152
+ export {
153
+ ExecutorStore,
154
+ privateDirectory,
155
+ readPrivateJson
156
+ };