ai-saas-guard 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,8 +73,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
73
73
  | JSON and SARIF output | Available |
74
74
  | Composite GitHub Action | Available |
75
75
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
76
- | Versioned Action tags | `v0.12.0`, `v0` |
77
- | npm package | `ai-saas-guard@0.12.0` |
76
+ | Versioned Action tags | `v0.14.0`, `v0` |
77
+ | npm package | `ai-saas-guard@0.14.0` |
78
78
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
79
79
 
80
80
  ## Quick Start
@@ -214,7 +214,7 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
214
214
 
215
215
  Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md). Core local scanning stays useful without an account; hosted plans may add workflow convenience, saved reports, team policy, and optional human review, but they do not gate local CLI scanning.
216
216
 
217
- Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, plus a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
217
+ Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
218
218
 
219
219
  A public hosted compact report schema fixture is available at [examples/hosted-compact-report.json](examples/hosted-compact-report.json). It is synthetic and public-safe: compact evidence only, no raw source, raw diffs, secrets, webhook payload bodies, customer payloads, private URLs, or worker checkout paths.
220
220
 
@@ -254,7 +254,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
254
254
 
255
255
  ## GitHub Action
256
256
 
257
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.12.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
257
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.14.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
258
258
 
259
259
  ```yaml
260
260
  name: ai-saas-guard
package/README.zh-CN.md CHANGED
@@ -55,7 +55,7 @@ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是
55
55
 
56
56
  这个仓库是公开 GitHub 仓库。
57
57
 
58
- CLI 已发布到 npm:`ai-saas-guard@0.12.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.12.0`。
58
+ CLI 已发布到 npm:`ai-saas-guard@0.14.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.14.0`。
59
59
 
60
60
  | 模块 | 状态 |
61
61
  | --- | --- |
@@ -66,8 +66,8 @@ CLI 已发布到 npm:`ai-saas-guard@0.12.0`。GitHub Action 支持 `v0` 浮动
66
66
  | Markdown PR summary | 已可用 |
67
67
  | GitHub Action | 已可用 |
68
68
  | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
69
- | 当前版本 | `0.12.0` |
70
- | Action 标签 | `v0.12.0`、`v0` |
69
+ | 当前版本 | `0.14.0` |
70
+ | Action 标签 | `v0.14.0`、`v0` |
71
71
  | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
72
72
 
73
73
  ## 快速开始
@@ -254,13 +254,15 @@ jobs:
254
254
 
255
255
  - pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
256
256
  - durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
257
+ - worker read-only scan planner:只用 trusted identity 规划临时 worker checkout,要求 repository `contents: read`,固定运行 `ai-saas-guard pr-risk --json`,并忽略 PR 正文里的 repo 名、token scope 或命令
257
258
  - webhook event parser
258
259
  - check-run summary renderer
260
+ - Check Run publication planner:要求 repository `checks: write`,只从 compact report 生成有长度上限的 Check Run payload,包含 review categories、优先 review 文件、verification steps 和本地 CLI 复现命令;MVP 不发 PR comment
259
261
  - queue cleanup planner
260
262
  - worker checkout cleanup planner
261
263
  - hosted compact report fixture:[examples/hosted-compact-report.json](examples/hosted-compact-report.json)
262
264
 
263
- 这些 helper 不会启动服务、不会调用 GitHub API、不会请求 installation token、不会写 check run,也不会上传源码。
265
+ 这些 helper 不会启动服务、不会调用 GitHub API、不会请求 installation token、不会真实写 check run、不会发 PR comment,也不会上传源码。
264
266
 
265
267
  ## 它不是什么
266
268
 
@@ -201,6 +201,81 @@ export interface HostedQueueCleanupPlan {
201
201
  deleteCustomerPayloads: false;
202
202
  }
203
203
  export type HostedWorkerCheckoutTerminalState = "success" | "failure" | "timeout" | "cancellation" | "cleanup_failure";
204
+ export type HostedWorkerReadOnlyScanRejectReason = InstallationScopeRejectReason | "contents_read_permission_required";
205
+ export interface HostedWorkerReadOnlyScanInput {
206
+ identity: HostedScanIdentity;
207
+ jobKey: string;
208
+ requestedAt: string;
209
+ installationId: number;
210
+ selectedRepositoryIds: number[];
211
+ removedRepositoryIds?: number[];
212
+ installationTokenPermissions: {
213
+ contents?: string;
214
+ };
215
+ checkoutRoot?: string;
216
+ untrustedRepositoryFullName?: string;
217
+ untrustedTokenPermissions?: unknown;
218
+ untrustedCommand?: string;
219
+ untrustedPrText?: string;
220
+ rawSource?: string;
221
+ rawDiff?: string;
222
+ secretValues?: string[];
223
+ customerPayload?: unknown;
224
+ }
225
+ export interface HostedWorkerReadOnlyScanPlan {
226
+ accepted: boolean;
227
+ reason?: HostedWorkerReadOnlyScanRejectReason;
228
+ jobKey: string;
229
+ requestedAt: string;
230
+ readOnly: true;
231
+ shouldFetchSource: boolean;
232
+ shouldRunCli: boolean;
233
+ shouldPersistRawSource: false;
234
+ shouldPersistRawDiffs: false;
235
+ shouldCreatePrComment: false;
236
+ installationTokenScope?: {
237
+ installationId: number;
238
+ repositoryId: number;
239
+ permissions: {
240
+ contents: "read";
241
+ };
242
+ selectedRepositoryOnly: true;
243
+ };
244
+ checkout?: {
245
+ repositoryId: number;
246
+ repositoryFullName: string;
247
+ pullRequestNumber: number;
248
+ baseSha: string;
249
+ targetCommitSha: string;
250
+ directoryScope: "temporary_worker_directory";
251
+ cleanupRequired: true;
252
+ returnsCheckoutPath: false;
253
+ };
254
+ cli?: {
255
+ command: "ai-saas-guard";
256
+ args: string[];
257
+ workingDirectory: "<worker-checkout>";
258
+ networkAccess: "disabled";
259
+ writeMode: "read_only";
260
+ };
261
+ output?: {
262
+ compactJsonOnly: true;
263
+ persistRawSource: false;
264
+ persistRawDiffs: false;
265
+ persistSecrets: false;
266
+ persistCustomerPayloads: false;
267
+ };
268
+ privacy: {
269
+ returnsCheckoutPath: false;
270
+ includesRawSource: false;
271
+ includesRawDiffs: false;
272
+ includesSecrets: false;
273
+ includesCustomerPayloads: false;
274
+ acceptsRepositoryIdentityFromPrText: false;
275
+ acceptsTokenScopeFromPrText: false;
276
+ acceptsCommandFromPrText: false;
277
+ };
278
+ }
204
279
  export type HostedWorkerCheckoutCleanupAction = "delete_checkout" | "record_cleanup_failure";
205
280
  export interface HostedWorkerCheckoutCleanupInput {
206
281
  identity: HostedScanIdentity;
@@ -313,6 +388,71 @@ export interface HostedCheckRunSummary {
313
388
  modelTraining: "disabled";
314
389
  };
315
390
  }
391
+ export type HostedCheckRunPublicationRejectReason = InstallationScopeRejectReason | "checks_write_permission_required";
392
+ export interface HostedCheckRunPublicationInput {
393
+ identity: HostedScanIdentity;
394
+ report: CompactHostedReport;
395
+ jobKey: string;
396
+ requestedAt: string;
397
+ installationId: number;
398
+ selectedRepositoryIds: number[];
399
+ removedRepositoryIds?: number[];
400
+ installationTokenPermissions: {
401
+ checks?: string;
402
+ };
403
+ failOnSeverity?: HostedCheckRunSeverityThreshold;
404
+ maxMarkdownChars?: number;
405
+ rawSource?: string;
406
+ rawDiff?: string;
407
+ secretValues?: string[];
408
+ untrustedPrText?: string;
409
+ customerPayload?: unknown;
410
+ }
411
+ export interface HostedCheckRunApiPayload {
412
+ name: "AI SaaS Guard";
413
+ head_sha: string;
414
+ status: "completed";
415
+ conclusion: HostedCheckRunConclusion;
416
+ external_id: string;
417
+ output: {
418
+ title: string;
419
+ summary: string;
420
+ text: string;
421
+ annotations: HostedCheckRunAnnotation[];
422
+ };
423
+ }
424
+ export interface HostedCheckRunPublicationPlan {
425
+ accepted: boolean;
426
+ reason?: HostedCheckRunPublicationRejectReason;
427
+ jobKey: string;
428
+ requestedAt: string;
429
+ operation?: "create";
430
+ shouldWriteCheckRun: boolean;
431
+ shouldCreatePrComment: false;
432
+ shouldCallGitHubApi: false;
433
+ installationTokenScope?: {
434
+ installationId: number;
435
+ repositoryId: number;
436
+ permissions: {
437
+ checks: "write";
438
+ };
439
+ selectedRepositoryOnly: true;
440
+ };
441
+ request?: {
442
+ method: "POST";
443
+ endpoint: string;
444
+ payload: HostedCheckRunApiPayload;
445
+ };
446
+ privacy: {
447
+ includesRawSource: false;
448
+ includesRawDiffs: false;
449
+ includesSecrets: false;
450
+ includesCustomerPayloads: false;
451
+ includesUntrustedPrText: false;
452
+ createsPrComment: false;
453
+ modelTraining: "disabled";
454
+ };
455
+ }
316
456
  export type HostedDeletionTrigger = "repository_removed" | "installation_deleted" | "repeated_cleanup";
317
457
  export interface HostedDeletionPlanInput {
318
458
  trigger: HostedDeletionTrigger;
@@ -361,12 +501,14 @@ export declare function getHostedQueueCleanupIdempotencyKey(input: {
361
501
  repositoryId?: number;
362
502
  }): string;
363
503
  export declare function createHostedQueueCleanupPlan(input: HostedQueueCleanupPlanInput): HostedQueueCleanupPlan;
504
+ export declare function planHostedWorkerReadOnlyScan(input: HostedWorkerReadOnlyScanInput): HostedWorkerReadOnlyScanPlan;
364
505
  export declare function createHostedWorkerCheckoutCleanupPlan(input: HostedWorkerCheckoutCleanupInput): HostedWorkerCheckoutCleanupPlan;
365
506
  export declare function resolveHostedRetentionDays(input?: {
366
507
  teamRequestedDays?: number;
367
508
  }): number;
368
509
  export declare function createCompactHostedReport(input: CompactHostedReportInput): CompactHostedReport;
369
510
  export declare function createHostedCheckRunSummary(input: HostedCheckRunSummaryInput): HostedCheckRunSummary;
511
+ export declare function planHostedCheckRunPublication(input: HostedCheckRunPublicationInput): HostedCheckRunPublicationPlan;
370
512
  export declare function getHostedDeletionIdempotencyKey(input: {
371
513
  trigger: HostedDeletionTrigger;
372
514
  installationId: number;
@@ -332,6 +332,69 @@ export function createHostedQueueCleanupPlan(input) {
332
332
  deleteCustomerPayloads: false
333
333
  };
334
334
  }
335
+ export function planHostedWorkerReadOnlyScan(input) {
336
+ const scopeDecision = authorizeInstallationTokenScope({
337
+ identity: input.identity,
338
+ installationId: input.installationId,
339
+ selectedRepositoryIds: input.selectedRepositoryIds,
340
+ removedRepositoryIds: input.removedRepositoryIds
341
+ });
342
+ if (!scopeDecision.authorized) {
343
+ return rejectHostedWorkerReadOnlyScan(input, scopeDecision.reason ?? "repository_not_installed");
344
+ }
345
+ if (input.installationTokenPermissions.contents !== "read") {
346
+ return rejectHostedWorkerReadOnlyScan(input, "contents_read_permission_required");
347
+ }
348
+ return {
349
+ accepted: true,
350
+ jobKey: input.jobKey,
351
+ requestedAt: input.requestedAt,
352
+ readOnly: true,
353
+ shouldFetchSource: true,
354
+ shouldRunCli: true,
355
+ shouldPersistRawSource: false,
356
+ shouldPersistRawDiffs: false,
357
+ shouldCreatePrComment: false,
358
+ installationTokenScope: {
359
+ installationId: input.identity.installationId,
360
+ repositoryId: input.identity.repositoryId,
361
+ permissions: { contents: "read" },
362
+ selectedRepositoryOnly: true
363
+ },
364
+ checkout: {
365
+ repositoryId: input.identity.repositoryId,
366
+ repositoryFullName: input.identity.repositoryFullName,
367
+ pullRequestNumber: input.identity.pullRequestNumber,
368
+ baseSha: input.identity.baseSha,
369
+ targetCommitSha: input.identity.headSha,
370
+ directoryScope: "temporary_worker_directory",
371
+ cleanupRequired: true,
372
+ returnsCheckoutPath: false
373
+ },
374
+ cli: {
375
+ command: "ai-saas-guard",
376
+ args: [
377
+ "pr-risk",
378
+ "--root",
379
+ "<worker-checkout>",
380
+ "--base",
381
+ input.identity.baseSha,
382
+ "--json"
383
+ ],
384
+ workingDirectory: "<worker-checkout>",
385
+ networkAccess: "disabled",
386
+ writeMode: "read_only"
387
+ },
388
+ output: {
389
+ compactJsonOnly: true,
390
+ persistRawSource: false,
391
+ persistRawDiffs: false,
392
+ persistSecrets: false,
393
+ persistCustomerPayloads: false
394
+ },
395
+ privacy: hostedWorkerReadOnlyScanPrivacy()
396
+ };
397
+ }
335
398
  export function createHostedWorkerCheckoutCleanupPlan(input) {
336
399
  const cleanupFailed = input.terminalState === "cleanup_failure";
337
400
  return {
@@ -431,6 +494,56 @@ export function createHostedCheckRunSummary(input) {
431
494
  }
432
495
  };
433
496
  }
497
+ export function planHostedCheckRunPublication(input) {
498
+ const scopeDecision = authorizeInstallationTokenScope({
499
+ identity: input.identity,
500
+ installationId: input.installationId,
501
+ selectedRepositoryIds: input.selectedRepositoryIds,
502
+ removedRepositoryIds: input.removedRepositoryIds
503
+ });
504
+ if (!scopeDecision.authorized) {
505
+ return rejectHostedCheckRunPublication(input, scopeDecision.reason ?? "repository_not_installed");
506
+ }
507
+ if (input.installationTokenPermissions.checks !== "write") {
508
+ return rejectHostedCheckRunPublication(input, "checks_write_permission_required");
509
+ }
510
+ const summary = createHostedCheckRunSummary({
511
+ report: input.report,
512
+ failOnSeverity: input.failOnSeverity,
513
+ maxMarkdownChars: input.maxMarkdownChars
514
+ });
515
+ return {
516
+ accepted: true,
517
+ jobKey: input.jobKey,
518
+ requestedAt: input.requestedAt,
519
+ operation: "create",
520
+ shouldWriteCheckRun: true,
521
+ shouldCreatePrComment: false,
522
+ shouldCallGitHubApi: false,
523
+ installationTokenScope: {
524
+ installationId: input.identity.installationId,
525
+ repositoryId: input.identity.repositoryId,
526
+ permissions: { checks: "write" },
527
+ selectedRepositoryOnly: true
528
+ },
529
+ request: {
530
+ method: "POST",
531
+ endpoint: `/repos/${input.identity.repositoryFullName}/check-runs`,
532
+ payload: {
533
+ name: summary.name,
534
+ head_sha: input.identity.headSha,
535
+ status: "completed",
536
+ conclusion: summary.conclusion,
537
+ external_id: input.jobKey,
538
+ output: {
539
+ ...summary.output,
540
+ annotations: summary.annotations
541
+ }
542
+ }
543
+ },
544
+ privacy: hostedCheckRunPublicationPrivacy()
545
+ };
546
+ }
434
547
  export function getHostedDeletionIdempotencyKey(input) {
435
548
  return [input.trigger, input.installationId, input.repositoryId ?? "all"].join(":");
436
549
  }
@@ -537,6 +650,56 @@ function hostedScanQueuePrivacy() {
537
650
  includesCustomerPayloads: false
538
651
  };
539
652
  }
653
+ function rejectHostedWorkerReadOnlyScan(input, reason) {
654
+ return {
655
+ accepted: false,
656
+ reason,
657
+ jobKey: input.jobKey,
658
+ requestedAt: input.requestedAt,
659
+ readOnly: true,
660
+ shouldFetchSource: false,
661
+ shouldRunCli: false,
662
+ shouldPersistRawSource: false,
663
+ shouldPersistRawDiffs: false,
664
+ shouldCreatePrComment: false,
665
+ privacy: hostedWorkerReadOnlyScanPrivacy()
666
+ };
667
+ }
668
+ function hostedWorkerReadOnlyScanPrivacy() {
669
+ return {
670
+ returnsCheckoutPath: false,
671
+ includesRawSource: false,
672
+ includesRawDiffs: false,
673
+ includesSecrets: false,
674
+ includesCustomerPayloads: false,
675
+ acceptsRepositoryIdentityFromPrText: false,
676
+ acceptsTokenScopeFromPrText: false,
677
+ acceptsCommandFromPrText: false
678
+ };
679
+ }
680
+ function rejectHostedCheckRunPublication(input, reason) {
681
+ return {
682
+ accepted: false,
683
+ reason,
684
+ jobKey: input.jobKey,
685
+ requestedAt: input.requestedAt,
686
+ shouldWriteCheckRun: false,
687
+ shouldCreatePrComment: false,
688
+ shouldCallGitHubApi: false,
689
+ privacy: hostedCheckRunPublicationPrivacy()
690
+ };
691
+ }
692
+ function hostedCheckRunPublicationPrivacy() {
693
+ return {
694
+ includesRawSource: false,
695
+ includesRawDiffs: false,
696
+ includesSecrets: false,
697
+ includesCustomerPayloads: false,
698
+ includesUntrustedPrText: false,
699
+ createsPrComment: false,
700
+ modelTraining: HOSTED_PRIVACY_DEFAULTS.modelTraining
701
+ };
702
+ }
540
703
  function parseJsonPayload(payload) {
541
704
  try {
542
705
  return JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
@@ -593,6 +756,8 @@ function formatCheckRunTitle(totalFindings, conclusion, failOnSeverity) {
593
756
  return `AI SaaS Guard found ${totalFindings} finding${totalFindings === 1 ? "" : "s"} to review`;
594
757
  }
595
758
  function formatCheckRunMarkdown(report, conclusion, localCliCommand) {
759
+ const categories = getHostedCheckRunCategories(report);
760
+ const filesToReview = getHostedCheckRunFiles(report);
596
761
  const findingLines = report.evidence.length === 0
597
762
  ? ["No findings in the compact hosted report."]
598
763
  : [
@@ -612,6 +777,17 @@ function formatCheckRunMarkdown(report, conclusion, localCliCommand) {
612
777
  "Summary:",
613
778
  ...severityOrder.map((severity) => `- ${capitalize(severity)}: ${report.summaryCounts[severity] ?? 0}`),
614
779
  "",
780
+ "Review categories:",
781
+ ...(categories.length === 0 ? ["- None"] : categories.map((category) => `- ${category}`)),
782
+ "",
783
+ "Files to review first:",
784
+ ...(filesToReview.length === 0 ? ["- None"] : filesToReview.map((file) => `- ${file}`)),
785
+ "",
786
+ "Verification steps:",
787
+ "- Review each listed file before release or merge.",
788
+ "- Reproduce locally with the CLI command above.",
789
+ "- Treat findings as review prompts; confirm behavior with app-specific tests.",
790
+ "",
615
791
  "Findings:",
616
792
  ...findingLines
617
793
  ].join("\n");
@@ -648,6 +824,27 @@ function annotationLevelForSeverity(severity) {
648
824
  function formatFindingLocation(finding) {
649
825
  return `${finding.file}${finding.line === undefined ? "" : `:${finding.line}`}`;
650
826
  }
827
+ function getHostedCheckRunCategories(report) {
828
+ const categories = report.evidence.map((finding) => categoryForRuleId(finding.ruleId));
829
+ return [...new Set(categories)];
830
+ }
831
+ function categoryForRuleId(ruleId) {
832
+ const prefix = ruleId.split(".")[0] ?? "review";
833
+ const categoryNames = {
834
+ api: "API routes",
835
+ deploy: "Deploy config",
836
+ mcp: "MCP tools",
837
+ pr: "Pull request risk",
838
+ "pr-risk": "Pull request risk",
839
+ secrets: "Secrets and env",
840
+ stripe: "Stripe billing",
841
+ supabase: "Supabase data access"
842
+ };
843
+ return categoryNames[prefix] ?? capitalize(prefix);
844
+ }
845
+ function getHostedCheckRunFiles(report) {
846
+ return [...new Set(report.evidence.map((finding) => finding.file))].slice(0, 10);
847
+ }
651
848
  function escapeMarkdownTableCell(value) {
652
849
  return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
653
850
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  `ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
4
4
 
