@osfactory/har 0.53.0 → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -69193,7 +69193,7 @@ function harnessExists(repoPath) {
69193
69193
  // src/harness/plugins.ts
69194
69194
  init_schema2();
69195
69195
  init_stages();
69196
- var PLUGIN_IDS = ["playwright", "rocketsim", "kerno", "gitleaks", "trivy"];
69196
+ var PLUGIN_IDS = ["playwright", "rocketsim", "kerno", "gitleaks", "trivy", "semgrep"];
69197
69197
  var PluginManifestFileSchema = external_exports.object({
69198
69198
  src: external_exports.string().min(1),
69199
69199
  dest: external_exports.string().min(1),
@@ -70294,7 +70294,7 @@ var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
70294
70294
  var PROFILE_HINTS = {
70295
70295
  default: "Web app profile (SaaS/full-stack) \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Launch provisions toolchain via harness.env (HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD) and writes paths to .env.agent.<id>. Identify the primary app agents modify; run supporting services shared.",
70296
70296
  cli: "CLI/library profile (typical SWE-bench) \u2014 no PM2. Optional Docker Compose via HARNESS_INFRA_SERVICES. Git worktree by default. Launch provisions toolchain declaratively (HARNESS_ECOSYSTEM auto-detects common ecosystems); verify must use resolved tool paths from .env.agent.<id>, never hardcoded interpreter or package-manager paths.",
70297
- ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it. Install RocketSim plugin (har env add-plugin rocketsim) for user-flow validation."
70297
+ ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it."
70298
70298
  };
70299
70299
  function loadTemplate(name) {
70300
70300
  const filePath = resolveTemplateFile(name);
@@ -1,7 +1,7 @@
1
1
  # HAR Stages — authoring guide
2
2
 
3
3
  Stages are the harness's single vocabulary for runnable checks and lifecycle
4
- actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
4
+ actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
5
5
  project's test/lint commands, bespoke validation scripts — registers in
6
6
  `.har/stages.json` with the same schema, and agents interact with stages only
7
7
  through that registry (CLI `har env verify`, MCP `har_run_stage` /
@@ -92,7 +92,8 @@ committed.
92
92
  `har env add-plugin --list` shows available plugins; `har env add-plugin
93
93
  playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
94
94
  add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
95
- stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
95
+ stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
96
+ or `har env add-plugin semgrep` (SAST, any stack)
96
97
  installs one. A
97
98
  plugin is just packaging: it copies files, merges `package.json` fragments,
98
99
  and registers stages through the exact same registry as `add-stage --custom`.
@@ -445,6 +445,35 @@ try {
445
445
  # optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
446
446
  # SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
447
447
  # SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
448
+ # Mission Control re-reads slot state only when something syncs, and a sync takes
449
+ # about twenty seconds — too long to make a slot change wait for it. Detached, and
450
+ # coalesced so a run of slot changes leaves one sync going, then one more if needed.
451
+ har_notify_control() {
452
+ local root bin lock
453
+ root="$(cd "${SCRIPT_DIR}/.." && pwd)"
454
+ bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
455
+ [ -x "$bin" ] || return 0
456
+
457
+ lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
458
+ : > "${lock}.pending"
459
+ mkdir "$lock" 2>/dev/null || return 0
460
+ (
461
+ trap "" HUP
462
+ trap 'rmdir "$lock" 2>/dev/null || true' EXIT
463
+ cd "$root" || exit 0
464
+ while [ -f "${lock}.pending" ]; do
465
+ rm -f "${lock}.pending"
466
+ if command -v timeout >/dev/null 2>&1; then
467
+ timeout 120 "$bin" control sync || true
468
+ else
469
+ "$bin" control sync || true
470
+ fi
471
+ done
472
+ # Redirected as a whole, not per command: a background job that keeps the inherited
473
+ # stdout open makes any caller reading that pipe wait for it.
474
+ ) >/dev/null 2>&1 &
475
+ }
476
+
448
477
  write_slot_registry() {
449
478
  local file
450
479
  file="$(slot_registry_file "$SLOT_AGENT_ID")"
@@ -474,10 +503,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
474
503
  }
475
504
  fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
476
505
  ' "$file"
506
+ har_notify_control
477
507
  }
478
508
 
479
509
  remove_slot_registry() {
480
510
  rm -f "$(slot_registry_file "$1")"
511
+ har_notify_control
481
512
  }
482
513
 
483
514
  # Exit 0 when the worktree has uncommitted or untracked changes.
@@ -1,7 +1,7 @@
1
1
  # HAR Stages — authoring guide
2
2
 
3
3
  Stages are the harness's single vocabulary for runnable checks and lifecycle
4
- actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
4
+ actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
5
5
  project's test/lint commands, bespoke validation scripts — registers in
6
6
  `.har/stages.json` with the same schema, and agents interact with stages only
7
7
  through that registry (CLI `har env verify`, MCP `har_run_stage` /
@@ -92,7 +92,8 @@ committed.
92
92
  `har env add-plugin --list` shows available plugins; `har env add-plugin
93
93
  playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
94
94
  add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
95
- stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
95
+ stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
96
+ or `har env add-plugin semgrep` (SAST, any stack)
96
97
  installs one. A
97
98
  plugin is just packaging: it copies files, merges `package.json` fragments,
98
99
  and registers stages through the exact same registry as `add-stage --custom`.
@@ -427,6 +427,35 @@ try {
427
427
  # optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
428
428
  # SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
429
429
  # SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
430
+ # Mission Control re-reads slot state only when something syncs, and a sync takes
431
+ # about twenty seconds — too long to make a slot change wait for it. Detached, and
432
+ # coalesced so a run of slot changes leaves one sync going, then one more if needed.
433
+ har_notify_control() {
434
+ local root bin lock
435
+ root="$(cd "${SCRIPT_DIR}/.." && pwd)"
436
+ bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
437
+ [ -x "$bin" ] || return 0
438
+
439
+ lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
440
+ : > "${lock}.pending"
441
+ mkdir "$lock" 2>/dev/null || return 0
442
+ (
443
+ trap "" HUP
444
+ trap 'rmdir "$lock" 2>/dev/null || true' EXIT
445
+ cd "$root" || exit 0
446
+ while [ -f "${lock}.pending" ]; do
447
+ rm -f "${lock}.pending"
448
+ if command -v timeout >/dev/null 2>&1; then
449
+ timeout 120 "$bin" control sync || true
450
+ else
451
+ "$bin" control sync || true
452
+ fi
453
+ done
454
+ # Redirected as a whole, not per command: a background job that keeps the inherited
455
+ # stdout open makes any caller reading that pipe wait for it.
456
+ ) >/dev/null 2>&1 &
457
+ }
458
+
430
459
  write_slot_registry() {
431
460
  local file
432
461
  file="$(slot_registry_file "$SLOT_AGENT_ID")"
@@ -456,10 +485,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
456
485
  }
457
486
  fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
458
487
  ' "$file"
488
+ har_notify_control
459
489
  }
460
490
 
461
491
  remove_slot_registry() {
462
492
  rm -f "$(slot_registry_file "$1")"
493
+ har_notify_control
463
494
  }
464
495
 
465
496
  # Exit 0 when the worktree has uncommitted or untracked changes.
@@ -109,6 +109,24 @@ launch fails and lists the models this machine can create.
109
109
  If `HARNESS_SIMULATOR_NAME` is not a model but names an existing device, that
110
110
  device is used as-is and never deleted — how a hand-renamed simulator is targeted.
111
111
 
112
+ ### Running from an agent sandbox
113
+
114
+ `xcrun simctl` reaches CoreSimulatorService over XPC. Coding agents that sandbox
115
+ their shell (Codex, Claude Code and others) usually deny that lookup, so `simctl`
116
+ fails while `xcodebuild` keeps working — every simulator command then dies with
117
+ `CoreSimulatorService connection became invalid`. Nothing is missing on the machine.
118
+
119
+ Launch and teardown say so when it happens. Either run `har env launch <id>` and
120
+ `har env teardown <id>` from a normal terminal and let the agent work in the
121
+ worktree, or grant the agent unsandboxed access to `xcrun simctl` — the escalation
122
+ must cover every subcommand the harness uses (`list`, `create`, `boot`,
123
+ `bootstatus`, `shutdown`, `delete`), not just `list`.
124
+
125
+ The same messages distinguish a second cause: if `xcrun simctl` is missing from
126
+ the selected developer directory — no Xcode, or `xcode-select` pointing at the
127
+ Command Line Tools — they say so and print the current selection instead of
128
+ blaming a sandbox.
129
+
112
130
  The device lands in `.env.agent.<id>` as `HARNESS_SIMULATOR_UDID`,
113
131
  `HARNESS_SIMULATOR_DEVICE_NAME` and `HARNESS_IOS_DESTINATION` — the model stays in
114
132
  `HARNESS_SIMULATOR_NAME`, so the two never mean the same thing in one place; what a slot holds is tracked in `.har/simulators/`.
@@ -1,7 +1,7 @@
1
1
  # HAR Stages — authoring guide
2
2
 
3
3
  Stages are the harness's single vocabulary for runnable checks and lifecycle
4
- actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`), your
4
+ actions. Everything — shipped plugins (`playwright`, `rocketsim`, `kerno`, `gitleaks`, `trivy`, `semgrep`), your
5
5
  project's test/lint commands, bespoke validation scripts — registers in
6
6
  `.har/stages.json` with the same schema, and agents interact with stages only
7
7
  through that registry (CLI `har env verify`, MCP `har_run_stage` /
@@ -92,7 +92,8 @@ committed.
92
92
  `har env add-plugin --list` shows available plugins; `har env add-plugin
93
93
  playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
94
94
  add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
95
- stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack)
95
+ stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
96
+ or `har env add-plugin semgrep` (SAST, any stack)
96
97
  installs one. A
97
98
  plugin is just packaging: it copies files, merges `package.json` fragments,
98
99
  and registers stages through the exact same registry as `add-stage --custom`.
@@ -95,6 +95,35 @@ try {
95
95
  # optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
96
96
  # SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
97
97
  # SLOT_STATUS, SLOT_LAST_ERROR, SLOT_WORK_UNIT_ID, SLOT_ATTEMPT_ID
98
+ # Mission Control re-reads slot state only when something syncs, and a sync takes
99
+ # about twenty seconds — too long to make a slot change wait for it. Detached, and
100
+ # coalesced so a run of slot changes leaves one sync going, then one more if needed.
101
+ har_notify_control() {
102
+ local root bin lock
103
+ root="$(cd "${SCRIPT_DIR}/.." && pwd)"
104
+ bin="$(command -v har 2>/dev/null || echo "${root}/node_modules/.bin/har")"
105
+ [ -x "$bin" ] || return 0
106
+
107
+ lock="${TMPDIR:-/tmp}/har-control-sync.$(id -u)"
108
+ : > "${lock}.pending"
109
+ mkdir "$lock" 2>/dev/null || return 0
110
+ (
111
+ trap "" HUP
112
+ trap 'rmdir "$lock" 2>/dev/null || true' EXIT
113
+ cd "$root" || exit 0
114
+ while [ -f "${lock}.pending" ]; do
115
+ rm -f "${lock}.pending"
116
+ if command -v timeout >/dev/null 2>&1; then
117
+ timeout 120 "$bin" control sync || true
118
+ else
119
+ "$bin" control sync || true
120
+ fi
121
+ done
122
+ # Redirected as a whole, not per command: a background job that keeps the inherited
123
+ # stdout open makes any caller reading that pipe wait for it.
124
+ ) >/dev/null 2>&1 &
125
+ }
126
+
98
127
  write_slot_registry() {
99
128
  local file
100
129
  file="$(slot_registry_file "$SLOT_AGENT_ID")"
@@ -124,10 +153,12 @@ for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PR
124
153
  }
125
154
  fs.writeFileSync(process.argv[1], JSON.stringify(entry, null, 2) + "\n");
126
155
  ' "$file"
156
+ har_notify_control
127
157
  }
128
158
 
129
159
  remove_slot_registry() {
130
160
  rm -f "$(slot_registry_file "$1")"
161
+ har_notify_control
131
162
  }
132
163
 
133
164
  # Exit 0 when the worktree has uncommitted or untracked changes.
@@ -19,6 +19,14 @@ source "$SCRIPT_DIR/simulator.sh"
19
19
 
20
20
  log() { echo "==> $*" >&2; }
21
21
 
22
+ # simctl could not be asked, so nothing it did not return is evidence of anything
23
+ # missing on this machine. The wording lives in simulator.sh so launch and this
24
+ # script cannot drift apart on which of the two causes they name.
25
+ fail_simctl_unavailable() {
26
+ har_sim_log_simctl_unavailable
27
+ exit 1
28
+ }
29
+
22
30
  # ── Xcode check ───────────────────────────────────────────────────────────────
23
31
  if ! command -v xcodebuild >/dev/null 2>&1; then
24
32
  echo "Error: xcodebuild not found — install Xcode from the App Store." >&2
@@ -40,6 +48,8 @@ if har_sim_per_slot_enabled; then
40
48
  PLAN_STATUS="${PLAN%%$'\t'*}"
41
49
  if [ "$PLAN_STATUS" = "OK" ]; then
42
50
  log "Per-slot simulators: launch creates one $(printf '%s' "$PLAN" | cut -f3) per agent."
51
+ elif [ "$PLAN_STATUS" = "SIMCTL_UNAVAILABLE" ]; then
52
+ fail_simctl_unavailable
43
53
  elif [ "$PLAN_STATUS" = "NO_MODEL" ] && [ -n "$(har_sim_device_by_name "${HARNESS_SIMULATOR_NAME:-}")" ]; then
44
54
  # Supported fallback: the name is an existing device rather than a model.
45
55
  log "Per-slot simulators: launch reuses the existing device '${SIMULATOR_NAME}'."
@@ -63,6 +73,7 @@ else
63
73
  UDID="${RESOLVED%%$'\t'*}"
64
74
 
65
75
  if [ -z "$UDID" ]; then
76
+ har_sim_simctl_available || fail_simctl_unavailable
66
77
  echo "Error: Simulator '${SIMULATOR_NAME}' not found in available devices." >&2
67
78
  echo " Available simulators:" >&2
68
79
  xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone|iPad' | head -20 >&2
@@ -108,10 +108,25 @@ har_sim_claim_owner_of() {
108
108
 
109
109
  # ── Device discovery ──────────────────────────────────────────────────────────
110
110
 
111
+ # Every simctl call goes through here, so "simctl could not be asked" stays
112
+ # distinguishable from "simctl answered, and the answer is empty" at the one place
113
+ # that still knows the difference. Non-zero means the first; callers check it
114
+ # instead of reading an empty list as a fact about the machine.
115
+ har_sim_simctl() {
116
+ local out rc=0
117
+ out="$(xcrun simctl "$@" 2>/dev/null)" || rc=$?
118
+ if [ "$rc" -ne 0 ] || [ -z "$out" ]; then
119
+ return 1
120
+ fi
121
+ printf '%s' "$out"
122
+ }
123
+
111
124
  # Unavailable devices are skipped: a device whose runtime was uninstalled must
112
125
  # not pass validation only to fail at boot.
113
126
  har_sim_device_field() {
114
- xcrun simctl list devices --json 2>/dev/null | node -e '
127
+ local devices
128
+ devices="$(har_sim_simctl list devices --json)" || return 1
129
+ printf '%s' "$devices" | node -e '
115
130
  const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
116
131
  const [want, field] = process.argv.slice(1);
117
132
  const device = Object.values(data.devices || {})
@@ -128,9 +143,10 @@ har_sim_device_name() { har_sim_device_field "$1" name; }
128
143
  # UDID of an existing device with this exact name, newest runtime first.
129
144
  # A substring match would resolve "iPhone 16" to "iPhone 16 Pro Max".
130
145
  har_sim_device_by_name() {
131
- local want="${1:-}"
146
+ local want="${1:-}" devices
132
147
  [ -n "$want" ] || return 0
133
- xcrun simctl list devices available --json 2>/dev/null | node -e '
148
+ devices="$(har_sim_simctl list devices available --json)" || return 1
149
+ printf '%s' "$devices" | node -e '
134
150
  const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
135
151
  const want = process.argv[1];
136
152
  const rows = [];
@@ -157,22 +173,66 @@ har_sim_resolve_configured() {
157
173
  printf '%s\t%s' "$udid" "${HARNESS_SIMULATOR_NAME}"
158
174
  }
159
175
 
160
- # `simctl list` is a subprocess and a JSON parse; neither answer changes during
161
- # a launch, so each is fetched at most once per process.
176
+ # `simctl list` is a subprocess and a JSON parse, so each answer is cached — but in
177
+ # the calling shell, not the process: `$(har_sim_runtimes_json)` fetches, then throws
178
+ # the cache and the availability flag away with the subshell that held them. Call
179
+ # these directly and read the variable whenever either one matters.
162
180
  har_sim_devicetypes_json() {
163
181
  if [ -z "${_HAR_SIM_TYPES_JSON:-}" ]; then
164
- _HAR_SIM_TYPES_JSON="$(xcrun simctl list devicetypes --json 2>/dev/null || echo '{}')"
182
+ _HAR_SIM_TYPES_JSON="$(har_sim_simctl list devicetypes --json)" || _HAR_SIM_TYPES_JSON='{}'
165
183
  fi
166
184
  printf '%s' "$_HAR_SIM_TYPES_JSON"
167
185
  }
168
186
 
169
187
  har_sim_runtimes_json() {
170
188
  if [ -z "${_HAR_SIM_RUNTIMES_JSON:-}" ]; then
171
- _HAR_SIM_RUNTIMES_JSON="$(xcrun simctl list runtimes --json 2>/dev/null || echo '{}')"
189
+ if _HAR_SIM_RUNTIMES_JSON="$(har_sim_simctl list runtimes --json)"; then
190
+ _HAR_SIM_SIMCTL_OK=true
191
+ else
192
+ _HAR_SIM_RUNTIMES_JSON='{}'
193
+ _HAR_SIM_SIMCTL_OK=false
194
+ # A second call, but only ever when simctl is already failing — and failing
195
+ # is the fast path. Its stderr is the only thing that tells the causes apart.
196
+ _HAR_SIM_SIMCTL_ERROR="$(xcrun simctl list runtimes --json 2>&1 >/dev/null || true)"
197
+ fi
172
198
  fi
173
199
  printf '%s' "$_HAR_SIM_RUNTIMES_JSON"
174
200
  }
175
201
 
202
+ # Ask simctl once, recording in this shell whether it answered and why not.
203
+ har_sim_probe_simctl() {
204
+ har_sim_runtimes_json >/dev/null
205
+ }
206
+
207
+ har_sim_simctl_available() {
208
+ har_sim_probe_simctl
209
+ [ "${_HAR_SIM_SIMCTL_OK:-true}" = "true" ]
210
+ }
211
+
212
+ # Two unrelated problems make simctl unusable — a sandbox denying the
213
+ # CoreSimulatorService lookup, and a developer directory with no simctl in it.
214
+ # Naming the wrong one costs the reader the hunt this check exists to prevent.
215
+ har_sim_log_simctl_unavailable() {
216
+ # Teardown hits two failures in a row; the cause only needs saying once.
217
+ [ -z "${_HAR_SIM_SIMCTL_CAUSE_REPORTED:-}" ] || return 0
218
+ _HAR_SIM_SIMCTL_CAUSE_REPORTED=1
219
+ # For a launch the failing call happened inside the command substitution around
220
+ # har_sim_plan_creation, so the reason died with that subshell — ask again here.
221
+ har_sim_probe_simctl
222
+ case "${_HAR_SIM_SIMCTL_ERROR:-}" in
223
+ *"not a developer tool"*|*"unable to find utility"*|*"command not found"*)
224
+ har_sim_log "'xcrun simctl' is not in the selected developer directory"
225
+ har_sim_log " install Xcode, then point the toolchain at it:"
226
+ har_sim_log " sudo xcode-select -s /Applications/Xcode.app"
227
+ har_sim_log " current selection: $(xcode-select -p 2>/dev/null || echo 'none')"
228
+ return
229
+ ;;
230
+ esac
231
+ har_sim_log "cannot reach CoreSimulatorService — 'xcrun simctl' is unusable here"
232
+ har_sim_log " this usually means the command is running inside an agent sandbox"
233
+ har_sim_log " run it from a normal terminal, or let the agent run 'xcrun simctl' unsandboxed"
234
+ }
235
+
176
236
  # Model and runtime to create this slot's device from. Failures carry the models
177
237
  # this machine can create, so the caller never has to re-derive that list.
178
238
  # Prints one of:
@@ -180,10 +240,18 @@ har_sim_runtimes_json() {
180
240
  # NO_RUNTIME no iOS runtime installed
181
241
  # NO_MODEL <models> HARNESS_SIMULATOR_NAME is not a device model
182
242
  # NO_RUNTIME_FOR_MODEL <models> model exists, no installed runtime supports it
243
+ # SIMCTL_UNAVAILABLE simctl could not be asked at all
183
244
  har_sim_plan_creation() {
184
245
  local types runtimes
185
246
  types="$(har_sim_devicetypes_json)"
186
- runtimes="$(har_sim_runtimes_json)"
247
+ # Not `$(...)`: that would discard the availability flag with the subshell.
248
+ har_sim_runtimes_json >/dev/null
249
+ runtimes="$_HAR_SIM_RUNTIMES_JSON"
250
+ # Checked before the empty lists are read as an answer about the machine.
251
+ if ! har_sim_simctl_available; then
252
+ printf 'SIMCTL_UNAVAILABLE'
253
+ return
254
+ fi
187
255
  node -e '
188
256
  const [typesRaw, runtimesRaw, wantName, family] = process.argv.slice(1);
189
257
  const types = JSON.parse(typesRaw).devicetypes || [];
@@ -240,7 +308,9 @@ process.stdout.write(plan());
240
308
  # Every device whose name is <prefix> or starts with "<prefix>-", so a slot's
241
309
  # leftovers are found whatever model they were created from.
242
310
  har_sim_devices_with_prefix() {
243
- xcrun simctl list devices --json 2>/dev/null | node -e '
311
+ local devices
312
+ devices="$(har_sim_simctl list devices --json)" || return 1
313
+ printf '%s' "$devices" | node -e '
244
314
  const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
245
315
  const prefix = process.argv[1];
246
316
  const matches = Object.values(data.devices || {})
@@ -252,12 +322,19 @@ process.stdout.write(matches.join("\n"));
252
322
  ' "$1" 2>/dev/null || true
253
323
  }
254
324
 
325
+ # Returns 0 even when it could not look: teardown.sh runs under `set -e`, and
326
+ # aborting there would leave the slot registry behind on top of the device.
255
327
  har_sim_delete_devices_with_prefix() {
256
- local udid
328
+ local udid list
329
+ if ! list="$(har_sim_devices_with_prefix "$1")"; then
330
+ har_sim_log "cannot check for leftover devices named ${1}* — none were removed"
331
+ har_sim_log_simctl_unavailable
332
+ return 0
333
+ fi
257
334
  while read -r udid; do
258
335
  [ -n "$udid" ] || continue
259
336
  xcrun simctl delete "$udid" >/dev/null 2>&1 || true
260
- done <<< "$(har_sim_devices_with_prefix "$1")"
337
+ done <<< "$list"
261
338
  }
262
339
 
263
340
  # ── Boot ──────────────────────────────────────────────────────────────────────
@@ -289,7 +366,11 @@ _har_sim_select() {
289
366
 
290
367
  # An explicit UDID is a deliberate override — never fall back off it.
291
368
  if [ -n "${HARNESS_SIMULATOR_UDID:-}" ]; then
292
- name="$(har_sim_device_name "$HARNESS_SIMULATOR_UDID")"
369
+ # A failed lookup is "could not ask"; an empty one is "no such device".
370
+ name="$(har_sim_device_name "$HARNESS_SIMULATOR_UDID")" || {
371
+ har_sim_log_simctl_unavailable
372
+ return 1
373
+ }
293
374
  if [ -z "$name" ]; then
294
375
  har_sim_log "HARNESS_SIMULATOR_UDID=${HARNESS_SIMULATOR_UDID} is not a known device"
295
376
  return 1
@@ -310,6 +391,11 @@ _har_sim_select() {
310
391
  models=""
311
392
  [ "$plan" != "$status" ] && models="${plan#*$'\t'}"
312
393
 
394
+ if [ "$status" = "SIMCTL_UNAVAILABLE" ]; then
395
+ har_sim_log_simctl_unavailable
396
+ return 1
397
+ fi
398
+
313
399
  if [ "$status" = "OK" ]; then
314
400
  IFS=$'\t' read -r _ device_type model runtime _ <<< "$plan"
315
401
  label="$(har_sim_device_label "$agent_id" "$model")"
@@ -388,8 +474,14 @@ har_sim_release() {
388
474
  rm -f "$file"
389
475
  if [ "$created" = "true" ] && [ -n "$udid" ]; then
390
476
  xcrun simctl shutdown "$udid" >/dev/null 2>&1 || true
391
- xcrun simctl delete "$udid" >/dev/null 2>&1 || true
392
- echo "✓ Deleted simulator created for this slot ($udid)"
477
+ if xcrun simctl delete "$udid" >/dev/null 2>&1; then
478
+ echo "✓ Deleted simulator created for this slot ($udid)"
479
+ else
480
+ # Naming it is the whole remedy left: the claim is gone, so only the name
481
+ # prefix sweep at the next launch can still find this device.
482
+ har_sim_log "could not delete ${udid}, created for agent ${agent_id} — it is still on this machine"
483
+ har_sim_simctl_available || har_sim_log_simctl_unavailable
484
+ fi
393
485
  fi
394
486
  fi
395
487
 
@@ -0,0 +1,39 @@
1
+ # Semgrep SAST — official recipe (https://semgrep.dev/docs/semgrep-ci/sample-ci-configs)
2
+ #
3
+ # With a SEMGREP_APP_TOKEN secret (Semgrep AppSec Platform → Settings), `semgrep ci`
4
+ # publishes findings to the AppSec Platform — the org-level evidence layer that
5
+ # compliance tools like Vanta ingest natively:
6
+ # https://help.vanta.com/en/articles/15705377-connecting-vanta-semgrep
7
+ # Without the token, this falls back to a Community Edition scan that still blocks
8
+ # the PR on findings but publishes nothing.
9
+ name: Semgrep
10
+
11
+ on:
12
+ pull_request: {}
13
+ push:
14
+ branches: ["main", "master"]
15
+ workflow_dispatch: {}
16
+ schedule:
17
+ # Daily scan so new registry rules surface without a code change
18
+ - cron: '17 5 * * *'
19
+
20
+ jobs:
21
+ semgrep:
22
+ name: semgrep/ci
23
+ runs-on: ubuntu-latest
24
+ container:
25
+ image: semgrep/semgrep
26
+ if: (github.actor != 'dependabot[bot]')
27
+ steps:
28
+ # Pinned to a SHA so semgrep's own mutable-action-tag rule stays quiet
29
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30
+ - name: Run Semgrep
31
+ env:
32
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
33
+ run: |
34
+ if [ -n "$SEMGREP_APP_TOKEN" ]; then
35
+ semgrep ci
36
+ else
37
+ echo "SEMGREP_APP_TOKEN not set — running Community Edition scan (no platform/Vanta reporting)"
38
+ semgrep scan --config auto --error
39
+ fi
@@ -0,0 +1,91 @@
1
+ # Semgrep SAST Stage
2
+
3
+ The `sast` stage scans the agent's session worktree with [Semgrep](https://semgrep.dev)
4
+ and fails verification when blocking findings exist. Full verify (`verify --full`)
5
+ runs it automatically once it is listed in `verificationStages`.
6
+
7
+ ## Running
8
+
9
+ ```bash
10
+ # Whole worktree
11
+ ./.har/stages/sast.sh 1
12
+
13
+ # Only specific paths
14
+ ./.har/stages/sast.sh 1 src/ lib/auth.py
15
+
16
+ # Included in full verify
17
+ ./.har/verify.sh 1 --full
18
+ ```
19
+
20
+ Reports land under `.har/artifacts/sast/` in the **main repo root** (not the worktree):
21
+
22
+ | File | Contents |
23
+ |------|----------|
24
+ | `semgrep.json` | Full findings report (machine-readable) |
25
+ | `semgrep.sarif` | SARIF report — upload to GitHub code scanning if desired |
26
+ | `scan.log` | Semgrep's own text output for the run |
27
+
28
+ Exit codes pass through from Semgrep: `0` clean, `1` findings, `>=2` scan error.
29
+
30
+ ## Choosing rules
31
+
32
+ The stage runs `semgrep scan --config "$HARNESS_SEMGREP_CONFIG"` (default `auto`).
33
+ Set the variable in `.har/harness.env` to pin behavior:
34
+
35
+ ```bash
36
+ # .har/harness.env
37
+ export HARNESS_SEMGREP_CONFIG="p/ci" # curated low-noise ruleset
38
+ # export HARNESS_SEMGREP_CONFIG="p/security-audit"
39
+ # export HARNESS_SEMGREP_CONFIG=".semgrep/" # local rules only (works offline)
40
+ ```
41
+
42
+ Honest notes about `--config auto` and registry rulesets (`p/...`):
43
+
44
+ - They **fetch rules from the Semgrep registry**, so the stage needs network access.
45
+ - Semgrep **sends pseudonymized metrics** to semgrep.dev when registry configs are
46
+ used (this cannot be disabled for registry configs). For fully offline, metric-free
47
+ scans, commit local rule files and point `HARNESS_SEMGREP_CONFIG` at them.
48
+ - `auto` picks rulesets based on the languages it detects — convenient, but pin a
49
+ named ruleset (`p/ci`) when you want reproducible results.
50
+
51
+ ## Tuning noise
52
+
53
+ - Semgrep respects `.gitignore` by default; add a [`.semgrepignore`](https://semgrep.dev/docs/ignoring-files-folders-code)
54
+ file for extra exclusions (vendored code, generated files, fixtures).
55
+ - The stage always passes `--exclude .har` — harness scaffolding is not your
56
+ project's code and semgrep cannot parse some of its bash.
57
+ - Suppress a single finding with a trailing `# nosemgrep: <rule-id>` comment —
58
+ prefer rule-scoped suppressions over blanket `# nosemgrep`.
59
+ - Write repo-specific rules under `.semgrep/` and add the directory to your config.
60
+
61
+ ## CI and compliance (Vanta)
62
+
63
+ Of the security plugins, this one has the strongest compliance story. The optional
64
+ workflow `.github/workflows/semgrep.yml` runs `semgrep ci` in the official
65
+ `semgrep/semgrep` container. With a `SEMGREP_APP_TOKEN` secret (generated in
66
+ Semgrep AppSec Platform → Settings), findings publish to the
67
+ [Semgrep AppSec Platform](https://semgrep.dev/login) — which has a
68
+ [native Vanta integration](https://help.vanta.com/en/articles/15705377-connecting-vanta-semgrep):
69
+ Vanta pulls findings, projects, and scan history from Semgrep automatically as
70
+ compliance evidence.
71
+
72
+ Division of labor:
73
+
74
+ - **This local stage** is the shift-left layer — agents catch findings *before*
75
+ code lands on the default branch. Local runs are invisible to Vanta (no noise).
76
+ - **The CI run** (`semgrep ci` on the default branch and PRs) is the org-level
77
+ evidence layer Vanta reads.
78
+
79
+ Without a token, the workflow falls back to `semgrep scan --config auto --error`
80
+ (Community Edition — CI still blocks on findings, but nothing publishes to the
81
+ platform and there is no Vanta evidence).
82
+
83
+ ## Adapting per repo
84
+
85
+ - Pin `HARNESS_SEMGREP_CONFIG` to the ruleset your team standardizes on.
86
+ - Add `.semgrepignore` entries for paths that produce noise.
87
+ - If findings should not block agents yet, keep `sast` out of `verificationStages`
88
+ in `.har/stages.json` and run it manually while you triage the baseline.
89
+ - The stage summarizes reports with `node`; it assumes a Node.js toolchain is
90
+ available (true for HAR-provisioned slots on node ecosystems — on other stacks,
91
+ ensure `node` is on PATH or adapt the summary block).
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # Semgrep SAST scan for an agent slot.
3
+ # Scans the session worktree with Semgrep and writes JSON + SARIF reports.
4
+ # Outputs JSON to stdout, human-readable progress to stderr.
5
+ #
6
+ # Usage: ./.har/stages/sast.sh <agent-id> [paths...]
7
+ # paths (optional): scan only these paths instead of the whole worktree
8
+ #
9
+ # Config: HARNESS_SEMGREP_CONFIG (default "auto") — any semgrep --config value,
10
+ # e.g. "p/ci", "p/security-audit", or a local rules file/dir. Set it in
11
+ # .har/harness.env to pin rulesets. Registry configs (auto, p/...) need
12
+ # network access and send pseudonymized metrics to semgrep.dev.
13
+ #
14
+ # Prerequisite: ./.har/launch.sh <agent-id> AND the semgrep CLI installed
15
+ # See: ./.har/stages/SEMGREP.md for the full adaptation guide.
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ HARNESS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
+ REPO_ROOT="$(cd "$HARNESS_DIR/.." && pwd)"
21
+ # agent-slot.sh expects SCRIPT_DIR to be .har/ (slot registry lives there)
22
+ SCRIPT_DIR="$HARNESS_DIR"
23
+
24
+ # shellcheck source=/dev/null
25
+ source "$HARNESS_DIR/harness.env"
26
+ # shellcheck source=/dev/null
27
+ source "$HARNESS_DIR/agent-slot.sh"
28
+
29
+ AGENT_ID="${1:?Usage: sast.sh <agent-id> [paths...]}"
30
+ shift
31
+ SCAN_PATHS=("$@")
32
+
33
+ validate_agent_id "$AGENT_ID"
34
+
35
+ log() { echo "==> [semgrep agent-$AGENT_ID] $*" >&2; }
36
+
37
+ # ── Preflight ─────────────────────────────────────────────────────────────────
38
+ if ! command -v semgrep >/dev/null 2>&1; then
39
+ echo "Error: semgrep CLI not found." >&2
40
+ echo " Install with one of:" >&2
41
+ echo " pipx install semgrep # recommended" >&2
42
+ echo " pip install semgrep" >&2
43
+ echo " brew install semgrep # may lag behind the latest release" >&2
44
+ echo " Then re-run: ./.har/stages/sast.sh ${AGENT_ID}" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # ── Resolve agent env ─────────────────────────────────────────────────────────
49
+ ENV_FILE="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT")" || {
50
+ echo "No .env.agent.${AGENT_ID} found." >&2
51
+ har_suggest_launch "$AGENT_ID" >&2
52
+ exit 1
53
+ }
54
+
55
+ # .env.agent.<id> exports REPO_ROOT pointing at the worktree — keep the main
56
+ # checkout path so artifacts land in the main repo root, not the worktree.
57
+ MAIN_REPO_ROOT="$REPO_ROOT"
58
+
59
+ set -a
60
+ # shellcheck source=/dev/null
61
+ source "$ENV_FILE"
62
+ set +a
63
+
64
+ WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
65
+
66
+ # ── Artifact directory (main repo root, not the worktree) ─────────────────────
67
+ ARTIFACT_DIR="$MAIN_REPO_ROOT/.har/artifacts/sast"
68
+ mkdir -p "$ARTIFACT_DIR"
69
+
70
+ SEMGREP_CONFIG="${HARNESS_SEMGREP_CONFIG:-auto}"
71
+ JSON_REPORT="$ARTIFACT_DIR/semgrep.json"
72
+ SARIF_REPORT="$ARTIFACT_DIR/semgrep.sarif"
73
+ SCAN_LOG="$ARTIFACT_DIR/scan.log"
74
+
75
+ # ── Run semgrep ───────────────────────────────────────────────────────────────
76
+ log "Scanning $WORK_DIR (config: $SEMGREP_CONFIG)..."
77
+ START=$(now_ms)
78
+
79
+ set +e
80
+ (
81
+ cd "$WORK_DIR" &&
82
+ semgrep scan \
83
+ --config "$SEMGREP_CONFIG" \
84
+ --error \
85
+ --exclude .har \
86
+ --json-output "$JSON_REPORT" \
87
+ --sarif-output "$SARIF_REPORT" \
88
+ ${SCAN_PATHS[@]+"${SCAN_PATHS[@]}"}
89
+ ) >"$SCAN_LOG" 2>&1
90
+ SEMGREP_EXIT=$?
91
+ set -e
92
+
93
+ END=$(now_ms)
94
+ TOTAL_MS=$(( END - START ))
95
+
96
+ # Surface the scan tail so failures are diagnosable without opening the log
97
+ tail -20 "$SCAN_LOG" | sed 's/^/ /' >&2
98
+
99
+ # semgrep exit codes: 0 = clean, 1 = findings (--error), >=2 = scan error
100
+ if [ "$SEMGREP_EXIT" = "0" ]; then
101
+ STATUS="pass"
102
+ log "✓ No blocking findings (${TOTAL_MS}ms)"
103
+ elif [ "$SEMGREP_EXIT" = "1" ]; then
104
+ STATUS="fail"
105
+ log "✗ Semgrep reported findings (${TOTAL_MS}ms) — see $JSON_REPORT"
106
+ else
107
+ STATUS="fail"
108
+ log "✗ Semgrep scan errored with exit code $SEMGREP_EXIT (${TOTAL_MS}ms) — see $SCAN_LOG"
109
+ fi
110
+
111
+ # ── Output ────────────────────────────────────────────────────────────────────
112
+ node -e "
113
+ const fs = require('fs');
114
+ let findings = null;
115
+ let errors = null;
116
+ try {
117
+ const report = JSON.parse(fs.readFileSync('$JSON_REPORT', 'utf8'));
118
+ findings = report.results ? report.results.length : 0;
119
+ errors = report.errors ? report.errors.length : 0;
120
+ } catch { /* report missing when the scan itself errored */ }
121
+ const out = {
122
+ status: '$STATUS',
123
+ stageId: 'sast',
124
+ kind: 'test',
125
+ agent_id: $AGENT_ID,
126
+ total_ms: $TOTAL_MS,
127
+ exit_code: $SEMGREP_EXIT,
128
+ config: '$SEMGREP_CONFIG',
129
+ findings,
130
+ scan_errors: errors,
131
+ artifacts: [
132
+ { path: '.har/artifacts/sast', kind: 'directory' },
133
+ { path: '.har/artifacts/sast/semgrep.json', kind: 'report' }
134
+ ]
135
+ };
136
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
137
+ "
138
+
139
+ if [ "$SEMGREP_EXIT" != "0" ]; then
140
+ exit "$SEMGREP_EXIT"
141
+ fi
@@ -0,0 +1,57 @@
1
+ # Semgrep plugin
2
+
3
+ Add a Semgrep SAST stage to your harness so agents catch security findings
4
+ before code lands.
5
+
6
+ ```bash
7
+ har env add-plugin semgrep
8
+ ```
9
+
10
+ ## What this installs
11
+
12
+ | File | Purpose |
13
+ |------|---------|
14
+ | `.har/stages/sast.sh` | Stage runner — scans the session worktree with Semgrep |
15
+ | `.har/stages/SEMGREP.md` | Adaptation guide: rulesets, noise tuning, CI/Vanta story |
16
+ | `.github/workflows/semgrep.yml` | Optional CI workflow (`--skip-ci` to omit) — official `semgrep ci` recipe |
17
+
18
+ ## Workflow
19
+
20
+ ```
21
+ Code change
22
+ │
23
+ launch slot
24
+ │
25
+ typecheck + unit-tests
26
+ │
27
+ sast ◄── semgrep scan over the worktree
28
+ │
29
+ pass/fail
30
+ ```
31
+
32
+ Full verify (`verify --full`) runs the scan after lint. Reports (JSON + SARIF)
33
+ land under `.har/artifacts/sast/`.
34
+
35
+ ## Requirements
36
+
37
+ - `semgrep` CLI: `pipx install semgrep` (recommended), `pip install semgrep`,
38
+ or `brew install semgrep`
39
+ - Network access for registry rulesets (`auto`, `p/ci`, …) — or commit local
40
+ rules and set `HARNESS_SEMGREP_CONFIG` to their path for offline scans
41
+
42
+ ## Compliance (Vanta)
43
+
44
+ The local stage is shift-left only — compliance platforms never see it. The CI
45
+ workflow's `semgrep ci` run (with a `SEMGREP_APP_TOKEN` secret) publishes to the
46
+ Semgrep AppSec Platform, which Vanta ingests natively as vulnerability-management
47
+ evidence. See `.har/stages/SEMGREP.md` for the full story.
48
+
49
+ ## Quick start after install
50
+
51
+ ```bash
52
+ pipx install semgrep
53
+ ./.har/launch.sh 1
54
+ ./.har/stages/sast.sh 1
55
+ # reports: .har/artifacts/sast/semgrep.json (+ .sarif, scan.log)
56
+ ./.har/verify.sh 1 --full
57
+ ```
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "semgrep",
3
+ "stageId": "sast",
4
+ "verificationStages": [
5
+ "typecheck",
6
+ "unit-tests",
7
+ "lint",
8
+ "sast"
9
+ ],
10
+ "stage": {
11
+ "id": "sast",
12
+ "kind": "test",
13
+ "description": "Static analysis (SAST) with Semgrep — scans the session worktree for security findings",
14
+ "script": "stages/sast.sh",
15
+ "requiresAgentId": true,
16
+ "artifacts": [
17
+ {
18
+ "path": ".har/artifacts/sast",
19
+ "kind": "directory",
20
+ "description": "Semgrep JSON + SARIF reports and scan log"
21
+ },
22
+ {
23
+ "path": ".har/artifacts/sast/semgrep.json",
24
+ "kind": "report"
25
+ }
26
+ ]
27
+ },
28
+ "files": [
29
+ { "src": ".har/stages/sast.sh", "dest": ".har/stages/sast.sh", "executable": true },
30
+ { "src": ".har/stages/SEMGREP.md", "dest": ".har/stages/SEMGREP.md" }
31
+ ],
32
+ "optionalFiles": [
33
+ { "src": ".github/workflows/semgrep.yml", "dest": ".github/workflows/semgrep.yml", "skipFlag": "skipCi" }
34
+ ],
35
+ "nextSteps": [
36
+ "pipx install semgrep # or: pip install semgrep / brew install semgrep",
37
+ "./.har/launch.sh 1",
38
+ "./.har/stages/sast.sh 1",
39
+ "Open .har/artifacts/sast/ for the JSON and SARIF reports",
40
+ "Read .har/stages/SEMGREP.md to pin rulesets (HARNESS_SEMGREP_CONFIG) or add local rules",
41
+ "CI: set the SEMGREP_APP_TOKEN secret so semgrep ci reports to the Semgrep AppSec Platform (compliance evidence, e.g. Vanta)"
42
+ ],
43
+ "docsPath": ".har/stages/SEMGREP.md"
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.53.0",
3
+ "version": "0.54.1",
4
4
  "description": "Open harness for reproducible multi-agent coding workflows",
5
5
  "keywords": [
6
6
  "har",