@hasna/recordings 0.3.1 → 0.3.2

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.
@@ -151,6 +151,7 @@ function localOnlyApprovedTargetsMessage(policyPath?: string): string {
151
151
  }
152
152
 
153
153
  export type ArtifactPolicy = "release" | "local_only";
154
+ export type ArtifactVariant = "full" | "bar";
154
155
  export type TargetIdentityKind =
155
156
  | typeof LEGACY_LOCAL_TARGET_IDENTITY_KIND
156
157
  | "tailscale_node_id_sha256";
@@ -159,6 +160,7 @@ type OperatorTargetIdentityKind = TargetIdentityKind | "none";
159
160
  export type BuildProvenance = {
160
161
  schema_version: 3 | 4;
161
162
  artifact_policy?: "local_only";
163
+ variant?: ArtifactVariant;
162
164
  approved_target?: string;
163
165
  approved_target_identity_kind?: TargetIdentityKind;
164
166
  approved_target_identity_sha256?: string;
@@ -181,6 +183,9 @@ export type BuildProvenance = {
181
183
 
182
184
  export type MacOSArtifactManifest = BuildProvenance & {
183
185
  artifact_type: "recordings-macos-app";
186
+ /// The top-level bundle directory name inside the artifact (Recordings.app for the
187
+ /// full app, HasnaRecordings.app for the bar variant per the fleet naming rule).
188
+ bundle_name: string;
184
189
  app_sha256: string;
185
190
  binding: {
186
191
  bundle_tree_sha256: string;
@@ -210,7 +215,7 @@ export type MacOSArtifactManifest = BuildProvenance & {
210
215
  };
211
216
  container: {
212
217
  type: "zip";
213
- install_locations: ["/Applications/Recordings.app"] | ["~/Applications/Recordings.app"];
218
+ install_locations: [string];
214
219
  };
215
220
  nested_code_policy: {
216
221
  allowlist_sha256: string;
@@ -825,6 +830,22 @@ function manifestPolicy(manifest: MacOSArtifactManifest): ArtifactPolicy {
825
830
  return manifest.schema_version === LOCAL_ARTIFACT_SCHEMA_VERSION ? "local_only" : "release";
826
831
  }
827
832
 
833
+ /**
834
+ * The variant an artifact was built as. Older manifests predate the field; they are
835
+ * full artifacts by definition ("full" was the only variant that existed).
836
+ */
837
+ function manifestVariant(manifest: MacOSArtifactManifest): ArtifactVariant {
838
+ const value = manifest.variant ?? "full";
839
+ if (value !== "full" && value !== "bar") {
840
+ throw new Error(`manifest variant is unsupported: ${value}`);
841
+ }
842
+ return value;
843
+ }
844
+
845
+ function manifestBundleName(manifest: MacOSArtifactManifest): string {
846
+ return manifest.bundle_name ?? "Recordings.app";
847
+ }
848
+
828
849
  function manifestTargetIdentityKind(manifest: MacOSArtifactManifest): OperatorTargetIdentityKind {
829
850
  if (manifestPolicy(manifest) === "release") return "none";
830
851
  return manifest.approved_target_identity_kind ?? LEGACY_LOCAL_TARGET_IDENTITY_KIND;
@@ -1405,6 +1426,7 @@ export function withPrivatelyExtractedArchiveApp<T>(
1405
1426
  operation: (appPath: string) => T,
1406
1427
  platformArchiveTool = "/usr/bin/ditto",
1407
1428
  expectedArchiveSha256?: string,
1429
+ expectedBundleName = "Recordings.app",
1408
1430
  ): T {
1409
1431
  const privateRoot = mkdtempSync(join(tmpdir(), "recordings-artifact-extract-"));
1410
1432
  chmodSync(privateRoot, 0o700);
@@ -1449,13 +1471,17 @@ export function withPrivatelyExtractedArchiveApp<T>(
1449
1471
  inspectZipArchive(pinnedArchivePath);
1450
1472
  run(platformArchiveTool, ["-x", "-k", pinnedArchivePath, extractionRoot]);
1451
1473
  const rootEntries = readdirSync(extractionRoot);
1452
- if (rootEntries.length !== 1 || rootEntries[0] !== "Recordings.app") {
1453
- throw new Error("release archive must contain exactly one top-level Recordings.app");
1474
+ if (rootEntries.length !== 1 || rootEntries[0] !== expectedBundleName) {
1475
+ throw new Error(
1476
+ `release archive must contain exactly one top-level ${expectedBundleName}`,
1477
+ );
1454
1478
  }
1455
- const extractedAppPath = join(extractionRoot, "Recordings.app");
1479
+ const extractedAppPath = join(extractionRoot, expectedBundleName);
1456
1480
  const appDetails = lstatSync(extractedAppPath);
1457
1481
  if (appDetails.isSymbolicLink() || !appDetails.isDirectory()) {
1458
- throw new Error("release archive top-level Recordings.app must be a regular directory");
1482
+ throw new Error(
1483
+ `release archive top-level ${expectedBundleName} must be a regular directory`,
1484
+ );
1459
1485
  }
1460
1486
  assertRegularArchiveTree(extractedAppPath);
1461
1487
  return operation(extractedAppPath);
@@ -1478,6 +1504,7 @@ export function extractVerifiedArchiveToStaging(
1478
1504
  expectedApprovedTarget: string,
1479
1505
  expectedApprovedTargetIdentitySha256: string,
1480
1506
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
1507
+ expectedVariant: ArtifactVariant = "full",
1481
1508
  platformArchiveTool = "/usr/bin/ditto",
1482
1509
  ): void {
1483
1510
  if (resolve(stagingTarget) !== stagingTarget) {
@@ -1510,16 +1537,19 @@ export function extractVerifiedArchiveToStaging(
1510
1537
  withPrivatelyExtractedArchiveApp(
1511
1538
  archivePath,
1512
1539
  (appPath) => {
1513
- const targetAppPath = join(stagingTarget, "Recordings.app");
1540
+ const targetAppPath = join(stagingTarget, manifestBundleName(manifest));
1514
1541
  renameSync(appPath, targetAppPath);
1515
1542
  assertRegularArchiveTree(targetAppPath);
1516
1543
  const entries = readdirSync(stagingTarget);
1517
- if (entries.length !== 1 || entries[0] !== "Recordings.app") {
1518
- throw new Error("archive extraction did not produce exactly Recordings.app in staging");
1544
+ if (entries.length !== 1 || entries[0] !== manifestBundleName(manifest)) {
1545
+ throw new Error(
1546
+ `archive extraction did not produce exactly ${manifestBundleName(manifest)} in staging`,
1547
+ );
1519
1548
  }
1520
1549
  },
1521
1550
  platformArchiveTool,
1522
1551
  manifest.archive.sha256,
1552
+ manifestBundleName(manifest),
1523
1553
  );
1524
1554
  }
1525
1555
 
@@ -1547,6 +1577,20 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
1547
1577
  }
1548
1578
  if (manifest.artifact_type !== "recordings-macos-app") throw new Error("unexpected artifact type");
1549
1579
  if (manifest.bundle_id !== BUNDLE_ID) throw new Error("unexpected bundle identifier");
1580
+ if (
1581
+ manifest.bundle_name !== undefined &&
1582
+ (typeof manifest.bundle_name !== "string" ||
1583
+ !/^[A-Za-z0-9][A-Za-z0-9 .-]*\.app$/.test(manifest.bundle_name))
1584
+ ) {
1585
+ throw new Error("manifest bundle name must be a *.app bundle directory name");
1586
+ }
1587
+ if (
1588
+ manifest.variant !== undefined &&
1589
+ manifest.variant !== "full" &&
1590
+ manifest.variant !== "bar"
1591
+ ) {
1592
+ throw new Error(`manifest variant is unsupported: ${manifest.variant}`);
1593
+ }
1550
1594
  const artifactPolicy = manifestPolicy(manifest);
1551
1595
  for (const [label, value] of [
1552
1596
  ["bundle version", manifest.bundle_version],
@@ -1755,6 +1799,7 @@ export function verifyArchiveManifest(
1755
1799
  expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
1756
1800
  expectedApprovedTargetIdentitySha256: string = "none",
1757
1801
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
1802
+ expectedVariant: ArtifactVariant = "full",
1758
1803
  ): MacOSArtifactManifest {
1759
1804
  if (!expectedTeamId) throw new Error("expected Team ID is required");
1760
1805
  const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
@@ -1765,6 +1810,11 @@ export function verifyArchiveManifest(
1765
1810
  if (manifestPolicy(manifest) !== expectedPolicy) {
1766
1811
  throw new Error("manifest artifact policy does not match the explicit operator selection");
1767
1812
  }
1813
+ if (manifestVariant(manifest) !== expectedVariant) {
1814
+ throw new Error(
1815
+ `manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
1816
+ );
1817
+ }
1768
1818
  const actualApprovedTarget = manifest.approved_target ?? RELEASE_APPROVED_TARGET;
1769
1819
  if (actualApprovedTarget !== expectedApprovedTarget) {
1770
1820
  throw new Error("manifest approved target does not match the exact operator-approved target");
@@ -1817,6 +1867,7 @@ function assertProvenanceMatchesManifest(
1817
1867
  "builder_identity_kind",
1818
1868
  "builder_identity_sha256",
1819
1869
  "non_notarized",
1870
+ "variant",
1820
1871
  "bundle_id",
1821
1872
  "bundle_version",
1822
1873
  "git_sha",
@@ -1989,11 +2040,17 @@ export function verifyExtractedApp(
1989
2040
  expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
1990
2041
  expectedApprovedTargetIdentitySha256: string = "none",
1991
2042
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
2043
+ expectedVariant: ArtifactVariant = "full",
1992
2044
  ): MacOSArtifactManifest {
1993
2045
  const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
1994
2046
  manifestPath,
1995
2047
  expectedManifestSha256,
1996
2048
  );
2049
+ if (manifestVariant(manifest) !== expectedVariant) {
2050
+ throw new Error(
2051
+ `manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
2052
+ );
2053
+ }
1997
2054
  verifyAppAgainstManifest(
1998
2055
  appPath,
1999
2056
  manifest,
@@ -2063,6 +2120,7 @@ function verifySuppliedAndArchivedApps(
2063
2120
  },
2064
2121
  "/usr/bin/ditto",
2065
2122
  manifest.archive.sha256,
2123
+ manifestBundleName(manifest),
2066
2124
  );
2067
2125
  if (sha256ArchiveFile(archivePath) !== manifest.archive.sha256) {
2068
2126
  throw new Error("release archive changed during exact-byte verification");
@@ -2080,6 +2138,7 @@ function writeProvenance(
2080
2138
  approvedTargetIdentitySha256: string,
2081
2139
  builderIdentityKind: OperatorTargetIdentityKind,
2082
2140
  builderIdentitySha256: string,
2141
+ variant: ArtifactVariant,
2083
2142
  ): void {
2084
2143
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
2085
2144
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
@@ -2089,6 +2148,7 @@ function writeProvenance(
2089
2148
  artifactPolicy === "release"
2090
2149
  ? RELEASE_ARTIFACT_SCHEMA_VERSION
2091
2150
  : LOCAL_ARTIFACT_SCHEMA_VERSION,
2151
+ variant,
2092
2152
  bundle_id: plistValue(appPath, "CFBundleIdentifier"),
2093
2153
  bundle_version: plistValue(appPath, "CFBundleShortVersionString"),
2094
2154
  bundle_build_version: plistValue(appPath, "CFBundleVersion"),
@@ -2151,6 +2211,7 @@ function finalizeArtifact(
2151
2211
  notaryLogPath: string,
2152
2212
  notarySubmissionId: string,
2153
2213
  submittedArchiveSha256: string,
2214
+ expectedVariant: ArtifactVariant = "full",
2154
2215
  ): void {
2155
2216
  assertCurrentSourceRevision(packageRoot, expectedSourceSha);
2156
2217
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
@@ -2160,6 +2221,14 @@ function finalizeArtifact(
2160
2221
  if (provenance.git_sha !== expectedSourceSha) {
2161
2222
  throw new Error("embedded provenance does not match the pinned source SHA");
2162
2223
  }
2224
+ if (expectedVariant !== "full" && expectedVariant !== "bar") {
2225
+ throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
2226
+ }
2227
+ if ((provenance.variant ?? "full") !== expectedVariant) {
2228
+ throw new Error(
2229
+ `finalize variant ${expectedVariant} does not match the embedded provenance variant`,
2230
+ );
2231
+ }
2163
2232
  assertExpectedCodeLayout(appPath);
2164
2233
  const outerSigning = signingEvidence(appPath, expectedTeamId, APP_ENTITLEMENTS, executablePath);
2165
2234
  const helperSigning = signingEvidence(helperPath, expectedTeamId, HELPER_ENTITLEMENTS);
@@ -2172,9 +2241,14 @@ function finalizeArtifact(
2172
2241
  const notaryLog = JSON.parse(notaryLogSnapshot.toString("utf8")) as unknown;
2173
2242
  assertAcceptedNotaryLog(notaryLog, notarySubmissionId, submittedArchiveSha256);
2174
2243
  const items = nestedItems(appPath, expectedTeamId, "release", outerSigning, helperSigning);
2244
+ const bundleName = basename(appPath);
2245
+ if (!bundleName.endsWith(".app")) {
2246
+ throw new Error(`release app bundle must be named *.app: ${appPath}`);
2247
+ }
2175
2248
  const manifest: MacOSArtifactManifest = {
2176
2249
  ...provenance,
2177
2250
  artifact_type: "recordings-macos-app",
2251
+ bundle_name: bundleName,
2178
2252
  app_sha256: sha256File(executablePath),
2179
2253
  binding: { bundle_tree_sha256: treeDigest(appPath) },
2180
2254
  provenance_sha256: sha256File(embeddedPath),
@@ -2201,7 +2275,7 @@ function finalizeArtifact(
2201
2275
  },
2202
2276
  container: {
2203
2277
  type: "zip",
2204
- install_locations: ["/Applications/Recordings.app"],
2278
+ install_locations: [`/Applications/${bundleName}`],
2205
2279
  },
2206
2280
  nested_code_policy: {
2207
2281
  allowlist_sha256: nestedPolicyDigest(items),
@@ -2278,11 +2352,15 @@ function finalizeLocalArtifact(
2278
2352
  approvedTargetIdentityKind: TargetIdentityKind,
2279
2353
  approvedTargetIdentitySha256: string,
2280
2354
  expectedTeamId: string = "ADHOC",
2355
+ expectedVariant: ArtifactVariant = "full",
2281
2356
  ): void {
2282
2357
  assertCurrentSourceRevision(packageRoot, expectedSourceSha);
2283
2358
  if (approvedTargetIdentityKind !== "tailscale_node_id_sha256") {
2284
2359
  throw new Error("new local-only artifacts require a Tailscale node ID identity hash");
2285
2360
  }
2361
+ if (expectedVariant !== "full" && expectedVariant !== "bar") {
2362
+ throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
2363
+ }
2286
2364
  const localSigningMode = localOnlySigningMode(expectedTeamId);
2287
2365
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
2288
2366
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
@@ -2300,6 +2378,11 @@ function finalizeLocalArtifact(
2300
2378
  ) {
2301
2379
  throw new Error("embedded provenance is not approved for this local-only target");
2302
2380
  }
2381
+ if ((provenance.variant ?? "full") !== expectedVariant) {
2382
+ throw new Error(
2383
+ `finalize variant ${expectedVariant} does not match the embedded provenance variant`,
2384
+ );
2385
+ }
2303
2386
  assertExpectedCodeLayout(appPath);
2304
2387
  const outerSigning = signingEvidence(
2305
2388
  appPath,
@@ -2316,9 +2399,14 @@ function finalizeLocalArtifact(
2316
2399
  "local_only",
2317
2400
  );
2318
2401
  const items = nestedItems(appPath, expectedTeamId, "local_only", outerSigning, helperSigning);
2402
+ const bundleName = basename(appPath);
2403
+ if (!bundleName.endsWith(".app")) {
2404
+ throw new Error(`local app bundle must be named *.app: ${appPath}`);
2405
+ }
2319
2406
  const manifest: MacOSArtifactManifest = {
2320
2407
  ...provenance,
2321
2408
  artifact_type: "recordings-macos-app",
2409
+ bundle_name: bundleName,
2322
2410
  app_sha256: sha256File(executablePath),
2323
2411
  binding: { bundle_tree_sha256: treeDigest(appPath) },
2324
2412
  provenance_sha256: sha256File(embeddedPath),
@@ -2346,7 +2434,7 @@ function finalizeLocalArtifact(
2346
2434
  },
2347
2435
  container: {
2348
2436
  type: "zip",
2349
- install_locations: ["~/Applications/Recordings.app"],
2437
+ install_locations: [`~/Applications/${bundleName}`],
2350
2438
  },
2351
2439
  nested_code_policy: {
2352
2440
  allowlist_sha256: nestedPolicyDigest(items),
@@ -2516,6 +2604,7 @@ function verifyActiveApp(
2516
2604
  expectedApprovedTarget: string,
2517
2605
  expectedApprovedTargetIdentitySha256: string,
2518
2606
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
2607
+ expectedVariant: ArtifactVariant = "full",
2519
2608
  ): void {
2520
2609
  const manifest = verifyExtractedApp(
2521
2610
  appPath,
@@ -2526,6 +2615,7 @@ function verifyActiveApp(
2526
2615
  expectedApprovedTarget,
2527
2616
  expectedApprovedTargetIdentitySha256,
2528
2617
  expectedApprovedTargetIdentityKind,
2618
+ expectedVariant,
2529
2619
  );
2530
2620
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
2531
2621
  if (companionVersion(helperPath) !== manifest.companion.version) {
@@ -5188,6 +5278,9 @@ function manifestGet(path: string, expectedManifestSha256: string, field: string
5188
5278
  }
5189
5279
  else if (field === "approved_target_identity_sha256") console.log(manifest.approved_target_identity_sha256);
5190
5280
  else if (field === "builder_identity_kind") console.log(manifestBuilderIdentityKind(manifest));
5281
+ else if (field === "variant") console.log(manifestVariant(manifest));
5282
+ // Pre-variant manifests carry no bundle_name; they are full builds by definition.
5283
+ else if (field === "bundle_name") console.log(manifestBundleName(manifest));
5191
5284
  else throw new Error(`unsupported manifest field: ${field}`);
5192
5285
  }
5193
5286
 
@@ -5214,6 +5307,14 @@ function repeatedArguments(name: string): string[] {
5214
5307
  return values;
5215
5308
  }
5216
5309
 
5310
+ function variantArgument(defaultVariant: ArtifactVariant): ArtifactVariant {
5311
+ const value = optionalArgument("--variant") ?? defaultVariant;
5312
+ if (value !== "full" && value !== "bar") {
5313
+ throw new Error(`artifact variant must be full or bar; got: ${value}`);
5314
+ }
5315
+ return value;
5316
+ }
5317
+
5217
5318
  function artifactPolicyArgument(): ArtifactPolicy {
5218
5319
  const value = argument("--artifact-policy");
5219
5320
  if (value !== "release" && value !== "local_only") {
@@ -5276,6 +5377,7 @@ function main(): void {
5276
5377
  argument("--approved-target-identity-sha256"),
5277
5378
  builderIdentityKindArgument(policy),
5278
5379
  argument("--builder-identity-sha256"),
5380
+ variantArgument("full"),
5279
5381
  );
5280
5382
  } else if (command === "finalize") {
5281
5383
  const sourceSha = argument("--source-sha");
@@ -5290,6 +5392,7 @@ function main(): void {
5290
5392
  argument("--notary-log"),
5291
5393
  argument("--notary-submission-id"),
5292
5394
  argument("--submitted-archive-sha256"),
5395
+ variantArgument("full"),
5293
5396
  );
5294
5397
  } else if (command === "finalize-local") {
5295
5398
  const sourceSha = argument("--source-sha");
@@ -5303,6 +5406,7 @@ function main(): void {
5303
5406
  targetIdentityKindArgument(undefined, true),
5304
5407
  argument("--approved-target-identity-sha256"),
5305
5408
  optionalArgument("--expected-team-id") ?? "ADHOC",
5409
+ variantArgument("full"),
5306
5410
  );
5307
5411
  } else if (command === "verify-archive") {
5308
5412
  const teamId = argument("--team-id");
@@ -5318,6 +5422,7 @@ function main(): void {
5318
5422
  argument("--approved-target"),
5319
5423
  argument("--approved-target-identity-sha256"),
5320
5424
  targetIdentityKindArgument(policy),
5425
+ variantArgument("full"),
5321
5426
  );
5322
5427
  } else if (command === "extract-verified-archive") {
5323
5428
  const teamId = argument("--team-id");
@@ -5334,6 +5439,7 @@ function main(): void {
5334
5439
  argument("--approved-target"),
5335
5440
  argument("--approved-target-identity-sha256"),
5336
5441
  targetIdentityKindArgument(policy),
5442
+ variantArgument("full"),
5337
5443
  );
5338
5444
  } else if (command === "verify-app") {
5339
5445
  const teamId = argument("--team-id");
@@ -5347,6 +5453,7 @@ function main(): void {
5347
5453
  argument("--approved-target"),
5348
5454
  argument("--approved-target-identity-sha256"),
5349
5455
  targetIdentityKindArgument(policy),
5456
+ variantArgument("full"),
5350
5457
  );
5351
5458
  } else if (command === "verify-active") {
5352
5459
  const teamId = argument("--team-id");
@@ -5360,6 +5467,7 @@ function main(): void {
5360
5467
  argument("--approved-target"),
5361
5468
  argument("--approved-target-identity-sha256"),
5362
5469
  targetIdentityKindArgument(policy),
5470
+ variantArgument("full"),
5363
5471
  );
5364
5472
  } else if (command === "assert-release") {
5365
5473
  assertExpectedRelease(
@@ -2,10 +2,26 @@
2
2
  set -euo pipefail
3
3
  umask 077
4
4
 
5
- if [ "$#" -ne 2 ]; then
6
- echo "Usage: $0 <Recordings.app> <explicit absolute Bun executable path>" >&2
7
- exit 2
5
+ SMOKE_VARIANT="full"
6
+ if [ "$#" -ge 3 ]; then
7
+ case "$3" in
8
+ --variant)
9
+ SMOKE_VARIANT="${4:-full}"
10
+ ;;
11
+ *)
12
+ echo "Usage: $0 <Recordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
13
+ exit 2
14
+ ;;
15
+ esac
16
+ case "$SMOKE_VARIANT" in
17
+ full|bar) ;;
18
+ *)
19
+ echo "SMOKE_VARIANT must be full or bar; got: $SMOKE_VARIANT" >&2
20
+ exit 2
21
+ ;;
22
+ esac
8
23
  fi
24
+ readonly SMOKE_VARIANT
9
25
 
10
26
  HOST_UNAME_EXECUTABLE="/usr/bin/uname"
11
27
  SYSTEM_DIRNAME_EXECUTABLE="/usr/bin/dirname"
@@ -345,6 +361,12 @@ run_smoke() {
345
361
  if [ "$mode" = "permission-helper" ]; then
346
362
  arguments=(--request-permissions "${arguments[@]}")
347
363
  fi
364
+ if [ "$SMOKE_VARIANT" = "bar" ]; then
365
+ # Bar builds are bar-only by construction (compile-time RECORDINGS_BAR_ONLY); the
366
+ # explicit argument is a belt-and-suspenders self-describing launch record so the
367
+ # launch is attributable to the variant even without the build define.
368
+ arguments=(--bar-only "${arguments[@]}")
369
+ fi
348
370
 
349
371
  SMOKE_PID=""
350
372
  SMOKE_PID_START_IDENTITY=""
@@ -428,6 +450,7 @@ run_smoke() {
428
450
  PATH="$SANITIZED_PATH" \
429
451
  TMPDIR="$WORK_DIR" \
430
452
  SMOKE_MODE="$mode" \
453
+ SMOKE_VARIANT="$SMOKE_VARIANT" \
431
454
  SMOKE_FOCUS_EVIDENCE="$SMOKE_FOCUS_EVIDENCE" \
432
455
  EXPECTED_HELPER="$APP_PATH/Contents/Helpers/recordings" \
433
456
  "$BUN_EXECUTABLE" -e '
@@ -454,19 +477,38 @@ run_smoke() {
454
477
  if (JSON.stringify(result.renderedStatusLabels) !== JSON.stringify(expected)) {
455
478
  fail(`unexpected status labels ${JSON.stringify(result.renderedStatusLabels)}`);
456
479
  }
457
- if (result.windowCreationCount !== 1 || result.windowActivationCount !== 2) {
458
- fail("retained-window activation path was not exercised twice");
459
- }
460
- if (result.retainedWindowReused !== true) fail("main window was not retained");
461
- if (result.applicationActivationPolicy !== 0) fail("main window did not set regular activation policy");
462
- if (!result.mainWindowIsVisible || !result.mainWindowCanBecomeKey) {
463
- fail("retained main window was not visible and capable of becoming key");
464
- }
465
- if (!result.applicationIsActive || !result.mainWindowIsKey) {
466
- if (process.env.SMOKE_FOCUS_EVIDENCE !== "ssh-unavailable") {
467
- fail("Open Recordings did not make the retained window active and key");
480
+ if (process.env.SMOKE_VARIANT === "bar") {
481
+ // The bar variant launches windowless by construction: no workspace window is
482
+ // ever created, activated, or reused, the app stays an accessory (LSUIElement),
483
+ // and no window is exposed. This is the runtime half of the P1 #2 regression —
484
+ // the full-build smoke below must keep creating the window.
485
+ if (result.windowCreationCount !== 0 || result.windowActivationCount !== 0) {
486
+ fail("bar-only launch created or activated a workspace window");
487
+ }
488
+ if (result.retainedWindowReused !== false) {
489
+ fail("bar-only launch reused a workspace window");
490
+ }
491
+ if (result.mainWindowIsVisible || result.mainWindowCanBecomeKey || result.mainWindowIsKey) {
492
+ fail("bar-only launch exposed a main window");
493
+ }
494
+ if (result.applicationActivationPolicy !== 1) {
495
+ fail("bar-only launch did not stay an accessory activation policy");
496
+ }
497
+ } else {
498
+ if (result.windowCreationCount !== 1 || result.windowActivationCount !== 2) {
499
+ fail("retained-window activation path was not exercised twice");
500
+ }
501
+ if (result.retainedWindowReused !== true) fail("main window was not retained");
502
+ if (result.applicationActivationPolicy !== 0) fail("main window did not set regular activation policy");
503
+ if (!result.mainWindowIsVisible || !result.mainWindowCanBecomeKey) {
504
+ fail("retained main window was not visible and capable of becoming key");
505
+ }
506
+ if (!result.applicationIsActive || !result.mainWindowIsKey) {
507
+ if (process.env.SMOKE_FOCUS_EVIDENCE !== "ssh-unavailable") {
508
+ fail("Open Recordings did not make the retained window active and key");
509
+ }
510
+ focusEvidenceStatus = "ssh-unavailable";
468
511
  }
469
- focusEvidenceStatus = "ssh-unavailable";
470
512
  }
471
513
  } else if (mode === "resolver") {
472
514
  if (result.menuBarSurfaceCount !== 0 || result.globalHandlersInstalled !== false) {
@@ -570,4 +612,8 @@ run_smoke() {
570
612
  run_smoke normal
571
613
  run_smoke permission-helper
572
614
  run_smoke resolver
573
- echo "Recordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
615
+ if [ "$SMOKE_VARIANT" = "bar" ]; then
616
+ echo "Recordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
617
+ else
618
+ echo "Recordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
619
+ fi
@@ -24,6 +24,9 @@ struct MenuBarStatusLabel: View {
24
24
  struct MenuBarStatusView: View {
25
25
  @ObservedObject var store: RecordingsStore
26
26
  let openRecordings: () -> Void
27
+ /// Bar-only builds have no workspace window to open; the entry point is hidden so
28
+ /// the affordance does not exist where the surface cannot exist.
29
+ let barOnly: Bool
27
30
 
28
31
  var body: some View {
29
32
  VStack(alignment: .leading, spacing: 12) {
@@ -50,11 +53,13 @@ struct MenuBarStatusView: View {
50
53
 
51
54
  Divider()
52
55
 
53
- Button(action: openRecordings) {
54
- Label("Open Recordings", systemImage: "macwindow")
55
- .frame(maxWidth: .infinity, alignment: .leading)
56
+ if !barOnly {
57
+ Button(action: openRecordings) {
58
+ Label("Open Recordings", systemImage: "macwindow")
59
+ .frame(maxWidth: .infinity, alignment: .leading)
60
+ }
61
+ .buttonStyle(.plain)
56
62
  }
57
- .buttonStyle(.plain)
58
63
 
59
64
  SettingsLink {
60
65
  Label("Settings", systemImage: "gearshape")
@@ -56,6 +56,8 @@ final class RecordingsAppDelegate: NSObject, NSApplicationDelegate {
56
56
  final class RecordingsAppState: ObservableObject {
57
57
  let store: RecordingsStore?
58
58
  let declaresMenuBar: Bool
59
+ let barOnly: Bool
60
+ let declaresWindow: Bool
59
61
  let runtimeSmokeProbe: RuntimeSmokeProbe?
60
62
  private let runtimeSmokeMode: String?
61
63
  private let runtimeSmokeOutputPath: String?
@@ -67,6 +69,8 @@ final class RecordingsAppState: ObservableObject {
67
69
 
68
70
  init(plan: PermissionRequestLaunchPlan) {
69
71
  declaresMenuBar = plan.declaresMenuBar
72
+ barOnly = plan.isBarOnly
73
+ declaresWindow = plan.declaresWindow
70
74
  runtimeSmokeMode = plan.runtimeSmokeMode
71
75
  runtimeSmokeOutputPath = plan.runtimeSmokeOutputPath
72
76
  runtimeSmokeAcknowledgementPath = plan.runtimeSmokeAcknowledgementPath
@@ -86,6 +90,14 @@ final class RecordingsAppState: ObservableObject {
86
90
  }
87
91
 
88
92
  func openRecordings() {
93
+ // Bar launches never create the workspace window — on any path, including the
94
+ // reopen handler and the runtime smoke. Keyed on declaresWindow (NOT on
95
+ // declaresMainWindow, which excludes every runtime smoke): a full build's
96
+ // normal-mode smoke must keep exercising the window, and only bar builds finish
97
+ // windowless. This was the cycle-2 P1 of the terminated PR #269 lineage — a
98
+ // windowless branch keyed on declaresMainWindow made the FULL smoke fail
99
+ // deterministically on every build.
100
+ guard declaresWindow else { return }
89
101
  if let store {
90
102
  showWindow(contentView: NSHostingView(rootView: ContentView(store: store)))
91
103
  } else if runtimeSmokeMode == "normal" {
@@ -181,6 +193,23 @@ final class RecordingsAppState: ObservableObject {
181
193
  runtimeSmokeProbe.completed = { [weak self, weak runtimeSmokeProbe] in
182
194
  guard let self, let runtimeSmokeProbe else { return }
183
195
  let accessibility = RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
196
+ if self.barOnly {
197
+ // Windowless by construction: the bar smoke must never create the
198
+ // workspace window, and it reports the windowless state directly. This
199
+ // branch keys on barOnly, NOT on declaresMainWindow — that property
200
+ // excludes every runtime smoke and made the FULL build's smoke fail
201
+ // deterministically in the terminated PR #269 lineage (cycle-2 P1). A
202
+ // nil-window comparison would also read as a retained window (nil ===
203
+ // nil), so the windowless result is finished here explicitly.
204
+ self.finishRuntimeSmoke(
205
+ mode: mode,
206
+ surfaceCount: runtimeSmokeProbe.surfaceAppearances,
207
+ labels: runtimeSmokeProbe.renderedLabels,
208
+ accessibility: accessibility,
209
+ retainedWindowReused: false
210
+ )
211
+ return
212
+ }
184
213
  self.openRecordings()
185
214
  let firstWindow = self.mainWindow
186
215
  self.openRecordings()
@@ -202,6 +231,9 @@ final class RecordingsAppState: ObservableObject {
202
231
  retainedWindowReused: Bool,
203
232
  attempt: Int = 0
204
233
  ) {
234
+ // Only the non-bar smoke reaches this (bar launches finish windowless in the
235
+ // probe completion handler above), so the settled state is exactly the retained
236
+ // window being key, unchanged from the base contract.
205
237
  let windowSettled = NSApplication.shared.isActive && (mainWindow?.isKeyWindow ?? false)
206
238
  guard windowSettled || attempt >= 60 else {
207
239
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
@@ -316,7 +348,11 @@ struct RecordingsApp: App {
316
348
  @SceneBuilder var body: some Scene {
317
349
  MenuBarExtra(isInserted: menuBarInsertion) {
318
350
  if let store = state.store {
319
- MenuBarStatusView(store: store, openRecordings: state.openRecordings)
351
+ MenuBarStatusView(
352
+ store: store,
353
+ openRecordings: state.openRecordings,
354
+ barOnly: state.barOnly
355
+ )
320
356
  } else if state.runtimeSmokeProbe != nil {
321
357
  EmptyView()
322
358
  }
@@ -17,11 +17,11 @@
17
17
  <key>CFBundleIdentifier</key>
18
18
  <string>com.hasna.recordings</string>
19
19
  <key>CFBundleVersion</key>
20
- <string>0.3.1</string>
20
+ <string>0.3.2</string>
21
21
  <key>CFBundlePackageType</key>
22
22
  <string>APPL</string>
23
23
  <key>CFBundleShortVersionString</key>
24
- <string>0.3.1</string>
24
+ <string>0.3.2</string>
25
25
  <key>LSMinimumSystemVersion</key>
26
26
  <string>26.0</string>
27
27
  <key>NSMicrophoneUsageDescription</key>
@@ -7,10 +7,19 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
7
7
  public let runtimeSmokeOutputPath: String?
8
8
  public let runtimeSmokeAcknowledgementPath: String?
9
9
  public let runtimeSmokeCompletionPath: String?
10
+ public let isBarOnly: Bool
10
11
 
11
12
  public var isRuntimeSmoke: Bool { runtimeSmokeMode != nil }
12
13
  public var installsGlobalHandlers: Bool { !isHelper && !isRuntimeSmoke }
13
- public var declaresMainWindow: Bool { !isHelper && !isRuntimeSmoke }
14
+ /// The workspace window may be created only when this holds: never for the permission
15
+ /// helper, never for a bar launch, and regardless of runtime-smoke mode — a full
16
+ /// build's smoke keeps exercising the window deterministically, and only bar builds
17
+ /// finish windowless.
18
+ public var declaresWindow: Bool { !isHelper && !isBarOnly }
19
+ /// Controls the init auto-open and the reopen handler ONLY. Runtime smoke launches
20
+ /// are excluded so the smoke controls window creation deterministically, and bar
21
+ /// launches are excluded so a real bar launch never auto-opens the workspace window.
22
+ public var declaresMainWindow: Bool { !isHelper && !isRuntimeSmoke && !isBarOnly }
14
23
  public var declaresMenuBar: Bool {
15
24
  if isRuntimeSmoke { return runtimeSmokeMode == "normal" }
16
25
  return !isHelper
@@ -31,6 +40,17 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
31
40
  "--runtime-smoke-completion",
32
41
  arguments: arguments
33
42
  )
43
+ #if RECORDINGS_BAR_ONLY
44
+ // A bar build is bar-only by construction on EVERY launch path — installer
45
+ // relaunch, manual `open`, LaunchAgent, smoke. Relying on the argument alone left
46
+ // real launches of the installed bar behaviorally identical to the full app
47
+ // (review lineage hasna/apps#269, cycle-1 P1). The explicit --bar-only argument
48
+ // remains a belt-and-suspenders self-describing launch record; it is never the
49
+ // only carrier of the bar property.
50
+ isBarOnly = true
51
+ #else
52
+ isBarOnly = arguments.contains("--bar-only")
53
+ #endif
34
54
  }
35
55
 
36
56
  private static func optionValue(_ name: String, arguments: [String]) -> String? {