5
- Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.12.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
5
+ Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.14.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
6
 
7
7
  ## PR Summary
8
8
 
@@ -47,6 +47,29 @@ Queue payload boundaries:
47
47
 
48
48
  The exported helper is `planHostedScanQueueUpsert`. It is intended to be the queue-provider-independent contract for the first real hosted queue implementation.
49
49
 
50
+ ## Worker Read-Only Scan Planner
51
+
52
+ The worker read-only scan planner defines how a future hosted worker should prepare a scan after the durable queue hands it a trusted job. It is a pure planner only: it does not request installation tokens, create directories, checkout repositories, run the CLI, persist reports, delete files, or call GitHub APIs.
53
+
54
+ Default behavior:
55
+
56
+ - authorize the same installation and selected-repository scope before any checkout is planned
57
+ - require installation token permissions to be repository `contents: read`
58
+ - derive repository ID, repository full name, pull request number, base SHA, head SHA, and scanner version only from trusted scan identity
59
+ - plan checkout of the trusted head commit into a temporary worker directory
60
+ - plan a fixed read-only CLI invocation: `ai-saas-guard pr-risk --root <worker-checkout> --base <baseSha> --json`
61
+ - collect compact JSON output only
62
+ - require checkout cleanup after every terminal worker state
63
+ - keep PR comments disabled for the first hosted slice
64
+
65
+ Trust boundaries:
66
+
67
+ - ignore PR-authored repository names, token scopes, and commands
68
+ - do not accept worker command, checkout target, installation ID, repository ID, repository name, or token permissions from PR title, body, comments, branch names, README, or code
69
+ - do not return checkout paths, raw source, raw diffs, secret values, customer payloads, private URLs, or installation token values
70
+
71
+ The exported helper is `planHostedWorkerReadOnlyScan`. It is intended to be the worker-provider-independent contract for the first real hosted worker implementation.
72
+
50
73
  ## Webhook Event Parser
