codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4

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 (231) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +43 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
  7. package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
  8. package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
  9. package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
  10. package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
  11. package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
  12. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  13. package/contracts/v1/fixtures/operation-action.json +13 -0
  14. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  15. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  16. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  17. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  18. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  19. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  20. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  21. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  22. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  23. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  24. package/contracts/v1/fixtures/operation-event.json +18 -0
  25. package/contracts/v1/fixtures/operation-handle.json +10 -0
  26. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  27. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  28. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  29. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  30. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  31. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  32. package/contracts/v1/fixtures/operation-request.json +33 -0
  33. package/contracts/v1/fixtures/operation-state.json +144 -0
  34. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  35. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  36. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  37. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  38. package/contracts/v1/manifest.json +185 -1
  39. package/contracts/v1/parity-suite.json +106 -8
  40. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  41. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  42. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  43. package/contracts/v1/schemas/manifest.schema.json +57 -0
  44. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  45. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  46. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  47. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  48. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  49. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  50. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  51. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  52. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  53. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  54. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  55. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  56. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  57. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  58. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  59. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  60. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  61. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  62. package/contracts/v1/surface-drift-policy.json +50 -0
  63. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  64. package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
  65. package/dist/codex-chatgpt-control-journal.mjs +5547 -0
  66. package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
  67. package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
  68. package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
  69. package/dist/src/backend/client.d.ts +103 -2
  70. package/dist/src/backend/client.js +1436 -97
  71. package/dist/src/backend/compatibility.d.ts +12 -0
  72. package/dist/src/backend/compatibility.js +208 -0
  73. package/dist/src/backend/protocol.d.ts +84 -1
  74. package/dist/src/backend/protocol.js +31 -5
  75. package/dist/src/backend/runtime-identity.d.ts +10 -0
  76. package/dist/src/backend/runtime-identity.js +141 -0
  77. package/dist/src/backend/session.d.ts +10 -2
  78. package/dist/src/backend/session.js +478 -7
  79. package/dist/src/backend/stdio-server.d.ts +4 -1
  80. package/dist/src/backend/stdio-server.js +240 -28
  81. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  82. package/dist/src/browser/active-composer-file-input.js +33 -0
  83. package/dist/src/browser/attach.d.ts +4 -1
  84. package/dist/src/browser/attach.js +136 -28
  85. package/dist/src/browser/chatgpt-url.d.ts +2 -0
  86. package/dist/src/browser/chatgpt-url.js +9 -0
  87. package/dist/src/browser/downloads.d.ts +17 -1
  88. package/dist/src/browser/downloads.js +150 -28
  89. package/dist/src/browser/page-state.js +6 -0
  90. package/dist/src/client.d.ts +43 -0
  91. package/dist/src/client.js +1279 -59
  92. package/dist/src/commands/artifacts.js +6 -4
  93. package/dist/src/commands/chat-popover.d.ts +37 -0
  94. package/dist/src/commands/chat-popover.js +537 -0
  95. package/dist/src/commands/configuration.d.ts +5 -0
  96. package/dist/src/commands/configuration.js +134 -21
  97. package/dist/src/commands/context.js +10 -3
  98. package/dist/src/commands/doctor.d.ts +1 -1
  99. package/dist/src/commands/doctor.js +27 -4
  100. package/dist/src/commands/experience.d.ts +5 -0
  101. package/dist/src/commands/experience.js +104 -21
  102. package/dist/src/commands/files.js +14 -33
  103. package/dist/src/commands/modes.js +85 -21
  104. package/dist/src/commands/power-discovery.d.ts +130 -0
  105. package/dist/src/commands/power-discovery.js +669 -0
  106. package/dist/src/commands/project-sources.js +325 -31
  107. package/dist/src/commands/sequence.js +59 -32
  108. package/dist/src/commands/session.js +19 -6
  109. package/dist/src/commands/work.js +31 -0
  110. package/dist/src/dom/composer-text.d.ts +6 -0
  111. package/dist/src/dom/composer-text.js +85 -0
  112. package/dist/src/errors.d.ts +9 -0
  113. package/dist/src/errors.js +22 -0
  114. package/dist/src/index.d.ts +6 -0
  115. package/dist/src/index.js +9 -0
  116. package/dist/src/operations/artifact-output.d.ts +69 -0
  117. package/dist/src/operations/artifact-output.js +1748 -0
  118. package/dist/src/operations/artifact-stream.d.ts +19 -0
  119. package/dist/src/operations/artifact-stream.js +35 -0
  120. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  121. package/dist/src/operations/artifact-transfer.js +1312 -0
  122. package/dist/src/operations/browser-adapter.d.ts +167 -0
  123. package/dist/src/operations/browser-adapter.js +2103 -0
  124. package/dist/src/operations/browser-observation.d.ts +110 -0
  125. package/dist/src/operations/browser-observation.js +1256 -0
  126. package/dist/src/operations/browser-target.d.ts +105 -0
  127. package/dist/src/operations/browser-target.js +531 -0
  128. package/dist/src/operations/canonical.d.ts +4 -0
  129. package/dist/src/operations/canonical.js +412 -0
  130. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  131. package/dist/src/operations/chatgpt-runtime.js +1232 -0
  132. package/dist/src/operations/client.d.ts +138 -0
  133. package/dist/src/operations/client.js +1145 -0
  134. package/dist/src/operations/collector.d.ts +195 -0
  135. package/dist/src/operations/collector.js +1039 -0
  136. package/dist/src/operations/configuration-routing.d.ts +4 -0
  137. package/dist/src/operations/configuration-routing.js +14 -0
  138. package/dist/src/operations/control.d.ts +372 -0
  139. package/dist/src/operations/control.js +1498 -0
  140. package/dist/src/operations/file-identity.d.ts +33 -0
  141. package/dist/src/operations/file-identity.js +153 -0
  142. package/dist/src/operations/handle.d.ts +19 -0
  143. package/dist/src/operations/handle.js +727 -0
  144. package/dist/src/operations/index.d.ts +25 -0
  145. package/dist/src/operations/index.js +25 -0
  146. package/dist/src/operations/journal-authority.d.ts +12 -0
  147. package/dist/src/operations/journal-authority.js +1 -0
  148. package/dist/src/operations/journal-rpc-client.d.ts +12 -0
  149. package/dist/src/operations/journal-rpc-client.js +225 -0
  150. package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
  151. package/dist/src/operations/journal-rpc-protocol.js +214 -0
  152. package/dist/src/operations/journal-rpc-server.d.ts +15 -0
  153. package/dist/src/operations/journal-rpc-server.js +272 -0
  154. package/dist/src/operations/journal.d.ts +143 -0
  155. package/dist/src/operations/journal.js +1957 -0
  156. package/dist/src/operations/production-attachments.d.ts +118 -0
  157. package/dist/src/operations/production-attachments.js +1035 -0
  158. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  159. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  160. package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
  161. package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
  162. package/dist/src/operations/production-configuration.d.ts +42 -0
  163. package/dist/src/operations/production-configuration.js +1544 -0
  164. package/dist/src/operations/production-primitives.d.ts +53 -0
  165. package/dist/src/operations/production-primitives.js +856 -0
  166. package/dist/src/operations/production-work-steer.d.ts +203 -0
  167. package/dist/src/operations/production-work-steer.js +1218 -0
  168. package/dist/src/operations/recovery.d.ts +80 -0
  169. package/dist/src/operations/recovery.js +174 -0
  170. package/dist/src/operations/runtime-adapter.d.ts +126 -0
  171. package/dist/src/operations/runtime-adapter.js +802 -0
  172. package/dist/src/operations/send-once.d.ts +224 -0
  173. package/dist/src/operations/send-once.js +1081 -0
  174. package/dist/src/operations/service.d.ts +286 -0
  175. package/dist/src/operations/service.js +2831 -0
  176. package/dist/src/operations/staging.d.ts +136 -0
  177. package/dist/src/operations/staging.js +630 -0
  178. package/dist/src/operations/state-machine.d.ts +29 -0
  179. package/dist/src/operations/state-machine.js +2047 -0
  180. package/dist/src/operations/submission.d.ts +423 -0
  181. package/dist/src/operations/submission.js +1676 -0
  182. package/dist/src/operations/transactional-chat-power.d.ts +17 -0
  183. package/dist/src/operations/transactional-chat-power.js +212 -0
  184. package/dist/src/operations/turn-ownership.d.ts +202 -0
  185. package/dist/src/operations/turn-ownership.js +700 -0
  186. package/dist/src/operations/types.d.ts +454 -0
  187. package/dist/src/operations/types.js +20 -0
  188. package/dist/src/operations/wire-requests.d.ts +21 -0
  189. package/dist/src/operations/wire-requests.js +396 -0
  190. package/dist/src/operations/wire-results.d.ts +97 -0
  191. package/dist/src/operations/wire-results.js +818 -0
  192. package/dist/src/runner/responses.d.ts +3 -1
  193. package/dist/src/runner/responses.js +19 -1
  194. package/dist/src/runner/result.js +227 -55
  195. package/dist/src/runner/types.d.ts +12 -0
  196. package/dist/src/runtime/command-routing.d.ts +149 -0
  197. package/dist/src/runtime/command-routing.js +431 -0
  198. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  199. package/dist/src/runtime/coordinated-browser.js +315 -0
  200. package/dist/src/runtime/coordinated-page.d.ts +41 -0
  201. package/dist/src/runtime/coordinated-page.js +651 -0
  202. package/dist/src/runtime/operation-context.d.ts +142 -0
  203. package/dist/src/runtime/operation-context.js +410 -0
  204. package/dist/src/runtime/runtime-session.d.ts +95 -0
  205. package/dist/src/runtime/runtime-session.js +314 -0
  206. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  207. package/dist/src/runtime/tab-coordinator.js +1200 -0
  208. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  209. package/dist/src/runtime/value-boundaries.js +49 -0
  210. package/dist/src/safety/blockers.js +8 -1
  211. package/dist/src/safety/untrusted-output.js +2 -1
  212. package/dist/src/scripts/backend-server.js +4 -1
  213. package/dist/src/scripts/capture-surface-profile.js +7 -2
  214. package/dist/src/scripts/journal-server.d.ts +2 -0
  215. package/dist/src/scripts/journal-server.js +37 -0
  216. package/dist/src/scripts/live-smoke/harness.js +79 -7
  217. package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
  218. package/dist/src/scripts/live-smoke/scenarios.js +147 -30
  219. package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
  220. package/dist/src/scripts/live-smoke/transactional.js +254 -0
  221. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  222. package/dist/src/scripts/release-canary-module.js +17 -4
  223. package/dist/src/types.d.ts +38 -1
  224. package/package.json +8 -3
  225. package/references/2026-08-16-transactional-operations.md +459 -0
  226. package/references/2026-09-06-journal-service.md +140 -0
  227. package/references/agents-runner.md +34 -0
  228. package/references/backend-protocol.md +176 -0
  229. package/references/python-parity.md +137 -0
  230. package/references/responses-adapter.md +19 -0
  231. package/references/streaming.md +6 -0
