@contextecf/guardian-cli 0.1.6 → 0.1.8

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.
@@ -17061,6 +17061,8 @@ async function runGuardianCli(argv, options = {}) {
17061
17061
  return runReleaseCommand(argv.slice(1), flags, options);
17062
17062
  case "open":
17063
17063
  return runControlTowerOpenCommand(flags, options);
17064
+ case "connect":
17065
+ return runExtensionConnectCommand(flags, options);
17064
17066
  case "status":
17065
17067
  write(stdout, await statusGuardian(flags, options));
17066
17068
  return 0;
@@ -17072,6 +17074,14 @@ async function runGuardianCli(argv, options = {}) {
17072
17074
  return runLaunchCommand(flags, options);
17073
17075
  case "stop":
17074
17076
  return runStopCommand(flags, options);
17077
+ case "pair": {
17078
+ if (!flags.has("json")) {
17079
+ return runExtensionConnectCommand(flags, options);
17080
+ }
17081
+ const result = await daemonPair(flags, options);
17082
+ write(result.exitCode === 0 ? stdout : stderr, result.message);
17083
+ return result.exitCode;
17084
+ }
17075
17085
  case "daemon":
17076
17086
  return runDaemonCommand(argv.slice(1), flags, options);
17077
17087
  case "service":
@@ -17094,6 +17104,9 @@ async function runGuardianCli(argv, options = {}) {
17094
17104
  return runPrivacyCommand(argv.slice(1), flags, options);
17095
17105
  case "mcp":
17096
17106
  return runMcpCommand(argv.slice(1), flags, options);
17107
+ case "coach":
17108
+ case "prompt-coach":
17109
+ return runCoachCommand(argv.slice(1), flags, options);
17097
17110
  case "policy":
17098
17111
  return runPolicyCommand(argv.slice(1), flags, options);
17099
17112
  case "marketplace":
@@ -17116,7 +17129,7 @@ async function runGuardianCli(argv, options = {}) {
17116
17129
  case "help":
17117
17130
  case "--help":
17118
17131
  case "-h":
17119
- write(stdout, `${HELP_TEXT}
17132
+ write(stdout, `${flags.has("advanced") ? ADVANCED_HELP_TEXT : HELP_TEXT}
17120
17133
  `);
17121
17134
  return 0;
17122
17135
  default:
@@ -17195,11 +17208,13 @@ async function installGuardian(flags, options = {}) {
17195
17208
  "guardian start",
17196
17209
  "guardian open",
17197
17210
  "guardian mcp install",
17198
- "guardian daemon pair --json",
17199
17211
  "guardian service install",
17200
17212
  "guardian key-store install --yes",
17201
- "guardian extension open-setup",
17202
- "guardian extension native-host install --extension-id=<browser-extension-id>"
17213
+ "guardian connect"
17214
+ ],
17215
+ advancedCommands: [
17216
+ `guardian extension native-host install --extension-id=${GUARDIAN_HALO_CHROME_EXTENSION_ID}`,
17217
+ "guardian pair --json"
17203
17218
  ]
17204
17219
  };
17205
17220
  return flags.has("json") ? `${JSON.stringify(result, null, 2)}
@@ -17212,7 +17227,8 @@ async function installGuardian(flags, options = {}) {
17212
17227
  `First run: ${GUARDIAN_FIRST_RUN_COMMAND}`,
17213
17228
  `Open Control Tower: ${GUARDIAN_OPEN_CONTROL_TOWER_COMMAND}`,
17214
17229
  `No global install: ${GUARDIAN_NPX_SETUP_COMMAND}`,
17215
- "Next: guardian setup; guardian doctor; guardian readiness; guardian launch; guardian open; guardian mcp install; guardian extension open-setup; guardian daemon pair --json; guardian extension native-host install --extension-id=<browser-extension-id>"
17230
+ "Next: guardian setup; open ContextECF Halo and click Connect; guardian open",
17231
+ "Help: run guardian doctor if setup needs attention."
17216
17232
  ].join("\n")}
17217
17233
  `;
17218
17234
  }
@@ -17220,8 +17236,9 @@ async function runSetupCommand(flags, options) {
17220
17236
  const stdout = options.stdout ?? process.stdout;
17221
17237
  const stderr = options.stderr ?? process.stderr;
17222
17238
  const home = resolveGuardianHome(options.env, options.platform);
17223
- const extensionId = stringFlag(flags, "extension-id") ?? stringFlag(flags, "id");
17224
- if (extensionId && !isValidChromeExtensionId(extensionId)) {
17239
+ const requestedExtensionId = stringFlag(flags, "extension-id") ?? stringFlag(flags, "id");
17240
+ const extensionId = requestedExtensionId ?? GUARDIAN_HALO_CHROME_EXTENSION_ID;
17241
+ if (requestedExtensionId && !isValidChromeExtensionId(requestedExtensionId)) {
17225
17242
  write(
17226
17243
  stderr,
17227
17244
  "Invalid --extension-id. Chrome extension ids must be 32 lowercase letters from a-p.\n"
@@ -17277,10 +17294,80 @@ async function runSetupCommand(flags, options) {
17277
17294
  }
17278
17295
  launch = JSON.parse(launchOutput);
17279
17296
  }
17297
+ const requestedHost = stringFlag(flags, "host") ?? "127.0.0.1";
17298
+ const requestedPort = numberFlag(flags, "port") ?? 4317;
17299
+ const requestedControlTowerUrl = `${formatDaemonBaseUrl(requestedHost, requestedPort)}/control-tower`;
17280
17300
  const resolvedControlTowerUrl = resolveControlTowerUrl(/* @__PURE__ */ new Map(), options.env, profile);
17281
- const controlTowerUrl = launch?.controlTowerUrl ?? (resolvedControlTowerUrl.ok ? resolvedControlTowerUrl.url : GUARDIAN_DEFAULT_CONTROL_TOWER_URL);
17301
+ const controlTowerUrl = launch?.controlTowerUrl ?? (flags.has("host") || flags.has("port") ? requestedControlTowerUrl : resolvedControlTowerUrl.ok ? resolvedControlTowerUrl.url : GUARDIAN_DEFAULT_CONTROL_TOWER_URL);
17302
+ if (!launch && (flags.has("host") || flags.has("port"))) {
17303
+ profile.control_tower = {
17304
+ url: controlTowerUrl,
17305
+ source: "env_or_flag"
17306
+ };
17307
+ await writeProfile(home, profile);
17308
+ }
17282
17309
  const setupUrl = buildControlTowerSetupUrl(controlTowerUrl);
17283
- const nativeHostCommand = `guardian extension native-host install --extension-id=${extensionId ?? "<browser-extension-id>"}`;
17310
+ const nativeHostCommand = `guardian extension native-host install --extension-id=${extensionId}`;
17311
+ let browserHelper;
17312
+ if (flags.has("no-browser-connection") || flags.has("no-browser-helper")) {
17313
+ browserHelper = {
17314
+ success: false,
17315
+ skipped: true,
17316
+ tokenPrinted: false
17317
+ };
17318
+ } else {
17319
+ let nativeHostOutput = "";
17320
+ let nativeHostError = "";
17321
+ const nativeHostFlags = new Map([
17322
+ ["json", true],
17323
+ ["force", true],
17324
+ ["extension-id", extensionId],
17325
+ ...copyStringFlags(flags, ["browser", "target"])
17326
+ ]);
17327
+ if ((options.platform ?? process.platform) === "win32") {
17328
+ nativeHostFlags.set("write-registry", true);
17329
+ nativeHostFlags.set("yes", true);
17330
+ }
17331
+ const nativeHostExitCode = await runExtensionCommand(
17332
+ ["native-host", "install"],
17333
+ nativeHostFlags,
17334
+ {
17335
+ ...options,
17336
+ stdout: {
17337
+ write: (chunk) => {
17338
+ nativeHostOutput += chunk.toString();
17339
+ return true;
17340
+ }
17341
+ },
17342
+ stderr: {
17343
+ write: (chunk) => {
17344
+ nativeHostError += chunk.toString();
17345
+ return true;
17346
+ }
17347
+ }
17348
+ }
17349
+ );
17350
+ if (nativeHostExitCode === 0) {
17351
+ const nativeHost = JSON.parse(nativeHostOutput);
17352
+ browserHelper = {
17353
+ success: true,
17354
+ skipped: false,
17355
+ browser: nativeHost.browser,
17356
+ hostName: nativeHost.hostName,
17357
+ manifestPath: nativeHost.manifestPath,
17358
+ allowedOrigins: nativeHost.allowedOrigins,
17359
+ tokenPrinted: nativeHost.tokenPrinted
17360
+ };
17361
+ } else {
17362
+ browserHelper = {
17363
+ success: false,
17364
+ skipped: false,
17365
+ error: nativeHostError.trim() || "Guardian browser connection setup failed.",
17366
+ exitCode: nativeHostExitCode,
17367
+ tokenPrinted: false
17368
+ };
17369
+ }
17370
+ }
17284
17371
  const result = {
17285
17372
  schema_version: "contextecf/project-guardian-setup/v1",
17286
17373
  success: true,
@@ -17330,10 +17417,14 @@ async function runSetupCommand(flags, options) {
17330
17417
  },
17331
17418
  browserSetup: {
17332
17419
  setupUrl,
17333
- extensionId: extensionId ?? null,
17420
+ extensionId,
17421
+ defaultExtensionId: GUARDIAN_HALO_CHROME_EXTENSION_ID,
17334
17422
  opened: false,
17335
17423
  tokenPrinted: false,
17336
- nativeHostCommand
17424
+ nativeHostCommand,
17425
+ connectInstruction: "Open ContextECF Halo and click Connect. No setup code is needed.",
17426
+ manualFallbackCommand: "guardian pair --json",
17427
+ nativeHost: browserHelper
17337
17428
  },
17338
17429
  nextCommands: [
17339
17430
  "guardian status",
@@ -17342,12 +17433,10 @@ async function runSetupCommand(flags, options) {
17342
17433
  "guardian policy list",
17343
17434
  "guardian posture list",
17344
17435
  "guardian preferences show",
17345
- "guardian extension open-setup",
17346
- "guardian daemon pair --json",
17347
17436
  "guardian service install",
17348
- "guardian key-store install --yes",
17349
- nativeHostCommand
17350
- ]
17437
+ "guardian key-store install --yes"
17438
+ ],
17439
+ advancedCommands: ["guardian extension connect", nativeHostCommand, "guardian pair --json"]
17351
17440
  };
17352
17441
  write(
17353
17442
  stdout,
@@ -17360,8 +17449,11 @@ async function runSetupCommand(flags, options) {
17360
17449
  result.launch.opened ? `Opening: ${result.launch.controlTowerUrl}` : void 0,
17361
17450
  `MCP artifacts: ${result.mcp.writtenFiles.length > 0 ? result.mcp.writtenFiles.join(", ") : "skipped" in result.mcp && result.mcp.skipped ? "skipped" : "none"}`,
17362
17451
  `Browser setup: ${result.browserSetup.setupUrl}`,
17452
+ result.browserSetup.nativeHost.success ? `Browser connection: ready for ContextECF Halo (${result.browserSetup.extensionId})` : result.browserSetup.nativeHost.skipped ? "Browser connection: skipped" : `Browser connection: needs attention${result.browserSetup.nativeHost.error ? ` (${result.browserSetup.nativeHost.error})` : ""}`,
17453
+ "Extension setup: open ContextECF Halo and click Connect. No setup code needed.",
17363
17454
  "Token printed: no",
17364
- `Next: ${result.nextCommands.join("; ")}`
17455
+ `Next: ${result.nextCommands.join("; ")}`,
17456
+ "Help: run guardian connect if the browser says Local Guardian is not connected."
17365
17457
  ].filter(Boolean).join("\n")}
17366
17458
  `
17367
17459
  );
@@ -17436,8 +17528,8 @@ function buildGuardianDistributionReadiness(npmPreviewReady) {
17436
17528
  },
17437
17529
  aiToolIntegration: {
17438
17530
  mcpInstallCommand: "guardian mcp install --client=all --write",
17439
- browserSetupCommand: "guardian extension open-setup",
17440
- nativeHostInstallCommand: "guardian extension native-host install --extension-id=<browser-extension-id>"
17531
+ browserSetupCommand: "guardian connect",
17532
+ nativeHostInstallCommand: `guardian extension native-host install --extension-id=${GUARDIAN_HALO_CHROME_EXTENSION_ID}`
17441
17533
  },
17442
17534
  browserStoreHandoff: {
17443
17535
  status: npmPreviewReady ? "operator_handoff_ready_not_submitted" : "blocked_until_preview_ready",
@@ -17461,7 +17553,7 @@ function buildGuardianDistributionReadiness(npmPreviewReady) {
17461
17553
  boundaries: [
17462
17554
  "Use dry_run=true before public publication.",
17463
17555
  "dry_run=false requires the repository secret NPM_TOKEN and must not print npm tokens, OTP values, or .npmrc contents.",
17464
- "The workflow can publish the npm preview package, upload evidence, and run public install smokes; it does not sign native installers or approve future browser-store listings.",
17556
+ "The workflow can publish the npm preview package, upload evidence, submit future Chrome Web Store updates for review when browser_store_publish=api, and run public install smokes; it does not sign native installers or prove future browser-store approval.",
17465
17557
  "GitHub Actions OIDC/provenance applies to CI release context; manual local publishes are recorded separately and should not claim CI provenance."
17466
17558
  ]
17467
17559
  },
@@ -17482,12 +17574,16 @@ function buildGuardianReleaseDispatchCommand(dryRun) {
17482
17574
  });
17483
17575
  }
17484
17576
  function buildGuardianReleaseDispatchCommandFor(input2) {
17577
+ const publishAuth = input2.publishAuth ?? "token";
17578
+ const browserStorePublish = input2.browserStorePublish ?? "manual";
17485
17579
  return [
17486
17580
  `gh workflow run ${GUARDIAN_RELEASE_DISPATCH_WORKFLOW} \\`,
17487
17581
  ` -f release_version=${input2.releaseVersion} \\`,
17488
17582
  ` -f channels=${input2.channels} \\`,
17489
17583
  ` -f dry_run=${input2.dryRun ? "true" : "false"} \\`,
17490
- ` -f run_docker_smoke=${input2.runDockerSmoke ? "true" : "false"}`
17584
+ ` -f run_docker_smoke=${input2.runDockerSmoke ? "true" : "false"} \\`,
17585
+ ` -f publish_auth=${publishAuth} \\`,
17586
+ ` -f browser_store_publish=${browserStorePublish}`
17491
17587
  ].join("\n");
17492
17588
  }
17493
17589
  async function runReleaseCommand(argv, flags, options) {
@@ -17594,11 +17690,23 @@ async function runReleaseCommand(argv, flags, options) {
17594
17690
  }
17595
17691
  const dryRun = flags.has("publish") ? false : booleanFlag(flags, "dry-run", true);
17596
17692
  const runDockerSmoke = booleanFlag(flags, "run-docker-smoke", true);
17693
+ const publishAuth = stringFlag(flags, "publish-auth") ?? "token";
17694
+ if (!isValidReleasePublishAuth(publishAuth)) {
17695
+ write(stderr, "Invalid --publish-auth. Expected one of: token, trusted_publisher.\n");
17696
+ return 1;
17697
+ }
17698
+ const browserStorePublish = stringFlag(flags, "browser-store-publish") ?? "manual";
17699
+ if (!isValidBrowserStorePublish(browserStorePublish)) {
17700
+ write(stderr, "Invalid --browser-store-publish. Expected one of: manual, api.\n");
17701
+ return 1;
17702
+ }
17597
17703
  const result = buildGuardianReleaseDispatchCommandResult({
17598
17704
  releaseVersion,
17599
17705
  channels,
17600
17706
  dryRun,
17601
- runDockerSmoke
17707
+ runDockerSmoke,
17708
+ publishAuth,
17709
+ browserStorePublish
17602
17710
  });
17603
17711
  write(stdout, renderGuardianReleaseDispatchCommand(result, flags));
17604
17712
  return 0;
@@ -17666,6 +17774,7 @@ function renderGuardianReleaseHelp(result, flags) {
17666
17774
  `;
17667
17775
  }
17668
17776
  function buildGuardianReleaseDispatchCommandResult(input2) {
17777
+ const npmLaneSelected = input2.channels === "all" || input2.channels === "npm";
17669
17778
  return {
17670
17779
  schema_version: "contextecf/project-guardian-release-dispatch-command/v1",
17671
17780
  cli_version: GUARDIAN_CLI_VERSION,
@@ -17674,13 +17783,21 @@ function buildGuardianReleaseDispatchCommandResult(input2) {
17674
17783
  channels: input2.channels,
17675
17784
  dryRun: input2.dryRun,
17676
17785
  runDockerSmoke: input2.runDockerSmoke,
17786
+ publishAuth: input2.publishAuth,
17787
+ browserStorePublish: input2.browserStorePublish,
17677
17788
  command: buildGuardianReleaseDispatchCommandFor(input2),
17678
- ...input2.dryRun ? {} : { requiredPublishCredentialName: "NPM_TOKEN" },
17789
+ ...input2.dryRun || input2.publishAuth === "trusted_publisher" || !npmLaneSelected ? {} : { requiredPublishCredentialName: "NPM_TOKEN" },
17790
+ ...input2.browserStorePublish === "api" && !input2.dryRun ? {
17791
+ requiredBrowserStoreCredentialNames: [...GUARDIAN_CHROME_WEB_STORE_API_SECRET_NAMES]
17792
+ } : {},
17679
17793
  provenance: "github_actions_oidc_supported",
17680
17794
  boundaries: [
17681
17795
  "Use dry_run=true before public publication.",
17682
- "dry_run=false requires the repository secret NPM_TOKEN and must not print npm tokens, OTP values, or .npmrc contents.",
17683
- "This dispatch can publish the npm preview package, upload evidence, and run public install smokes; it does not sign native installers or approve future browser-store listings.",
17796
+ "dry_run=false with publish_auth=token requires the repository secret NPM_TOKEN and must not print npm tokens, OTP values, or .npmrc contents.",
17797
+ "dry_run=false with publish_auth=trusted_publisher requires npm Trusted Publishing to be configured for this GitHub Actions workflow and package.",
17798
+ "browser_store_publish=manual keeps Chrome Web Store upload as an operator ZIP handoff.",
17799
+ "browser_store_publish=api uploads an existing Chrome Web Store item update and submits it for review when Chrome Web Store API secrets are configured.",
17800
+ "This dispatch can publish the npm preview package, upload evidence, submit future Chrome Web Store updates for review, and run public install smokes; it does not sign native installers or prove future browser-store approval.",
17684
17801
  "A release-dispatch command is operator guidance, not production-gate proof."
17685
17802
  ],
17686
17803
  tokenPrinted: false,
@@ -17699,7 +17816,10 @@ function renderGuardianReleaseDispatchCommand(result, flags) {
17699
17816
  `Workflow: ${result.workflow}`,
17700
17817
  `Channels: ${result.channels}`,
17701
17818
  `Docker smoke: ${result.runDockerSmoke ? "yes" : "no"}`,
17819
+ `Publish auth: ${result.publishAuth}`,
17820
+ `Browser store publish: ${result.browserStorePublish}`,
17702
17821
  result.requiredPublishCredentialName ? `Required repository secret: ${result.requiredPublishCredentialName}` : "Required repository secret: not needed for dry run",
17822
+ result.requiredBrowserStoreCredentialNames ? `Required browser-store secrets: ${result.requiredBrowserStoreCredentialNames.join(", ")}` : "Required browser-store secrets: not needed for manual browser handoff",
17703
17823
  "Command:",
17704
17824
  result.command,
17705
17825
  "Boundaries:",
@@ -17714,6 +17834,7 @@ function buildGuardianBrowserStoreHandoffCommandResult() {
17714
17834
  cli_version: GUARDIAN_CLI_VERSION,
17715
17835
  status: "chrome_distribution_proven_next_submission_pack_ready",
17716
17836
  primaryStore: "chrome_web_store",
17837
+ publicListingUrl: GUARDIAN_HALO_CHROME_WEB_STORE_LISTING_URL,
17717
17838
  productionGate: {
17718
17839
  id: "browser-store-distribution",
17719
17840
  status: "proven",
@@ -17780,6 +17901,7 @@ function renderGuardianBrowserStoreHandoffCommand(result, flags) {
17780
17901
  "Project Guardian Browser Store Handoff",
17781
17902
  `Status: ${result.status}`,
17782
17903
  `Primary store: ${result.primaryStore}`,
17904
+ `Public listing: ${result.publicListingUrl}`,
17783
17905
  `Production gate: ${result.productionGate.id} (${result.productionGate.status})`,
17784
17906
  "1. Build submission pack:",
17785
17907
  result.commands.submissionPack,
@@ -17800,15 +17922,23 @@ function renderGuardianBrowserStoreHandoffCommand(result, flags) {
17800
17922
  `;
17801
17923
  }
17802
17924
  function buildGuardianReleaseSecretSetup() {
17925
+ const chromeWebStoreSecretMatcher = GUARDIAN_CHROME_WEB_STORE_API_SECRET_NAMES.join("|");
17803
17926
  return {
17804
17927
  schema_version: "contextecf/project-guardian-release-secret-setup/v1",
17805
17928
  cli_version: GUARDIAN_CLI_VERSION,
17806
17929
  repository: GUARDIAN_RELEASE_REPOSITORY,
17807
17930
  requiredForPublish: "NPM_TOKEN",
17931
+ requiredForBrowserStoreApi: GUARDIAN_CHROME_WEB_STORE_API_SECRET_NAMES,
17808
17932
  githubActionsSecretUrl: `https://github.com/${GUARDIAN_RELEASE_REPOSITORY}/settings/secrets/actions/new`,
17809
17933
  commands: {
17810
17934
  setNpmToken: `gh secret set NPM_TOKEN --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17935
+ setChromeWebStoreClientId: `gh secret set CHROME_WEB_STORE_CLIENT_ID --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17936
+ setChromeWebStoreClientSecret: `gh secret set CHROME_WEB_STORE_CLIENT_SECRET --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17937
+ setChromeWebStoreRefreshToken: `gh secret set CHROME_WEB_STORE_REFRESH_TOKEN --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17938
+ setChromeWebStorePublisherId: `gh secret set CHROME_WEB_STORE_PUBLISHER_ID --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17939
+ setChromeWebStoreExtensionId: `gh secret set CHROME_WEB_STORE_EXTENSION_ID --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions`,
17811
17940
  verifyNpmTokenPresence: `gh secret list --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions | rg '^NPM_TOKEN\\b'`,
17941
+ verifyChromeWebStoreSecretsPresence: `gh secret list --repo ${GUARDIAN_RELEASE_REPOSITORY} --app actions | rg '^(${chromeWebStoreSecretMatcher})\\b'`,
17812
17942
  dryRunDispatch: buildGuardianReleaseDispatchCommandFor({
17813
17943
  dryRun: true,
17814
17944
  releaseVersion: "<version>",
@@ -17820,13 +17950,23 @@ function buildGuardianReleaseSecretSetup() {
17820
17950
  releaseVersion: "<version>",
17821
17951
  channels: "all",
17822
17952
  runDockerSmoke: true
17953
+ }),
17954
+ browserStoreApiPublishDispatch: buildGuardianReleaseDispatchCommandFor({
17955
+ dryRun: false,
17956
+ releaseVersion: "<version>",
17957
+ channels: "browser",
17958
+ runDockerSmoke: true,
17959
+ publishAuth: "trusted_publisher",
17960
+ browserStorePublish: "api"
17823
17961
  })
17824
17962
  },
17825
17963
  boundaries: [
17826
17964
  "NPM_TOKEN belongs only in GitHub Actions repository secrets for guarded dry_run=false npm publication.",
17965
+ "Chrome Web Store API OAuth values belong only in GitHub Actions repository secrets for guarded browser_store_publish=api upload/submission.",
17827
17966
  "Do not paste npm tokens, OTP values, .npmrc contents, signing keys, browser-store credentials, raw prompts, raw responses, private policy packs, or customer content into chat, PR bodies, release evidence, or CLI output.",
17828
17967
  "Use dry_run=true before dry_run=false; dry-run dispatch does not require NPM_TOKEN.",
17829
- "Adding NPM_TOKEN enables the npm preview publish lane only; it does not close signed-installer, browser-store, high-risk desktop, or autonomous app production gates."
17968
+ "Adding NPM_TOKEN enables the npm preview publish lane only; it does not close signed-installer, browser-store, high-risk desktop, or autonomous app production gates.",
17969
+ "Chrome Web Store API secrets enable browser update upload/submission only; official review approval evidence is still required for production browser-store claims."
17830
17970
  ],
17831
17971
  tokenPrinted: false,
17832
17972
  rawContentIncluded: false
@@ -17840,16 +17980,27 @@ function renderGuardianReleaseSecretSetup(result, flags) {
17840
17980
  return `${[
17841
17981
  "Project Guardian Release Secret Setup",
17842
17982
  `Repository: ${result.repository}`,
17843
- `Required for publish: ${result.requiredForPublish}`,
17983
+ `Required for npm publish: ${result.requiredForPublish}`,
17984
+ `Required for Chrome Web Store API: ${result.requiredForBrowserStoreApi.join(", ")}`,
17844
17985
  `GitHub Actions secret page: ${result.githubActionsSecretUrl}`,
17845
- "Set secret:",
17986
+ "NPM publish secret:",
17846
17987
  result.commands.setNpmToken,
17847
- "Verify secret presence:",
17988
+ "Verify npm secret presence:",
17848
17989
  result.commands.verifyNpmTokenPresence,
17990
+ "Chrome Web Store API secrets:",
17991
+ result.commands.setChromeWebStoreClientId,
17992
+ result.commands.setChromeWebStoreClientSecret,
17993
+ result.commands.setChromeWebStoreRefreshToken,
17994
+ result.commands.setChromeWebStorePublisherId,
17995
+ result.commands.setChromeWebStoreExtensionId,
17996
+ "Verify Chrome Web Store API secret presence:",
17997
+ result.commands.verifyChromeWebStoreSecretsPresence,
17849
17998
  "Dry-run release command:",
17850
17999
  result.commands.dryRunDispatch,
17851
- "Publish release command:",
18000
+ "Default publish command with manual browser-store upload:",
17852
18001
  result.commands.publishDispatch,
18002
+ "Browser-store API publish command:",
18003
+ result.commands.browserStoreApiPublishDispatch,
17853
18004
  "Boundaries:",
17854
18005
  ...result.boundaries.map((boundary) => `- ${boundary}`),
17855
18006
  "Token printed: no"
@@ -18070,9 +18221,9 @@ function buildGuardianReleaseVerify(releaseVersion) {
18070
18221
  commands: {
18071
18222
  registryVersion: `npm view ${GUARDIAN_NPM_PACKAGE_NAME}@${releaseVersion} version --registry=${GUARDIAN_NPM_REGISTRY}`,
18072
18223
  publicInstall: `npm install -g ${GUARDIAN_NPM_PACKAGE_NAME}@${releaseVersion}`,
18224
+ chromeWebStoreListing: GUARDIAN_HALO_CHROME_WEB_STORE_LISTING_URL,
18073
18225
  installedVersion: "guardian --version",
18074
18226
  setup: "guardian setup",
18075
- launch: "guardian launch",
18076
18227
  status: "guardian status",
18077
18228
  openControlTower: "guardian open",
18078
18229
  copyPasteLocalSmoke,
@@ -18101,9 +18252,8 @@ function buildGuardianReleasePaste(releaseVersion) {
18101
18252
  "guardian --version",
18102
18253
  "guardian stop",
18103
18254
  "guardian setup",
18104
- "guardian launch",
18105
- "guardian open",
18106
- "guardian status"
18255
+ "guardian status",
18256
+ "guardian open"
18107
18257
  ];
18108
18258
  return {
18109
18259
  schema_version: "contextecf/project-guardian-release-paste/v1",
@@ -18114,9 +18264,9 @@ function buildGuardianReleasePaste(releaseVersion) {
18114
18264
  command: commands.join("\n"),
18115
18265
  commands,
18116
18266
  boundaries: [
18117
- "This paste block verifies public npm visibility, global install, first-run setup, daemon launch, Control Tower open, and status.",
18267
+ "This paste block verifies public npm visibility, global install, stale-process cleanup, first-run setup, Local Guardian status, and Control Tower open.",
18118
18268
  "It does not publish to npm and must not include npm tokens, OTP values, .npmrc contents, signing keys, browser-store credentials, raw prompts, raw responses, private policy packs, or customer content.",
18119
- "guardian stop is included before setup so stale local daemons or local token mismatches are cleared before launch."
18269
+ "guardian stop is included before setup so stale Local Guardian processes or local token mismatches are cleared before setup relaunches the local service."
18120
18270
  ],
18121
18271
  tokenPrinted: false,
18122
18272
  rawContentIncluded: false
@@ -18143,18 +18293,19 @@ function renderGuardianReleaseVerify(result, flags) {
18143
18293
  `Status: ${result.status}`,
18144
18294
  "1. Confirm registry visibility:",
18145
18295
  result.commands.registryVersion,
18146
- "2. Install the published version:",
18296
+ "2. Confirm public Chrome Web Store listing:",
18297
+ result.commands.chromeWebStoreListing,
18298
+ "3. Install the published version:",
18147
18299
  result.commands.publicInstall,
18148
- "3. Confirm installed CLI version:",
18300
+ "4. Confirm installed CLI version:",
18149
18301
  result.commands.installedVersion,
18150
- "4. Run first-use smoke:",
18302
+ "5. Run first-use smoke:",
18151
18303
  result.commands.setup,
18152
- result.commands.launch,
18153
18304
  result.commands.status,
18154
18305
  result.commands.openControlTower,
18155
- "5. Copy-paste local smoke block:",
18306
+ "6. Copy-paste local smoke block:",
18156
18307
  result.commands.copyPasteLocalSmoke,
18157
- "6. Verify release evidence:",
18308
+ "7. Verify release evidence:",
18158
18309
  result.commands.publishStatusVerification,
18159
18310
  result.commands.finishLineStatus,
18160
18311
  "Evidence outputs:",
@@ -18171,6 +18322,12 @@ function isValidReleaseVersion(value) {
18171
18322
  function isValidReleaseChannels(value) {
18172
18323
  return ["all", "npm", "browser", "evidence"].includes(value);
18173
18324
  }
18325
+ function isValidReleasePublishAuth(value) {
18326
+ return value === "token" || value === "trusted_publisher";
18327
+ }
18328
+ function isValidBrowserStorePublish(value) {
18329
+ return value === "manual" || value === "api";
18330
+ }
18174
18331
  function renderGuardianProductionReadiness(result, flags) {
18175
18332
  if (flags.has("json")) {
18176
18333
  return `${JSON.stringify(result, null, 2)}
@@ -18273,9 +18430,9 @@ async function statusGuardian(flags, options) {
18273
18430
  `Profile: ${home}`,
18274
18431
  `Runtime profile: ${result.runtime.profileDaemonStatus}`,
18275
18432
  `Control Tower: ${result.controlTowerUrl ?? "not configured"}`,
18276
- `Daemon health: ${result.daemon.status}${result.daemon.statusCode ? ` (HTTP ${result.daemon.statusCode})` : ""}`,
18277
- result.daemon.error ? `Daemon detail: ${result.daemon.error}` : void 0,
18278
- result.daemonVersionMismatch ? `Daemon version: ${result.daemonVersionMismatch.daemonVersion} (this CLI is ${result.daemonVersionMismatch.cliVersion} \u2014 versions differ, likely because the CLI was reinstalled or changed version while this daemon kept running; run guardian stop && guardian launch to restart it on the current version)` : void 0,
18433
+ `Local Guardian: ${result.daemon.status}${result.daemon.statusCode ? ` (HTTP ${result.daemon.statusCode})` : ""}`,
18434
+ result.daemon.error ? `Connection detail: ${result.daemon.error}` : void 0,
18435
+ result.daemonVersionMismatch ? `Local Guardian version: ${result.daemonVersionMismatch.daemonVersion} (this CLI is ${result.daemonVersionMismatch.cliVersion} \u2014 versions differ, likely because the CLI was reinstalled or changed version while Local Guardian kept running; run guardian stop && guardian launch to restart it on the current version)` : void 0,
18279
18436
  `MCP stdio: ${result.runtime.mcpStdioAvailable ? "available" : "unavailable"}`,
18280
18437
  `Posture Profile: ${result.postureProfile.selected}`,
18281
18438
  `Prompt Coach: ${result.preferences.promptCoachMode}`,
@@ -18382,7 +18539,7 @@ async function runLaunchCommand(flags, options) {
18382
18539
  nextCommands: [
18383
18540
  "guardian open",
18384
18541
  "guardian status",
18385
- "guardian daemon pair --json",
18542
+ "Open ContextECF Halo and click Connect",
18386
18543
  "guardian policy list"
18387
18544
  ]
18388
18545
  };
@@ -18391,7 +18548,7 @@ async function runLaunchCommand(flags, options) {
18391
18548
  flags.has("json") ? `${JSON.stringify(result, null, 2)}
18392
18549
  ` : `${[
18393
18550
  "Project Guardian launched.",
18394
- `Daemon: ${result.daemonUrl}${result.reusedExistingDaemon ? " (already running)" : ""}`,
18551
+ `Local Guardian: ${result.daemonUrl}${result.reusedExistingDaemon ? " (already running)" : ""}`,
18395
18552
  `Control Tower: ${result.controlTowerUrl}`,
18396
18553
  shouldOpen ? `Opening: ${result.controlTowerUrl}` : void 0,
18397
18554
  "Next: guardian open; guardian status"
@@ -18502,14 +18659,14 @@ async function runStartCommand(flags, options) {
18502
18659
  controlTowerUrl: requestedControlTowerUrl,
18503
18660
  healthUrl,
18504
18661
  healthStatus: "reachable",
18505
- nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
18662
+ nextCommands: ["guardian open", "guardian status", "Open ContextECF Halo and click Connect"]
18506
18663
  };
18507
18664
  write(
18508
18665
  stdout,
18509
18666
  flags.has("json") ? `${JSON.stringify(result2, null, 2)}
18510
18667
  ` : `${[
18511
- "Guardian daemon already running.",
18512
- `Daemon: ${requestedDaemonUrl}`,
18668
+ "Local Guardian already running.",
18669
+ `Local Guardian: ${requestedDaemonUrl}`,
18513
18670
  `Control Tower: ${requestedControlTowerUrl}`,
18514
18671
  "Next: guardian open"
18515
18672
  ].join("\n")}
@@ -18518,7 +18675,7 @@ async function runStartCommand(flags, options) {
18518
18675
  return 0;
18519
18676
  }
18520
18677
  if (!options.startDetachedDaemon) {
18521
- write(stderr, "Guardian detached daemon launcher is unavailable in this runtime.\n");
18678
+ write(stderr, "Guardian local background launcher is unavailable in this runtime.\n");
18522
18679
  return 1;
18523
18680
  }
18524
18681
  const started = await options.startDetachedDaemon({ home, host, port: selected.port });
@@ -18539,14 +18696,14 @@ async function runStartCommand(flags, options) {
18539
18696
  pid: started.pid,
18540
18697
  healthUrl,
18541
18698
  healthStatus: existing.error ? "unreachable" : "not_reachable",
18542
- nextCommands: ["guardian open", "guardian status", "guardian daemon pair --json"]
18699
+ nextCommands: ["guardian open", "guardian status", "Open ContextECF Halo and click Connect"]
18543
18700
  };
18544
18701
  write(
18545
18702
  stdout,
18546
18703
  flags.has("json") ? `${JSON.stringify(result, null, 2)}
18547
18704
  ` : `${[
18548
- "Guardian daemon start requested.",
18549
- `Daemon: ${daemonUrl}`,
18705
+ "Local Guardian start requested.",
18706
+ `Local Guardian: ${daemonUrl}`,
18550
18707
  `Control Tower: ${controlTowerUrl}`,
18551
18708
  started.pid ? `PID: ${started.pid}` : void 0,
18552
18709
  "Next: guardian open"
@@ -18557,8 +18714,8 @@ async function runStartCommand(flags, options) {
18557
18714
  }
18558
18715
  function renderDaemonTokenMismatchMessage(requestedDaemonUrl, port) {
18559
18716
  return [
18560
- `Guardian found another daemon or container at ${requestedDaemonUrl}, but it rejected this profile's local runtime token.`,
18561
- "This usually means an older Guardian daemon or Docker demo is still using the port.",
18717
+ `Guardian found another local service or container at ${requestedDaemonUrl}, but it rejected this profile's local connection token.`,
18718
+ "This usually means an older Guardian background service or Docker demo is still using the port.",
18562
18719
  `Recovery: stop the other process/container, or run Guardian on a different port with guardian launch --port=${port + 1}.`,
18563
18720
  `Diagnostics: guardian status; docker ps --filter publish=${port}`,
18564
18721
  ""
@@ -18591,7 +18748,7 @@ async function runStopCommand(flags, options) {
18591
18748
  if (!stopped.ok) {
18592
18749
  write(
18593
18750
  stderr,
18594
- `Guardian daemon did not stop cleanly${stopped.statusCode ? ` (HTTP ${stopped.statusCode})` : ""}: ${stopped.error ?? "unknown_error"}
18751
+ `Local Guardian did not stop cleanly${stopped.statusCode ? ` (HTTP ${stopped.statusCode})` : ""}: ${stopped.error ?? "unknown_error"}
18595
18752
  `
18596
18753
  );
18597
18754
  return 1;
@@ -18602,12 +18759,12 @@ async function runStopCommand(flags, options) {
18602
18759
  success: true,
18603
18760
  daemonStatus: profile.runtime.daemon_status,
18604
18761
  stopUrl: stopUrl.url,
18605
- note: "Guardian daemon stop requested."
18762
+ note: "Local Guardian stop requested."
18606
18763
  };
18607
18764
  write(
18608
18765
  stdout,
18609
18766
  flags.has("json") ? `${JSON.stringify(result, null, 2)}
18610
- ` : `${["Guardian daemon stop requested.", "Status: not_started_mvp"].join("\n")}
18767
+ ` : `${["Local Guardian stop requested.", "Status: not_started_mvp"].join("\n")}
18611
18768
  `
18612
18769
  );
18613
18770
  return 0;
@@ -18679,7 +18836,7 @@ Proof: ${result2.proofPath}
18679
18836
  }
18680
18837
  const host = stringFlag(flags, "host") ?? "127.0.0.1";
18681
18838
  if (!isLoopbackHost(host)) {
18682
- write(stderr, "Guardian service supervision only accepts loopback daemon hosts.\n");
18839
+ write(stderr, "Guardian service supervision only accepts loopback Local Guardian hosts.\n");
18683
18840
  return 1;
18684
18841
  }
18685
18842
  const port = numberFlag(flags, "port") ?? 4317;
@@ -18936,7 +19093,7 @@ async function daemonPair(flags, options) {
18936
19093
  [GUARDIAN_BRIDGE_CONFIG_STORAGE_KEY]: bridgeConfig,
18937
19094
  [GUARDIAN_BRIDGE_TOKEN_STORAGE_KEY]: token
18938
19095
  },
18939
- note: "Store this token in the Guardian browser extension local settings, then run guardian daemon serve."
19096
+ note: "Advanced fallback only. Most users should run guardian setup, open ContextECF Halo, and click Connect."
18940
19097
  };
18941
19098
  if (flags.has("json")) {
18942
19099
  return { exitCode: 0, message: `${JSON.stringify(pairing, null, 2)}
@@ -18945,13 +19102,10 @@ async function daemonPair(flags, options) {
18945
19102
  return {
18946
19103
  exitCode: 0,
18947
19104
  message: `${[
18948
- "Guardian browser bridge pairing",
18949
- `Storage key: ${pairing.storageKey}`,
18950
- `Loopback URL: ${pairing.loopbackBaseUrl}`,
18951
- `Transport: ${pairing.transport}`,
18952
- `Native host: ${pairing.nativeHostName}`,
18953
- `Token: ${pairing.token}`,
18954
- pairing.note
19105
+ "Guardian browser connection uses one-click setup now.",
19106
+ "Run guardian connect, then open ContextECF Halo and click Connect.",
19107
+ "Support setup codes are only printed by guardian pair --json.",
19108
+ "Token printed: no"
18955
19109
  ].join("\n")}
18956
19110
  `
18957
19111
  };
@@ -18960,6 +19114,9 @@ async function runExtensionCommand(argv, flags, options) {
18960
19114
  const subcommand = argv[0] ?? "native-host";
18961
19115
  const stdout = options.stdout ?? process.stdout;
18962
19116
  const stderr = options.stderr ?? process.stderr;
19117
+ if (subcommand === "connect") {
19118
+ return runExtensionConnectCommand(flags, options);
19119
+ }
18963
19120
  if (subcommand === "open-setup") {
18964
19121
  return runExtensionOpenSetupCommand(flags, options);
18965
19122
  }
@@ -19147,13 +19304,18 @@ Re-run with --force to replace it.
19147
19304
  registryMutationRequested: writeRegistry,
19148
19305
  registryProofPath,
19149
19306
  tokenPrinted: false,
19150
- nextCommands: ["guardian launch", "guardian daemon pair --json"]
19307
+ nextCommands: [
19308
+ "guardian launch",
19309
+ "Open ContextECF Halo and click Connect",
19310
+ "guardian extension native-host status"
19311
+ ],
19312
+ advancedCommands: ["guardian pair --json"]
19151
19313
  };
19152
19314
  write(
19153
19315
  stdout,
19154
19316
  flags.has("json") ? `${JSON.stringify(result, null, 2)}
19155
19317
  ` : `${[
19156
- "Guardian native host installed.",
19318
+ "Guardian browser connection installed.",
19157
19319
  `Browser: ${browser}`,
19158
19320
  `Host: ${plan.hostName}`,
19159
19321
  `Manifest: ${plan.manifestPath}`,
@@ -19168,6 +19330,121 @@ Re-run with --force to replace it.
19168
19330
  );
19169
19331
  return 0;
19170
19332
  }
19333
+ async function runExtensionConnectCommand(flags, options) {
19334
+ const stdout = options.stdout ?? process.stdout;
19335
+ const stderr = options.stderr ?? process.stderr;
19336
+ const requestedExtensionId = stringFlag(flags, "extension-id") ?? stringFlag(flags, "id");
19337
+ const extensionId = requestedExtensionId ?? GUARDIAN_HALO_CHROME_EXTENSION_ID;
19338
+ if (requestedExtensionId && !isValidChromeExtensionId(requestedExtensionId)) {
19339
+ write(
19340
+ stderr,
19341
+ "Invalid --extension-id. Chrome extension ids must be 32 lowercase letters from a-p.\n"
19342
+ );
19343
+ return 1;
19344
+ }
19345
+ const install = JSON.parse(await installGuardian(/* @__PURE__ */ new Map([["json", true]]), options));
19346
+ let launchOutput = "";
19347
+ const launchFlags = new Map([
19348
+ ["json", true],
19349
+ ["print", true],
19350
+ ...copyStringFlags(flags, ["host", "port"])
19351
+ ]);
19352
+ const launchExitCode = await runLaunchCommand(launchFlags, {
19353
+ ...options,
19354
+ stdout: {
19355
+ write: (chunk) => {
19356
+ launchOutput += chunk.toString();
19357
+ return true;
19358
+ }
19359
+ },
19360
+ stderr
19361
+ });
19362
+ if (launchExitCode !== 0) {
19363
+ return launchExitCode;
19364
+ }
19365
+ const launch = JSON.parse(launchOutput);
19366
+ const helperFlags = new Map([
19367
+ ["json", true],
19368
+ ["force", true],
19369
+ ["extension-id", extensionId],
19370
+ ...copyStringFlags(flags, ["browser", "target"])
19371
+ ]);
19372
+ if ((options.platform ?? process.platform) === "win32") {
19373
+ helperFlags.set("write-registry", true);
19374
+ helperFlags.set("yes", true);
19375
+ }
19376
+ let helperOutput = "";
19377
+ let helperError = "";
19378
+ const helperExitCode = await runExtensionCommand(["native-host", "install"], helperFlags, {
19379
+ ...options,
19380
+ stdout: {
19381
+ write: (chunk) => {
19382
+ helperOutput += chunk.toString();
19383
+ return true;
19384
+ }
19385
+ },
19386
+ stderr: {
19387
+ write: (chunk) => {
19388
+ helperError += chunk.toString();
19389
+ return true;
19390
+ }
19391
+ }
19392
+ });
19393
+ if (helperExitCode !== 0) {
19394
+ write(
19395
+ stderr,
19396
+ helperError.trim() ? `${helperError.trim()}
19397
+ ` : "Guardian could not prepare the browser connection. Run guardian setup, then try again.\n"
19398
+ );
19399
+ return helperExitCode;
19400
+ }
19401
+ const helper = JSON.parse(helperOutput);
19402
+ const result = {
19403
+ schema_version: "contextecf/project-guardian-browser-connect/v1",
19404
+ success: true,
19405
+ extensionId,
19406
+ defaultExtensionId: GUARDIAN_HALO_CHROME_EXTENSION_ID,
19407
+ install: {
19408
+ profileDir: install.profileDir,
19409
+ tokenCreated: install.tokenCreated,
19410
+ tokenPrinted: false
19411
+ },
19412
+ localGuardian: {
19413
+ running: true,
19414
+ opened: launch.opened,
19415
+ reusedExistingDaemon: launch.reusedExistingDaemon,
19416
+ daemonUrl: launch.daemonUrl,
19417
+ controlTowerUrl: launch.controlTowerUrl,
19418
+ pid: launch.pid,
19419
+ healthStatus: launch.healthStatus
19420
+ },
19421
+ browser: helper.browser,
19422
+ helperInstalled: true,
19423
+ helperStatus: {
19424
+ hostName: helper.hostName,
19425
+ manifestPath: helper.manifestPath,
19426
+ allowedOrigins: helper.allowedOrigins,
19427
+ tokenPrinted: helper.tokenPrinted
19428
+ },
19429
+ tokenPrinted: false,
19430
+ nextCommands: ["Open ContextECF Halo and click Connect", "guardian open", "guardian status"],
19431
+ supportCommands: ["guardian extension open-setup --print", "guardian pair --json"]
19432
+ };
19433
+ write(
19434
+ stdout,
19435
+ flags.has("json") ? `${JSON.stringify(result, null, 2)}
19436
+ ` : `${[
19437
+ "Guardian browser connection is ready.",
19438
+ `Local Guardian: ${result.localGuardian.daemonUrl}${result.localGuardian.reusedExistingDaemon ? " (already running)" : ""}`,
19439
+ `Browser: ${result.browser}`,
19440
+ "Next: open ContextECF Halo and click Connect.",
19441
+ "No setup code needed.",
19442
+ "Token printed: no"
19443
+ ].join("\n")}
19444
+ `
19445
+ );
19446
+ return 0;
19447
+ }
19171
19448
  async function runExtensionOpenSetupCommand(flags, options) {
19172
19449
  const stdout = options.stdout ?? process.stdout;
19173
19450
  const stderr = options.stderr ?? process.stderr;
@@ -19177,8 +19454,9 @@ async function runExtensionOpenSetupCommand(flags, options) {
19177
19454
  write(stderr, "Guardian is not installed. Run guardian install first.\n");
19178
19455
  return 1;
19179
19456
  }
19180
- const extensionId = stringFlag(flags, "extension-id") ?? stringFlag(flags, "id");
19181
- if (extensionId && !isValidChromeExtensionId(extensionId)) {
19457
+ const requestedExtensionId = stringFlag(flags, "extension-id") ?? stringFlag(flags, "id");
19458
+ const extensionId = requestedExtensionId ?? GUARDIAN_HALO_CHROME_EXTENSION_ID;
19459
+ if (requestedExtensionId && !isValidChromeExtensionId(requestedExtensionId)) {
19182
19460
  write(
19183
19461
  stderr,
19184
19462
  "Invalid --extension-id. Chrome extension ids must be 32 lowercase letters from a-p.\n"
@@ -19192,22 +19470,24 @@ async function runExtensionOpenSetupCommand(flags, options) {
19192
19470
  return 1;
19193
19471
  }
19194
19472
  const setupUrl = buildControlTowerSetupUrl(resolved.url);
19195
- const nativeHostCommand = `guardian extension native-host install --extension-id=${extensionId ?? "<browser-extension-id>"}`;
19473
+ const nativeHostCommand = `guardian extension native-host install --extension-id=${extensionId}`;
19196
19474
  const result = {
19197
19475
  schema_version: "contextecf/project-guardian-extension-setup/v1",
19198
19476
  success: true,
19199
19477
  opened: !flags.has("print"),
19200
19478
  controlTowerUrl: resolved.url,
19201
19479
  setupUrl,
19202
- extensionId: extensionId ?? null,
19480
+ extensionId,
19481
+ defaultExtensionId: GUARDIAN_HALO_CHROME_EXTENSION_ID,
19203
19482
  tokenPrinted: false,
19204
19483
  nextCommands: [
19205
19484
  "guardian launch",
19206
- "guardian daemon pair --json",
19207
19485
  nativeHostCommand,
19208
- "guardian extension native-host status"
19486
+ "guardian extension native-host status",
19487
+ "Open ContextECF Halo and click Connect"
19209
19488
  ],
19210
- note: extensionId ? "Guardian browser setup opened with a concrete native host install command." : "Guardian browser setup opened with placeholder extension-id guidance."
19489
+ advancedCommands: ["guardian pair --json"],
19490
+ note: extensionId ? "Guardian browser setup opened for ContextECF Halo." : "Guardian browser setup opened."
19211
19491
  };
19212
19492
  if (result.opened) {
19213
19493
  const opener = options.openUrl ?? ((url2) => openUrlInDefaultBrowser(url2, options));
@@ -19219,7 +19499,8 @@ async function runExtensionOpenSetupCommand(flags, options) {
19219
19499
  ` : `${[
19220
19500
  result.opened ? `Opening Guardian browser setup: ${setupUrl}` : `Guardian browser setup: ${setupUrl}`,
19221
19501
  "Token printed: no",
19222
- `Next: ${result.nextCommands.join("; ")}`
19502
+ `Next: ${result.nextCommands.join("; ")}`,
19503
+ `Advanced fallback: ${result.advancedCommands.join("; ")}`
19223
19504
  ].join("\n")}
19224
19505
  `
19225
19506
  );
@@ -19746,7 +20027,7 @@ function buildGuardianServiceArtifact(input2) {
19746
20027
  if (input2.platform === "systemd") {
19747
20028
  return `${[
19748
20029
  "[Unit]",
19749
- "Description=Project Guardian local daemon",
20030
+ "Description=Project Guardian Local Guardian background service",
19750
20031
  "After=network.target",
19751
20032
  "",
19752
20033
  "[Service]",
@@ -19765,7 +20046,7 @@ function buildGuardianServiceArtifact(input2) {
19765
20046
  '<?xml version="1.0" encoding="UTF-16"?>',
19766
20047
  '<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
19767
20048
  " <RegistrationInfo>",
19768
- " <Description>Project Guardian local daemon</Description>",
20049
+ " <Description>Project Guardian Local Guardian background service</Description>",
19769
20050
  " </RegistrationInfo>",
19770
20051
  " <Triggers>",
19771
20052
  " <LogonTrigger><Enabled>true</Enabled></LogonTrigger>",
@@ -20747,6 +21028,59 @@ async function runReceiptsCommand(argv, flags, options) {
20747
21028
  write(stdout, await verifyReceipts(flags, options));
20748
21029
  return 0;
20749
21030
  }
21031
+ async function runCoachCommand(argv, flags, options) {
21032
+ const subcommand = argv[0] ?? "options";
21033
+ const stdout = options.stdout ?? process.stdout;
21034
+ const stderr = options.stderr ?? process.stderr;
21035
+ if (subcommand === "options" || subcommand === "catalog") {
21036
+ write(stdout, renderGuardianPromptCardOptions(buildGuardianPromptCardOptions(), flags));
21037
+ return 0;
21038
+ }
21039
+ write(stderr, `Unknown Guardian coach command: ${subcommand}
21040
+ `);
21041
+ return 1;
21042
+ }
21043
+ function buildGuardianPromptCardOptions() {
21044
+ return {
21045
+ schema_version: "contextecf/project-guardian-prompt-card-options/v1",
21046
+ cli_version: GUARDIAN_CLI_VERSION,
21047
+ status: "catalog_ready",
21048
+ summary: "Guardian prompt cards keep the user in control: warnings and safer rewrites can be sent as-is, while hard blocks cannot.",
21049
+ catalog: GUARDIAN_PROMPT_CARD_OPTIONS_CATALOG,
21050
+ rules: [
21051
+ "Show a clear reason before asking the user to decide.",
21052
+ "Offer Send as-is for warnings and safer rewrites when policy allows override.",
21053
+ "Never offer Send as-is for hard blocks or unavailable Local Guardian checks.",
21054
+ "Keep Edit prompt available as the safe path for every visible send-time card.",
21055
+ "Do not store raw sensitive prompt text in this catalog or CLI output."
21056
+ ],
21057
+ tokenPrinted: false,
21058
+ rawContentIncluded: false
21059
+ };
21060
+ }
21061
+ function renderGuardianPromptCardOptions(result, flags) {
21062
+ if (flags.has("json")) {
21063
+ return `${JSON.stringify(result, null, 2)}
21064
+ `;
21065
+ }
21066
+ return `${[
21067
+ "Guardian Prompt Card Options",
21068
+ result.summary,
21069
+ "",
21070
+ "Programmed cards:",
21071
+ ...result.catalog.map((entry) => {
21072
+ const visibleActions = entry.actions.filter((action) => action.visibleToUser).map((action) => action.label);
21073
+ const labels = visibleActions.length > 0 ? visibleActions.join(", ") : "Prompt sends automatically";
21074
+ return `- ${entry.cardStyle}: ${labels}`;
21075
+ }),
21076
+ "",
21077
+ "Rules:",
21078
+ ...result.rules.map((rule) => `- ${rule}`),
21079
+ "Token printed: no",
21080
+ "Raw prompt content included: no"
21081
+ ].join("\n")}
21082
+ `;
21083
+ }
20750
21084
  async function runPolicyCommand(argv, flags, options) {
20751
21085
  const subcommand = argv[0] ?? "list";
20752
21086
  const stdout = options.stdout ?? process.stdout;
@@ -23166,13 +23500,13 @@ function openUrlInDefaultBrowser(url2, options = {}) {
23166
23500
  });
23167
23501
  });
23168
23502
  }
23169
- var GUARDIAN_CLI_VERSION, GUARDIAN_PROFILE_SCHEMA_VERSION, GUARDIAN_NPM_PACKAGE_NAME, GUARDIAN_NPM_REGISTRY, GUARDIAN_GLOBAL_INSTALL_COMMAND, GUARDIAN_NPX_SETUP_COMMAND, GUARDIAN_FIRST_RUN_COMMAND, GUARDIAN_OPEN_CONTROL_TOWER_COMMAND, GUARDIAN_RELEASE_DISPATCH_WORKFLOW, GUARDIAN_RELEASE_REPOSITORY, GUARDIAN_DEFAULT_CONTROL_TOWER_URL, GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ENV, GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ID_ENV, GUARDIAN_SQLITE_PAGE_ENCRYPTION_KEY_ENV, GUARDIAN_SQLITE_PAGE_ENCRYPTION_KEY_ID_ENV, GUARDIAN_DESKTOP_ADAPTER_SIMULATE_OPEN_ENV, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_ID, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_VERSION, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_PROOF_SCHEMA_VERSION, GUARDIAN_SERVICE_PROOF_SCHEMA_VERSION, GUARDIAN_OS_KEY_STORAGE_PROOF_SCHEMA_VERSION, GUARDIAN_WINDOWS_NATIVE_HOST_REGISTRY_PROOF_SCHEMA_VERSION, PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION2, PROMPT_COACH_MODES, DEFAULT_PROMPT_COACH_DAILY_SOFT_CARD_LIMIT, GUARDIAN_POSTURE_PROFILE_IDS, GUARDIAN_POSTURE_PROFILE_CATALOG, GUARDIAN_POLICY_PACK_MARKETPLACE_CATALOG, GUARDIAN_PROVIDER_IDS, GUARDIAN_SOURCE_TYPES, GUARDIAN_NOT_PROVEN_PRODUCTION_GATES, DEFAULT_POLICY_PACKS, GUARDIAN_BRIDGE_CONFIG_STORAGE_KEY, GUARDIAN_BRIDGE_TOKEN_STORAGE_KEY, GUARDIAN_BRIDGE_CONFIG_SCHEMA_VERSION, GUARDIAN_NATIVE_HOST_NAME, MCP_CLIENTS, NATIVE_HOST_BROWSERS, HELP_TEXT, normalizeGuardianPreferences, GUARDIAN_DATA_FILE_NAMES, GUARDIAN_DATA_DIR_NAMES, GUARDIAN_MIN_SUPPORTED_NODE_VERSION;
23503
+ var GUARDIAN_CLI_VERSION, GUARDIAN_PROFILE_SCHEMA_VERSION, GUARDIAN_NPM_PACKAGE_NAME, GUARDIAN_NPM_REGISTRY, GUARDIAN_GLOBAL_INSTALL_COMMAND, GUARDIAN_NPX_SETUP_COMMAND, GUARDIAN_FIRST_RUN_COMMAND, GUARDIAN_OPEN_CONTROL_TOWER_COMMAND, GUARDIAN_HALO_CHROME_EXTENSION_ID, GUARDIAN_HALO_CHROME_WEB_STORE_LISTING_URL, GUARDIAN_RELEASE_DISPATCH_WORKFLOW, GUARDIAN_RELEASE_REPOSITORY, GUARDIAN_CHROME_WEB_STORE_API_SECRET_NAMES, GUARDIAN_DEFAULT_CONTROL_TOWER_URL, GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ENV, GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ID_ENV, GUARDIAN_SQLITE_PAGE_ENCRYPTION_KEY_ENV, GUARDIAN_SQLITE_PAGE_ENCRYPTION_KEY_ID_ENV, GUARDIAN_DESKTOP_ADAPTER_SIMULATE_OPEN_ENV, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_ID, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_VERSION, GUARDIAN_DESKTOP_URL_OPEN_ADAPTER_PROOF_SCHEMA_VERSION, GUARDIAN_SERVICE_PROOF_SCHEMA_VERSION, GUARDIAN_OS_KEY_STORAGE_PROOF_SCHEMA_VERSION, GUARDIAN_WINDOWS_NATIVE_HOST_REGISTRY_PROOF_SCHEMA_VERSION, PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION2, PROMPT_COACH_MODES, DEFAULT_PROMPT_COACH_DAILY_SOFT_CARD_LIMIT, GUARDIAN_PROMPT_CARD_OPTIONS_CATALOG, GUARDIAN_POSTURE_PROFILE_IDS, GUARDIAN_POSTURE_PROFILE_CATALOG, GUARDIAN_POLICY_PACK_MARKETPLACE_CATALOG, GUARDIAN_PROVIDER_IDS, GUARDIAN_SOURCE_TYPES, GUARDIAN_NOT_PROVEN_PRODUCTION_GATES, DEFAULT_POLICY_PACKS, GUARDIAN_BRIDGE_CONFIG_STORAGE_KEY, GUARDIAN_BRIDGE_TOKEN_STORAGE_KEY, GUARDIAN_BRIDGE_CONFIG_SCHEMA_VERSION, GUARDIAN_NATIVE_HOST_NAME, MCP_CLIENTS, NATIVE_HOST_BROWSERS, HELP_TEXT, ADVANCED_HELP_TEXT, normalizeGuardianPreferences, GUARDIAN_DATA_FILE_NAMES, GUARDIAN_DATA_DIR_NAMES, GUARDIAN_MIN_SUPPORTED_NODE_VERSION;
23170
23504
  var init_runtime = __esm({
23171
23505
  "packages/guardian-cli/src/runtime.ts"() {
23172
23506
  "use strict";
23173
23507
  init_src();
23174
23508
  init_src2();
23175
- GUARDIAN_CLI_VERSION = "0.1.6";
23509
+ GUARDIAN_CLI_VERSION = "0.1.8";
23176
23510
  GUARDIAN_PROFILE_SCHEMA_VERSION = "contextecf/project-guardian-profile/v1";
23177
23511
  GUARDIAN_NPM_PACKAGE_NAME = "@contextecf/guardian-cli";
23178
23512
  GUARDIAN_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -23180,8 +23514,17 @@ var init_runtime = __esm({
23180
23514
  GUARDIAN_NPX_SETUP_COMMAND = `npx ${GUARDIAN_NPM_PACKAGE_NAME}@latest setup`;
23181
23515
  GUARDIAN_FIRST_RUN_COMMAND = "guardian setup";
23182
23516
  GUARDIAN_OPEN_CONTROL_TOWER_COMMAND = "guardian open";
23517
+ GUARDIAN_HALO_CHROME_EXTENSION_ID = "dopkdppbiakhbglhgjfacfdhpheejkck";
23518
+ GUARDIAN_HALO_CHROME_WEB_STORE_LISTING_URL = `https://chromewebstore.google.com/detail/contextecf-halo/${GUARDIAN_HALO_CHROME_EXTENSION_ID}`;
23183
23519
  GUARDIAN_RELEASE_DISPATCH_WORKFLOW = "guardian-release-dispatch.yml";
23184
23520
  GUARDIAN_RELEASE_REPOSITORY = "Intelligent-Context-AI-Inc/ContextECF-GITHUB";
23521
+ GUARDIAN_CHROME_WEB_STORE_API_SECRET_NAMES = [
23522
+ "CHROME_WEB_STORE_CLIENT_ID",
23523
+ "CHROME_WEB_STORE_CLIENT_SECRET",
23524
+ "CHROME_WEB_STORE_REFRESH_TOKEN",
23525
+ "CHROME_WEB_STORE_PUBLISHER_ID",
23526
+ "CHROME_WEB_STORE_EXTENSION_ID"
23527
+ ];
23185
23528
  GUARDIAN_DEFAULT_CONTROL_TOWER_URL = "http://127.0.0.1:4317/control-tower";
23186
23529
  GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ENV = "GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY";
23187
23530
  GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ID_ENV = "GUARDIAN_LOCAL_DATA_ENCRYPTION_KEY_ID";
@@ -23197,6 +23540,195 @@ var init_runtime = __esm({
23197
23540
  PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION2 = "contextecf/personal-fabric-contracts/v1";
23198
23541
  PROMPT_COACH_MODES = ["off", "quiet", "balanced", "hands_on"];
23199
23542
  DEFAULT_PROMPT_COACH_DAILY_SOFT_CARD_LIMIT = 6;
23543
+ GUARDIAN_PROMPT_CARD_OPTIONS_CATALOG = [
23544
+ {
23545
+ disposition: "allow",
23546
+ cardStyle: "No visible card",
23547
+ whenShown: "Local Guardian approves the prompt before it leaves the browser.",
23548
+ actions: [
23549
+ {
23550
+ id: "auto-send",
23551
+ label: "Send prompt",
23552
+ role: "none",
23553
+ sendsPrompt: true,
23554
+ sendsRevisedPrompt: false,
23555
+ requiresConfirmation: false,
23556
+ visibleToUser: false
23557
+ }
23558
+ ],
23559
+ sendAsIsAllowed: true,
23560
+ hardBlock: false,
23561
+ receiptExpected: true
23562
+ },
23563
+ {
23564
+ disposition: "warn",
23565
+ cardStyle: "Quick check",
23566
+ whenShown: "Guardian found a low or medium-risk concern, but policy still allows the user to continue.",
23567
+ actions: [
23568
+ {
23569
+ id: "send-as-is",
23570
+ label: "Send as-is",
23571
+ role: "allowed_override",
23572
+ sendsPrompt: true,
23573
+ sendsRevisedPrompt: false,
23574
+ requiresConfirmation: false,
23575
+ visibleToUser: true
23576
+ },
23577
+ {
23578
+ id: "edit-prompt",
23579
+ label: "Edit prompt",
23580
+ role: "safe_escape",
23581
+ sendsPrompt: false,
23582
+ sendsRevisedPrompt: false,
23583
+ requiresConfirmation: false,
23584
+ visibleToUser: true
23585
+ }
23586
+ ],
23587
+ sendAsIsAllowed: true,
23588
+ hardBlock: false,
23589
+ receiptExpected: true
23590
+ },
23591
+ {
23592
+ disposition: "rewrite",
23593
+ cardStyle: "Safer version ready",
23594
+ whenShown: "Guardian prepared a safer prompt, usually by masking sensitive text or narrowing scope.",
23595
+ actions: [
23596
+ {
23597
+ id: "send-revised-prompt",
23598
+ label: "Send revised prompt",
23599
+ role: "recommended",
23600
+ sendsPrompt: true,
23601
+ sendsRevisedPrompt: true,
23602
+ requiresConfirmation: false,
23603
+ visibleToUser: true
23604
+ },
23605
+ {
23606
+ id: "send-as-is",
23607
+ label: "Send as-is",
23608
+ role: "allowed_override",
23609
+ sendsPrompt: true,
23610
+ sendsRevisedPrompt: false,
23611
+ requiresConfirmation: false,
23612
+ visibleToUser: true
23613
+ },
23614
+ {
23615
+ id: "edit-prompt",
23616
+ label: "Edit prompt",
23617
+ role: "safe_escape",
23618
+ sendsPrompt: false,
23619
+ sendsRevisedPrompt: false,
23620
+ requiresConfirmation: false,
23621
+ visibleToUser: true
23622
+ }
23623
+ ],
23624
+ sendAsIsAllowed: true,
23625
+ hardBlock: false,
23626
+ receiptExpected: true
23627
+ },
23628
+ {
23629
+ disposition: "confirm",
23630
+ cardStyle: "Review needed",
23631
+ whenShown: "Guardian needs deliberate approval before sending higher-risk content or action requests.",
23632
+ actions: [
23633
+ {
23634
+ id: "approve-and-send",
23635
+ label: "Approve and send",
23636
+ role: "recommended",
23637
+ sendsPrompt: true,
23638
+ sendsRevisedPrompt: false,
23639
+ requiresConfirmation: true,
23640
+ visibleToUser: true
23641
+ },
23642
+ {
23643
+ id: "edit-prompt",
23644
+ label: "Edit prompt",
23645
+ role: "safe_escape",
23646
+ sendsPrompt: false,
23647
+ sendsRevisedPrompt: false,
23648
+ requiresConfirmation: false,
23649
+ visibleToUser: true
23650
+ }
23651
+ ],
23652
+ sendAsIsAllowed: false,
23653
+ hardBlock: false,
23654
+ receiptExpected: true
23655
+ },
23656
+ {
23657
+ disposition: "block",
23658
+ cardStyle: "Blocked",
23659
+ whenShown: "The prompt violates policy, contains protected secrets, or targets a denied destination.",
23660
+ actions: [
23661
+ {
23662
+ id: "edit-prompt",
23663
+ label: "Edit prompt",
23664
+ role: "safe_escape",
23665
+ sendsPrompt: false,
23666
+ sendsRevisedPrompt: false,
23667
+ requiresConfirmation: false,
23668
+ visibleToUser: true
23669
+ }
23670
+ ],
23671
+ sendAsIsAllowed: false,
23672
+ hardBlock: true,
23673
+ receiptExpected: true
23674
+ },
23675
+ {
23676
+ disposition: "unavailable",
23677
+ cardStyle: "Setup needed",
23678
+ whenShown: "The browser cannot reach Local Guardian, so the prompt is held back.",
23679
+ actions: [
23680
+ {
23681
+ id: "edit-prompt",
23682
+ label: "Edit prompt",
23683
+ role: "safe_escape",
23684
+ sendsPrompt: false,
23685
+ sendsRevisedPrompt: false,
23686
+ requiresConfirmation: false,
23687
+ visibleToUser: true
23688
+ }
23689
+ ],
23690
+ sendAsIsAllowed: false,
23691
+ hardBlock: false,
23692
+ receiptExpected: false
23693
+ },
23694
+ {
23695
+ disposition: "live_sensitive_warning",
23696
+ cardStyle: "Early heads-up",
23697
+ whenShown: "Guardian notices sensitive text while the user is typing, before the formal send-time check.",
23698
+ actions: [
23699
+ {
23700
+ id: "use-placeholder",
23701
+ label: "Use placeholder",
23702
+ role: "recommended",
23703
+ sendsPrompt: false,
23704
+ sendsRevisedPrompt: true,
23705
+ requiresConfirmation: false,
23706
+ visibleToUser: true
23707
+ },
23708
+ {
23709
+ id: "send-as-is",
23710
+ label: "Send as-is",
23711
+ role: "allowed_override",
23712
+ sendsPrompt: true,
23713
+ sendsRevisedPrompt: false,
23714
+ requiresConfirmation: false,
23715
+ visibleToUser: true
23716
+ },
23717
+ {
23718
+ id: "keep-drafting",
23719
+ label: "Keep drafting",
23720
+ role: "safe_escape",
23721
+ sendsPrompt: false,
23722
+ sendsRevisedPrompt: false,
23723
+ requiresConfirmation: false,
23724
+ visibleToUser: true
23725
+ }
23726
+ ],
23727
+ sendAsIsAllowed: true,
23728
+ hardBlock: false,
23729
+ receiptExpected: false
23730
+ }
23731
+ ];
23200
23732
  GUARDIAN_POSTURE_PROFILE_IDS = [
23201
23733
  "calm",
23202
23734
  "balanced",
@@ -23689,10 +24221,49 @@ Open Control Tower:
23689
24221
  ${GUARDIAN_OPEN_CONTROL_TOWER_COMMAND}
23690
24222
  start aliases: guardian launch, guardian tower, guardian control-tower
23691
24223
 
24224
+ Everyday commands:
24225
+ guardian --version
24226
+ guardian setup
24227
+ guardian launch
24228
+ guardian open
24229
+ guardian status
24230
+ guardian doctor
24231
+ guardian connect
24232
+ guardian coach options
24233
+ guardian policy list
24234
+ guardian marketplace list
24235
+ guardian posture list
24236
+ guardian privacy show
24237
+
24238
+ Recommended first run:
24239
+ ${GUARDIAN_GLOBAL_INSTALL_COMMAND}
24240
+ guardian setup
24241
+ Open ContextECF Halo and click Connect.
24242
+ guardian open
24243
+
24244
+ Support and developer commands:
24245
+ guardian help --advanced
24246
+
24247
+ Defaults are local-first: raw capture, cloud sync, learning graph, and app autopilot stay off.`;
24248
+ ADVANCED_HELP_TEXT = `guardian \u2014 Project Guardian Personal Context Fabric
24249
+
24250
+ Global install:
24251
+ ${GUARDIAN_GLOBAL_INSTALL_COMMAND}
24252
+
24253
+ First run:
24254
+ ${GUARDIAN_FIRST_RUN_COMMAND}
24255
+
24256
+ No global install:
24257
+ ${GUARDIAN_NPX_SETUP_COMMAND}
24258
+
24259
+ Open Control Tower:
24260
+ ${GUARDIAN_OPEN_CONTROL_TOWER_COMMAND}
24261
+ start aliases: guardian launch, guardian tower, guardian control-tower
24262
+
23692
24263
  Usage:
23693
24264
  guardian --version
23694
24265
  guardian version [--json]
23695
- guardian setup [--json] [--open] [--print] [--host=127.0.0.1] [--port=4317] [--no-launch] [--no-mcp] [--extension-id=<chrome-extension-id>]
24266
+ guardian setup [--json] [--open] [--print] [--host=127.0.0.1] [--port=4317] [--no-launch] [--no-mcp] [--no-browser-connection] [--extension-id=<chrome-extension-id>]
23696
24267
  guardian install [--json]
23697
24268
  guardian doctor [--json]
23698
24269
  guardian readiness [--json] [--strict]
@@ -23700,7 +24271,7 @@ Usage:
23700
24271
  guardian release flow --version=<semver> [--json]
23701
24272
  guardian release verify --version=<semver> [--json]
23702
24273
  guardian release paste --version=<semver> [--json]
23703
- guardian release command --version=<semver> [--publish|--dry-run=false] [--channels=all|npm|browser|evidence] [--run-docker-smoke=true|false] [--json]
24274
+ guardian release command --version=<semver> [--publish|--dry-run=false] [--channels=all|npm|browser|evidence] [--run-docker-smoke=true|false] [--publish-auth=token|trusted_publisher] [--browser-store-publish=manual|api] [--json]
23704
24275
  guardian release secrets [--json]
23705
24276
  guardian release browser-store [--json]
23706
24277
  guardian open [--json] [--print] [--url=http://127.0.0.1:<port>]
@@ -23708,6 +24279,8 @@ Usage:
23708
24279
  guardian start [--json] [--host=127.0.0.1] [--port=4317]
23709
24280
  guardian launch|tower|control-tower [--json] [--open] [--print] [--host=127.0.0.1] [--port=4317]
23710
24281
  guardian stop [--json] [--url=http://127.0.0.1:<port>/control-tower]
24282
+ guardian pair [--json]
24283
+ guardian connect [--json] [--browser=chrome|chromium|edge] [--extension-id=<chrome-extension-id>] [--host=127.0.0.1] [--port=4317]
23711
24284
  guardian service install [--json] [--platform=auto|launchd|systemd|windows-task-scheduler] [--force]
23712
24285
  guardian service status [--json]
23713
24286
  guardian service uninstall [--yes] [--json]
@@ -23716,6 +24289,7 @@ Usage:
23716
24289
  guardian key-store uninstall [--yes] [--json]
23717
24290
  guardian daemon serve [--host=127.0.0.1] [--port=4317]
23718
24291
  guardian daemon pair [--json]
24292
+ guardian extension connect [--json] [--browser=chrome|chromium|edge] [--extension-id=<chrome-extension-id>]
23719
24293
  guardian extension open-setup [--json] [--print] [--url=http://127.0.0.1:<port>/control-tower] [--extension-id=<chrome-extension-id>]
23720
24294
  guardian extension native-host install --extension-id=<chrome-extension-id> [--browser=chrome|chromium|edge] [--target=/path/to/manifest] [--force] [--write-registry --yes] [--json]
23721
24295
  guardian extension native-host status [--browser=chrome|chromium|edge] [--target=/path/to/manifest] [--json]
@@ -23731,6 +24305,7 @@ Usage:
23731
24305
  guardian privacy mode on [--json]
23732
24306
  guardian mcp install [--json] [--client=generic|codex|claude-desktop|cursor|all] [--write] [--target=/path/to/config] [--force]
23733
24307
  guardian mcp serve
24308
+ guardian coach options [--json]
23734
24309
  guardian policy list [--json]
23735
24310
  guardian policy enable <pack-id> [--json]
23736
24311
  guardian policy disable <pack-id> [--json]
@@ -24067,11 +24642,12 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
24067
24642
  return void 0;
24068
24643
  }
24069
24644
  const fullSsnRisk = hasFullSsnRisk(riskReport.matched_policy_refs);
24645
+ const riskCopy = buildPromptCoachRiskCopy(riskReport, fullSsnRisk);
24070
24646
  const card = {
24071
24647
  card_id: `coach:${riskReport.risk_report_id}`,
24072
24648
  intervention_level: coachDecision.intervention_level,
24073
- title: disposition === "block" ? "Guardian blocked this prompt" : disposition === "confirm" ? "Guardian needs your confirmation" : coachDecision.intervention_level === "soft_tag_along" ? "Guardian found useful context" : fullSsnRisk ? "Guardian found an identity number" : "Guardian can improve this prompt",
24074
- body: disposition === "confirm" ? "This prompt includes higher-risk content and needs confirmation before it leaves your machine." : coachDecision.intervention_level === "soft_tag_along" ? "This prompt may work better with a little more local context attached first." : fullSsnRisk ? "Guardian can mask the Social Security number before anything leaves your machine." : "Guardian found context or privacy improvements that may make this prompt safer and more useful.",
24649
+ title: disposition === "block" ? "Guardian blocked this prompt" : disposition === "confirm" ? "Guardian needs your confirmation" : coachDecision.intervention_level === "soft_tag_along" ? "Guardian found useful context" : riskCopy.title,
24650
+ body: disposition === "confirm" ? `${riskCopy.body} Please review before it leaves your machine.` : coachDecision.intervention_level === "soft_tag_along" ? "This prompt may work better with a little more local context attached first." : riskCopy.body,
24075
24651
  recommended_choice_id: disposition === "confirm" ? "confirm-after-review" : coachDecision.intervention_level === "soft_tag_along" ? "add-context" : "use-guardian-version",
24076
24652
  choices: disposition === "confirm" ? [
24077
24653
  {
@@ -24096,8 +24672,8 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
24096
24672
  resulting_disposition: "warn"
24097
24673
  },
24098
24674
  {
24099
- choice_id: "continue",
24100
- label: "Continue",
24675
+ choice_id: "send-as-is",
24676
+ label: "Send as-is",
24101
24677
  description: "Send without adding context.",
24102
24678
  resulting_disposition: "allow"
24103
24679
  }
@@ -24114,6 +24690,12 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
24114
24690
  label: "Cancel",
24115
24691
  description: "Do not send this prompt.",
24116
24692
  resulting_disposition: "block"
24693
+ },
24694
+ {
24695
+ choice_id: "send-as-is",
24696
+ label: "Send as-is",
24697
+ description: "Send the original prompt after reviewing the identity-number risk.",
24698
+ resulting_disposition: "allow"
24117
24699
  }
24118
24700
  ] : [
24119
24701
  {
@@ -24134,6 +24716,60 @@ function buildCoachCard(disposition, riskReport, policyPacks, learningSignals) {
24134
24716
  const parsedCard = PromptCoachCardSchema.parse(card);
24135
24717
  return shouldQuietPromptCoachCard(parsedCard, disposition, learningSignals) ? void 0 : parsedCard;
24136
24718
  }
24719
+ function buildPromptCoachRiskCopy(riskReport, fullSsnRisk) {
24720
+ if (fullSsnRisk) {
24721
+ return {
24722
+ title: "Guardian found an identity number",
24723
+ body: "Guardian can mask the Social Security number before anything leaves your machine."
24724
+ };
24725
+ }
24726
+ if (riskReport.detected_risks.includes("secret")) {
24727
+ return {
24728
+ title: "Guardian found a secret or credential",
24729
+ body: "Guardian can remove tokens, keys, or passwords before this prompt leaves your machine."
24730
+ };
24731
+ }
24732
+ if (riskReport.detected_risks.includes("financial_data")) {
24733
+ return {
24734
+ title: "Guardian found financial information",
24735
+ body: "Guardian can mask financial details or help you keep them out of the prompt."
24736
+ };
24737
+ }
24738
+ if (riskReport.detected_risks.includes("healthcare_data")) {
24739
+ return {
24740
+ title: "Guardian found health information",
24741
+ body: "Guardian can help remove identifying health details before you send this prompt."
24742
+ };
24743
+ }
24744
+ if (riskReport.detected_risks.includes("prompt_injection")) {
24745
+ return {
24746
+ title: "Guardian found prompt-injection language",
24747
+ body: "Guardian noticed instructions that may try to override your intended task or safety rules."
24748
+ };
24749
+ }
24750
+ if (riskReport.detected_risks.includes("unsafe_destination")) {
24751
+ return {
24752
+ title: "Guardian found an unfamiliar destination",
24753
+ body: "Guardian noticed this prompt may be going somewhere that needs an extra check."
24754
+ };
24755
+ }
24756
+ if (riskReport.detected_risks.includes("pii")) {
24757
+ return {
24758
+ title: "Guardian found personal information",
24759
+ body: "Guardian can mask personal details before this prompt leaves your machine."
24760
+ };
24761
+ }
24762
+ if (riskReport.detected_risks.includes("work_data")) {
24763
+ return {
24764
+ title: "Guardian found work or customer data",
24765
+ body: "Guardian can help keep private work details out of the prompt or replace them with placeholders."
24766
+ };
24767
+ }
24768
+ return {
24769
+ title: "Guardian can improve this prompt",
24770
+ body: "Guardian found context or privacy improvements that may make this prompt safer and more useful."
24771
+ };
24772
+ }
24137
24773
  function shouldQuietPromptCoachCard(card, disposition, learningSignals) {
24138
24774
  if (!learningSignals || disposition === "block" || disposition === "confirm") {
24139
24775
  return false;
@@ -25781,10 +26417,14 @@ __export(daemon_exports, {
25781
26417
  processGuardianControlTowerActivityRequest: () => processGuardianControlTowerActivityRequest,
25782
26418
  processGuardianControlTowerAppPermissionRequest: () => processGuardianControlTowerAppPermissionRequest,
25783
26419
  processGuardianControlTowerDataRequest: () => processGuardianControlTowerDataRequest,
26420
+ processGuardianControlTowerMarketplaceRequest: () => processGuardianControlTowerMarketplaceRequest,
26421
+ processGuardianControlTowerNoticeRequest: () => processGuardianControlTowerNoticeRequest,
25784
26422
  processGuardianControlTowerPolicyRequest: () => processGuardianControlTowerPolicyRequest,
25785
26423
  processGuardianControlTowerPreferenceRequest: () => processGuardianControlTowerPreferenceRequest,
26424
+ processGuardianControlTowerPrimePackRequest: () => processGuardianControlTowerPrimePackRequest,
25786
26425
  processGuardianControlTowerProviderPermissionRequest: () => processGuardianControlTowerProviderPermissionRequest,
25787
26426
  processGuardianControlTowerSourcePermissionRequest: () => processGuardianControlTowerSourcePermissionRequest,
26427
+ processGuardianControlTowerViewModelRequest: () => processGuardianControlTowerViewModelRequest,
25788
26428
  processGuardianDaemonHealthRequest: () => processGuardianDaemonHealthRequest,
25789
26429
  processGuardianDaemonStopRequest: () => processGuardianDaemonStopRequest,
25790
26430
  processGuardianLiveCoachEventRequest: () => processGuardianLiveCoachEventRequest,
@@ -25794,7 +26434,7 @@ __export(daemon_exports, {
25794
26434
  });
25795
26435
  import { createHash as createHash5, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
25796
26436
  import { existsSync as existsSync2 } from "node:fs";
25797
- import { mkdir as mkdir4, readFile as readFile4, readdir as readdir2, stat as stat3 } from "node:fs/promises";
26437
+ import { mkdir as mkdir4, readFile as readFile4, readdir as readdir2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
25798
26438
  import { createServer } from "node:http";
25799
26439
  import path4 from "node:path";
25800
26440
  import { StringDecoder } from "node:string_decoder";
@@ -25920,12 +26560,22 @@ async function handleRequest(input2) {
25920
26560
  return;
25921
26561
  }
25922
26562
  if (input2.request.method === "GET" && (input2.request.url === "/control-tower" || input2.request.url === "/control-tower/")) {
25923
- const [receipts, searchGraph, learningEvents, localDataStatus, releaseDoctorStatus] = await Promise.all([
26563
+ const [
26564
+ receipts,
26565
+ searchGraph,
26566
+ learningEvents,
26567
+ localDataStatus,
26568
+ releaseDoctorStatus,
26569
+ adminNotices,
26570
+ marketplaceRequests
26571
+ ] = await Promise.all([
25924
26572
  input2.store.listReceipts(),
25925
26573
  input2.store.searchGraph({ limit: CONTROL_TOWER_GRAPH_NODE_LIMIT }),
25926
26574
  input2.store.listLearningEvents({ limit: CONTROL_TOWER_LEARNING_EVENT_LIMIT }),
25927
26575
  buildControlTowerLocalDataStatus(input2.profile, input2.env),
25928
- buildControlTowerReleaseDoctorStatus(input2.profile, input2.env)
26576
+ buildControlTowerReleaseDoctorStatus(input2.profile, input2.env),
26577
+ readControlTowerAdminNotices(input2.profile.profile_dir),
26578
+ readControlTowerMarketplaceRequests(input2.profile.profile_dir)
25929
26579
  ]);
25930
26580
  writeHtml(
25931
26581
  input2.response,
@@ -25936,7 +26586,9 @@ async function handleRequest(input2) {
25936
26586
  searchGraph,
25937
26587
  learningEvents,
25938
26588
  localDataStatus,
25939
- releaseDoctorStatus
26589
+ releaseDoctorStatus,
26590
+ adminNotices,
26591
+ marketplaceRequests
25940
26592
  ),
25941
26593
  {
25942
26594
  controlToken: input2.controlToken
@@ -25945,6 +26597,37 @@ async function handleRequest(input2) {
25945
26597
  return;
25946
26598
  }
25947
26599
  const activityUrl = parseGuardianUrl(input2.request.url);
26600
+ if (input2.request.method === "GET" && activityUrl?.pathname === "/v1/guardian/control-tower/view-model") {
26601
+ const [
26602
+ receipts,
26603
+ searchGraph,
26604
+ learningEvents,
26605
+ localDataStatus,
26606
+ releaseDoctorStatus,
26607
+ adminNotices,
26608
+ marketplaceRequests
26609
+ ] = await Promise.all([
26610
+ input2.store.listReceipts(),
26611
+ input2.store.searchGraph({ limit: CONTROL_TOWER_GRAPH_NODE_LIMIT }),
26612
+ input2.store.listLearningEvents({ limit: CONTROL_TOWER_LEARNING_EVENT_LIMIT }),
26613
+ buildControlTowerLocalDataStatus(input2.profile, input2.env),
26614
+ buildControlTowerReleaseDoctorStatus(input2.profile, input2.env),
26615
+ readControlTowerAdminNotices(input2.profile.profile_dir),
26616
+ readControlTowerMarketplaceRequests(input2.profile.profile_dir)
26617
+ ]);
26618
+ const result2 = processGuardianControlTowerViewModelRequest(
26619
+ input2.profile,
26620
+ receipts,
26621
+ searchGraph,
26622
+ learningEvents,
26623
+ localDataStatus,
26624
+ releaseDoctorStatus,
26625
+ adminNotices,
26626
+ marketplaceRequests
26627
+ );
26628
+ writeJson(input2.response, result2.statusCode, result2.body);
26629
+ return;
26630
+ }
25948
26631
  if (input2.request.method === "GET" && activityUrl?.pathname === "/v1/guardian/activity") {
25949
26632
  const filter = parseActivityFilter(activityUrl.searchParams.get("filter") ?? "all");
25950
26633
  const receipts = (await input2.store.listReceipts()).slice().sort((left, right) => right.created_at.localeCompare(left.created_at)).slice(0, CONTROL_TOWER_RECEIPT_LIMIT);
@@ -25989,6 +26672,24 @@ async function handleRequest(input2) {
25989
26672
  writeAdminResponse(input2.response, result2.statusCode, result2.body, parsedBody2.redirect);
25990
26673
  return;
25991
26674
  }
26675
+ if (input2.request.method === "POST" && input2.request.url === "/v1/guardian/admin/prime-pack") {
26676
+ const parsedBody2 = await readGuardianDaemonAdminBody(input2.request);
26677
+ if (!parsedBody2.ok) {
26678
+ writeJson(input2.response, parsedBody2.statusCode, parsedBody2.body);
26679
+ return;
26680
+ }
26681
+ const result2 = await processGuardianControlTowerPrimePackRequest({
26682
+ profile: input2.profile,
26683
+ token: input2.token,
26684
+ controlToken: input2.controlToken,
26685
+ suppliedToken: headerValue(input2.request, ADMIN_HEADER) ?? readCookieValue(input2.request, CONTROL_TOWER_COOKIE),
26686
+ body: parsedBody2.body,
26687
+ now: input2.now,
26688
+ store: input2.store
26689
+ });
26690
+ writeAdminResponse(input2.response, result2.statusCode, result2.body, parsedBody2.redirect);
26691
+ return;
26692
+ }
25992
26693
  if (input2.request.method === "POST" && input2.request.url === "/v1/guardian/admin/preferences") {
25993
26694
  const parsedBody2 = await readGuardianDaemonAdminBody(input2.request);
25994
26695
  if (!parsedBody2.ok) {
@@ -26069,6 +26770,42 @@ async function handleRequest(input2) {
26069
26770
  writeAdminResponse(input2.response, result2.statusCode, result2.body, parsedBody2.redirect);
26070
26771
  return;
26071
26772
  }
26773
+ if (input2.request.method === "POST" && input2.request.url === "/v1/guardian/admin/notice") {
26774
+ const parsedBody2 = await readGuardianDaemonAdminBody(input2.request);
26775
+ if (!parsedBody2.ok) {
26776
+ writeJson(input2.response, parsedBody2.statusCode, parsedBody2.body);
26777
+ return;
26778
+ }
26779
+ const result2 = await processGuardianControlTowerNoticeRequest({
26780
+ profile: input2.profile,
26781
+ token: input2.token,
26782
+ controlToken: input2.controlToken,
26783
+ suppliedToken: headerValue(input2.request, ADMIN_HEADER) ?? readCookieValue(input2.request, CONTROL_TOWER_COOKIE),
26784
+ body: parsedBody2.body,
26785
+ now: input2.now,
26786
+ store: input2.store
26787
+ });
26788
+ writeAdminResponse(input2.response, result2.statusCode, result2.body, parsedBody2.redirect);
26789
+ return;
26790
+ }
26791
+ if (input2.request.method === "POST" && input2.request.url === "/v1/guardian/admin/marketplace-request") {
26792
+ const parsedBody2 = await readGuardianDaemonAdminBody(input2.request);
26793
+ if (!parsedBody2.ok) {
26794
+ writeJson(input2.response, parsedBody2.statusCode, parsedBody2.body);
26795
+ return;
26796
+ }
26797
+ const result2 = await processGuardianControlTowerMarketplaceRequest({
26798
+ profile: input2.profile,
26799
+ token: input2.token,
26800
+ controlToken: input2.controlToken,
26801
+ suppliedToken: headerValue(input2.request, ADMIN_HEADER) ?? readCookieValue(input2.request, CONTROL_TOWER_COOKIE),
26802
+ body: parsedBody2.body,
26803
+ now: input2.now,
26804
+ store: input2.store
26805
+ });
26806
+ writeAdminResponse(input2.response, result2.statusCode, result2.body, parsedBody2.redirect);
26807
+ return;
26808
+ }
26072
26809
  if (input2.request.method === "POST" && input2.request.url === "/v1/guardian/live-coach-event") {
26073
26810
  const suppliedToken2 = headerValue(input2.request, ADMIN_HEADER);
26074
26811
  const parsedBody2 = await readGuardianDaemonJsonBody(input2.request);
@@ -26141,6 +26878,23 @@ function processGuardianControlTowerActivityRequest(receipts, filterValue) {
26141
26878
  }
26142
26879
  };
26143
26880
  }
26881
+ function processGuardianControlTowerViewModelRequest(profile, receipts = [], searchGraph = { nodes: [], edges: [] }, learningEvents = [], localDataStatus = createEmptyControlTowerLocalDataStatus(profile), releaseDoctorStatus = createEmptyControlTowerReleaseDoctorStatus(
26882
+ profile
26883
+ ), adminNotices = [], marketplaceRequests = []) {
26884
+ return {
26885
+ statusCode: 200,
26886
+ body: buildControlTowerViewModel(
26887
+ profile,
26888
+ receipts,
26889
+ searchGraph,
26890
+ learningEvents,
26891
+ localDataStatus,
26892
+ releaseDoctorStatus,
26893
+ adminNotices,
26894
+ marketplaceRequests
26895
+ )
26896
+ };
26897
+ }
26144
26898
  function processGuardianDaemonStopRequest(input2) {
26145
26899
  if (!isAuthorized(input2.token, input2.suppliedToken)) {
26146
26900
  return {
@@ -26206,6 +26960,105 @@ async function processGuardianControlTowerPolicyRequest(input2) {
26206
26960
  };
26207
26961
  }
26208
26962
  }
26963
+ async function processGuardianControlTowerPrimePackRequest(input2) {
26964
+ if (!isAdminAuthorized(input2)) {
26965
+ return {
26966
+ statusCode: 401,
26967
+ body: {
26968
+ ok: false,
26969
+ error: "unauthorized"
26970
+ }
26971
+ };
26972
+ }
26973
+ const action = parsePrimePackAction(input2.body.action);
26974
+ if (!action) {
26975
+ return {
26976
+ statusCode: 400,
26977
+ body: {
26978
+ ok: false,
26979
+ error: "invalid_prime_pack_action"
26980
+ }
26981
+ };
26982
+ }
26983
+ const inputs = {
26984
+ sensitive_terms: parsePrimePackEntries([
26985
+ input2.body.sensitiveTerms,
26986
+ parsePrimePackImportEntries(input2.body.importContent, "sensitive_terms")
26987
+ ]),
26988
+ approved_domains: parsePrimePackEntries(
26989
+ [
26990
+ input2.body.approvedDomains,
26991
+ parsePrimePackImportEntries(input2.body.importContent, "approved_domains")
26992
+ ],
26993
+ { lowercase: true }
26994
+ ),
26995
+ blocked_destinations: parsePrimePackEntries(
26996
+ [
26997
+ input2.body.blockedDestinations,
26998
+ parsePrimePackImportEntries(input2.body.importContent, "blocked_destinations")
26999
+ ],
27000
+ {
27001
+ lowercase: true
27002
+ }
27003
+ ),
27004
+ escalation_phrases: parsePrimePackEntries([
27005
+ input2.body.escalationPhrases,
27006
+ parsePrimePackImportEntries(input2.body.importContent, "escalation_phrases")
27007
+ ])
27008
+ };
27009
+ const preview = buildPrimePackPreview(inputs);
27010
+ if (preview.total_inputs === 0) {
27011
+ return {
27012
+ statusCode: 400,
27013
+ body: {
27014
+ ok: false,
27015
+ error: "empty_prime_pack"
27016
+ }
27017
+ };
27018
+ }
27019
+ const now = (input2.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
27020
+ const draftSeed = JSON.stringify(inputs);
27021
+ const transactionId = `txn:prime-pack:${digest4(`${draftSeed}:${now}`).slice(0, 20)}`;
27022
+ const guardianReceipt = action === "deploy" ? buildPrimePackDeployGuardianReceipt(inputs, preview, transactionId, now) : void 0;
27023
+ const draft = {
27024
+ schema_version: "contextecf/project-guardian-prime-pack-draft/v1",
27025
+ draft_id: `prime-pack:${digest4(draftSeed).slice(0, 16)}`,
27026
+ status: action === "deploy" ? "deployed_metadata_only" : "preview_ready",
27027
+ updated_at: now,
27028
+ ...action === "deploy" ? { deployed_at: now } : {},
27029
+ inputs,
27030
+ preview,
27031
+ ...guardianReceipt ? {
27032
+ local_receipt: buildPrimePackDeployReceipt(guardianReceipt)
27033
+ } : {},
27034
+ raw_prompt_content_included: false,
27035
+ token_printed: false
27036
+ };
27037
+ if (guardianReceipt && input2.store) {
27038
+ await input2.store.putReceipt(guardianReceipt);
27039
+ await input2.store.appendEvent(
27040
+ buildPrimePackDeployEvent(inputs, preview, transactionId, now, guardianReceipt)
27041
+ );
27042
+ }
27043
+ await writeControlTowerPrimePackDraft(input2.profile.profile_dir, draft);
27044
+ return {
27045
+ statusCode: 200,
27046
+ body: {
27047
+ ok: true,
27048
+ schema_version: "contextecf/project-guardian-control-tower-prime-pack/v1",
27049
+ action,
27050
+ status: draft.status,
27051
+ draft_id: draft.draft_id,
27052
+ file: CONTROL_TOWER_PRIME_PACK_FILE_NAME,
27053
+ preview,
27054
+ local_receipt: draft.local_receipt,
27055
+ receipt_id: guardianReceipt?.receipt_id,
27056
+ stored_locally: true,
27057
+ raw_prompt_content_included: false,
27058
+ token_printed: false
27059
+ }
27060
+ };
27061
+ }
26209
27062
  async function processGuardianControlTowerPreferenceRequest(input2) {
26210
27063
  if (!isAdminAuthorized(input2)) {
26211
27064
  return {
@@ -26492,6 +27345,190 @@ async function processGuardianControlTowerDataRequest(input2) {
26492
27345
  };
26493
27346
  }
26494
27347
  }
27348
+ async function processGuardianControlTowerNoticeRequest(input2) {
27349
+ if (!isAdminAuthorized(input2)) {
27350
+ return {
27351
+ statusCode: 401,
27352
+ body: {
27353
+ ok: false,
27354
+ error: "unauthorized"
27355
+ }
27356
+ };
27357
+ }
27358
+ const action = input2.body.action?.trim() || "publish";
27359
+ if (action === "clear") {
27360
+ const clearedAt = input2.now().toISOString();
27361
+ const transactionId2 = `txn:admin-notice:${digest4(`clear:${clearedAt}`).slice(0, 20)}`;
27362
+ const receipt2 = buildAdminNoticeReceipt({
27363
+ action,
27364
+ transactionId: transactionId2,
27365
+ timestamp: clearedAt
27366
+ });
27367
+ await input2.store.putReceipt(receipt2);
27368
+ await input2.store.appendEvent(
27369
+ buildAdminNoticeEvent({ action, transactionId: transactionId2, timestamp: clearedAt, receipt: receipt2 })
27370
+ );
27371
+ await writeControlTowerAdminNotices(input2.profile.profile_dir, []);
27372
+ return {
27373
+ statusCode: 200,
27374
+ body: {
27375
+ ok: true,
27376
+ schema_version: "contextecf/project-guardian-control-tower-admin-notice/v1",
27377
+ action,
27378
+ notices: [],
27379
+ receipt_id: receipt2.receipt_id,
27380
+ raw_content_included: false,
27381
+ token_printed: false
27382
+ }
27383
+ };
27384
+ }
27385
+ if (action !== "publish") {
27386
+ return {
27387
+ statusCode: 400,
27388
+ body: {
27389
+ ok: false,
27390
+ error: "invalid_notice_action"
27391
+ }
27392
+ };
27393
+ }
27394
+ const title = boundedNoticeText(input2.body.title, CONTROL_TOWER_ADMIN_NOTICE_TITLE_LIMIT);
27395
+ const message = boundedNoticeText(input2.body.message, CONTROL_TOWER_ADMIN_NOTICE_MESSAGE_LIMIT);
27396
+ const audience = parseControlTowerAdminNoticeAudience(input2.body.audience);
27397
+ const tone = parseControlTowerAdminNoticeTone(input2.body.tone);
27398
+ if (!title || !message || !audience || !tone) {
27399
+ return {
27400
+ statusCode: 400,
27401
+ body: {
27402
+ ok: false,
27403
+ error: "invalid_notice_request"
27404
+ }
27405
+ };
27406
+ }
27407
+ const existingNotices = await readControlTowerAdminNotices(input2.profile.profile_dir);
27408
+ const createdAt = input2.now().toISOString();
27409
+ const transactionId = `txn:admin-notice:${digest4(
27410
+ `${title}:${message}:${audience}:${tone}:${createdAt}`
27411
+ ).slice(0, 20)}`;
27412
+ const receipt = buildAdminNoticeReceipt({
27413
+ action,
27414
+ transactionId,
27415
+ timestamp: createdAt,
27416
+ title,
27417
+ message,
27418
+ audience,
27419
+ tone
27420
+ });
27421
+ const notice = {
27422
+ id: `notice:${digest4(`${title}:${message}:${createdAt}`).slice(0, 16)}`,
27423
+ title,
27424
+ message,
27425
+ audience,
27426
+ tone,
27427
+ created_at: createdAt,
27428
+ receipt_id: receipt.receipt_id,
27429
+ receipt_hash: receipt.receipt_hash,
27430
+ raw_content_included: false
27431
+ };
27432
+ const notices = [notice, ...existingNotices].slice(0, CONTROL_TOWER_ADMIN_NOTICE_LIMIT);
27433
+ await input2.store.putReceipt(receipt);
27434
+ await input2.store.appendEvent(
27435
+ buildAdminNoticeEvent({
27436
+ action,
27437
+ transactionId,
27438
+ timestamp: createdAt,
27439
+ receipt,
27440
+ title,
27441
+ message,
27442
+ audience,
27443
+ tone
27444
+ })
27445
+ );
27446
+ await writeControlTowerAdminNotices(input2.profile.profile_dir, notices);
27447
+ return {
27448
+ statusCode: 200,
27449
+ body: {
27450
+ ok: true,
27451
+ schema_version: "contextecf/project-guardian-control-tower-admin-notice/v1",
27452
+ action,
27453
+ notice,
27454
+ notices,
27455
+ receipt_id: receipt.receipt_id,
27456
+ raw_content_included: false,
27457
+ token_printed: false
27458
+ }
27459
+ };
27460
+ }
27461
+ async function processGuardianControlTowerMarketplaceRequest(input2) {
27462
+ if (!isAdminAuthorized(input2)) {
27463
+ return {
27464
+ statusCode: 401,
27465
+ body: {
27466
+ ok: false,
27467
+ error: "unauthorized"
27468
+ }
27469
+ };
27470
+ }
27471
+ const action = parseMarketplaceRequestAction(input2.body.action);
27472
+ const targetId = input2.body.packId?.trim();
27473
+ const manifest = targetId ? findControlTowerMarketplaceManifest(targetId) : void 0;
27474
+ if (!action || !manifest) {
27475
+ return {
27476
+ statusCode: 400,
27477
+ body: {
27478
+ ok: false,
27479
+ error: "invalid_marketplace_request"
27480
+ }
27481
+ };
27482
+ }
27483
+ const requestedAt = input2.now().toISOString();
27484
+ const transactionId = `txn:marketplace-request:${digest4(
27485
+ `${manifest.marketplace_manifest_id}:${action}:${requestedAt}`
27486
+ ).slice(0, 20)}`;
27487
+ const receipt = buildMarketplaceRequestReceipt(manifest, action, transactionId, requestedAt);
27488
+ const request = {
27489
+ schema_version: "contextecf/project-guardian-marketplace-request/v1",
27490
+ request_id: `request:${transactionId}`,
27491
+ pack_id: manifest.pack_id,
27492
+ marketplace_manifest_id: manifest.marketplace_manifest_id,
27493
+ pack_version: manifest.pack_version,
27494
+ pack_name: manifest.display_name,
27495
+ publisher: manifest.publisher_display_name,
27496
+ action,
27497
+ status: "requested_metadata_only",
27498
+ requested_at: requestedAt,
27499
+ receipt_id: receipt.receipt_id,
27500
+ receipt_hash: receipt.receipt_hash,
27501
+ raw_content_included: false,
27502
+ token_printed: false
27503
+ };
27504
+ const existingRequests = await readControlTowerMarketplaceRequests(input2.profile.profile_dir);
27505
+ const requests = [
27506
+ request,
27507
+ ...existingRequests.filter(
27508
+ (existing) => existing.marketplace_manifest_id !== manifest.marketplace_manifest_id
27509
+ )
27510
+ ].slice(0, CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT);
27511
+ await input2.store.putReceipt(receipt);
27512
+ await input2.store.appendEvent(
27513
+ buildMarketplaceRequestEvent(manifest, action, transactionId, requestedAt, receipt)
27514
+ );
27515
+ await writeControlTowerMarketplaceRequests(input2.profile.profile_dir, requests);
27516
+ return {
27517
+ statusCode: 200,
27518
+ body: {
27519
+ ok: true,
27520
+ schema_version: "contextecf/project-guardian-control-tower-marketplace-request/v1",
27521
+ action,
27522
+ status: request.status,
27523
+ request,
27524
+ request_file: CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME,
27525
+ receipt_id: receipt.receipt_id,
27526
+ stored_locally: true,
27527
+ raw_content_included: false,
27528
+ token_printed: false
27529
+ }
27530
+ };
27531
+ }
26495
27532
  function parseAppPermissionAction(value) {
26496
27533
  return value === "revoke" || value === "allow" ? value : void 0;
26497
27534
  }
@@ -26507,6 +27544,25 @@ function parseGuardianSourceType2(value) {
26507
27544
  }
26508
27545
  return GUARDIAN_SOURCE_TYPES.includes(value) ? value : void 0;
26509
27546
  }
27547
+ function parseControlTowerAdminNoticeAudience(value) {
27548
+ return value === "all_users" || value === "admins" || value === "developers" ? value : void 0;
27549
+ }
27550
+ function parseControlTowerAdminNoticeTone(value) {
27551
+ return value === "info" || value === "warning" || value === "success" ? value : void 0;
27552
+ }
27553
+ function boundedNoticeText(value, limit) {
27554
+ if (typeof value !== "string") {
27555
+ return void 0;
27556
+ }
27557
+ const trimmed = value.replace(/\s+/gu, " ").trim();
27558
+ if (!trimmed) {
27559
+ return void 0;
27560
+ }
27561
+ return trimmed.slice(0, limit);
27562
+ }
27563
+ function isRecord(value) {
27564
+ return typeof value === "object" && value !== null;
27565
+ }
26510
27566
  async function processGuardianBrowserBridgeRequest(input2) {
26511
27567
  if (!isAuthorized(input2.token, input2.suppliedToken)) {
26512
27568
  return {
@@ -26647,6 +27703,11 @@ async function processBrowserPrompt(input2) {
26647
27703
  visible_message: decision.visible_message,
26648
27704
  required_confirmation: decision.required_confirmation,
26649
27705
  coach_card: decision.coach_card,
27706
+ risk_report: {
27707
+ risk_tier: decision.risk_report.risk_tier,
27708
+ detected_risks: decision.risk_report.detected_risks,
27709
+ matched_policy_refs: decision.risk_report.matched_policy_refs
27710
+ },
26650
27711
  receipt_id: decision.receipt.receipt_id
26651
27712
  };
26652
27713
  }
@@ -26837,6 +27898,114 @@ function buildLiveCoachEvent(request, transactionId, timestamp, receipt) {
26837
27898
  redaction_state: "metadata_only"
26838
27899
  };
26839
27900
  }
27901
+ function parseMarketplaceRequestAction(value) {
27902
+ return value === "request_review" ? value : void 0;
27903
+ }
27904
+ function findControlTowerMarketplaceManifest(targetId) {
27905
+ return GUARDIAN_POLICY_PACK_MARKETPLACE_CATALOG.find(
27906
+ (manifest) => manifest.pack_id === targetId || manifest.marketplace_manifest_id === targetId
27907
+ );
27908
+ }
27909
+ function buildMarketplaceRequestReceipt(manifest, action, transactionId, timestamp) {
27910
+ const core = {
27911
+ transaction_id: transactionId,
27912
+ pack_id: manifest.pack_id,
27913
+ marketplace_manifest_id: manifest.marketplace_manifest_id,
27914
+ pack_version: manifest.pack_version,
27915
+ action,
27916
+ created_at: timestamp,
27917
+ raw_content_included: false
27918
+ };
27919
+ return {
27920
+ schema_version: PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION,
27921
+ receipt_id: `receipt:${transactionId}:marketplace-request`,
27922
+ transaction_id: transactionId,
27923
+ receipt_type: "policy",
27924
+ disposition: "allow",
27925
+ policy_basis: [
27926
+ "marketplace-request-metadata-only",
27927
+ `pack:${manifest.pack_id}`,
27928
+ `manifest:${manifest.marketplace_manifest_id}`
27929
+ ],
27930
+ receipt_hash: `sha256:${digest4(JSON.stringify(core))}`,
27931
+ raw_content_included: false,
27932
+ created_at: timestamp,
27933
+ summary: `Guardian recorded a marketplace review request for ${manifest.display_name}.`
27934
+ };
27935
+ }
27936
+ function buildMarketplaceRequestEvent(manifest, action, transactionId, timestamp, receipt) {
27937
+ const payload = {
27938
+ pack_id: manifest.pack_id,
27939
+ marketplace_manifest_id: manifest.marketplace_manifest_id,
27940
+ pack_version: manifest.pack_version,
27941
+ action,
27942
+ status: "requested_metadata_only",
27943
+ raw_content_included: false
27944
+ };
27945
+ return {
27946
+ event_id: `event:${transactionId}:marketplace-request`,
27947
+ transaction_id: transactionId,
27948
+ event_type: "policy_decided",
27949
+ occurred_at: timestamp,
27950
+ actor: "guardian-daemon",
27951
+ summary: `Marketplace pack review requested for ${manifest.display_name}.`,
27952
+ payload_hash: `sha256:${digest4(JSON.stringify(payload))}`,
27953
+ receipt_id: receipt.receipt_id,
27954
+ graph_refs: [`policy-pack:${manifest.pack_id}`, `receipt:${receipt.receipt_id}`],
27955
+ redaction_state: "metadata_only"
27956
+ };
27957
+ }
27958
+ function buildAdminNoticeReceipt(input2) {
27959
+ const core = {
27960
+ transaction_id: input2.transactionId,
27961
+ action: input2.action,
27962
+ title_hash: input2.title ? digest4(input2.title) : void 0,
27963
+ message_hash: input2.message ? digest4(input2.message) : void 0,
27964
+ audience: input2.audience ?? "all_users",
27965
+ tone: input2.tone ?? "info",
27966
+ created_at: input2.timestamp,
27967
+ raw_content_included: false
27968
+ };
27969
+ const audienceLabel = adminNoticeAudienceLabel(input2.audience ?? "all_users");
27970
+ return {
27971
+ schema_version: PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION,
27972
+ receipt_id: `receipt:${input2.transactionId}:admin-notice`,
27973
+ transaction_id: input2.transactionId,
27974
+ receipt_type: "policy",
27975
+ disposition: "allow",
27976
+ policy_basis: [
27977
+ "admin-notice-metadata-only",
27978
+ `action:${input2.action}`,
27979
+ `audience:${input2.audience ?? "all_users"}`
27980
+ ],
27981
+ receipt_hash: `sha256:${digest4(JSON.stringify(core))}`,
27982
+ raw_content_included: false,
27983
+ created_at: input2.timestamp,
27984
+ summary: input2.action === "clear" ? "Guardian cleared local admin notices." : `Guardian published a local admin notice for ${audienceLabel}.`
27985
+ };
27986
+ }
27987
+ function buildAdminNoticeEvent(input2) {
27988
+ const payload = {
27989
+ action: input2.action,
27990
+ title_hash: input2.title ? digest4(input2.title) : void 0,
27991
+ message_hash: input2.message ? digest4(input2.message) : void 0,
27992
+ audience: input2.audience ?? "all_users",
27993
+ tone: input2.tone ?? "info",
27994
+ raw_content_included: false
27995
+ };
27996
+ return {
27997
+ event_id: `event:${input2.transactionId}:admin-notice`,
27998
+ transaction_id: input2.transactionId,
27999
+ event_type: "policy_decided",
28000
+ occurred_at: input2.timestamp,
28001
+ actor: "guardian-daemon",
28002
+ summary: input2.action === "clear" ? "Admin notice bar cleared." : `Admin notice bar updated for ${adminNoticeAudienceLabel(input2.audience ?? "all_users")}.`,
28003
+ payload_hash: `sha256:${digest4(JSON.stringify(payload))}`,
28004
+ receipt_id: input2.receipt.receipt_id,
28005
+ graph_refs: [`admin-notice:${input2.action}`, `receipt:${input2.receipt.receipt_id}`],
28006
+ redaction_state: "metadata_only"
28007
+ };
28008
+ }
26840
28009
  function liveCoachCategoryLabel(category) {
26841
28010
  if (category === "ssn") return "possible SSN sharing";
26842
28011
  if (category === "password") return "possible credential sharing";
@@ -26952,6 +28121,218 @@ function isAdminAuthorized(input2) {
26952
28121
  function parsePolicyPackAction(value) {
26953
28122
  return value === "enable" || value === "disable" ? value : void 0;
26954
28123
  }
28124
+ function parsePrimePackAction(value) {
28125
+ return value === "preview" || value === "deploy" ? value : void 0;
28126
+ }
28127
+ function parsePrimePackEntries(value, options = {}) {
28128
+ const seen = /* @__PURE__ */ new Set();
28129
+ const entries = [];
28130
+ for (const rawEntry of flattenPrimePackEntryValues(value)) {
28131
+ const normalized = rawEntry.replace(/\s+/gu, " ").trim();
28132
+ if (!normalized) {
28133
+ continue;
28134
+ }
28135
+ const bounded = normalized.slice(0, CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT);
28136
+ const entry = options.lowercase ? bounded.toLowerCase() : bounded;
28137
+ const key = entry.toLowerCase();
28138
+ if (seen.has(key)) {
28139
+ continue;
28140
+ }
28141
+ seen.add(key);
28142
+ entries.push(entry);
28143
+ if (entries.length >= CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT) {
28144
+ break;
28145
+ }
28146
+ }
28147
+ return entries;
28148
+ }
28149
+ function flattenPrimePackEntryValues(value) {
28150
+ if (Array.isArray(value)) {
28151
+ return value.flatMap((entry) => flattenPrimePackEntryValues(entry));
28152
+ }
28153
+ if (typeof value !== "string") {
28154
+ return [];
28155
+ }
28156
+ return value.split(/[\n,;]+/u);
28157
+ }
28158
+ function parsePrimePackImportEntries(value, key) {
28159
+ if (typeof value !== "string" || !value.trim()) {
28160
+ return [];
28161
+ }
28162
+ const bounded = value.slice(0, CONTROL_TOWER_PRIME_PACK_IMPORT_LIMIT);
28163
+ const parsedJson = parsePrimePackImportJson(bounded, key);
28164
+ if (parsedJson.length > 0) {
28165
+ return parsedJson;
28166
+ }
28167
+ const rows = parsePrimePackImportRows(bounded);
28168
+ return rows.filter((row) => normalizePrimePackImportKey(row[0]) === key).flatMap((row) => row.slice(1));
28169
+ }
28170
+ function parsePrimePackImportJson(value, key) {
28171
+ const trimmed = value.trim();
28172
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
28173
+ return [];
28174
+ }
28175
+ try {
28176
+ const parsed = JSON.parse(trimmed);
28177
+ return extractPrimePackImportJsonValues(parsed, key);
28178
+ } catch {
28179
+ return [];
28180
+ }
28181
+ }
28182
+ function extractPrimePackImportJsonValues(value, key) {
28183
+ if (Array.isArray(value)) {
28184
+ return value.flatMap((entry) => extractPrimePackImportJsonValues(entry, key));
28185
+ }
28186
+ if (!isRecord(value)) {
28187
+ return [];
28188
+ }
28189
+ const direct = value[key];
28190
+ if (Array.isArray(direct)) {
28191
+ return direct.filter((entry) => typeof entry === "string");
28192
+ }
28193
+ if (typeof direct === "string") {
28194
+ return [direct];
28195
+ }
28196
+ const type = normalizePrimePackImportKey(value.type);
28197
+ const term = value.value ?? value.term ?? value.pattern ?? value.phrase ?? value.domain ?? value.destination;
28198
+ if (type === key && typeof term === "string") {
28199
+ return [term];
28200
+ }
28201
+ return [];
28202
+ }
28203
+ function parsePrimePackImportRows(value) {
28204
+ return value.split(/\r?\n/u).map((line) => splitPrimePackImportRow(line).map((cell) => cell.trim())).filter((row) => row.some(Boolean));
28205
+ }
28206
+ function splitPrimePackImportRow(line) {
28207
+ const delimiter = line.includes(" ") ? " " : ",";
28208
+ return line.split(delimiter);
28209
+ }
28210
+ function normalizePrimePackImportKey(value) {
28211
+ if (typeof value !== "string") {
28212
+ return void 0;
28213
+ }
28214
+ const normalized = value.trim().toLowerCase().replace(/[\s-]+/gu, "_");
28215
+ switch (normalized) {
28216
+ case "sensitive":
28217
+ case "sensitive_term":
28218
+ case "sensitive_terms":
28219
+ case "term":
28220
+ case "terms":
28221
+ return "sensitive_terms";
28222
+ case "approved_domain":
28223
+ case "approved_domains":
28224
+ case "allow_domain":
28225
+ case "allowed_domain":
28226
+ case "domain":
28227
+ return "approved_domains";
28228
+ case "blocked_destination":
28229
+ case "blocked_destinations":
28230
+ case "blocked_domain":
28231
+ case "deny_destination":
28232
+ case "destination":
28233
+ return "blocked_destinations";
28234
+ case "escalation":
28235
+ case "escalation_phrase":
28236
+ case "escalation_phrases":
28237
+ case "confirmation_phrase":
28238
+ case "phrase":
28239
+ return "escalation_phrases";
28240
+ default:
28241
+ return void 0;
28242
+ }
28243
+ }
28244
+ function buildPrimePackPreview(inputs) {
28245
+ const totalInputs = inputs.sensitive_terms.length + inputs.approved_domains.length + inputs.blocked_destinations.length + inputs.escalation_phrases.length;
28246
+ const examples = [
28247
+ inputs.sensitive_terms.length > 0 ? `${inputs.sensitive_terms.length} sensitive term(s) will be flagged before prompt send.` : "",
28248
+ inputs.blocked_destinations.length > 0 ? `${inputs.blocked_destinations.length} blocked destination rule(s) will stop risky sharing.` : "",
28249
+ inputs.escalation_phrases.length > 0 ? `${inputs.escalation_phrases.length} escalation phrase(s) will require confirmation.` : "",
28250
+ inputs.approved_domains.length > 0 ? `${inputs.approved_domains.length} approved domain(s) will lower false positives.` : ""
28251
+ ].filter(Boolean);
28252
+ return {
28253
+ total_inputs: totalInputs,
28254
+ flag_rules: inputs.sensitive_terms.length,
28255
+ block_rules: inputs.blocked_destinations.length,
28256
+ confirm_rules: inputs.escalation_phrases.length,
28257
+ examples
28258
+ };
28259
+ }
28260
+ function buildPrimePackDeployReceipt(receipt) {
28261
+ return {
28262
+ receipt_id: receipt.receipt_id,
28263
+ action: "deploy",
28264
+ created_at: receipt.created_at,
28265
+ receipt_hash: receipt.receipt_hash,
28266
+ raw_prompt_content_included: false,
28267
+ token_printed: false
28268
+ };
28269
+ }
28270
+ function buildPrimePackDeployGuardianReceipt(inputs, preview, transactionId, timestamp) {
28271
+ const core = {
28272
+ transaction_id: transactionId,
28273
+ action: "deploy",
28274
+ input_counts: buildPrimePackInputCounts(inputs),
28275
+ preview,
28276
+ input_hashes: buildPrimePackInputHashes(inputs),
28277
+ created_at: timestamp,
28278
+ raw_content_included: false
28279
+ };
28280
+ return {
28281
+ schema_version: PERSONAL_FABRIC_CONTRACTS_SCHEMA_VERSION,
28282
+ receipt_id: `receipt:${transactionId}:prime-pack-deploy`,
28283
+ transaction_id: transactionId,
28284
+ receipt_type: "policy",
28285
+ disposition: "allow",
28286
+ policy_basis: [
28287
+ "prime-pack-deploy-metadata-only",
28288
+ `inputs:${preview.total_inputs}`,
28289
+ `flag_rules:${preview.flag_rules}`,
28290
+ `block_rules:${preview.block_rules}`,
28291
+ `confirm_rules:${preview.confirm_rules}`
28292
+ ],
28293
+ receipt_hash: `sha256:${digest4(JSON.stringify(core))}`,
28294
+ raw_content_included: false,
28295
+ created_at: timestamp,
28296
+ summary: `Guardian deployed a local Prime Pack with ${preview.total_inputs} metadata rule input(s).`
28297
+ };
28298
+ }
28299
+ function buildPrimePackDeployEvent(inputs, preview, transactionId, timestamp, receipt) {
28300
+ const payload = {
28301
+ action: "deploy",
28302
+ input_counts: buildPrimePackInputCounts(inputs),
28303
+ input_hashes: buildPrimePackInputHashes(inputs),
28304
+ preview,
28305
+ raw_content_included: false
28306
+ };
28307
+ return {
28308
+ event_id: `event:${transactionId}:prime-pack-deploy`,
28309
+ transaction_id: transactionId,
28310
+ event_type: "policy_decided",
28311
+ occurred_at: timestamp,
28312
+ actor: "guardian-daemon",
28313
+ summary: "Prime Pack deployment recorded with metadata-only hashes.",
28314
+ payload_hash: `sha256:${digest4(JSON.stringify(payload))}`,
28315
+ receipt_id: receipt.receipt_id,
28316
+ graph_refs: ["policy-pack:prime-pack", `receipt:${receipt.receipt_id}`],
28317
+ redaction_state: "metadata_only"
28318
+ };
28319
+ }
28320
+ function buildPrimePackInputCounts(inputs) {
28321
+ return {
28322
+ sensitive_terms: inputs.sensitive_terms.length,
28323
+ approved_domains: inputs.approved_domains.length,
28324
+ blocked_destinations: inputs.blocked_destinations.length,
28325
+ escalation_phrases: inputs.escalation_phrases.length
28326
+ };
28327
+ }
28328
+ function buildPrimePackInputHashes(inputs) {
28329
+ return {
28330
+ sensitive_terms: inputs.sensitive_terms.map((entry) => `sha256:${digest4(entry)}`),
28331
+ approved_domains: inputs.approved_domains.map((entry) => `sha256:${digest4(entry)}`),
28332
+ blocked_destinations: inputs.blocked_destinations.map((entry) => `sha256:${digest4(entry)}`),
28333
+ escalation_phrases: inputs.escalation_phrases.map((entry) => `sha256:${digest4(entry)}`)
28334
+ };
28335
+ }
26955
28336
  function parsePromptCoachMode2(value) {
26956
28337
  return PROMPT_COACH_MODES2.includes(value) ? value : void 0;
26957
28338
  }
@@ -27031,7 +28412,7 @@ function writeHtml(response, statusCode, body, options = {}) {
27031
28412
  response.writeHead(statusCode, {
27032
28413
  "Content-Type": "text/html; charset=utf-8",
27033
28414
  "Referrer-Policy": "no-referrer",
27034
- "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
28415
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'",
27035
28416
  "X-Content-Type-Options": "nosniff"
27036
28417
  });
27037
28418
  response.end(body);
@@ -27058,19 +28439,168 @@ function closeGuardianDaemonServer(server) {
27058
28439
  } catch {
27059
28440
  }
27060
28441
  }
27061
- function parseGuardianUrl(url2) {
27062
- if (!url2) {
27063
- return void 0;
27064
- }
27065
- try {
27066
- return new URL(url2, "http://127.0.0.1");
27067
- } catch {
27068
- return void 0;
27069
- }
28442
+ function parseGuardianUrl(url2) {
28443
+ if (!url2) {
28444
+ return void 0;
28445
+ }
28446
+ try {
28447
+ return new URL(url2, "http://127.0.0.1");
28448
+ } catch {
28449
+ return void 0;
28450
+ }
28451
+ }
28452
+ function buildControlTowerViewModel(profile, receipts, searchGraph, learningEvents, localDataStatus, releaseDoctorStatus, adminNotices = [], marketplaceRequests = []) {
28453
+ const recentReceipts = receipts.slice().sort((left, right) => right.created_at.localeCompare(left.created_at)).slice(0, CONTROL_TOWER_RECEIPT_LIMIT);
28454
+ const blocked = recentReceipts.filter((receipt) => receipt.disposition === "block").length;
28455
+ const flagged = recentReceipts.filter(
28456
+ (receipt) => receipt.disposition === "warn" || receipt.disposition === "confirm"
28457
+ ).length;
28458
+ const allowed = recentReceipts.filter(
28459
+ (receipt) => receipt.disposition === "allow" || receipt.disposition === "rewrite"
28460
+ ).length;
28461
+ const privacyDefaults = profile.privacy.privacy_mode_enabled && !profile.privacy.raw_content_capture_enabled && !profile.privacy.cloud_sync_enabled && !profile.privacy.local_app_autopilot_enabled ? "on" : "needs review";
28462
+ const protectedState = releaseDoctorStatus.doctor.checkCounts.fail === 0 && privacyDefaults === "on";
28463
+ return {
28464
+ ok: true,
28465
+ schema_version: "contextecf/project-guardian-control-tower-view-model/v1",
28466
+ surfaces: [
28467
+ {
28468
+ id: "personal",
28469
+ label: "Personal Guardian",
28470
+ audience: "Individual users",
28471
+ purpose: "Protection status, prompt coaching, privacy, receipts, and browser connection."
28472
+ },
28473
+ {
28474
+ id: "admin",
28475
+ label: "Guardian Admin",
28476
+ audience: "Organizations",
28477
+ purpose: "Groups, policy packs, marketplace approvals, Prime Pack, scope, and audit."
28478
+ },
28479
+ {
28480
+ id: "power_user",
28481
+ label: "Power User",
28482
+ audience: "Developers and technical operators",
28483
+ purpose: "MCP, local data, doctor checks, receipts, and release evidence."
28484
+ },
28485
+ {
28486
+ id: "marketplace_contributor",
28487
+ label: "Marketplace Contributor",
28488
+ audience: "Policy pack creators",
28489
+ purpose: "Submission metadata, examples, license terms, and review state."
28490
+ }
28491
+ ],
28492
+ personal: {
28493
+ protectionLabel: protectedState ? "You're Protected" : "Needs Review",
28494
+ protectionDetail: protectedState ? "Guardian is running locally with metadata-only privacy defaults." : "Guardian is running, but one or more install checks need attention.",
28495
+ privacyDefaults,
28496
+ activePolicyPacks: [...profile.policy_packs.active],
28497
+ activityCounts: { blocked, flagged, allowed }
28498
+ },
28499
+ admin: {
28500
+ organizationLabel: "Example organization deployment",
28501
+ notices: adminNotices.slice(0, CONTROL_TOWER_ADMIN_NOTICE_LIMIT),
28502
+ marketplaceRequests: marketplaceRequests.slice(0, CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT),
28503
+ managedGroups: buildControlTowerDeploymentGroups(profile),
28504
+ primePack: {
28505
+ status: "draft_ready",
28506
+ supportedInputs: ["typed terms", "text upload", "CSV upload", "JSON upload"],
28507
+ deployable: true,
28508
+ receiptRequired: true
28509
+ }
28510
+ },
28511
+ marketplace: buildControlTowerMarketplaceRows(profile, marketplaceRequests),
28512
+ releaseAutomation: {
28513
+ repository: GUARDIAN_RELEASE_REPOSITORY,
28514
+ workflow: GUARDIAN_RELEASE_DISPATCH_WORKFLOW,
28515
+ npmPackage: GUARDIAN_NPM_PACKAGE_NAME,
28516
+ registry: GUARDIAN_NPM_REGISTRY,
28517
+ chromeExtensionId: GUARDIAN_HALO_CHROME_EXTENSION_ID,
28518
+ dryRunCommand: CONTROL_TOWER_DRY_RUN_COMMAND_TEMPLATE,
28519
+ publishCommand: CONTROL_TOWER_RELEASE_COMMAND_TEMPLATE,
28520
+ humanIntervention: [
28521
+ "Chrome Web Store upload/review remains a store-console step until Google approval APIs are wired.",
28522
+ "Native installer signing still requires platform signing credentials.",
28523
+ `Latest doctor status: ${releaseDoctorStatus.doctor.overall}.`,
28524
+ `Search Graph has ${searchGraph.nodes.length} node(s); Learning Graph has ${learningEvents.length} event(s).`,
28525
+ `Local data encryption: ${localDataStatus.encryptionStatus}.`
28526
+ ]
28527
+ },
28528
+ raw_content_included: false,
28529
+ token_printed: false
28530
+ };
28531
+ }
28532
+ function buildControlTowerDeploymentGroups(profile) {
28533
+ return [
28534
+ {
28535
+ id: "group:individual-default",
28536
+ label: "Personal default",
28537
+ users: 1,
28538
+ posture: profile.posture_profile.selected,
28539
+ packs: [...profile.policy_packs.active]
28540
+ },
28541
+ {
28542
+ id: "group:employees",
28543
+ label: "Employees",
28544
+ users: 24,
28545
+ posture: "balanced",
28546
+ packs: ["privacy-first", "executive"]
28547
+ },
28548
+ {
28549
+ id: "group:technical-users",
28550
+ label: "Technical users",
28551
+ users: 8,
28552
+ posture: "guided",
28553
+ packs: ["privacy-first", "developer"]
28554
+ },
28555
+ {
28556
+ id: "group:high-risk-functions",
28557
+ label: "High-risk functions",
28558
+ users: 5,
28559
+ posture: "high_assurance",
28560
+ packs: ["privacy-first", "finance"]
28561
+ }
28562
+ ];
28563
+ }
28564
+ function buildControlTowerMarketplaceRows(profile, marketplaceRequests = []) {
28565
+ const activePacks = new Set(profile.policy_packs.active);
28566
+ const requestsByManifest = new Map(
28567
+ marketplaceRequests.map((request) => [request.marketplace_manifest_id, request])
28568
+ );
28569
+ return GUARDIAN_POLICY_PACK_MARKETPLACE_CATALOG.map((manifest) => {
28570
+ const request = requestsByManifest.get(manifest.marketplace_manifest_id);
28571
+ const connected = activePacks.has(manifest.pack_id);
28572
+ return {
28573
+ packId: manifest.pack_id,
28574
+ name: manifest.display_name,
28575
+ version: manifest.pack_version,
28576
+ audience: manifest.audiences.join(", "),
28577
+ category: manifest.policy_domains.join(", "),
28578
+ publisher: manifest.publisher_display_name,
28579
+ price: manifest.price_model,
28580
+ licenseRef: manifest.license_ref,
28581
+ supportRef: manifest.support_ref ?? "Not provided",
28582
+ state: connected ? "connected" : request?.status ?? manifest.listing_status,
28583
+ permissions: [
28584
+ manifest.provider_rules_summary,
28585
+ manifest.source_rules_summary,
28586
+ manifest.app_permission_summary
28587
+ ],
28588
+ protectedDataTypes: manifest.protected_data_types,
28589
+ examples: [
28590
+ manifest.short_description,
28591
+ manifest.confirmation_rules_summary,
28592
+ manifest.data_handling_summary
28593
+ ],
28594
+ risk: manifest.limitations.length > 0 ? manifest.limitations[0] : "Standard false-positive review recommended before wide deployment.",
28595
+ requestState: connected ? "connected" : request?.status ?? "not_requested",
28596
+ requestReceiptId: request?.receipt_id,
28597
+ requestedAt: request?.requested_at
28598
+ };
28599
+ });
27070
28600
  }
27071
28601
  function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { nodes: [], edges: [] }, learningEvents = [], localDataStatus = createEmptyControlTowerLocalDataStatus(profile), releaseDoctorStatus = createEmptyControlTowerReleaseDoctorStatus(
27072
28602
  profile
27073
- )) {
28603
+ ), adminNotices = [], marketplaceRequests = []) {
27074
28604
  const policyPacks = profile.policy_packs.available.length;
27075
28605
  const activePolicyPacks = profile.policy_packs.active.join(", ") || "none";
27076
28606
  const recentReceipts = receipts.slice().sort((left, right) => right.created_at.localeCompare(left.created_at)).slice(0, CONTROL_TOWER_RECEIPT_LIMIT);
@@ -27248,9 +28778,28 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27248
28778
  <button type="submit" class="provider-pill" aria-label="Revoke ${escapeHtml(providerDisplayName(row.id))}"><span></span>${escapeHtml(providerDisplayName(row.id))}</button>
27249
28779
  </form>`
27250
28780
  ).join("\n");
28781
+ const viewModel = buildControlTowerViewModel(
28782
+ profile,
28783
+ recentReceipts,
28784
+ searchGraph,
28785
+ learningEvents,
28786
+ localDataStatus,
28787
+ releaseDoctorStatus,
28788
+ adminNotices,
28789
+ marketplaceRequests
28790
+ );
28791
+ const roleCardsHtml = renderControlTowerRoleCards(viewModel);
28792
+ const adminNoticeBarHtml = renderAdminNoticeBar(viewModel.admin.notices);
28793
+ const adminPanelsHtml = renderGuardianAdminSurface(
28794
+ viewModel,
28795
+ profile,
28796
+ recentReceipts,
28797
+ appPermissionRows
28798
+ );
28799
+ const localGuardianStatus = renderLocalGuardianStatus(profile.runtime.daemon_status);
27251
28800
  const rows = [
27252
28801
  ["Profile", profile.profile_dir],
27253
- ["Daemon", profile.runtime.daemon_status],
28802
+ ["Local Guardian", localGuardianStatus],
27254
28803
  [
27255
28804
  "Posture Profile",
27256
28805
  `${profile.posture_profile.selected}; managed baseline ${profile.posture_profile.managed_baseline_id ?? "none"}`
@@ -27418,6 +28967,161 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27418
28967
  background: rgba(15, 23, 42, 0.78);
27419
28968
  font-size: 16px;
27420
28969
  }
28970
+ .surface-tabs, .role-grid, .admin-grid, .marketplace-grid {
28971
+ display: grid;
28972
+ gap: 12px;
28973
+ }
28974
+ .surface-tabs {
28975
+ grid-template-columns: repeat(2, minmax(0, 1fr));
28976
+ margin: 28px 0 16px;
28977
+ }
28978
+ .surface-tab {
28979
+ display: grid;
28980
+ gap: 6px;
28981
+ width: 100%;
28982
+ min-height: 78px;
28983
+ padding: 15px 16px;
28984
+ border-color: rgba(148, 163, 184, 0.2);
28985
+ background: rgba(13, 24, 40, 0.84);
28986
+ color: #eff6ff;
28987
+ text-align: left;
28988
+ }
28989
+ .surface-tab strong, .role-card strong, .admin-card strong { font-size: 17px; }
28990
+ .surface-tab span, .role-card span, .admin-card span, .marketplace-card span {
28991
+ color: #98a7bc;
28992
+ font-size: 13px;
28993
+ line-height: 1.35;
28994
+ }
28995
+ .surface-tab.is-active, .surface-tab[aria-pressed="true"] {
28996
+ border-color: rgba(34, 197, 94, 0.5);
28997
+ background: rgba(6, 45, 37, 0.78);
28998
+ box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.12);
28999
+ }
29000
+ .surface-panel[hidden] { display: none; }
29001
+ .role-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
29002
+ .role-card, .admin-card, .marketplace-card, .prime-pack-builder, .release-command-card {
29003
+ border: 1px solid rgba(148, 163, 184, 0.18);
29004
+ border-radius: 8px;
29005
+ background: rgba(11, 24, 40, 0.82);
29006
+ padding: 18px;
29007
+ }
29008
+ .admin-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
29009
+ .marketplace-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
29010
+ .marketplace-card h3, .admin-card h3, .prime-pack-builder h3, .release-command-card h3 {
29011
+ margin: 0 0 8px;
29012
+ color: #f6f8ff;
29013
+ font-size: 18px;
29014
+ }
29015
+ .marketplace-card p, .admin-card p, .prime-pack-builder p, .release-command-card p {
29016
+ margin-bottom: 12px;
29017
+ }
29018
+ .marketplace-detail-drawer {
29019
+ margin-top: 14px;
29020
+ border-top: 1px solid rgba(148, 163, 184, 0.16);
29021
+ padding-top: 12px;
29022
+ }
29023
+ .marketplace-detail-drawer summary {
29024
+ cursor: pointer;
29025
+ color: #dce7f7;
29026
+ font-size: 14px;
29027
+ font-weight: 800;
29028
+ }
29029
+ .marketplace-detail-drawer dl {
29030
+ display: grid;
29031
+ gap: 8px;
29032
+ margin: 12px 0 0;
29033
+ }
29034
+ .marketplace-detail-drawer dt {
29035
+ color: #f6f8ff;
29036
+ font-size: 13px;
29037
+ font-weight: 800;
29038
+ }
29039
+ .marketplace-detail-drawer dd {
29040
+ margin: 0;
29041
+ color: #98a7bc;
29042
+ font-size: 13px;
29043
+ line-height: 1.45;
29044
+ }
29045
+ .marketplace-request-form {
29046
+ display: grid;
29047
+ gap: 8px;
29048
+ margin-top: 14px;
29049
+ }
29050
+ .marketplace-request-form span,
29051
+ .marketplace-request-state {
29052
+ margin: 14px 0 0;
29053
+ color: #9fb0c6;
29054
+ font-size: 13px;
29055
+ line-height: 1.45;
29056
+ }
29057
+ .marketplace-request-list {
29058
+ display: grid;
29059
+ gap: 10px;
29060
+ margin-top: 16px;
29061
+ }
29062
+ .marketplace-request-card {
29063
+ display: grid;
29064
+ gap: 6px;
29065
+ padding: 12px 14px;
29066
+ border: 1px solid rgba(34, 197, 94, 0.28);
29067
+ border-radius: 8px;
29068
+ background: rgba(6, 45, 37, 0.52);
29069
+ }
29070
+ .marketplace-request-card span {
29071
+ color: #9fb0c6;
29072
+ font-size: 13px;
29073
+ }
29074
+ .metadata-list {
29075
+ display: grid;
29076
+ gap: 8px;
29077
+ margin: 14px 0 0;
29078
+ padding: 0;
29079
+ list-style: none;
29080
+ }
29081
+ .metadata-list li {
29082
+ display: flex;
29083
+ gap: 8px;
29084
+ justify-content: space-between;
29085
+ border-top: 1px solid rgba(148, 163, 184, 0.12);
29086
+ padding-top: 8px;
29087
+ color: #c8d2e3;
29088
+ font-size: 13px;
29089
+ }
29090
+ .metadata-list b { color: #eff6ff; }
29091
+ .builder-grid {
29092
+ display: grid;
29093
+ grid-template-columns: repeat(2, minmax(0, 1fr));
29094
+ gap: 12px;
29095
+ margin-top: 12px;
29096
+ }
29097
+ .button-row {
29098
+ display: flex;
29099
+ flex-wrap: wrap;
29100
+ gap: 10px;
29101
+ margin-top: 12px;
29102
+ }
29103
+ textarea {
29104
+ width: 100%;
29105
+ min-height: 104px;
29106
+ border: 1px solid rgba(148, 163, 184, 0.24);
29107
+ border-radius: 8px;
29108
+ background: rgba(8, 13, 24, 0.92);
29109
+ color: #f6f8ff;
29110
+ padding: 10px;
29111
+ font: inherit;
29112
+ resize: vertical;
29113
+ }
29114
+ .command-box {
29115
+ display: block;
29116
+ width: 100%;
29117
+ padding: 12px;
29118
+ border: 1px solid rgba(148, 163, 184, 0.2);
29119
+ border-radius: 8px;
29120
+ background: rgba(5, 10, 20, 0.88);
29121
+ color: #d8f8e7;
29122
+ overflow-x: auto;
29123
+ white-space: nowrap;
29124
+ }
27421
29125
  .eyebrow {
27422
29126
  width: fit-content;
27423
29127
  margin-bottom: 16px;
@@ -27599,6 +29303,22 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27599
29303
  }
27600
29304
  .provider-pill:hover { background: rgba(30, 41, 59, 0.78); }
27601
29305
  .provider-pill span { width: 8px; height: 8px; border-radius: 999px; background: #22c55e; }
29306
+ .notice-bar { display: grid; gap: 10px; margin: 20px 0 26px; }
29307
+ .notice-card {
29308
+ display: grid;
29309
+ gap: 6px;
29310
+ padding: 14px 16px;
29311
+ border: 1px solid rgba(96, 165, 250, 0.32);
29312
+ border-radius: 8px;
29313
+ color: #d8e1f0;
29314
+ background: rgba(15, 23, 42, 0.78);
29315
+ }
29316
+ .notice-card.warning { border-color: rgba(245, 158, 11, 0.46); background: rgba(39, 26, 9, 0.82); }
29317
+ .notice-card.success { border-color: rgba(34, 197, 94, 0.46); background: rgba(6, 45, 37, 0.82); }
29318
+ .notice-card strong { color: #f6f8ff; font-size: 16px; }
29319
+ .notice-card span { color: #91a1b8; font-size: 13px; font-weight: 700; }
29320
+ .notice-card p { margin: 0; color: #d8e1f0; line-height: 1.45; }
29321
+ .notice-form { display: grid; gap: 12px; }
27602
29322
  .activity-heading { justify-content: space-between; gap: 16px; margin-bottom: 12px; }
27603
29323
  .activity-heading h2 { margin: 0; }
27604
29324
  .filter-chips { gap: 8px; flex-wrap: wrap; }
@@ -27619,6 +29339,14 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27619
29339
  .chip:hover { color: #f5f7fb; background: rgba(30, 41, 59, 0.78); }
27620
29340
  .chip.is-active, .chip[aria-pressed="true"] { border-color: rgba(34, 197, 94, 0.38); color: #d7ffe5; background: rgba(21, 128, 61, 0.22); }
27621
29341
  .activity-card[hidden], .empty-filter-state[hidden] { display: none; }
29342
+ .receipt-row.is-selected-receipt {
29343
+ outline: 2px solid rgba(56, 230, 120, 0.7);
29344
+ outline-offset: 4px;
29345
+ background: rgba(34, 197, 94, 0.08);
29346
+ }
29347
+ .receipt-row.is-selected-receipt details {
29348
+ border-radius: 8px;
29349
+ }
27622
29350
  .empty-filter-state {
27623
29351
  padding: 22px;
27624
29352
  border: 1px dashed rgba(148, 163, 184, 0.24);
@@ -27676,6 +29404,7 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27676
29404
  .protection-card { min-height: 320px; }
27677
29405
  .activity-card { grid-template-columns: 48px 1fr; }
27678
29406
  .activity-badge { grid-column: 2; width: fit-content; }
29407
+ .surface-tabs, .role-grid, .admin-grid, .marketplace-grid, .builder-grid { grid-template-columns: 1fr; }
27679
29408
  section { padding: 16px; overflow-x: auto; }
27680
29409
  table { min-width: 720px; }
27681
29410
  .activity-heading { align-items: flex-start; flex-direction: column; }
@@ -27688,7 +29417,7 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27688
29417
  <header class="topbar">
27689
29418
  <div class="brand"><span class="brand-mark">G</span><span>Guardian</span></div>
27690
29419
  <div class="topbar-actions">
27691
- <span class="status-pill"><span class="status-dot"></span>${escapeHtml(profile.runtime.daemon_status)}</span>
29420
+ <span class="status-pill"><span class="status-dot"></span>${escapeHtml(localGuardianStatus)}</span>
27692
29421
  <span class="icon-button" aria-label="Settings">&#9881;</span>
27693
29422
  </div>
27694
29423
  </header>
@@ -27696,6 +29425,25 @@ function renderControlTowerStatusPage(profile, receipts = [], searchGraph = { no
27696
29425
  <div class="eyebrow">Local-first status</div>
27697
29426
  <h1>Project Guardian Control Tower</h1>
27698
29427
  <p class="intro">This local page confirms Guardian is watching your AI surface with privacy-first, metadata-only governance. It never exposes the runtime token, raw prompts, raw responses, or local action payloads.</p>
29428
+ ${adminNoticeBarHtml}
29429
+ <div class="surface-tabs" aria-label="Control Tower surfaces">
29430
+ <button type="button" class="surface-tab is-active" data-surface-tab="personal" aria-pressed="true">
29431
+ <strong>Personal Guardian</strong>
29432
+ <span>Simple protection, privacy, coaching, browser connection, and receipts.</span>
29433
+ </button>
29434
+ <button type="button" class="surface-tab" data-surface-tab="admin" aria-pressed="false">
29435
+ <strong>Guardian Admin</strong>
29436
+ <span>Policy packs, marketplace approvals, Prime Pack, deployment, audit, and releases.</span>
29437
+ </button>
29438
+ </div>
29439
+ <section aria-labelledby="role-experiences">
29440
+ <h2 id="role-experiences">Role Experiences</h2>
29441
+ <p>Guardian ships one local brain with different surfaces for individuals, power users, enterprise admins, and marketplace contributors.</p>
29442
+ <div class="role-grid">
29443
+ ${roleCardsHtml}
29444
+ </div>
29445
+ </section>
29446
+ <div class="surface-panel" data-surface-panel="personal">
27699
29447
  <div class="hero-grid" aria-label="Guardian protection overview">
27700
29448
  <article class="protection-card">
27701
29449
  <div class="${protectionRingClass}"><div class="ring-core">G</div></div>
@@ -27717,6 +29465,7 @@ ${heroMetricCards}
27717
29465
  </div>
27718
29466
  </div>
27719
29467
  <p class="activity-filter-status" data-activity-filter-status aria-live="polite">Showing all local Guardian activity.</p>
29468
+ <p class="activity-filter-status" data-guardian-time-zone-label>Times use your browser's local timezone.</p>
27720
29469
  <div class="activity-list" data-activity-list>
27721
29470
  ${activityFeedHtml}
27722
29471
  <div class="empty-filter-state" data-activity-empty hidden>No activity matches this filter yet.</div>
@@ -27885,7 +29634,7 @@ ${releaseDoctorRows}
27885
29634
  </tbody>
27886
29635
  </table>
27887
29636
  </section>
27888
- <section aria-labelledby="receipt-drilldowns">
29637
+ <section id="receipts" aria-labelledby="receipt-drilldowns">
27889
29638
  <div class="activity-heading">
27890
29639
  <h2 id="receipt-drilldowns">What Guardian did today</h2>
27891
29640
  <div class="filter-chips" aria-label="Activity filters">
@@ -27895,7 +29644,7 @@ ${releaseDoctorRows}
27895
29644
  <span class="chip">Allowed</span>
27896
29645
  </div>
27897
29646
  </div>
27898
- <p><span class="visually-hidden">Activity &amp; Receipts.</span>Inspect recent Guardian decisions without raw prompt, response, or local-action payload content.</p>
29647
+ <p><span class="visually-hidden">Activity &amp; Receipts.</span>Inspect recent Guardian decisions without raw prompt, response, or local-action payload content. Links from Halo open the matching receipt here.</p>
27899
29648
  <table aria-label="Guardian receipt drilldowns">
27900
29649
  <thead>
27901
29650
  <tr><th scope="col">Receipt</th><th scope="col">Type</th><th scope="col">Decision</th><th scope="col">Details</th></tr>
@@ -27975,9 +29724,47 @@ ${learningEventRows}
27975
29724
  </section>
27976
29725
  </div>
27977
29726
  </details>
29727
+ </div>
29728
+ <div class="surface-panel" data-surface-panel="admin" hidden>
29729
+ ${adminPanelsHtml}
29730
+ </div>
27978
29731
  </main>
27979
29732
  <script>
27980
29733
  (() => {
29734
+ const surfaceTabs = Array.from(document.querySelectorAll('[data-surface-tab]'));
29735
+ const surfacePanels = Array.from(document.querySelectorAll('[data-surface-panel]'));
29736
+ const selectSurface = (surface) => {
29737
+ for (const tab of surfaceTabs) {
29738
+ const selected = tab.getAttribute('data-surface-tab') === surface;
29739
+ tab.classList.toggle('is-active', selected);
29740
+ tab.setAttribute('aria-pressed', selected ? 'true' : 'false');
29741
+ }
29742
+ for (const panel of surfacePanels) {
29743
+ panel.hidden = panel.getAttribute('data-surface-panel') !== surface;
29744
+ }
29745
+ };
29746
+ for (const tab of surfaceTabs) {
29747
+ tab.addEventListener('click', () => selectSurface(tab.getAttribute('data-surface-tab') || 'personal'));
29748
+ }
29749
+ const hydrateViewModel = async () => {
29750
+ const controller = new AbortController();
29751
+ const timeoutId = window.setTimeout(() => controller.abort(), 2500);
29752
+ try {
29753
+ const response = await fetch('/v1/guardian/control-tower/view-model', {
29754
+ headers: { accept: 'application/json' },
29755
+ signal: controller.signal,
29756
+ });
29757
+ if (!response.ok) return;
29758
+ const body = await response.json();
29759
+ if (!body || body.ok !== true) return;
29760
+ document.documentElement.setAttribute('data-view-model', body.schema_version || 'loaded');
29761
+ } catch {
29762
+ // Keep the server-rendered Control Tower if local hydration is unavailable.
29763
+ } finally {
29764
+ window.clearTimeout(timeoutId);
29765
+ }
29766
+ };
29767
+ hydrateViewModel();
27981
29768
  const filters = Array.from(document.querySelectorAll('[data-activity-filter]'));
27982
29769
  const list = document.querySelector('[data-activity-list]');
27983
29770
  const emptyState = document.querySelector('[data-activity-empty]');
@@ -28013,6 +29800,85 @@ ${learningEventRows}
28013
29800
  }
28014
29801
  setFilterCopy(selectedFilter);
28015
29802
  };
29803
+ const absoluteLocalTimeFormatter = new Intl.DateTimeFormat(navigator.language || undefined, {
29804
+ month: 'short',
29805
+ day: 'numeric',
29806
+ year: 'numeric',
29807
+ hour: 'numeric',
29808
+ minute: '2-digit',
29809
+ timeZoneName: 'short',
29810
+ });
29811
+ const relativeTimeFormatter = new Intl.RelativeTimeFormat(navigator.language || undefined, {
29812
+ numeric: 'auto',
29813
+ });
29814
+ const formatAbsoluteLocalTime = (createdAt) => {
29815
+ const date = new Date(createdAt);
29816
+ if (Number.isNaN(date.getTime())) return createdAt || '';
29817
+ return absoluteLocalTimeFormatter.format(date);
29818
+ };
29819
+ const formatLocalTime = (createdAt) => {
29820
+ const date = new Date(createdAt);
29821
+ if (Number.isNaN(date.getTime())) return createdAt || '';
29822
+ const diffMs = date.getTime() - Date.now();
29823
+ const absMs = Math.abs(diffMs);
29824
+ if (absMs < 60 * 1000) return 'just now';
29825
+ if (absMs < 60 * 60 * 1000) {
29826
+ return relativeTimeFormatter.format(Math.round(diffMs / (60 * 1000)), 'minute');
29827
+ }
29828
+ if (absMs < 24 * 60 * 60 * 1000) {
29829
+ return relativeTimeFormatter.format(Math.round(diffMs / (60 * 60 * 1000)), 'hour');
29830
+ }
29831
+ if (absMs < 7 * 24 * 60 * 60 * 1000) {
29832
+ return relativeTimeFormatter.format(Math.round(diffMs / (24 * 60 * 60 * 1000)), 'day');
29833
+ }
29834
+ return formatAbsoluteLocalTime(createdAt);
29835
+ };
29836
+ const hydrateLocalTimes = () => {
29837
+ for (const element of Array.from(document.querySelectorAll('[data-guardian-local-time]'))) {
29838
+ const createdAt = element.getAttribute('datetime') || element.getAttribute('data-created-at') || '';
29839
+ const formatted = formatLocalTime(createdAt);
29840
+ if (formatted) element.textContent = formatted;
29841
+ if (createdAt) {
29842
+ element.setAttribute('title', 'Your local time: ' + formatAbsoluteLocalTime(createdAt) + ' \xB7 Stored by Guardian as ' + createdAt);
29843
+ }
29844
+ }
29845
+ };
29846
+ const hydrateLocalTimeZoneLabels = () => {
29847
+ let timeZone = '';
29848
+ try {
29849
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
29850
+ } catch {
29851
+ timeZone = '';
29852
+ }
29853
+ const label = timeZone
29854
+ ? 'Times use your browser timezone: ' + timeZone + '.'
29855
+ : "Times use your browser's local timezone.";
29856
+ for (const element of Array.from(document.querySelectorAll('[data-guardian-time-zone-label]'))) {
29857
+ element.textContent = label;
29858
+ }
29859
+ };
29860
+ const receiptIdFromHash = () => {
29861
+ const hash = window.location.hash || '';
29862
+ if (!hash.startsWith('#receipt=')) return '';
29863
+ try {
29864
+ return decodeURIComponent(hash.slice('#receipt='.length));
29865
+ } catch {
29866
+ return hash.slice('#receipt='.length);
29867
+ }
29868
+ };
29869
+ const highlightReceiptFromHash = () => {
29870
+ const selectedReceiptId = receiptIdFromHash();
29871
+ let matchedRow = null;
29872
+ for (const row of Array.from(document.querySelectorAll('[data-receipt-row]'))) {
29873
+ const selected = Boolean(selectedReceiptId) && row.getAttribute('data-receipt-id') === selectedReceiptId;
29874
+ row.classList.toggle('is-selected-receipt', selected);
29875
+ if (selected) matchedRow = row;
29876
+ }
29877
+ if (!matchedRow) return;
29878
+ const details = matchedRow.querySelector('details');
29879
+ if (details) details.open = true;
29880
+ matchedRow.scrollIntoView({ block: 'center', behavior: 'smooth' });
29881
+ };
28016
29882
  const createActivityCard = (activity) => {
28017
29883
  const card = document.createElement('article');
28018
29884
  card.className = 'activity-card ' + activity.tone;
@@ -28026,7 +29892,16 @@ ${learningEventRows}
28026
29892
  title.textContent = activity.title;
28027
29893
  const meta = document.createElement('div');
28028
29894
  meta.className = 'activity-meta';
28029
- meta.textContent = activity.meta;
29895
+ if (activity.created_at) {
29896
+ const time = document.createElement('time');
29897
+ time.setAttribute('datetime', activity.created_at);
29898
+ time.setAttribute('data-guardian-local-time', '');
29899
+ time.textContent = formatLocalTime(activity.created_at);
29900
+ time.title = 'Your local time: ' + formatAbsoluteLocalTime(activity.created_at) + ' \xB7 Stored by Guardian as ' + activity.created_at;
29901
+ meta.append(time, ' \xB7 ' + (activity.receipt_type || 'receipt'));
29902
+ } else {
29903
+ meta.textContent = activity.meta;
29904
+ }
28030
29905
  body.append(title, meta);
28031
29906
  const badge = document.createElement('div');
28032
29907
  badge.className = 'activity-badge ' + activity.tone;
@@ -28069,6 +29944,11 @@ ${learningEventRows}
28069
29944
  applyFilter(filter.getAttribute('data-activity-filter') || 'all');
28070
29945
  });
28071
29946
  }
29947
+ window.addEventListener('hashchange', highlightReceiptFromHash);
29948
+ hydrateLocalTimeZoneLabels();
29949
+ hydrateLocalTimes();
29950
+ window.setInterval(hydrateLocalTimes, 60 * 1000);
29951
+ highlightReceiptFromHash();
28072
29952
  })();
28073
29953
  </script>
28074
29954
  </body>
@@ -28205,6 +30085,120 @@ function renderReleaseDoctorRows(status) {
28205
30085
  function renderDoctorCounts(doctor) {
28206
30086
  return `${doctor.checkCounts.pass} pass; ${doctor.checkCounts.warn} warn; ${doctor.checkCounts.fail} fail`;
28207
30087
  }
30088
+ async function readControlTowerAdminNotices(profileDir) {
30089
+ try {
30090
+ const raw = await readFile4(path4.join(profileDir, CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME), "utf8");
30091
+ const parsed = JSON.parse(raw);
30092
+ if (!isRecord(parsed) || !Array.isArray(parsed.notices)) {
30093
+ return [];
30094
+ }
30095
+ return parsed.notices.map(parseControlTowerAdminNotice).filter((notice) => Boolean(notice)).slice(0, CONTROL_TOWER_ADMIN_NOTICE_LIMIT);
30096
+ } catch {
30097
+ return [];
30098
+ }
30099
+ }
30100
+ async function writeControlTowerAdminNotices(profileDir, notices) {
30101
+ await writeFile2(
30102
+ path4.join(profileDir, CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME),
30103
+ `${JSON.stringify(
30104
+ {
30105
+ schema_version: "contextecf/project-guardian-control-tower-admin-notices/v1",
30106
+ notices: notices.slice(0, CONTROL_TOWER_ADMIN_NOTICE_LIMIT),
30107
+ raw_content_included: false,
30108
+ token_printed: false
30109
+ },
30110
+ null,
30111
+ 2
30112
+ )}
30113
+ `,
30114
+ "utf8"
30115
+ );
30116
+ }
30117
+ async function readControlTowerMarketplaceRequests(profileDir) {
30118
+ try {
30119
+ const raw = await readFile4(
30120
+ path4.join(profileDir, CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME),
30121
+ "utf8"
30122
+ );
30123
+ const parsed = JSON.parse(raw);
30124
+ if (!isRecord(parsed) || !Array.isArray(parsed.requests)) {
30125
+ return [];
30126
+ }
30127
+ return parsed.requests.map(parseControlTowerMarketplaceRequest).filter((request) => Boolean(request)).slice(0, CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT);
30128
+ } catch {
30129
+ return [];
30130
+ }
30131
+ }
30132
+ async function writeControlTowerMarketplaceRequests(profileDir, requests) {
30133
+ await writeFile2(
30134
+ path4.join(profileDir, CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME),
30135
+ `${JSON.stringify(
30136
+ {
30137
+ schema_version: "contextecf/project-guardian-marketplace-requests/v1",
30138
+ requests: requests.slice(0, CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT),
30139
+ raw_content_included: false,
30140
+ token_printed: false
30141
+ },
30142
+ null,
30143
+ 2
30144
+ )}
30145
+ `,
30146
+ "utf8"
30147
+ );
30148
+ }
30149
+ async function writeControlTowerPrimePackDraft(profileDir, draft) {
30150
+ await writeFile2(
30151
+ path4.join(profileDir, CONTROL_TOWER_PRIME_PACK_FILE_NAME),
30152
+ `${JSON.stringify(draft, null, 2)}
30153
+ `,
30154
+ "utf8"
30155
+ );
30156
+ }
30157
+ function parseControlTowerAdminNotice(value) {
30158
+ if (!isRecord(value)) {
30159
+ return null;
30160
+ }
30161
+ const audience = parseControlTowerAdminNoticeAudience(value.audience);
30162
+ const tone = parseControlTowerAdminNoticeTone(value.tone);
30163
+ if (typeof value.id !== "string" || typeof value.title !== "string" || typeof value.message !== "string" || typeof value.created_at !== "string" || !audience || !tone) {
30164
+ return null;
30165
+ }
30166
+ return {
30167
+ id: value.id.slice(0, 80),
30168
+ title: value.title.slice(0, CONTROL_TOWER_ADMIN_NOTICE_TITLE_LIMIT),
30169
+ message: value.message.slice(0, CONTROL_TOWER_ADMIN_NOTICE_MESSAGE_LIMIT),
30170
+ audience,
30171
+ tone,
30172
+ created_at: value.created_at,
30173
+ receipt_id: typeof value.receipt_id === "string" ? value.receipt_id.slice(0, 160) : void 0,
30174
+ receipt_hash: typeof value.receipt_hash === "string" ? value.receipt_hash.slice(0, 80) : void 0,
30175
+ raw_content_included: false
30176
+ };
30177
+ }
30178
+ function parseControlTowerMarketplaceRequest(value) {
30179
+ if (!isRecord(value)) {
30180
+ return null;
30181
+ }
30182
+ if (value.schema_version !== "contextecf/project-guardian-marketplace-request/v1" || value.action !== "request_review" || value.status !== "requested_metadata_only" || typeof value.request_id !== "string" || typeof value.pack_id !== "string" || typeof value.marketplace_manifest_id !== "string" || typeof value.pack_version !== "string" || typeof value.pack_name !== "string" || typeof value.publisher !== "string" || typeof value.requested_at !== "string" || typeof value.receipt_id !== "string" || typeof value.receipt_hash !== "string") {
30183
+ return null;
30184
+ }
30185
+ return {
30186
+ schema_version: "contextecf/project-guardian-marketplace-request/v1",
30187
+ request_id: value.request_id.slice(0, 120),
30188
+ pack_id: value.pack_id.slice(0, 120),
30189
+ marketplace_manifest_id: value.marketplace_manifest_id.slice(0, 160),
30190
+ pack_version: value.pack_version.slice(0, 40),
30191
+ pack_name: value.pack_name.slice(0, 120),
30192
+ publisher: value.publisher.slice(0, 120),
30193
+ action: "request_review",
30194
+ status: "requested_metadata_only",
30195
+ requested_at: value.requested_at,
30196
+ receipt_id: value.receipt_id.slice(0, 160),
30197
+ receipt_hash: value.receipt_hash.slice(0, 80),
30198
+ raw_content_included: false,
30199
+ token_printed: false
30200
+ };
30201
+ }
28208
30202
  async function buildControlTowerLocalDataStatus(profile, env = process.env) {
28209
30203
  const profileDir = profile.profile_dir;
28210
30204
  const encryption = buildControlTowerEncryptionStatus(
@@ -28469,6 +30463,8 @@ function buildActivityItems(receipts, filter) {
28469
30463
  tone: activityTone(receipt.disposition),
28470
30464
  icon: activityPlainIcon(receipt.disposition),
28471
30465
  title: receipt.summary,
30466
+ created_at: receipt.created_at,
30467
+ receipt_type: receipt.receipt_type,
28472
30468
  meta: `${formatActivityTime(receipt.created_at)} \xB7 ${receipt.receipt_type}`,
28473
30469
  label: activityLabel(receipt.disposition),
28474
30470
  raw_content_included: false
@@ -28481,14 +30477,14 @@ function renderActivityCard(receipt) {
28481
30477
  <div class="activity-icon">${activityIcon(receipt.disposition)}</div>
28482
30478
  <div>
28483
30479
  <div class="activity-title">${escapeHtml(receipt.summary)}</div>
28484
- <div class="activity-meta">${escapeHtml(formatActivityTime(receipt.created_at))} \xB7 ${escapeHtml(receipt.receipt_type)}</div>
30480
+ <div class="activity-meta">${renderLocalTime(receipt.created_at)} \xB7 ${escapeHtml(receipt.receipt_type)}</div>
28485
30481
  </div>
28486
30482
  <div class="activity-badge ${tone}">${escapeHtml(activityLabel(receipt.disposition))}</div>
28487
30483
  </article>`;
28488
30484
  }
28489
30485
  function renderReceiptRow(receipt) {
28490
30486
  const policyBasis = receipt.policy_basis.length > 0 ? receipt.policy_basis.join(", ") : "none recorded";
28491
- return ` <tr><th scope="row"><code>${escapeHtml(receipt.receipt_id)}</code></th><td>${escapeHtml(receipt.receipt_type)}</td><td>${escapeHtml(receipt.disposition)}</td><td><details><summary>${escapeHtml(receipt.summary)}</summary><dl><dt>Transaction</dt><dd><code>${escapeHtml(receipt.transaction_id)}</code></dd><dt>Created</dt><dd>${escapeHtml(receipt.created_at)}</dd><dt>Policy basis</dt><dd>${escapeHtml(policyBasis)}</dd><dt>Receipt hash</dt><dd><code>${escapeHtml(receipt.receipt_hash)}</code></dd><dt>Raw content</dt><dd>${receipt.raw_content_included ? "included" : "metadata-only"}</dd></dl></details></td></tr>`;
30487
+ return ` <tr class="receipt-row" data-receipt-row data-receipt-id="${escapeHtml(receipt.receipt_id)}"><th scope="row"><code>${escapeHtml(receipt.receipt_id)}</code></th><td>${escapeHtml(receipt.receipt_type)}</td><td>${escapeHtml(receipt.disposition)}</td><td><details><summary>${escapeHtml(receipt.summary)}</summary><dl><dt>Transaction</dt><dd><code>${escapeHtml(receipt.transaction_id)}</code></dd><dt>Created</dt><dd>${renderLocalTime(receipt.created_at)}</dd><dt>Policy basis</dt><dd>${escapeHtml(policyBasis)}</dd><dt>Receipt hash</dt><dd><code>${escapeHtml(receipt.receipt_hash)}</code></dd><dt>Raw content</dt><dd>${receipt.raw_content_included ? "included" : "metadata-only"}</dd></dl></details></td></tr>`;
28492
30488
  }
28493
30489
  function activityTone(disposition) {
28494
30490
  if (disposition === "block") return "block";
@@ -28517,17 +30513,321 @@ function activityFilterGroup(disposition) {
28517
30513
  if (disposition === "warn" || disposition === "confirm") return "flagged";
28518
30514
  return "allowed";
28519
30515
  }
28520
- function formatActivityTime(createdAt) {
30516
+ function formatActivityTime(createdAt, nowMs = Date.now()) {
28521
30517
  const parsed = Date.parse(createdAt);
28522
30518
  if (Number.isNaN(parsed)) {
28523
30519
  return createdAt;
28524
30520
  }
30521
+ const diffMs = parsed - nowMs;
30522
+ const absMs = Math.abs(diffMs);
30523
+ if (absMs < 60 * 1e3) {
30524
+ return "just now";
30525
+ }
30526
+ if (absMs < 60 * 60 * 1e3) {
30527
+ const minutes = Math.round(absMs / (60 * 1e3));
30528
+ return diffMs < 0 ? `${minutes} min ago` : `in ${minutes} min`;
30529
+ }
30530
+ if (absMs < 24 * 60 * 60 * 1e3) {
30531
+ const hours = Math.round(absMs / (60 * 60 * 1e3));
30532
+ return diffMs < 0 ? `${hours} hr ago` : `in ${hours} hr`;
30533
+ }
30534
+ if (absMs < 7 * 24 * 60 * 60 * 1e3) {
30535
+ const days = Math.round(absMs / (24 * 60 * 60 * 1e3));
30536
+ return diffMs < 0 ? `${days} day${days === 1 ? "" : "s"} ago` : `in ${days} day${days === 1 ? "" : "s"}`;
30537
+ }
28525
30538
  return new Date(parsed).toLocaleString("en-US", {
28526
- dateStyle: "medium",
28527
- timeStyle: "short",
28528
- timeZone: "UTC"
30539
+ month: "short",
30540
+ day: "numeric",
30541
+ year: "numeric",
30542
+ hour: "numeric",
30543
+ minute: "2-digit",
30544
+ timeZoneName: "short"
28529
30545
  });
28530
30546
  }
30547
+ function renderLocalTime(createdAt) {
30548
+ const parsed = Date.parse(createdAt);
30549
+ if (Number.isNaN(parsed)) {
30550
+ return escapeHtml(createdAt);
30551
+ }
30552
+ return `<time data-guardian-local-time datetime="${escapeHtml(createdAt)}" title="${escapeHtml(
30553
+ `Stored by Guardian as ${createdAt}`
30554
+ )}">${escapeHtml(formatActivityTime(createdAt))}</time>`;
30555
+ }
30556
+ function renderLocalGuardianStatus(status) {
30557
+ switch (status) {
30558
+ case "start_requested":
30559
+ return "Running";
30560
+ case "not_started_mvp":
30561
+ return "Not running";
30562
+ }
30563
+ }
30564
+ function renderControlTowerRoleCards(viewModel) {
30565
+ return viewModel.surfaces.map(
30566
+ (surface) => ` <article class="role-card">
30567
+ <strong>${escapeHtml(surface.label)}</strong>
30568
+ <span>${escapeHtml(surface.audience)}</span>
30569
+ <span>${escapeHtml(surface.purpose)}</span>
30570
+ </article>`
30571
+ ).join("\n");
30572
+ }
30573
+ function renderAdminNoticeBar(notices) {
30574
+ if (notices.length === 0) {
30575
+ return "";
30576
+ }
30577
+ return ` <section class="notice-bar" aria-label="Guardian notifications">
30578
+ ${notices.map((notice) => renderAdminNoticeCard(notice)).join("\n")}
30579
+ </section>`;
30580
+ }
30581
+ function renderAdminNoticeList(notices) {
30582
+ if (notices.length === 0) {
30583
+ return " <p>No local admin notices are published.</p>";
30584
+ }
30585
+ return ` <div class="notice-bar" aria-label="Current admin notices">
30586
+ ${notices.map((notice) => renderAdminNoticeCard(notice)).join("\n")}
30587
+ </div>`;
30588
+ }
30589
+ function renderAdminNoticeCard(notice) {
30590
+ return ` <article class="notice-card ${escapeHtml(notice.tone)}">
30591
+ <strong>${escapeHtml(notice.title)}</strong>
30592
+ <p>${escapeHtml(notice.message)}</p>
30593
+ <span>${escapeHtml(adminNoticeAudienceLabel(notice.audience))} \xB7 ${renderLocalTime(notice.created_at)}</span>
30594
+ ${notice.receipt_id ? `<code>${escapeHtml(notice.receipt_id)}</code>` : ""}
30595
+ </article>`;
30596
+ }
30597
+ function adminNoticeAudienceLabel(audience) {
30598
+ switch (audience) {
30599
+ case "admins":
30600
+ return "Admins";
30601
+ case "developers":
30602
+ return "Developers";
30603
+ case "all_users":
30604
+ return "Everyone";
30605
+ }
30606
+ }
30607
+ function renderGuardianAdminSurface(viewModel, profile, receipts, appPermissionRows) {
30608
+ return ` <section aria-labelledby="guardian-admin-overview">
30609
+ <h2 id="guardian-admin-overview">Guardian Admin</h2>
30610
+ <p>Manage policy packs, marketplace approvals, Prime Pack custom rules, deployment scope, receipts, and release automation from the same local Guardian brain used by the browser extension and MCP clients.</p>
30611
+ <div class="admin-grid">
30612
+ ${renderAdminSummaryCards(viewModel, profile, receipts, appPermissionRows)}
30613
+ </div>
30614
+ </section>
30615
+ <section aria-labelledby="admin-notifications">
30616
+ <h2 id="admin-notifications">Admin Notifications</h2>
30617
+ <p>Send a short local notice into the Control Tower notification bar. Notices are metadata-only and stay on this device in <code>${escapeHtml(CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME)}</code>.</p>
30618
+ <form class="notice-form" method="post" action="/v1/guardian/admin/notice">
30619
+ <input type="hidden" name="action" value="publish">
30620
+ <label>Title<input name="title" maxlength="${CONTROL_TOWER_ADMIN_NOTICE_TITLE_LIMIT}" placeholder="Policy reminder" required></label>
30621
+ <label>Message<textarea name="message" maxlength="${CONTROL_TOWER_ADMIN_NOTICE_MESSAGE_LIMIT}" placeholder="Use approved customer-data placeholders in external AI tools." required></textarea></label>
30622
+ <label>Audience<select name="audience"><option value="all_users">Everyone</option><option value="admins">Admins</option><option value="developers">Developers</option></select></label>
30623
+ <label>Tone<select name="tone"><option value="info">Helpful note</option><option value="warning">Watch out</option><option value="success">Good news</option></select></label>
30624
+ <button type="submit">Publish local notice</button>
30625
+ </form>
30626
+ <form method="post" action="/v1/guardian/admin/notice">
30627
+ <input type="hidden" name="action" value="clear">
30628
+ <button type="submit">Clear local notices</button>
30629
+ </form>
30630
+ ${renderAdminNoticeList(viewModel.admin.notices)}
30631
+ </section>
30632
+ <section aria-labelledby="admin-policy-packs">
30633
+ <h2 id="admin-policy-packs">Policy Pack Management</h2>
30634
+ <p>Turn built-in packs on or off for this device now. In enterprise deployments, the same model becomes group assignment.</p>
30635
+ <table aria-label="Admin policy pack assignment">
30636
+ <thead>
30637
+ <tr><th scope="col">Pack</th><th scope="col">State</th><th scope="col">Local action</th></tr>
30638
+ </thead>
30639
+ <tbody>
30640
+ ${profile.policy_packs.available.map((packId) => {
30641
+ const active = profile.policy_packs.active.includes(packId);
30642
+ const disabled = active && profile.policy_packs.active.length <= 1;
30643
+ return ` <tr><th scope="row">${escapeHtml(packId)}</th><td>${active ? "active" : "available"}</td><td>${renderPolicyPackForm(active ? "disable" : "enable", packId, disabled)}</td></tr>`;
30644
+ }).join("\n")}
30645
+ </tbody>
30646
+ </table>
30647
+ </section>
30648
+ <section aria-labelledby="admin-marketplace">
30649
+ <h2 id="admin-marketplace">Policy Pack Marketplace</h2>
30650
+ <p>Review pack metadata before connecting it to a deployment. Packs can start as request/approval metadata before billing automation is added.</p>
30651
+ <div class="marketplace-grid">
30652
+ ${renderMarketplaceCards(viewModel)}
30653
+ </div>
30654
+ ${renderMarketplaceRequestList(viewModel.admin.marketplaceRequests)}
30655
+ </section>
30656
+ <section aria-labelledby="admin-prime-pack">
30657
+ <h2 id="admin-prime-pack">Prime Pack Builder</h2>
30658
+ <p>Create one organization-specific pack from terms, domains, blocked destinations, and escalation phrases. Upload support is designed for text, CSV, and JSON sources.</p>
30659
+ <article class="prime-pack-builder">
30660
+ <h3>Custom protection inputs</h3>
30661
+ <form method="post" action="/v1/guardian/admin/prime-pack">
30662
+ <div class="builder-grid">
30663
+ <label>Sensitive terms<textarea name="sensitiveTerms" maxlength="${CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT * CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT}" placeholder="Customer IDs, internal code names, regulated phrases"></textarea></label>
30664
+ <label>Approved domains<textarea name="approvedDomains" maxlength="${CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT * CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT}" placeholder="company.com, secure.vendor.com"></textarea></label>
30665
+ <label>Blocked destinations<textarea name="blockedDestinations" maxlength="${CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT * CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT}" placeholder="paste sites, personal email domains, unknown file shares"></textarea></label>
30666
+ <label>Escalation phrases<textarea name="escalationPhrases" maxlength="${CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT * CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT}" placeholder="payment approval, publish externally, send to customer"></textarea></label>
30667
+ </div>
30668
+ <label>Import from text, CSV, or JSON<textarea name="importContent" maxlength="${CONTROL_TOWER_PRIME_PACK_IMPORT_LIMIT}" placeholder="Examples: sensitive_terms,Project Falcon&#10;blocked_destinations,paste.example&#10;or JSON with sensitive_terms, approved_domains, blocked_destinations, and escalation_phrases arrays"></textarea></label>
30669
+ <div class="button-row">
30670
+ <button type="submit" name="action" value="preview">Preview impact</button>
30671
+ <button type="submit" name="action" value="deploy">Deploy locally</button>
30672
+ </div>
30673
+ </form>
30674
+ <ul class="metadata-list">
30675
+ <li><b>Input formats</b><span>${escapeHtml(viewModel.admin.primePack.supportedInputs.join(", "))}</span></li>
30676
+ <li><b>Preview</b><span>stores counts and sample impact in <code>${escapeHtml(CONTROL_TOWER_PRIME_PACK_FILE_NAME)}</code></span></li>
30677
+ <li><b>Deploy</b><span>writes a local admin receipt before assignment</span></li>
30678
+ </ul>
30679
+ </article>
30680
+ </section>
30681
+ <section aria-labelledby="admin-deployment-scope">
30682
+ <h2 id="admin-deployment-scope">Deployment Scope</h2>
30683
+ <p>Assign posture and packs by user or group. Personal Guardian uses the first row; enterprise deployments add managed groups.</p>
30684
+ <table aria-label="Guardian deployment groups">
30685
+ <thead>
30686
+ <tr><th scope="col">Group</th><th scope="col">Users</th><th scope="col">Posture</th><th scope="col">Packs</th></tr>
30687
+ </thead>
30688
+ <tbody>
30689
+ ${viewModel.admin.managedGroups.map(
30690
+ (group) => ` <tr><th scope="row">${escapeHtml(group.label)}</th><td>${group.users}</td><td>${escapeHtml(group.posture)}</td><td>${escapeHtml(group.packs.join(", ") || "none")}</td></tr>`
30691
+ ).join("\n")}
30692
+ </tbody>
30693
+ </table>
30694
+ </section>
30695
+ <section aria-labelledby="admin-release-automation">
30696
+ <h2 id="admin-release-automation">Release Automation</h2>
30697
+ <p>Use GitHub Actions as the default release path so package evidence, browser artifacts, Docker smoke tests, and npm publication stay repeatable.</p>
30698
+ <div class="builder-grid">
30699
+ <article class="release-command-card">
30700
+ <h3>Dry run</h3>
30701
+ <code class="command-box">${escapeHtml(viewModel.releaseAutomation.dryRunCommand)}</code>
30702
+ </article>
30703
+ <article class="release-command-card">
30704
+ <h3>Publish</h3>
30705
+ <code class="command-box">${escapeHtml(viewModel.releaseAutomation.publishCommand)}</code>
30706
+ </article>
30707
+ </div>
30708
+ <ul class="metadata-list">
30709
+ <li><b>Repository</b><span>${escapeHtml(viewModel.releaseAutomation.repository)}</span></li>
30710
+ <li><b>Workflow</b><span>${escapeHtml(viewModel.releaseAutomation.workflow)}</span></li>
30711
+ <li><b>NPM package</b><span>${escapeHtml(viewModel.releaseAutomation.npmPackage)}</span></li>
30712
+ <li><b>Chrome extension</b><span>${escapeHtml(viewModel.releaseAutomation.chromeExtensionId)}</span></li>
30713
+ <li><b>Still manual</b><span>${escapeHtml(viewModel.releaseAutomation.humanIntervention[0])}</span></li>
30714
+ </ul>
30715
+ </section>
30716
+ <section aria-labelledby="admin-audit">
30717
+ <h2 id="admin-audit">Receipts And Audit</h2>
30718
+ <p>Admins can review metadata-only decisions without exposing raw prompt text, raw responses, local action payloads, or runtime tokens.</p>
30719
+ <table aria-label="Guardian admin receipt audit">
30720
+ <thead>
30721
+ <tr><th scope="col">Receipt</th><th scope="col">Type</th><th scope="col">Decision</th><th scope="col">Details</th></tr>
30722
+ </thead>
30723
+ <tbody>
30724
+ ${renderReceiptRows(receipts)}
30725
+ </tbody>
30726
+ </table>
30727
+ </section>`;
30728
+ }
30729
+ function renderAdminSummaryCards(viewModel, profile, receipts, appPermissionRows) {
30730
+ const cards = [
30731
+ {
30732
+ title: "Managed groups",
30733
+ value: String(viewModel.admin.managedGroups.length),
30734
+ detail: "Group-level posture and pack assignment model."
30735
+ },
30736
+ {
30737
+ title: "Marketplace packs",
30738
+ value: String(viewModel.marketplace.length),
30739
+ detail: "Metadata listings ready for approval review."
30740
+ },
30741
+ {
30742
+ title: "Prime Pack",
30743
+ value: viewModel.admin.primePack.status.replace(/_/gu, " "),
30744
+ detail: "Custom strings, domains, destinations, and escalation phrases."
30745
+ },
30746
+ {
30747
+ title: "Active local packs",
30748
+ value: String(profile.policy_packs.active.length),
30749
+ detail: profile.policy_packs.active.join(", ") || "none"
30750
+ },
30751
+ {
30752
+ title: "Governed apps",
30753
+ value: String(appPermissionRows.length),
30754
+ detail: "Local app access remains confirmation-gated."
30755
+ },
30756
+ {
30757
+ title: "Audit receipts",
30758
+ value: String(receipts.length),
30759
+ detail: "Metadata-only receipt chain."
30760
+ }
30761
+ ];
30762
+ return cards.map(
30763
+ (card) => ` <article class="admin-card">
30764
+ <h3>${escapeHtml(card.title)}</h3>
30765
+ <strong>${escapeHtml(card.value)}</strong>
30766
+ <span>${escapeHtml(card.detail)}</span>
30767
+ </article>`
30768
+ ).join("\n");
30769
+ }
30770
+ function renderMarketplaceCards(viewModel) {
30771
+ return viewModel.marketplace.map(
30772
+ (pack) => ` <article class="marketplace-card" data-marketplace-pack-id="${escapeHtml(pack.packId)}">
30773
+ <h3>${escapeHtml(pack.name)}</h3>
30774
+ <p>${escapeHtml(pack.examples[0] ?? "Policy pack metadata listing.")}</p>
30775
+ <ul class="metadata-list">
30776
+ <li><b>Audience</b><span>${escapeHtml(pack.audience)}</span></li>
30777
+ <li><b>Category</b><span>${escapeHtml(pack.category)}</span></li>
30778
+ <li><b>Maintainer</b><span>${escapeHtml(pack.publisher)}</span></li>
30779
+ <li><b>Version</b><span>${escapeHtml(pack.version)}</span></li>
30780
+ <li><b>Price/license</b><span>${escapeHtml(pack.price)}</span></li>
30781
+ <li><b>Approval state</b><span>${escapeHtml(pack.state)}</span></li>
30782
+ <li><b>False-positive risk</b><span>${escapeHtml(pack.risk)}</span></li>
30783
+ </ul>
30784
+ ${renderMarketplaceRequestAction(pack)}
30785
+ <details class="marketplace-detail-drawer">
30786
+ <summary>Review details before connecting</summary>
30787
+ <dl>
30788
+ <dt>Permissions requested</dt>
30789
+ <dd>${escapeHtml(pack.permissions.join(" "))}</dd>
30790
+ <dt>Protected data</dt>
30791
+ <dd>${escapeHtml(pack.protectedDataTypes.join(", "))}</dd>
30792
+ <dt>Examples</dt>
30793
+ <dd>${escapeHtml(pack.examples.join(" "))}</dd>
30794
+ <dt>License reference</dt>
30795
+ <dd><code>${escapeHtml(pack.licenseRef)}</code></dd>
30796
+ <dt>Support reference</dt>
30797
+ <dd><code>${escapeHtml(pack.supportRef)}</code></dd>
30798
+ </dl>
30799
+ </details>
30800
+ </article>`
30801
+ ).join("\n");
30802
+ }
30803
+ function renderMarketplaceRequestAction(pack) {
30804
+ if (pack.requestState === "connected") {
30805
+ return ' <p class="marketplace-request-state">Connected locally through active policy packs.</p>';
30806
+ }
30807
+ if (pack.requestState === "requested_metadata_only") {
30808
+ return ` <p class="marketplace-request-state">Review requested ${pack.requestedAt ? renderLocalTime(pack.requestedAt) : "locally"}.</p>`;
30809
+ }
30810
+ return ` <form class="marketplace-request-form" method="post" action="/v1/guardian/admin/marketplace-request">
30811
+ <input type="hidden" name="action" value="request_review">
30812
+ <input type="hidden" name="packId" value="${escapeHtml(pack.packId)}">
30813
+ <button type="submit">Request review</button>
30814
+ <span>Writes a local receipt; paid pack connection remains approval-gated.</span>
30815
+ </form>`;
30816
+ }
30817
+ function renderMarketplaceRequestList(requests) {
30818
+ if (requests.length === 0) {
30819
+ return " <p>No marketplace pack reviews have been requested on this device yet.</p>";
30820
+ }
30821
+ return ` <div class="marketplace-request-list" aria-label="Marketplace review requests">
30822
+ ${requests.map(
30823
+ (request) => ` <article class="marketplace-request-card">
30824
+ <strong>${escapeHtml(request.pack_name)}</strong>
30825
+ <span>${escapeHtml(request.publisher)} \xB7 ${renderLocalTime(request.requested_at)}</span>
30826
+ <code>${escapeHtml(request.receipt_id)}</code>
30827
+ </article>`
30828
+ ).join("\n")}
30829
+ </div>`;
30830
+ }
28531
30831
  function providerDisplayName(providerId) {
28532
30832
  if (providerId === "chatgpt") return "ChatGPT";
28533
30833
  if (providerId === "claude") return "Claude";
@@ -28823,7 +31123,7 @@ function listen(server, host, port) {
28823
31123
  });
28824
31124
  });
28825
31125
  }
28826
- var MAX_BODY_BYTES, ADMIN_HEADER, CONTROL_TOWER_COOKIE, PROMPT_COACH_MODES2, CONTROL_TOWER_RECEIPT_LIMIT, CONTROL_TOWER_GRAPH_NODE_LIMIT, CONTROL_TOWER_GRAPH_EDGE_LIMIT, CONTROL_TOWER_LEARNING_EVENT_LIMIT, CONTROL_TOWER_DATA_FILE_NAMES, CONTROL_TOWER_DATA_DIRECTORY_NAMES, CONTROL_TOWER_SUPPORTED_LIFECYCLE_ACTIONS;
31126
+ var MAX_BODY_BYTES, ADMIN_HEADER, CONTROL_TOWER_COOKIE, PROMPT_COACH_MODES2, CONTROL_TOWER_RECEIPT_LIMIT, CONTROL_TOWER_GRAPH_NODE_LIMIT, CONTROL_TOWER_GRAPH_EDGE_LIMIT, CONTROL_TOWER_LEARNING_EVENT_LIMIT, CONTROL_TOWER_ADMIN_NOTICE_LIMIT, CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME, CONTROL_TOWER_ADMIN_NOTICE_TITLE_LIMIT, CONTROL_TOWER_ADMIN_NOTICE_MESSAGE_LIMIT, CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT, CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME, CONTROL_TOWER_PRIME_PACK_FILE_NAME, CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT, CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT, CONTROL_TOWER_PRIME_PACK_IMPORT_LIMIT, CONTROL_TOWER_DATA_FILE_NAMES, CONTROL_TOWER_DATA_DIRECTORY_NAMES, CONTROL_TOWER_SUPPORTED_LIFECYCLE_ACTIONS, CONTROL_TOWER_RELEASE_COMMAND_TEMPLATE, CONTROL_TOWER_DRY_RUN_COMMAND_TEMPLATE;
28827
31127
  var init_daemon = __esm({
28828
31128
  "packages/guardian-cli/src/daemon.ts"() {
28829
31129
  "use strict";
@@ -28839,12 +31139,25 @@ var init_daemon = __esm({
28839
31139
  CONTROL_TOWER_GRAPH_NODE_LIMIT = 12;
28840
31140
  CONTROL_TOWER_GRAPH_EDGE_LIMIT = 16;
28841
31141
  CONTROL_TOWER_LEARNING_EVENT_LIMIT = 12;
31142
+ CONTROL_TOWER_ADMIN_NOTICE_LIMIT = 5;
31143
+ CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME = "admin-notices.json";
31144
+ CONTROL_TOWER_ADMIN_NOTICE_TITLE_LIMIT = 80;
31145
+ CONTROL_TOWER_ADMIN_NOTICE_MESSAGE_LIMIT = 240;
31146
+ CONTROL_TOWER_MARKETPLACE_REQUEST_LIMIT = 20;
31147
+ CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME = "marketplace-requests.json";
31148
+ CONTROL_TOWER_PRIME_PACK_FILE_NAME = "prime-pack-draft.json";
31149
+ CONTROL_TOWER_PRIME_PACK_VALUE_LIMIT = 120;
31150
+ CONTROL_TOWER_PRIME_PACK_ENTRY_LIMIT = 100;
31151
+ CONTROL_TOWER_PRIME_PACK_IMPORT_LIMIT = 24 * 1024;
28842
31152
  CONTROL_TOWER_DATA_FILE_NAMES = [
28843
31153
  "profile.json",
28844
31154
  "policy-packs.json",
28845
31155
  "posture-profile.json",
28846
31156
  "preferences.json",
28847
31157
  "permissions.json",
31158
+ CONTROL_TOWER_ADMIN_NOTICE_FILE_NAME,
31159
+ CONTROL_TOWER_MARKETPLACE_REQUEST_FILE_NAME,
31160
+ CONTROL_TOWER_PRIME_PACK_FILE_NAME,
28848
31161
  "runtime-token"
28849
31162
  ];
28850
31163
  CONTROL_TOWER_DATA_DIRECTORY_NAMES = ["ecl", "receipts", "mcp"];
@@ -28855,6 +31168,8 @@ var init_daemon = __esm({
28855
31168
  "restore",
28856
31169
  "delete"
28857
31170
  ];
31171
+ CONTROL_TOWER_RELEASE_COMMAND_TEMPLATE = `gh workflow run ${GUARDIAN_RELEASE_DISPATCH_WORKFLOW} -f release_version=<version> -f channels=all -f dry_run=false -f run_docker_smoke=true -f publish_auth=token -f browser_store_publish=manual`;
31172
+ CONTROL_TOWER_DRY_RUN_COMMAND_TEMPLATE = `gh workflow run ${GUARDIAN_RELEASE_DISPATCH_WORKFLOW} -f release_version=<version> -f channels=all -f dry_run=true -f run_docker_smoke=true -f publish_auth=token -f browser_store_publish=manual`;
28858
31173
  }
28859
31174
  });
28860
31175