51
74
 
52
75
  The webhook event parser runs after webhook signature verification. It converts a reduced GitHub `pull_request` webhook payload into a queue-safe scan request identity.
@@ -94,6 +117,7 @@ Default behavior:
94
117
  - include review-first language that tells readers to verify findings before release
95
118
  - state that the result is not a full security audit, pentest, or certification
96
119
  - include a local CLI link through the exact `npx ai-saas-guard@<version> pr-risk --root .` command
120
+ - include review categories, files to review first, and verification steps
97
121
  - cap check-run text with bounded Markdown so oversized reports cannot create unbounded API payloads
98
122
 
99
123
  Privacy boundaries:
@@ -103,6 +127,27 @@ Privacy boundaries:
103
127
  - do not include raw source, raw diffs, secret values, webhook payload bodies, customer payloads, or private URLs
104
128
  - preserve `modelTraining: disabled`
105
129
 
130
+ ## Check-Run Publication Planner
131
+
132
+ The check-run publication planner turns a compact hosted report into a GitHub Checks API request plan. It is a pure planner only: it does not call GitHub, request installation tokens, write check runs, post PR comments, fetch repositories, or store report data.
133
+
134
+ Default behavior:
135
+
136
+ - authorize the same installation and selected-repository scope before planning a Check Run write
137
+ - require installation token permissions to include repository `checks: write`
138
+ - create a Check Run payload for the trusted head SHA
139
+ - use conservative conclusions from the summary renderer: `success` for no findings, `neutral` for review-needed findings, and `failure` only when a configured policy threshold is met
140
+ - include bounded Markdown, annotations, categories, verification steps, and the local CLI reproduction command
141
+ - keep PR comments disabled for the MVP
142
+
143
+ Privacy boundaries:
144
+
145
+ - plan a Check Run from compact report fields only
146
+ - do not include raw source, raw diffs, secret values, untrusted PR text, webhook payload bodies, customer payloads, private URLs, or worker checkout paths
147
+ - do not create issue comments, review comments, or PR comments
148
+
149
+ The exported helper is `planHostedCheckRunPublication`. It is intended to be the GitHub-API-independent contract for the first real Check Run writer. PR comments remain an explicit later workflow or paid hosted feature, not part of this MVP contract.
150
+
106
151
  ## Queue Cleanup Planner