@@ -0,0 +1,25 @@
1
+ export * from "./types.js";
2
+ export * from "./canonical.js";
3
+ export * from "./state-machine.js";
4
+ export * from "./journal.js";
5
+ export * from "./journal-authority.js";
6
+ export * from "./journal-rpc-client.js";
7
+ export * from "./journal-rpc-server.js";
8
+ export * from "./recovery.js";
9
+ export * from "./file-identity.js";
10
+ export * from "./handle.js";
11
+ export * from "./submission.js";
12
+ export * from "./turn-ownership.js";
13
+ export * from "./collector.js";
14
+ export * from "./artifact-output.js";
15
+ export * from "./control.js";
16
+ export * from "./send-once.js";
17
+ export * from "./staging.js";
18
+ export * from "./browser-observation.js";
19
+ export * from "./browser-target.js";
20
+ export * from "./browser-adapter.js";
21
+ export * from "./runtime-adapter.js";
22
+ export * from "./chatgpt-runtime.js";
23
+ export * from "./production-primitives.js";
24
+ export * from "./service.js";
25
+ export * from "./client.js";
@@ -0,0 +1,25 @@
1
+ export * from "./types.js";
2
+ export * from "./canonical.js";
3
+ export * from "./state-machine.js";
4
+ export * from "./journal.js";
5
+ export * from "./journal-authority.js";
6
+ export * from "./journal-rpc-client.js";
7
+ export * from "./journal-rpc-server.js";
8
+ export * from "./recovery.js";
9
+ export * from "./file-identity.js";
10
+ export * from "./handle.js";
11
+ export * from "./submission.js";
12
+ export * from "./turn-ownership.js";
13
+ export * from "./collector.js";
14
+ export * from "./artifact-output.js";
15
+ export * from "./control.js";
16
+ export * from "./send-once.js";
17
+ export * from "./staging.js";
18
+ export * from "./browser-observation.js";
19
+ export * from "./browser-target.js";
20
+ export * from "./browser-adapter.js";
21
+ export * from "./runtime-adapter.js";
22
+ export * from "./chatgpt-runtime.js";
23
+ export * from "./production-primitives.js";
24
+ export * from "./service.js";
25
+ export * from "./client.js";
@@ -0,0 +1,12 @@
1
+ import type { OperationJournal } from "./journal.js";
2
+ /**
3
+ * The journal authority owns durable state, process locks and its root key.
4
+ * Local callers retain the existing synchronous cryptographic API; remote
5
+ * authorities resolve the same bounded values asynchronously. Browser action
6
+ * capabilities never cross this boundary.
7
+ */
8
+ type AuthorityMethods = Pick<OperationJournal, "create" | "append" | "load" | "submitRequestDigest" | "controlRequestDigest" | "evidenceDigest" | "handleFromState" | "validateHandle">;
9
+ export type OperationJournalAuthority = {
10
+ [Method in keyof AuthorityMethods]: (...args: Parameters<AuthorityMethods[Method]>) => ReturnType<AuthorityMethods[Method]> | Promise<Awaited<ReturnType<AuthorityMethods[Method]>>>;
11
+ };
12
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { OperationJournalAuthority } from "./journal-authority.js";
2
+ import { type JournalRpcDescriptor } from "./journal-rpc-protocol.js";
3
+ export type { JournalRpcDescriptor } from "./journal-rpc-protocol.js";
4
+ export type JournalRpcClientOptions = Pick<JournalRpcDescriptor, "directory" | "instanceId" | "token"> & {
5
+ timeoutMs?: number;
6
+ };
7
+ export declare function readJournalRpcDescriptor(descriptorPath: string): Promise<JournalRpcDescriptor>;
8
+ export declare function createJournalRpcClientFromDescriptor(descriptorPath: string, options?: {
9
+ timeoutMs?: number;
10
+ }): Promise<OperationJournalAuthority>;
11
+ /** Explicit private-filesystem transport. There is no subprocess or network fallback. */
12
+ export declare function createJournalRpcClient(options: JournalRpcClientOptions): OperationJournalAuthority;
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { unlink } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { OperationJournalError } from "./journal.js";
5
+ import { assertOperationEventShape, assertOperationStateShape } from "./state-machine.js";
6
+ import { OPERATION_EVENT_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION } from "./types.js";
7
+ import { JOURNAL_RPC_DESCRIPTOR_VERSION, JOURNAL_RPC_DIGEST, assertJournalRpcPlatform, assertPrivateDirectory, exactRecord, isMissing, openRpcValue, publishPrivateFile, readPrivateFile, rpcError, rpcSleep, rpcTimeout, sealRpcValue, validateDescriptor } from "./journal-rpc-protocol.js";
8
+ const SAFE_REMOTE_CODES = new Set([
9
+ "operation_receipt_expired", "operation_compacted", "creation_event_required", "duplicate_operation_created", "journal_corrupt",
10
+ "revision_conflict", "operation_not_found", "operation_binding_mismatch", "operation_request_mismatch", "operation_tombstoned",
11
+ "journal_quota_exceeded", "journal_lock_timeout", "lock_timeout", "journal_snapshot_corrupt",
12
+ "journal_record_corrupt", "journal_state_corrupt", "journal_quota_counter_corrupt", "journal_scan_limit",
13
+ "operation_handle_mismatch", "operation_handle_ahead", "operation_handle_state_mismatch", "operation_handle_target_mismatch",
14
+ "invalid_operation_handle", "invalid_operation_request", "invalid_expected_revision", "invalid_operation_event",
15
+ "invalid_evidence_domain", "invalid_evidence_material", "evidence_material_too_large",
16
+ "journal_rpc_protocol_error", "journal_rpc_unavailable", "journal_rpc_outcome_indeterminate", "journal_rpc_request_rejected"
17
+ ]);
18
+ export async function readJournalRpcDescriptor(descriptorPath) {
19
+ assertJournalRpcPlatform();
20
+ try {
21
+ await assertPrivateDirectory(dirname(descriptorPath));
22
+ const descriptor = validateDescriptor(JSON.parse(await readPrivateFile(descriptorPath, 4096)));
23
+ await assertPrivateDirectory(descriptor.directory);
24
+ await assertPrivateDirectory(join(descriptor.directory, "requests"));
25
+ await assertPrivateDirectory(join(descriptor.directory, "responses"));
26
+ return descriptor;
27
+ }
28
+ catch {
29
+ throw rpcError("journal_rpc_unavailable");
30
+ }
31
+ }
32
+ export async function createJournalRpcClientFromDescriptor(descriptorPath, options = {}) {
33
+ return createJournalRpcClient({ ...await readJournalRpcDescriptor(descriptorPath), ...options });
34
+ }
35
+ /** Explicit private-filesystem transport. There is no subprocess or network fallback. */
36
+ export function createJournalRpcClient(options) {
37
+ assertJournalRpcPlatform();
38
+ const descriptor = validateDescriptor({
39
+ schemaVersion: JOURNAL_RPC_DESCRIPTOR_VERSION, transport: "file",
40
+ directory: options.directory, instanceId: options.instanceId, token: options.token
41
+ });
42
+ const timeoutMs = rpcTimeout(options.timeoutMs);
43
+ const requests = join(descriptor.directory, "requests");
44
+ const responses = join(descriptor.directory, "responses");
45
+ // Backpressure precedes serialization/publication, including concurrent digest
46
+ // requests for large file manifests. Waiting calls never create mailbox files.
47
+ let active = 0;
48
+ const waiting = [];
49
+ async function acquire(deadlineAt) {
50
+ if (Date.now() >= deadlineAt)
51
+ throw rpcError("journal_rpc_unavailable");
52
+ if (active < 4) {
53
+ active += 1;
54
+ return;
55
+ }
56
+ if (waiting.length >= 1024)
57
+ throw rpcError("journal_rpc_limit_exceeded");
58
+ await new Promise((resolve, reject) => {
59
+ const waiter = {
60
+ deadlineAt, resolve, reject,
61
+ timer: setTimeout(() => {
62
+ const index = waiting.indexOf(waiter);
63
+ if (index < 0)
64
+ return;
65
+ waiting.splice(index, 1);
66
+ reject(rpcError("journal_rpc_unavailable"));
67
+ }, Math.max(1, deadlineAt - Date.now()))
68
+ };
69
+ waiting.push(waiter);
70
+ });
71
+ }
72
+ function release() {
73
+ while (waiting.length > 0) {
74
+ const next = waiting.shift();
75
+ clearTimeout(next.timer);
76
+ // The release microtask may run before an overdue queue timer.
77
+ if (Date.now() >= next.deadlineAt) {
78
+ next.reject(rpcError("journal_rpc_unavailable"));
79
+ continue;
80
+ }
81
+ next.resolve();
82
+ return;
83
+ }
84
+ active -= 1;
85
+ }
86
+ async function call(method, args) {
87
+ const issuedAt = Date.now();
88
+ const expiresAt = issuedAt + timeoutMs;
89
+ await acquire(expiresAt);
90
+ try {
91
+ return await dispatch(method, args, issuedAt, expiresAt);
92
+ }
93
+ finally {
94
+ release();
95
+ }
96
+ }
97
+ async function dispatch(method, args, issuedAt, expiresAt) {
98
+ const id = randomUUID();
99
+ let published = false;
100
+ let remoteError;
101
+ try {
102
+ if (Date.now() >= expiresAt)
103
+ throw rpcError("journal_rpc_unavailable");
104
+ const request = sealRpcValue(descriptor, id, "request", { method, args, issuedAt, expiresAt });
105
+ await assertPrivateDirectory(descriptor.directory);
106
+ await assertPrivateDirectory(requests);
107
+ await assertPrivateDirectory(responses);
108
+ if (Date.now() >= expiresAt)
109
+ throw rpcError("journal_rpc_unavailable");
110
+ // Once publication is attempted, a filesystem error cannot prove that
111
+ // the final link was never visible to the authority.
112
+ published = true;
113
+ await publishPrivateFile(requests, `${id}.request.json`, request);
114
+ do {
115
+ try {
116
+ const wire = await readPrivateFile(join(responses, `${id}.response.json`));
117
+ const value = openRpcValue(descriptor, id, "response", wire);
118
+ if (typeof value !== "object" || value === null || !("ok" in value))
119
+ throw rpcError("journal_rpc_protocol_error");
120
+ const record = value;
121
+ if (record.ok === true) {
122
+ exactRecord(record, ["ok", "result"]);
123
+ return record.result;
124
+ }
125
+ exactRecord(record, ["ok", "error"]);
126
+ exactRecord(record.error, ["code"]);
127
+ if (record.ok !== false || typeof record.error.code !== "string" || !SAFE_REMOTE_CODES.has(record.error.code)) {
128
+ throw rpcError("journal_rpc_protocol_error");
129
+ }
130
+ remoteError = rpcError(record.error.code);
131
+ throw remoteError;
132
+ }
133
+ catch (error) {
134
+ if (!isMissing(error) && !(error instanceof OperationJournalError && error.code === "journal_rpc_file_pending"))
135
+ throw error;
136
+ }
137
+ if (Date.now() >= expiresAt)
138
+ break;
139
+ await rpcSleep(Math.min(10, expiresAt - Date.now()));
140
+ } while (Date.now() <= expiresAt);
141
+ throw rpcError("journal_rpc_unavailable");
142
+ }
143
+ catch (error) {
144
+ if (remoteError !== undefined)
145
+ throw remoteError;
146
+ if (published && (method === "create" || method === "append"))
147
+ throw rpcError("journal_rpc_outcome_indeterminate");
148
+ if (error instanceof OperationJournalError)
149
+ throw error;
150
+ throw rpcError("journal_rpc_unavailable");
151
+ }
152
+ finally {
153
+ // Unclaimed expired requests cannot execute later. Admitted mutations
154
+ // may still finish; their timeout is indeterminate and never auto-retried.
155
+ await unlink(join(requests, `${id}.request.json`)).catch(() => undefined);
156
+ await unlink(join(responses, `${id}.response.json`)).catch(() => undefined);
157
+ }
158
+ }
159
+ async function checked(method, args, validate) {
160
+ const value = await call(method, args);
161
+ try {
162
+ return validate(value);
163
+ }
164
+ catch {
165
+ throw rpcError(method === "create" || method === "append" ? "journal_rpc_outcome_indeterminate" : "journal_rpc_protocol_error");
166
+ }
167
+ }
168
+ return {
169
+ create: async (event) => checked("create", [event], value => loaded(value, event.operationId, event.requestDigest)),
170
+ append: async (operationId, revision, event) => checked("append", [operationId, revision, event], value => loaded(value, operationId)),
171
+ load: async (operationId, digest) => checked("load", [operationId, digest], value => loaded(value, operationId, digest)),
172
+ submitRequestDigest: async (request, files) => checked("submitRequestDigest", [request, files], digest),
173
+ controlRequestDigest: async (request) => checked("controlRequestDigest", [request], digest),
174
+ evidenceDigest: async (domain, material) => checked("evidenceDigest", [domain, material], digest),
175
+ handleFromState: async (state) => checked("handleFromState", [state], value => handle(value, state)),
176
+ validateHandle: async (candidate, state) => checked("validateHandle", [candidate, state], result => {
177
+ exactRecord(result, ["stale", "current"]);
178
+ if (typeof result.stale !== "boolean" || result.stale !== (candidate.revision < state.revision))
179
+ throw rpcError("journal_rpc_protocol_error");
180
+ return { stale: result.stale, current: handle(result.current, state) };
181
+ })
182
+ };
183
+ }
184
+ function digest(value) {
185
+ if (typeof value !== "string" || !JOURNAL_RPC_DIGEST.test(value))
186
+ throw rpcError("journal_rpc_protocol_error");
187
+ return value;
188
+ }
189
+ function handle(value, state) {
190
+ const required = ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary"];
191
+ const hasTarget = typeof value === "object" && value !== null && Object.hasOwn(value, "targetBindingDigest");
192
+ exactRecord(value, hasTarget ? [...required, "targetBindingDigest"] : required);
193
+ if (value.schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION || value.operationId !== state.operationId
194
+ || value.requestDigest !== state.requestDigest || value.surface !== state.surface || value.revision !== state.revision
195
+ || value.phase !== state.phase || value.mutationBoundary !== state.mutationBoundary
196
+ || (state.target !== undefined && !hasTarget))
197
+ throw rpcError("journal_rpc_protocol_error");
198
+ digest(value.requestDigest);
199
+ if (hasTarget)
200
+ digest(value.targetBindingDigest);
201
+ return value;
202
+ }
203
+ function loaded(value, operationId, expectedDigest) {
204
+ const keys = ["state", "envelopes", "committedBytes", "partialTailBytes"];
205
+ const hasLast = typeof value === "object" && value !== null && Object.hasOwn(value, "lastEventDigest");
206
+ exactRecord(value, hasLast ? [...keys, "lastEventDigest"] : keys);
207
+ assertOperationStateShape(value.state);
208
+ if (value.state.operationId !== operationId || (expectedDigest !== undefined && value.state.requestDigest !== expectedDigest)
209
+ || !Array.isArray(value.envelopes) || value.envelopes.length > 32_768
210
+ || !Number.isSafeInteger(value.committedBytes) || value.committedBytes < 0
211
+ || !Number.isSafeInteger(value.partialTailBytes) || value.partialTailBytes < 0)
212
+ throw rpcError("journal_rpc_protocol_error");
213
+ for (const envelope of value.envelopes) {
214
+ exactRecord(envelope, ["schemaVersion", "revision", "previousEventDigest", "eventDigest", "event"]);
215
+ if (envelope.schemaVersion !== OPERATION_EVENT_SCHEMA_VERSION || !Number.isSafeInteger(envelope.revision)
216
+ || envelope.revision < 1)
217
+ throw rpcError("journal_rpc_protocol_error");
218
+ digest(envelope.previousEventDigest);
219
+ digest(envelope.eventDigest);
220
+ assertOperationEventShape(envelope.event);
221
+ }
222
+ if (hasLast)
223
+ digest(value.lastEventDigest);
224
+ return value;
225
+ }
@@ -0,0 +1,32 @@
1
+ import { OperationJournalError } from "./journal.js";
2
+ export declare const JOURNAL_RPC_DESCRIPTOR_VERSION: "chatgpt.journal_rpc.descriptor.v1";
3
+ export declare const JOURNAL_RPC_VERSION: "chatgpt.journal_rpc.v1";
4
+ export declare const JOURNAL_RPC_MAX_WIRE_BYTES: number;
5
+ export declare const JOURNAL_RPC_MAX_PLAINTEXT_BYTES: number;
6
+ export declare const JOURNAL_RPC_MAX_TIMEOUT_MS = 30000;
7
+ export declare const JOURNAL_RPC_UUID: RegExp;
8
+ export declare const JOURNAL_RPC_DIGEST: RegExp;
9
+ export type JournalRpcDescriptor = {
10
+ schemaVersion: typeof JOURNAL_RPC_DESCRIPTOR_VERSION;
11
+ transport: "file";
12
+ directory: string;
13
+ instanceId: string;
14
+ token: string;
15
+ };
16
+ export type JournalRpcDirection = "request" | "response";
17
+ export type JournalRpcMethod = "create" | "append" | "load" | "submitRequestDigest" | "controlRequestDigest" | "evidenceDigest" | "handleFromState" | "validateHandle";
18
+ export declare const JOURNAL_RPC_METHODS: Set<JournalRpcMethod>;
19
+ export declare function rpcError(code: string): OperationJournalError;
20
+ /** POSIX ownership/mode checks cannot establish private Windows ACLs. */
21
+ export declare function assertJournalRpcPlatform(): void;
22
+ export declare function exactRecord(value: unknown, keys: readonly string[]): asserts value is Record<string, unknown>;
23
+ export declare function validateDescriptor(value: unknown): JournalRpcDescriptor;
24
+ export declare function rpcTimeout(value: number | undefined): number;
25
+ export declare function sealRpcValue(descriptor: JournalRpcDescriptor, requestId: string, direction: JournalRpcDirection, value: unknown): string;
26
+ export declare function openRpcValue(descriptor: JournalRpcDescriptor, requestId: string, direction: JournalRpcDirection, wire: string): unknown;
27
+ export declare function assertPrivateDirectory(directory: string): Promise<void>;
28
+ export declare function readPrivateFile(path: string, maxBytes?: number): Promise<string>;
29
+ /** Exclusive final publication; readers can never observe a partial body. */
30
+ export declare function publishPrivateFile(directory: string, name: string, body: string): Promise<void>;
31
+ export declare function isMissing(error: unknown): boolean;
32
+ export declare const rpcSleep: (milliseconds: number) => Promise<void>;
@@ -0,0 +1,214 @@
1
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, realpath, link, unlink } from "node:fs/promises";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { platform, userInfo } from "node:os";
6
+ import { canonicalJson } from "./canonical.js";
7
+ import { OperationJournalError } from "./journal.js";
8
+ export const JOURNAL_RPC_DESCRIPTOR_VERSION = "chatgpt.journal_rpc.descriptor.v1";
9
+ export const JOURNAL_RPC_VERSION = "chatgpt.journal_rpc.v1";
10
+ export const JOURNAL_RPC_MAX_WIRE_BYTES = 24 * 1024 * 1024;
11
+ export const JOURNAL_RPC_MAX_PLAINTEXT_BYTES = 16 * 1024 * 1024;
12
+ export const JOURNAL_RPC_MAX_TIMEOUT_MS = 30_000;
13
+ export const JOURNAL_RPC_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
14
+ export const JOURNAL_RPC_DIGEST = /^hmac-sha256:[0-9a-f]{64}$/u;
15
+ const TOKEN = /^[A-Za-z0-9_-]{43}$/u;
16
+ const WIRE_KEYS = ["version", "instanceId", "requestId", "direction", "nonce", "body", "tag"];
17
+ export const JOURNAL_RPC_METHODS = new Set([
18
+ "create", "append", "load", "submitRequestDigest", "controlRequestDigest", "evidenceDigest", "handleFromState", "validateHandle"
19
+ ]);
20
+ export function rpcError(code) {
21
+ return new OperationJournalError(code, `Journal authority request failed (${code}).`);
22
+ }
23
+ /** POSIX ownership/mode checks cannot establish private Windows ACLs. */
24
+ export function assertJournalRpcPlatform() {
25
+ if (platform() === "win32")
26
+ throw rpcError("journal_rpc_unsupported_platform");
27
+ }
28
+ export function exactRecord(value, keys) {
29
+ if (typeof value !== "object" || value === null || Array.isArray(value)
30
+ || Object.keys(value).length !== keys.length || keys.some(key => !Object.hasOwn(value, key))) {
31
+ throw rpcError("journal_rpc_protocol_error");
32
+ }
33
+ }
34
+ export function validateDescriptor(value) {
35
+ exactRecord(value, ["schemaVersion", "transport", "directory", "instanceId", "token"]);
36
+ if (value.schemaVersion !== JOURNAL_RPC_DESCRIPTOR_VERSION || value.transport !== "file"
37
+ || typeof value.directory !== "string" || !isAbsolute(value.directory) || resolve(value.directory) !== value.directory
38
+ || typeof value.instanceId !== "string" || !JOURNAL_RPC_UUID.test(value.instanceId)
39
+ || typeof value.token !== "string" || !TOKEN.test(value.token)
40
+ || Buffer.from(value.token, "base64url").toString("base64url") !== value.token) {
41
+ throw rpcError("journal_rpc_protocol_error");
42
+ }
43
+ return value;
44
+ }
45
+ export function rpcTimeout(value) {
46
+ const timeout = value ?? JOURNAL_RPC_MAX_TIMEOUT_MS;
47
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > JOURNAL_RPC_MAX_TIMEOUT_MS) {
48
+ throw rpcError("journal_rpc_limit_exceeded");
49
+ }
50
+ return timeout;
51
+ }
52
+ export function sealRpcValue(descriptor, requestId, direction, value) {
53
+ const plaintext = canonicalJson(value);
54
+ if (Buffer.byteLength(plaintext) > JOURNAL_RPC_MAX_PLAINTEXT_BYTES)
55
+ throw rpcError("journal_rpc_limit_exceeded");
56
+ const nonce = randomBytes(12);
57
+ const cipher = createCipheriv("aes-256-gcm", directionKey(descriptor.token, direction), nonce);
58
+ cipher.setAAD(aad(descriptor.instanceId, requestId, direction));
59
+ const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
60
+ const wire = JSON.stringify({
61
+ version: JOURNAL_RPC_VERSION,
62
+ instanceId: descriptor.instanceId,
63
+ requestId,
64
+ direction,
65
+ nonce: nonce.toString("base64"),
66
+ body: body.toString("base64"),
67
+ tag: cipher.getAuthTag().toString("base64")
68
+ });
69
+ if (Buffer.byteLength(wire) > JOURNAL_RPC_MAX_WIRE_BYTES)
70
+ throw rpcError("journal_rpc_limit_exceeded");
71
+ return wire;
72
+ }
73
+ export function openRpcValue(descriptor, requestId, direction, wire) {
74
+ try {
75
+ if (Buffer.byteLength(wire) > JOURNAL_RPC_MAX_WIRE_BYTES)
76
+ throw new Error();
77
+ const envelope = JSON.parse(wire);
78
+ exactRecord(envelope, WIRE_KEYS);
79
+ if (envelope.version !== JOURNAL_RPC_VERSION || envelope.instanceId !== descriptor.instanceId
80
+ || envelope.requestId !== requestId || envelope.direction !== direction)
81
+ throw new Error();
82
+ const nonce = base64(envelope.nonce, 12);
83
+ const tag = base64(envelope.tag, 16);
84
+ const body = base64(envelope.body);
85
+ const decipher = createDecipheriv("aes-256-gcm", directionKey(descriptor.token, direction), nonce);
86
+ decipher.setAAD(aad(descriptor.instanceId, requestId, direction));
87
+ decipher.setAuthTag(tag);
88
+ const plaintext = Buffer.concat([decipher.update(body), decipher.final()]);
89
+ if (plaintext.length > JOURNAL_RPC_MAX_PLAINTEXT_BYTES)
90
+ throw new Error();
91
+ const text = plaintext.toString("utf8");
92
+ const value = decodeCanonical(JSON.parse(text));
93
+ // Reject lossy UTF-8, ambiguous tags, noncanonical numbers and encodings.
94
+ if (canonicalJson(value) !== text)
95
+ throw new Error();
96
+ return value;
97
+ }
98
+ catch {
99
+ throw rpcError("journal_rpc_authentication_failed");
100
+ }
101
+ }
102
+ function directionKey(token, direction) {
103
+ return createHmac("sha256", Buffer.from(token, "base64url"))
104
+ .update(`chatgpt/journal-rpc/${direction}/v1`).digest();
105
+ }
106
+ function aad(instanceId, requestId, direction) {
107
+ return Buffer.from(`${JOURNAL_RPC_VERSION}\0${instanceId}\0${requestId}\0${direction}`);
108
+ }
109
+ function base64(value, length) {
110
+ if (typeof value !== "string" || value.length > JOURNAL_RPC_MAX_WIRE_BYTES)
111
+ throw new Error();
112
+ const bytes = Buffer.from(value, "base64");
113
+ if (bytes.toString("base64") !== value || (length !== undefined && bytes.length !== length))
114
+ throw new Error();
115
+ return bytes;
116
+ }
117
+ function decodeCanonical(value, depth = 0, budget = { nodes: 0 }) {
118
+ budget.nodes += 1;
119
+ if (depth > 32 || budget.nodes > 100_000)
120
+ throw new Error();
121
+ if (value === null || typeof value !== "object")
122
+ return value;
123
+ if (Array.isArray(value)) {
124
+ if (value.length > 32_768)
125
+ throw new Error();
126
+ return value.map(item => decodeCanonical(item, depth + 1, budget));
127
+ }
128
+ const record = value;
129
+ const keys = Object.keys(record);
130
+ if (keys.length > 32_768)
131
+ throw new Error();
132
+ if (keys.length === 1 && record.$undefined === true)
133
+ return undefined;
134
+ if (keys.length === 1 && typeof record.$date === "string") {
135
+ const date = new Date(record.$date);
136
+ if (date.toISOString() !== record.$date)
137
+ throw new Error();
138
+ return date;
139
+ }
140
+ if (keys.length === 1 && typeof record.$bytes === "string")
141
+ return new Uint8Array(base64(record.$bytes));
142
+ const decoded = Object.create(null);
143
+ for (const key of keys) {
144
+ if (["$undefined", "$date", "$bytes"].includes(key))
145
+ throw new Error();
146
+ decoded[key] = decodeCanonical(record[key], depth + 1, budget);
147
+ }
148
+ return decoded;
149
+ }
150
+ export async function assertPrivateDirectory(directory) {
151
+ assertJournalRpcPlatform();
152
+ const info = await lstat(directory);
153
+ const uid = userInfo().uid;
154
+ if (!isAbsolute(directory) || resolve(directory) !== directory || await realpath(directory) !== directory
155
+ || !info.isDirectory() || info.isSymbolicLink() || uid < 0 || info.uid !== uid || (info.mode & 0o077) !== 0) {
156
+ throw rpcError("journal_rpc_unavailable");
157
+ }
158
+ }
159
+ export async function readPrivateFile(path, maxBytes = JOURNAL_RPC_MAX_WIRE_BYTES) {
160
+ assertJournalRpcPlatform();
161
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
162
+ try {
163
+ const info = await file.stat();
164
+ if (info.nlink === 2)
165
+ throw rpcError("journal_rpc_file_pending");
166
+ if (!info.isFile() || info.nlink !== 1 || info.uid !== userInfo().uid || (info.mode & 0o077) !== 0
167
+ || info.size < 0 || info.size > maxBytes)
168
+ throw rpcError("journal_rpc_limit_exceeded");
169
+ // A bounded buffer avoids reading an unbounded concurrently growing file.
170
+ const bytes = Buffer.alloc(info.size + 1);
171
+ let offset = 0;
172
+ while (offset < bytes.length) {
173
+ const read = await file.read(bytes, offset, bytes.length - offset, offset);
174
+ if (read.bytesRead === 0)
175
+ break;
176
+ offset += read.bytesRead;
177
+ }
178
+ const after = await file.stat();
179
+ if (offset !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs
180
+ || after.ctimeMs !== info.ctimeMs || after.nlink !== 1)
181
+ throw rpcError("journal_rpc_protocol_error");
182
+ return bytes.subarray(0, offset).toString("utf8");
183
+ }
184
+ finally {
185
+ await file.close();
186
+ }
187
+ }
188
+ /** Exclusive final publication; readers can never observe a partial body. */
189
+ export async function publishPrivateFile(directory, name, body) {
190
+ assertJournalRpcPlatform();
191
+ if (!/^[a-zA-Z0-9.-]+$/u.test(name))
192
+ throw rpcError("journal_rpc_protocol_error");
193
+ await assertPrivateDirectory(directory);
194
+ const finalPath = join(directory, name);
195
+ const temporary = join(directory, `.pending-${randomBytes(16).toString("hex")}`);
196
+ const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
197
+ try {
198
+ try {
199
+ await file.writeFile(body, "utf8");
200
+ await file.sync();
201
+ }
202
+ finally {
203
+ await file.close();
204
+ }
205
+ await link(temporary, finalPath);
206
+ }
207
+ finally {
208
+ await unlink(temporary).catch(() => undefined);
209
+ }
210
+ }
211
+ export function isMissing(error) {
212
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
213
+ }
214
+ export const rpcSleep = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds));
@@ -0,0 +1,15 @@
1
+ export type JournalRpcServerOptions = {
2
+ stateRoot: string;
3
+ /** A new session directory; existing directories are never reused or overwritten. */
4
+ directory: string;
5
+ pollIntervalMs?: number;
6
+ maxConcurrent?: number;
7
+ maxPendingEntries?: number;
8
+ maxRecentRequests?: number;
9
+ callTimeoutMs?: number;
10
+ };
11
+ export type JournalRpcServer = {
12
+ descriptorPath: string;
13
+ close(): Promise<void>;
14
+ };
15
+ export declare function startJournalRpcServer(options: JournalRpcServerOptions): Promise<JournalRpcServer>;