harnery 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/commander.js +3 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +9 -1
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +113 -12
  6. package/dist/commands/browse.d.ts +164 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +263 -51
  9. package/dist/commands/claude-desktop.d.ts.map +1 -1
  10. package/dist/commands/claude-desktop.js +52 -0
  11. package/dist/commands/disk.d.ts +6 -0
  12. package/dist/commands/disk.d.ts.map +1 -0
  13. package/dist/commands/disk.js +80 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +48 -1
  17. package/dist/commands/files.d.ts +16 -0
  18. package/dist/commands/files.d.ts.map +1 -1
  19. package/dist/commands/files.js +54 -2
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +43 -9
  23. package/dist/commands/rm.d.ts +4 -0
  24. package/dist/commands/rm.d.ts.map +1 -0
  25. package/dist/commands/rm.js +87 -0
  26. package/dist/commands/servers.d.ts +7 -0
  27. package/dist/commands/servers.d.ts.map +1 -0
  28. package/dist/commands/servers.js +394 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +82 -2
  31. package/dist/core/adapters/profiles.d.ts +9 -0
  32. package/dist/core/adapters/profiles.d.ts.map +1 -1
  33. package/dist/core/adapters/profiles.js +22 -1
  34. package/dist/core/adapters/types.d.ts +9 -0
  35. package/dist/core/adapters/types.d.ts.map +1 -1
  36. package/dist/core/agents/cli.js +13 -6
  37. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/session-context.js +2 -0
  39. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +60 -0
  42. package/dist/core/artifacts/delivery-card.js +9 -2
  43. package/dist/core/artifacts/index.d.ts +140 -4
  44. package/dist/core/artifacts/index.d.ts.map +1 -1
  45. package/dist/core/artifacts/index.js +522 -49
  46. package/dist/core/config.d.ts +68 -11
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +128 -3
  49. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  50. package/dist/core/diagnostics/bundle.js +2 -2
  51. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  52. package/dist/core/events/v3/producers/recorder.js +16 -0
  53. package/dist/core/events/v3/projection.js +9 -1
  54. package/dist/core/hooks/adapter/events.d.ts +17 -0
  55. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  56. package/dist/core/hooks/adapter/events.js +5 -0
  57. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  58. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  59. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  60. package/dist/core/hooks/adapter/wiring.js +62 -4
  61. package/dist/core/hooks/cli.js +45 -6
  62. package/dist/core/hooks/run-markers.d.ts +37 -0
  63. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  64. package/dist/core/hooks/run-markers.js +109 -0
  65. package/dist/core/servers/index.d.ts +211 -0
  66. package/dist/core/servers/index.d.ts.map +1 -0
  67. package/dist/core/servers/index.js +560 -0
  68. package/dist/core/servers/net.d.ts +58 -0
  69. package/dist/core/servers/net.d.ts.map +1 -0
  70. package/dist/core/servers/net.js +270 -0
  71. package/dist/core/servers/tunnels.d.ts +14 -0
  72. package/dist/core/servers/tunnels.d.ts.map +1 -0
  73. package/dist/core/servers/tunnels.js +33 -0
  74. package/dist/core/storage/builtins.js +1 -0
  75. package/dist/core/workflow/engine.d.ts.map +1 -1
  76. package/dist/core/workflow/engine.js +30 -1
  77. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  78. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  79. package/dist/core/workflow/sandbox-projection.js +23 -0
  80. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  81. package/dist/core/workflow/spawn-claude.js +6 -1
  82. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  83. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  84. package/dist/core/workflow/spawn-codex.js +20 -1
  85. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  86. package/dist/core/workflow/spawn-cursor.js +6 -1
  87. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  88. package/dist/core/workflow/spawn-opencode.js +4 -1
  89. package/dist/core/workflow/types.d.ts +47 -2
  90. package/dist/core/workflow/types.d.ts.map +1 -1
  91. package/dist/core/workflow/worker-access.d.ts +92 -0
  92. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  93. package/dist/core/workflow/worker-access.js +450 -0
  94. package/dist/lib/browser/batch-steps.d.ts +18 -0
  95. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  96. package/dist/lib/browser/batch-steps.js +82 -0
  97. package/dist/lib/browser/collect/collector.d.ts +99 -0
  98. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  99. package/dist/lib/browser/collect/collector.js +202 -0
  100. package/dist/lib/browser/collect/fields.d.ts +31 -0
  101. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  102. package/dist/lib/browser/collect/fields.js +75 -0
  103. package/dist/lib/browser/collect/index.d.ts +6 -0
  104. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  105. package/dist/lib/browser/collect/index.js +5 -0
  106. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  107. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  108. package/dist/lib/browser/collect/page-driver.js +306 -0
  109. package/dist/lib/browser/collect/presets.d.ts +52 -0
  110. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  111. package/dist/lib/browser/collect/presets.js +176 -0
  112. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  113. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  114. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  115. package/dist/lib/browser/index.d.ts +1 -1
  116. package/dist/lib/browser/index.d.ts.map +1 -1
  117. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  118. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  119. package/dist/lib/browser/page-review-judge.js +13 -0
  120. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  121. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  122. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  123. package/dist/lib/browser/qa-run.js +1 -0
  124. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  125. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  126. package/dist/lib/browser/windows-chrome.js +179 -0
  127. package/dist/lib/claude-desktop-share.d.ts +136 -0
  128. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  129. package/dist/lib/claude-desktop-share.js +394 -0
  130. package/dist/lib/disk-usage.d.ts +50 -0
  131. package/dist/lib/disk-usage.d.ts.map +1 -0
  132. package/dist/lib/disk-usage.js +303 -0
  133. package/dist/lib/docs-lint.d.ts +6 -0
  134. package/dist/lib/docs-lint.d.ts.map +1 -1
  135. package/dist/lib/docs-lint.js +8 -3
  136. package/dist/lib/guarded-remove.d.ts +34 -0
  137. package/dist/lib/guarded-remove.d.ts.map +1 -0
  138. package/dist/lib/guarded-remove.js +310 -0
  139. package/dist/lib/instructions/templates.js +8 -8
  140. package/dist/lib/tunnel/error-page.d.ts +1 -1
  141. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  142. package/dist/lib/tunnel/error-page.js +19 -8
  143. package/dist/lib/tunnel/gate.js +20 -1
  144. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  145. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  146. package/dist/lib/tunnel/path-scope.js +62 -0
  147. package/dist/lib/tunnel/state.d.ts +5 -0
  148. package/dist/lib/tunnel/state.d.ts.map +1 -1
  149. package/dist/lib/tunnel/state.js +17 -3
  150. package/package.json +6 -1
  151. package/schemas/config.schema.json +91 -0
  152. package/src/commander.ts +19 -0
  153. package/src/commands/agents.ts +14 -0
  154. package/src/commands/artifacts.ts +171 -19
  155. package/src/commands/browse.ts +400 -57
  156. package/src/commands/claude-desktop.ts +61 -0
  157. package/src/commands/disk.ts +114 -0
  158. package/src/commands/doctor.ts +52 -1
  159. package/src/commands/files.ts +93 -2
  160. package/src/commands/init.ts +55 -5
  161. package/src/commands/rm.ts +115 -0
  162. package/src/commands/servers.ts +506 -0
  163. package/src/commands/tunnel.ts +102 -2
  164. package/src/core/adapters/profiles.ts +22 -1
  165. package/src/core/adapters/types.ts +10 -0
  166. package/src/core/agents/cli.ts +14 -6
  167. package/src/core/agents/render/session-context.ts +1 -0
  168. package/src/core/agents/state/live-coordination-view.ts +73 -0
  169. package/src/core/artifacts/delivery-card.ts +10 -2
  170. package/src/core/artifacts/index.ts +697 -44
  171. package/src/core/config.ts +183 -4
  172. package/src/core/diagnostics/bundle.ts +2 -1
  173. package/src/core/events/v3/producers/recorder.ts +16 -0
  174. package/src/core/events/v3/projection.ts +11 -1
  175. package/src/core/hooks/adapter/events.ts +19 -0
  176. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  177. package/src/core/hooks/adapter/wiring.ts +116 -8
  178. package/src/core/hooks/cli.ts +47 -6
  179. package/src/core/hooks/run-markers.ts +132 -0
  180. package/src/core/servers/index.ts +712 -0
  181. package/src/core/servers/net.ts +283 -0
  182. package/src/core/servers/tunnels.ts +39 -0
  183. package/src/core/storage/builtins.ts +1 -0
  184. package/src/core/workflow/engine.ts +38 -1
  185. package/src/core/workflow/sandbox-projection.ts +38 -2
  186. package/src/core/workflow/spawn-claude.ts +5 -1
  187. package/src/core/workflow/spawn-codex.ts +18 -1
  188. package/src/core/workflow/spawn-cursor.ts +5 -1
  189. package/src/core/workflow/spawn-opencode.ts +4 -1
  190. package/src/core/workflow/types.ts +62 -2
  191. package/src/core/workflow/worker-access.ts +525 -0
  192. package/src/lib/browser/batch-steps.ts +79 -0
  193. package/src/lib/browser/collect/collector.ts +306 -0
  194. package/src/lib/browser/collect/fields.ts +87 -0
  195. package/src/lib/browser/collect/index.ts +47 -0
  196. package/src/lib/browser/collect/page-driver.ts +327 -0
  197. package/src/lib/browser/collect/presets.ts +237 -0
  198. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  199. package/src/lib/browser/index.ts +1 -0
  200. package/src/lib/browser/page-review-judge.ts +16 -0
  201. package/src/lib/browser/qa-run-contracts.ts +11 -0
  202. package/src/lib/browser/qa-run.ts +1 -0
  203. package/src/lib/browser/windows-chrome.ts +227 -0
  204. package/src/lib/claude-desktop-share.ts +511 -0
  205. package/src/lib/disk-usage.ts +347 -0
  206. package/src/lib/docs-lint.ts +8 -3
  207. package/src/lib/guarded-remove.ts +376 -0
  208. package/src/lib/instructions/templates.ts +8 -8
  209. package/src/lib/tunnel/error-page.ts +25 -9
  210. package/src/lib/tunnel/gate.ts +22 -1
  211. package/src/lib/tunnel/path-scope.ts +73 -0
  212. package/src/lib/tunnel/state.ts +20 -3
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Decide, per launch, whether a workflow worker runs without its vendor sandbox
3
+ * (ADR 0192).
4
+ *
5
+ * A worker gets full access only when three independent things agree:
6
+ *
7
+ * 1. The host project enabled it. Harnery ships it off.
8
+ * 2. The session that launched the run can itself be shown to have full
9
+ * access. A sandboxed parent never hands more access to a child than it
10
+ * holds, and a parent whose access cannot be established counts as
11
+ * sandboxed.
12
+ * 3. The worker's model and effort meet a floor the host configured for that
13
+ * adapter.
14
+ *
15
+ * Every other case keeps the ordinary sandboxed projection, byte for byte. The
16
+ * decision is recorded on every launch, so a run states what each worker was
17
+ * allowed to do and why.
18
+ */
19
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync, } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { builtinAdapterProfile } from "../adapters/profiles.js";
23
+ export const DISABLED_WORKER_FULL_ACCESS = Object.freeze({
24
+ enabled: false,
25
+ floors: Object.freeze({}),
26
+ });
27
+ function isPlainObject(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ function invalid(message) {
31
+ return { enabled: false, floors: {}, invalid: message };
32
+ }
33
+ /**
34
+ * Validate the raw `workflow.workerFullAccess` config value.
35
+ *
36
+ * Absent means disabled. Anything malformed disables the whole policy and says
37
+ * why, because honoring the valid half of a half-valid trust grant would hand
38
+ * out access the host never cleanly stated.
39
+ */
40
+ export function parseWorkerFullAccessPolicy(raw) {
41
+ if (raw === undefined || raw === null)
42
+ return DISABLED_WORKER_FULL_ACCESS;
43
+ if (!isPlainObject(raw))
44
+ return invalid("workflow.workerFullAccess must be an object");
45
+ for (const key of Object.keys(raw)) {
46
+ if (key !== "enabled" && key !== "floors") {
47
+ return invalid(`workflow.workerFullAccess has unknown key ${JSON.stringify(key)}`);
48
+ }
49
+ }
50
+ if (raw.enabled !== undefined && typeof raw.enabled !== "boolean") {
51
+ return invalid("workflow.workerFullAccess.enabled must be a boolean");
52
+ }
53
+ const floorsRaw = raw.floors ?? {};
54
+ if (!isPlainObject(floorsRaw))
55
+ return invalid("workflow.workerFullAccess.floors must be an object");
56
+ const floors = {};
57
+ for (const [adapter, entries] of Object.entries(floorsRaw)) {
58
+ const profile = builtinAdapterProfile(adapter);
59
+ if (!profile) {
60
+ return invalid(`workflow.workerFullAccess.floors names unknown adapter ${JSON.stringify(adapter)}`);
61
+ }
62
+ if (!Array.isArray(entries)) {
63
+ return invalid(`workflow.workerFullAccess.floors.${adapter} must be an array`);
64
+ }
65
+ const parsed = [];
66
+ for (const entry of entries) {
67
+ if (!isPlainObject(entry)) {
68
+ return invalid(`workflow.workerFullAccess.floors.${adapter} entries must be objects`);
69
+ }
70
+ for (const key of Object.keys(entry)) {
71
+ if (key !== "model" && key !== "minEffort") {
72
+ return invalid(`workflow.workerFullAccess.floors.${adapter} entry has unknown key ${JSON.stringify(key)}`);
73
+ }
74
+ }
75
+ const model = typeof entry.model === "string" ? entry.model.trim() : "";
76
+ if (!model) {
77
+ return invalid(`workflow.workerFullAccess.floors.${adapter} entry needs a model id`);
78
+ }
79
+ const minEffort = entry.minEffort;
80
+ if (profile.effortValues.length > 0) {
81
+ if (typeof minEffort !== "string" || !profile.effortValues.includes(minEffort)) {
82
+ return invalid(`workflow.workerFullAccess.floors.${adapter} entry for ${model} needs minEffort, one of: ${profile.effortValues.join(", ")}`);
83
+ }
84
+ parsed.push({ model, minEffort });
85
+ }
86
+ else {
87
+ if (minEffort !== undefined) {
88
+ return invalid(`workflow.workerFullAccess.floors.${adapter} has no effort scale; name the exact model id instead of minEffort`);
89
+ }
90
+ parsed.push({ model });
91
+ }
92
+ }
93
+ floors[adapter] = parsed;
94
+ }
95
+ return { enabled: raw.enabled === true, floors };
96
+ }
97
+ const sandboxed = (reason, detail) => ({
98
+ mode: "sandboxed",
99
+ reason,
100
+ detail,
101
+ });
102
+ /**
103
+ * The access decision for one launch. Conditions are checked from the host's
104
+ * own configuration outward, so the recorded reason names the first thing that
105
+ * kept the sandbox in place.
106
+ */
107
+ export function decideWorkerAccess(input) {
108
+ const { adapter, policy } = input;
109
+ if (policy.invalid) {
110
+ return sandboxed("config_invalid", `worker full access is off: ${policy.invalid}`);
111
+ }
112
+ if (!policy.enabled) {
113
+ return sandboxed("disabled", "the host has not enabled worker full access");
114
+ }
115
+ if (!builtinAdapterProfile(adapter)?.fullAccess) {
116
+ return sandboxed("adapter_unrepresentable", `${adapter} declares no full-access mode`);
117
+ }
118
+ if (input.filesystemPolicy?.mode === "read-only") {
119
+ // A read-only policy is a deliberate constraint on this run's children, not
120
+ // a sandbox limitation to route around.
121
+ return sandboxed("policy_read_only", "the run requested a read-only filesystem policy");
122
+ }
123
+ const floors = policy.floors[adapter] ?? [];
124
+ if (floors.length === 0) {
125
+ return sandboxed("model_not_listed", `no full-access floor is configured for ${adapter}`);
126
+ }
127
+ if (!input.model) {
128
+ return sandboxed("model_unset", "the worker names no model, so the adapter default cannot be checked against a floor");
129
+ }
130
+ const matching = floors.filter((floor) => floor.model === input.model);
131
+ if (matching.length === 0) {
132
+ return sandboxed("model_not_listed", `${input.model} is not on the ${adapter} full-access list`);
133
+ }
134
+ const scale = builtinAdapterProfile(adapter)?.effortValues ?? [];
135
+ if (scale.length > 0) {
136
+ if (!input.effort) {
137
+ return sandboxed("effort_unset", `the worker names no effort, so ${input.model} cannot be checked against its floor`);
138
+ }
139
+ const rank = scale.indexOf(input.effort);
140
+ const floor = matching.find((entry) => entry.minEffort !== undefined && rank >= scale.indexOf(entry.minEffort));
141
+ if (rank < 0 || !floor) {
142
+ const needed = matching.map((entry) => entry.minEffort).join(" or ");
143
+ return sandboxed("effort_below_floor", `${input.model} at effort ${input.effort} is below the ${needed} floor`);
144
+ }
145
+ }
146
+ const parent = input.parent();
147
+ if (parent.state === "sandboxed") {
148
+ return sandboxed("parent_sandboxed", `the launching session is sandboxed: ${parent.detail}`);
149
+ }
150
+ if (parent.state !== "full-access") {
151
+ return sandboxed("parent_unknown", `the launching session's access could not be established: ${parent.detail}`);
152
+ }
153
+ const effortPart = input.effort ? ` at effort ${input.effort}` : "";
154
+ return {
155
+ mode: "full-access",
156
+ reason: "qualified",
157
+ detail: `${input.model}${effortPart} meets the ${adapter} floor and ${parent.detail}`,
158
+ };
159
+ }
160
+ function defaultDeps() {
161
+ return {
162
+ env: process.env,
163
+ platform: process.platform,
164
+ homeDir: homedir(),
165
+ procStatus: () => {
166
+ try {
167
+ return readFileSync("/proc/self/status", "utf8");
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ },
173
+ windowsUserRoots: () => {
174
+ try {
175
+ return readdirSync("/mnt/c/Users").map((name) => join("/mnt/c/Users", name));
176
+ }
177
+ catch {
178
+ return [];
179
+ }
180
+ },
181
+ };
182
+ }
183
+ const nonEmpty = (value) => {
184
+ const trimmed = value?.trim();
185
+ return trimmed ? trimmed : undefined;
186
+ };
187
+ /**
188
+ * Establish whether the session that launched this process runs without a
189
+ * sandbox. Only positive evidence counts.
190
+ *
191
+ * Every adapter session marker in the environment has to prove full access on
192
+ * its own. A nested launch (one agent CLI inside another) inherits both
193
+ * markers, and the outer session's sandbox would still bind the inner one, so
194
+ * one sandboxed or unprovable marker is enough to keep workers sandboxed.
195
+ *
196
+ * - Process confinement is checked first. On Linux, the Codex and Claude Code
197
+ * sandboxes both set `no_new_privs`, and Codex also exports
198
+ * `CODEX_SANDBOX*` variables. Either one means this process is confined.
199
+ * - Codex: the thread's rollout file must show the latest turn ran under the
200
+ * `danger-full-access` sandbox policy.
201
+ * - Claude Code: the session transcript's latest permission mode must be
202
+ * `bypassPermissions`. Claude Code's Bash sandbox is covered by the Linux
203
+ * confinement check; on other platforms it cannot be ruled out, so a Claude
204
+ * Code parent there is unknown.
205
+ * - Cursor and OpenCode record no readable access state, so they are unknown.
206
+ */
207
+ export function detectParentAccess(deps = defaultDeps()) {
208
+ const env = deps.env;
209
+ const markers = [];
210
+ const claudeId = nonEmpty(env.CLAUDE_CODE_SESSION_ID);
211
+ if (claudeId)
212
+ markers.push({ adapter: "claude-code", id: claudeId });
213
+ const codexId = nonEmpty(env.CODEX_THREAD_ID);
214
+ if (codexId)
215
+ markers.push({ adapter: "codex", id: codexId });
216
+ const cursorId = nonEmpty(env.CURSOR_SESSION_ID) ?? nonEmpty(env.CURSOR_CONVERSATION_ID);
217
+ if (cursorId)
218
+ markers.push({ adapter: "cursor", id: cursorId });
219
+ const openCodeId = nonEmpty(env.OPENCODE_SESSION_ID);
220
+ if (openCodeId)
221
+ markers.push({ adapter: "opencode", id: openCodeId });
222
+ const adapters = markers.map((marker) => marker.adapter);
223
+ const result = (verdict) => ({ ...verdict, adapters });
224
+ if (markers.length === 0) {
225
+ return result({ state: "unknown", detail: "no adapter session marker is present" });
226
+ }
227
+ const sandboxVars = Object.keys(env).filter((key) => key.startsWith("CODEX_SANDBOX") && nonEmpty(env[key]));
228
+ if (sandboxVars.length > 0) {
229
+ return result({ state: "sandboxed", detail: `${sandboxVars.join(", ")} is set` });
230
+ }
231
+ let confinement;
232
+ if (deps.platform === "linux") {
233
+ const status = deps.procStatus();
234
+ const match = status?.match(/^NoNewPrivs:\s*(\d)/m);
235
+ if (!match) {
236
+ return result({ state: "unknown", detail: "process confinement could not be read" });
237
+ }
238
+ if (match[1] !== "0") {
239
+ return result({ state: "sandboxed", detail: "this process runs with no_new_privs set" });
240
+ }
241
+ confinement = { state: "full-access", detail: "no_new_privs is clear" };
242
+ }
243
+ else {
244
+ confinement = { state: "unknown", detail: `no confinement probe on ${deps.platform}` };
245
+ }
246
+ const verdicts = [];
247
+ for (const marker of markers) {
248
+ if (marker.adapter === "codex") {
249
+ verdicts.push(codexParentAccess(marker.id, deps));
250
+ }
251
+ else if (marker.adapter === "claude-code") {
252
+ if (confinement.state !== "full-access") {
253
+ verdicts.push({
254
+ state: "unknown",
255
+ detail: `Claude Code's Bash sandbox cannot be ruled out (${confinement.detail})`,
256
+ });
257
+ }
258
+ else {
259
+ verdicts.push(claudeParentAccess(marker.id, deps));
260
+ }
261
+ }
262
+ else {
263
+ verdicts.push({
264
+ state: "unknown",
265
+ detail: `${marker.adapter} records no readable access state`,
266
+ });
267
+ }
268
+ }
269
+ const sandboxedVerdict = verdicts.find((verdict) => verdict.state === "sandboxed");
270
+ if (sandboxedVerdict)
271
+ return result(sandboxedVerdict);
272
+ const unknownVerdict = verdicts.find((verdict) => verdict.state === "unknown");
273
+ if (unknownVerdict)
274
+ return result(unknownVerdict);
275
+ const details = verdicts.map((verdict) => verdict.detail);
276
+ if (confinement.state === "full-access")
277
+ details.push(confinement.detail);
278
+ return result({ state: "full-access", detail: details.join("; ") });
279
+ }
280
+ /** The last line of `path` containing `needle`, reading backwards so a large
281
+ * session log is not loaded whole. Null when absent or past `maxBytes`. */
282
+ export function lastLineContaining(path, needle, maxBytes = 256 * 1024 * 1024) {
283
+ let fd;
284
+ try {
285
+ fd = openSync(path, "r");
286
+ }
287
+ catch {
288
+ return null;
289
+ }
290
+ try {
291
+ const size = statSync(path).size;
292
+ const chunkSize = 1024 * 1024;
293
+ const needleBytes = Buffer.from(needle);
294
+ let position = size;
295
+ let tail = Buffer.alloc(0);
296
+ while (position > 0 && size - position < maxBytes) {
297
+ const length = Math.min(chunkSize, position);
298
+ position -= length;
299
+ const chunk = Buffer.alloc(length);
300
+ readSync(fd, chunk, 0, length, position);
301
+ tail = Buffer.concat([chunk, tail]);
302
+ const hit = tail.lastIndexOf(needleBytes);
303
+ if (hit < 0)
304
+ continue;
305
+ const start = tail.lastIndexOf(0x0a, hit);
306
+ if (start < 0 && position > 0)
307
+ continue; // the line starts in an unread chunk
308
+ const end = tail.indexOf(0x0a, hit);
309
+ return tail.subarray(start + 1, end < 0 ? tail.length : end).toString("utf8");
310
+ }
311
+ return null;
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ finally {
317
+ closeSync(fd);
318
+ }
319
+ }
320
+ /** Candidate UTC dates (YYYY/MM/DD) for a UUIDv7 thread id: its creation day
321
+ * and the days either side, since rollout folders use the creator's local date. */
322
+ function rolloutDays(threadId) {
323
+ const hex = threadId.replace(/-/g, "");
324
+ if (!/^[0-9a-f]{32}$/i.test(hex) || hex[12] !== "7")
325
+ return null;
326
+ const ms = Number.parseInt(hex.slice(0, 12), 16);
327
+ const days = [];
328
+ for (const offset of [0, -1, 1]) {
329
+ const date = new Date(ms + offset * 86_400_000);
330
+ const y = date.getUTCFullYear();
331
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
332
+ const d = String(date.getUTCDate()).padStart(2, "0");
333
+ days.push(`${y}/${m}/${d}`);
334
+ }
335
+ return days;
336
+ }
337
+ function codexHomes(deps) {
338
+ const homes = [];
339
+ const explicit = nonEmpty(deps.env.CODEX_HOME);
340
+ if (explicit)
341
+ homes.push(explicit);
342
+ homes.push(join(deps.homeDir, ".codex"));
343
+ // A Windows Codex session driving a WSL checkout keeps its rollouts in the
344
+ // Windows profile, which the guest sees under /mnt/c/Users.
345
+ if (deps.platform === "linux" && nonEmpty(deps.env.WSL_DISTRO_NAME)) {
346
+ for (const root of deps.windowsUserRoots())
347
+ homes.push(join(root, ".codex"));
348
+ }
349
+ return [...new Set(homes)];
350
+ }
351
+ function findCodexRollout(threadId, deps) {
352
+ const days = rolloutDays(threadId);
353
+ if (!days)
354
+ return null;
355
+ const suffix = `-${threadId}.jsonl`;
356
+ for (const home of codexHomes(deps)) {
357
+ for (const day of days) {
358
+ const dir = join(home, "sessions", day);
359
+ let names;
360
+ try {
361
+ names = readdirSync(dir);
362
+ }
363
+ catch {
364
+ continue;
365
+ }
366
+ const name = names.find((entry) => entry.startsWith("rollout-") && entry.endsWith(suffix));
367
+ if (name)
368
+ return join(dir, name);
369
+ }
370
+ }
371
+ return null;
372
+ }
373
+ function codexParentAccess(threadId, deps) {
374
+ const rollout = findCodexRollout(threadId, deps);
375
+ if (!rollout) {
376
+ return { state: "unknown", detail: `no Codex rollout was found for thread ${threadId}` };
377
+ }
378
+ const line = lastLineContaining(rollout, '"type":"turn_context"');
379
+ if (!line)
380
+ return { state: "unknown", detail: "the Codex rollout records no turn context" };
381
+ let policyType;
382
+ try {
383
+ const record = JSON.parse(line);
384
+ if (record.type !== "turn_context") {
385
+ return { state: "unknown", detail: "the Codex rollout's latest turn context is unreadable" };
386
+ }
387
+ policyType = record.payload?.sandbox_policy?.type;
388
+ }
389
+ catch {
390
+ return { state: "unknown", detail: "the Codex rollout's latest turn context is unreadable" };
391
+ }
392
+ if (typeof policyType !== "string") {
393
+ return { state: "unknown", detail: "the Codex turn context names no sandbox policy" };
394
+ }
395
+ if (policyType === "danger-full-access") {
396
+ return {
397
+ state: "full-access",
398
+ detail: "the Codex parent's latest turn ran danger-full-access",
399
+ };
400
+ }
401
+ return { state: "sandboxed", detail: `the Codex parent's latest turn ran ${policyType}` };
402
+ }
403
+ function findClaudeTranscript(sessionId, deps) {
404
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
405
+ return null;
406
+ const configDir = nonEmpty(deps.env.CLAUDE_CONFIG_DIR) ?? join(deps.homeDir, ".claude");
407
+ const projects = join(configDir, "projects");
408
+ let dirs;
409
+ try {
410
+ dirs = readdirSync(projects);
411
+ }
412
+ catch {
413
+ return null;
414
+ }
415
+ for (const dir of dirs) {
416
+ const candidate = join(projects, dir, `${sessionId}.jsonl`);
417
+ if (existsSync(candidate))
418
+ return candidate;
419
+ }
420
+ return null;
421
+ }
422
+ function claudeParentAccess(sessionId, deps) {
423
+ const transcript = findClaudeTranscript(sessionId, deps);
424
+ if (!transcript) {
425
+ return {
426
+ state: "unknown",
427
+ detail: `no Claude Code transcript was found for session ${sessionId}`,
428
+ };
429
+ }
430
+ const line = lastLineContaining(transcript, '"permissionMode":"');
431
+ if (!line)
432
+ return { state: "unknown", detail: "the Claude Code transcript records no permission mode" };
433
+ let mode;
434
+ try {
435
+ mode = JSON.parse(line).permissionMode;
436
+ }
437
+ catch {
438
+ return {
439
+ state: "unknown",
440
+ detail: "the Claude Code transcript's latest permission mode is unreadable",
441
+ };
442
+ }
443
+ if (mode === "bypassPermissions") {
444
+ return { state: "full-access", detail: "the Claude Code parent runs bypassPermissions" };
445
+ }
446
+ return {
447
+ state: "sandboxed",
448
+ detail: `the Claude Code parent runs ${typeof mode === "string" ? mode : "an unrecognized"} permission mode`,
449
+ };
450
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Split a `browse --batch` string into steps.
3
+ *
4
+ * Steps are separated by `;`. Two rules keep JavaScript intact:
5
+ *
6
+ * 1. A step that starts with the `eval` verb ends only at a `;` that sits
7
+ * outside JavaScript quotes ('...', "...", `...`) and outside (), [] and {}.
8
+ * So `eval (() => { const a = 1; return a })(); wait 500` is two steps.
9
+ * 2. Every other step ends at the next `;`, exactly as before. Selectors and
10
+ * fill values may contain apostrophes or brackets that are not balanced
11
+ * JavaScript, so they get no quote or bracket tracking.
12
+ *
13
+ * `\;` always inserts a literal `;` without ending the step, in any verb.
14
+ * A top-level `;` inside an eval (two bare statements) still ends the step:
15
+ * wrap multiple statements in a block or an arrow function.
16
+ */
17
+ export declare function splitBatchSteps(input: string): string[];
18
+ //# sourceMappingURL=batch-steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch-steps.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/batch-steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8DvD"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Split a `browse --batch` string into steps.
3
+ *
4
+ * Steps are separated by `;`. Two rules keep JavaScript intact:
5
+ *
6
+ * 1. A step that starts with the `eval` verb ends only at a `;` that sits
7
+ * outside JavaScript quotes ('...', "...", `...`) and outside (), [] and {}.
8
+ * So `eval (() => { const a = 1; return a })(); wait 500` is two steps.
9
+ * 2. Every other step ends at the next `;`, exactly as before. Selectors and
10
+ * fill values may contain apostrophes or brackets that are not balanced
11
+ * JavaScript, so they get no quote or bracket tracking.
12
+ *
13
+ * `\;` always inserts a literal `;` without ending the step, in any verb.
14
+ * A top-level `;` inside an eval (two bare statements) still ends the step:
15
+ * wrap multiple statements in a block or an arrow function.
16
+ */
17
+ export function splitBatchSteps(input) {
18
+ const steps = [];
19
+ let buf = "";
20
+ let stepStart = true;
21
+ let jsAware = false;
22
+ let quote = null;
23
+ let depth = 0;
24
+ const endStep = () => {
25
+ if (buf.trim())
26
+ steps.push(buf);
27
+ buf = "";
28
+ stepStart = true;
29
+ jsAware = false;
30
+ quote = null;
31
+ depth = 0;
32
+ };
33
+ for (let i = 0; i < input.length; i++) {
34
+ if (stepStart) {
35
+ // Decide the splitting rule once per step from its leading verb.
36
+ jsAware = /^\s*eval(\s|$)/i.test(input.slice(i));
37
+ stepStart = false;
38
+ }
39
+ const ch = input[i];
40
+ if (ch === "\\" && input[i + 1] === ";") {
41
+ buf += ";";
42
+ i++;
43
+ continue;
44
+ }
45
+ if (jsAware) {
46
+ if (quote) {
47
+ buf += ch;
48
+ if (ch === "\\" && i + 1 < input.length) {
49
+ buf += input[i + 1];
50
+ i++;
51
+ }
52
+ else if (ch === quote) {
53
+ quote = null;
54
+ }
55
+ continue;
56
+ }
57
+ if (ch === "'" || ch === '"' || ch === "`") {
58
+ quote = ch;
59
+ buf += ch;
60
+ continue;
61
+ }
62
+ if (ch === "(" || ch === "[" || ch === "{")
63
+ depth++;
64
+ else if (ch === ")" || ch === "]" || ch === "}")
65
+ depth = Math.max(0, depth - 1);
66
+ if (ch === ";" && depth === 0) {
67
+ endStep();
68
+ continue;
69
+ }
70
+ buf += ch;
71
+ continue;
72
+ }
73
+ if (ch === ";") {
74
+ endStep();
75
+ continue;
76
+ }
77
+ buf += ch;
78
+ }
79
+ if (buf.trim())
80
+ steps.push(buf);
81
+ return steps;
82
+ }
@@ -0,0 +1,99 @@
1
+ import { type Rng, type ScrollPacing, type ScrollStepPlan, type WheelTick } from "./scroll-plan.js";
2
+ /** One item as read from the page in a single harvest. */
3
+ export interface HarvestedItem {
4
+ key: string;
5
+ fields: Record<string, unknown>;
6
+ }
7
+ export interface CollectedItem {
8
+ key: string;
9
+ /** Step index the item was first seen at (0 is the initial harvest). */
10
+ firstSeenStep: number;
11
+ [field: string]: unknown;
12
+ }
13
+ export interface PageMetrics {
14
+ scrollTop: number;
15
+ scrollHeight: number;
16
+ viewportHeight: number;
17
+ atBottom: boolean;
18
+ }
19
+ /** What the collector needs from a page. The Playwright driver implements it. */
20
+ export interface CollectDriver {
21
+ harvest(): Promise<HarvestedItem[]>;
22
+ metrics(): Promise<PageMetrics>;
23
+ /** Scroll by these wheel notches. Returns true when a non-wheel fallback was used. */
24
+ wheel(ticks: WheelTick[]): Promise<boolean>;
25
+ /** Click up to `limit` unexpanded "show more" buttons; returns how many were clicked. */
26
+ expand(limit: number): Promise<number>;
27
+ /** A short reason when the page shows a login wall or rate limit, else null. */
28
+ blocked(): Promise<string | null>;
29
+ sleep(ms: number): Promise<void>;
30
+ now(): number;
31
+ }
32
+ export interface CollectLimits {
33
+ /** Stop once this many unique items are collected (null: no item cap). */
34
+ maxItems: number | null;
35
+ /** Stop after this many scroll steps. */
36
+ maxScrolls: number;
37
+ /** Stop once this much wall time has passed, in ms. */
38
+ maxMs: number;
39
+ /** Stop after this many consecutive steps with no new items and no page growth. */
40
+ idleSteps: number;
41
+ /** Expand clicks per step (0 disables expansion). */
42
+ expandPerStep: number;
43
+ /** Expand clicks per run. */
44
+ expandTotal: number;
45
+ }
46
+ export declare const DEFAULT_COLLECT_LIMITS: CollectLimits;
47
+ export type StopReason = "max-items" | "max-scrolls" | "time-budget" | "idle" | "end-of-feed" | "blocked" | "interrupted";
48
+ export interface CollectStats {
49
+ /** Scroll steps taken (the initial harvest is step 0 and not counted). */
50
+ steps: number;
51
+ harvests: number;
52
+ items: number;
53
+ /** Sightings of an already-collected key across all harvests. */
54
+ duplicates: number;
55
+ expanded: number;
56
+ readingPauses: number;
57
+ scrollBacks: number;
58
+ /** Steps where wheel scrolling did not move the page and a fallback ran. */
59
+ scrollFallbacks: number;
60
+ elapsedMs: number;
61
+ stopReason: StopReason;
62
+ stopDetail: string | null;
63
+ }
64
+ export interface CollectResult {
65
+ items: CollectedItem[];
66
+ stats: CollectStats;
67
+ }
68
+ export interface CollectRunOptions {
69
+ limits: CollectLimits;
70
+ pacing: ScrollPacing;
71
+ rng: Rng;
72
+ /** Polled between and during pauses; true stops the run with "interrupted". */
73
+ shouldStop?: () => boolean;
74
+ onStep?: (info: {
75
+ step: number;
76
+ items: number;
77
+ added: number;
78
+ plan: ScrollStepPlan;
79
+ }) => void;
80
+ }
81
+ /**
82
+ * Ordered, de-duplicated item store. The first sighting fixes an item's
83
+ * position and step; later sightings only fill in what was missing: empty
84
+ * fields, longer text (an expanded "show more"), and new array members.
85
+ */
86
+ export declare class ItemStore {
87
+ private byKey;
88
+ duplicates: number;
89
+ get size(): number;
90
+ add(items: HarvestedItem[], step: number, maxItems?: number | null): number;
91
+ items(): CollectedItem[];
92
+ }
93
+ /**
94
+ * Scroll a feed like a reader and collect every item that passes through the
95
+ * DOM. Each step harvests right after scrolling and again after the pause, so
96
+ * a virtualized list cannot drop an item between two harvests.
97
+ */
98
+ export declare function runCollect(driver: CollectDriver, options: CollectRunOptions): Promise<CollectResult>;
99
+ //# sourceMappingURL=collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../../../src/lib/browser/collect/collector.ts"],"names":[],"mappings":"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"}