107
152
 
108
153
  The queue cleanup planner turns repository removal, installation deletion, and repeated cleanup events into a safe cancellation plan for hosted scan jobs. It is a pure planner only: it does not connect to a queue provider, mutate jobs, delete worker files, call GitHub, or retry work.
@@ -189,6 +234,9 @@ Automated tests must cover:
189
234
  - duplicate deliveries reuse queued, running, and completed jobs without enqueueing duplicate worker work
190
235
  - completed duplicate jobs reuse compact reports
191
236
  - manual reruns increment attempt without changing the logical scan key
237
+ - worker read-only scan planning requires repository `contents: read` permissions
238
+ - worker read-only scan planning uses trusted identity for checkout target and fixed CLI command
239
+ - worker read-only scan planning does not persist raw source, raw diffs, secrets, customer payloads, checkout paths, PR-authored commands, or PR-authored token scopes
192
240
  - accepted pull request events build the expected trusted scan identity
193
241
  - unsupported actions are rejected
194
242
  - draft pull requests are rejected by default
@@ -197,7 +245,11 @@ Automated tests must cover:
197
245
  - untrusted PR text cannot override trusted identity
198
246
  - check-run summary renderer conclusions stay success, neutral, or failure based on explicit compact-report rules
199
247
  - bounded Markdown truncates large check-run text and points readers to the local CLI
248
+ - rendered summaries include categories, files to review first, and verification steps
200
249
  - rendered summaries do not expose raw source, raw diffs, secret values, or customer payloads
250
+ - check-run publication planning requires repository `checks: write` permissions
251
+ - check-run publication planning creates bounded Check Run payloads from compact reports only
252
+ - check-run publication planning keeps PR comments disabled
201
253
  - queue cleanup planner cancels only matching repository-scoped queued work
202
254
  - queue cleanup planner handles installation-scoped cleanup without touching other installations
203
255
  - idempotent repeated cleanup preserves terminal jobs and does not create duplicate cancellation work
@@ -5,10 +5,10 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.12.0`
8
+ - Current version: `0.14.0`
9
9
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
10
10
  - First npm-published version: `0.1.1`
11
- - GitHub Release: `v0.12.0`
11
+ - GitHub Release: `v0.14.0`
12
12
  - Publish workflow: `.github/workflows/npm-publish.yml`
13
13
  - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
14
14
  - Long-lived npm publish token: not required
@@ -17,7 +17,7 @@
17
17
 
18
18
  Use GitHub Actions with npm Trusted Publisher/OIDC:
19
19
 
20
- 1. Create and review a release tag such as `v0.12.0`.
20
+ 1. Create and review a release tag such as `v0.14.0`.
21
21
  2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
22
22
  3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
23
23
  4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
@@ -1,6 +1,6 @@
1
1
  # Project Handoff
2
2
 
3
- Last updated: 2026-05-23
3
+ Last updated: 2026-05-24
4
4
 
5
5
  Use this public-safe document when moving `ai-saas-guard` into a new GitHub-facing ChatGPT/Codex Project or a new conversation.
6
6
 
@@ -57,7 +57,7 @@ Implemented surfaces:
57
57
  - hosted operational release gate document requiring hosted CI, webhook replay, dependency and container scanning, privacy and retention verification, worker cleanup, monitoring and alerting, manual rollback, and incident response evidence before exposure
58
58
  - hosted uninstall and data deletion document defining repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup idempotency, and user-facing deletion wording
59
59
  - hosted pricing and packaging document defining open-source CLI boundaries, free/public repo hosted behavior, private repo hosted behavior, PR comments, saved reports, team policy, optional Launch Review, and no pentest/certification/full-audit claims
60
- - hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
60
+ - hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, worker read-only scan planning, Check Run publication planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
61
61
  - implementation-ready hosted GitHub App permission contract for required permissions, optional PR comment permissions, selected repository installation, and out-of-scope broad permissions
62
62
  - pure hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable scan queue idempotency, compact reports, and retention limits
63
63
  - GitHub issue templates for bug reports, false positives, false negatives, rule requests, and public-safe security reports
@@ -114,19 +114,21 @@ GitHub Project:
114
114
  Current issue set:
115
115
 
116
116
  - Closed hosted MVP issue: #24 webhook intake.
117
- - Open hosted MVP roadmap issues: #25 idempotent queue contract, #26 read-only worker checkout, #27 Check summaries, #28 retention/uninstall cleanup, and #29 hosted operational release gate.
117
+ - Closed hosted MVP issue: #25 idempotent queue contract.
118
+ - Closed hosted MVP issue: #26 read-only worker checkout.
119
+ - Open hosted MVP roadmap issues: #27 Check summaries, #28 retention/uninstall cleanup, and #29 hosted operational release gate.
118
120
 
119
121
  CI:
120
122
 
121
123
  - Workflow: `.github/workflows/ci.yml`
122
124
  - Runs on pull requests and pushes to `main`
123
125
  - Uses `permissions: contents: read`
124
- - Latest verified run for the hosted report fixture batch succeeded
126
+ - Latest verified run for the hosted read-only worker plan release succeeded
125
127
 
126
128
  Publishing:
127
129
 
128
130
  - npm package: `ai-saas-guard`
129
- - Current release line: `v0.12.0`
131
+ - Current release line: `v0.14.0`
130
132
  - Publish workflow: `.github/workflows/npm-publish.yml`
131
133
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
132
134
  - Long-lived npm publish tokens should not be required.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",