ai-saas-guard 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -235,13 +235,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
235
235
  | Area | Status |
236
236
  | --- | --- |
237
237
  | Public GitHub repository | Available |
238
- | npm CLI | `ai-saas-guard@0.40.0` |
239
- | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.40.0` |
238
+ | npm CLI | `ai-saas-guard@0.42.0` |
239
+ | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.42.0` |
240
240
  | Outputs | Launch decision queue, short summary, terminal, JSON, SARIF, and PR-focused markdown |
241
241
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
242
242
  | Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
243
- | Versioned Action tags | `v0.40.0`, `v0` |
244
- | Current release | `0.40.0` groups hosted Check Run output by launch-risk area, adds machine-readable hosted smoke evidence, clarifies CLI/Action/Hosted path selection, and documents the next source-checkout boundary |
243
+ | Versioned Action tags | `v0.42.0`, `v0` |
244
+ | Current release | `0.42.0` adds a single Phase 3 source-checkout trial gate that combines plan, stage evidence, scan proof, live smoke, rollback, monitoring, and incident-owner checks before hosted beta |
245
245
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
246
246
  | Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
247
247
  | Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; public install/privacy notes are in [docs/hosted-install-privacy.md](docs/hosted-install-privacy.md); signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
@@ -367,7 +367,7 @@ Deployed worker staging evidence is documented in [docs/hosted-deployed-worker-s
367
367
 
368
368
  The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/install-info`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The Worker verifies signatures, stores compact pull request identity records, exchanges a scoped installation token, fetches PR file metadata from GitHub, classifies PR-risk hotspots, and publishes a bounded selected-repository hosted check with a review queue and manual proof prompt. Signed installation deletion and repository removal events delete matching compact records. Current deployed evidence is tracked in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md): health, signed webhook delivery, compact KV records, cleanup, and Check Run publication pass in staging. The Cloudflare Worker still does not run a full source checkout scan worker or store raw webhook payloads, PR title/body text, raw diffs, source, secrets, checkout paths, or installation tokens.
369
369
 
370
- The next hosted source-checkout step is intentionally narrow: deploy the existing read-only checkout worker behind the same selected-repository identity, keep the fixed `pr-risk --json` command, write only compact findings to the Check Run, and require deployed cleanup/log-boundary/rollback evidence before broader trial use.
370
+ The next hosted source-checkout step is intentionally narrow: deploy the existing read-only checkout worker behind the same selected-repository identity, keep the fixed `pr-risk --json` command, write only compact findings to the Check Run, and require deployed cleanup/log-boundary/rollback evidence before broader trial use. The hosted worker export includes `createHostedSourceCheckoutTrialPlan`, `createHostedSourceCheckoutEvidence`, and `evaluateHostedSourceCheckoutTrialGate` so Phase 3 has one machine-checkable gate for checkout start/end, token removal, CLI start/end, compact report write, Check Run write, cleanup status, live smoke, rollback, monitoring, and incident-owner proof before Phase 4 beta.
371
371
 
372
372
  Hosted install and privacy details are summarized in [docs/hosted-install-privacy.md](docs/hosted-install-privacy.md): selected-repository permissions, supported events, Check Run data boundaries, uninstall cleanup, and why the local CLI remains the private/offline path.
373
373
 
@@ -423,7 +423,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
423
423
 
424
424
  ## GitHub Action
425
425
 
426
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.40.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
426
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.42.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
427
427
 
428
428
  ```yaml
429
429
  name: ai-saas-guard
@@ -487,7 +487,7 @@ export function createHostedCheckRunSummary(input) {
487
487
  title: formatCheckRunTitle(totalFindings, conclusion, input.failOnSeverity),
488
488
  summary: [
489
489
  `Launch-risk gate: ${launchGate}. Launch gate: ${launchGate}.`,
490
- "Review task: inspect the files below before merge.",
490
+ "Review task: inspect risk areas and files before merge.",
491
491
  "Manual proof: prove changed auth, billing, data, deploy, or tests fail closed.",
492
492
  "Boundary: selected repository only; not an AI reviewer, pentest, full audit, or certification."
493
493
  ].join(" "),
@@ -1154,7 +1154,7 @@ function formatCheckRunMarkdown(report, conclusion, localCliCommand, launchGate)
1154
1154
  return [
1155
1155
  "### AI SaaS Guard Launch-risk gate",
1156
1156
  "",
1157
- "Review task: inspect the files below before merge.",
1157
+ "Review task: inspect risk areas and files before merge.",
1158
1158
  "Manual proof: prove changed auth, billing, data, deploy, or tests fail closed.",
1159
1159
  "Boundary: selected repository only; not an AI reviewer, pentest, full audit, or certification.",
1160
1160
  "",
@@ -27,6 +27,73 @@ export interface HostedReadOnlyCheckoutScanRunnerOptions {
27
27
  installationTokenProvider: HostedInstallationTokenProvider;
28
28
  commandRunner?: HostedReadOnlyCheckoutCommandRunner;
29
29
  }
30
+ export interface HostedSourceCheckoutTrialPlanInput {
31
+ requestedAt: string;
32
+ repositoryFullName: string;
33
+ selectedRepositoryOnly: boolean;
34
+ permissions: {
35
+ contents: "read" | "write" | "none";
36
+ checks: "write" | "read" | "none";
37
+ };
38
+ command: string[];
39
+ storesRawSource: boolean;
40
+ storesRawDiffs: boolean;
41
+ storesInstallationToken: boolean;
42
+ exposesPublicScanner: boolean;
43
+ }
44
+ export interface HostedSourceCheckoutTrialPlan {
45
+ readyForTrial: boolean;
46
+ blockedReasons: Array<"repository_not_selected" | "contents_read_required" | "checks_write_required" | "fixed_pr_risk_json_command_required" | "raw_source_storage_blocked" | "raw_diff_storage_blocked" | "installation_token_storage_blocked" | "public_scanner_claim_blocked">;
47
+ requestedAt: string;
48
+ repositoryFullName: string;
49
+ permissions: {
50
+ contents: "read";
51
+ checks: "write";
52
+ };
53
+ fixedCommand: ["ai-saas-guard", "pr-risk", "--root", "<worker-checkout>", "--base", "<trusted-base-sha>", "--json"];
54
+ stages: Array<{
55
+ id: "checkout_start" | "token_remove" | "cli_start" | "cli_end" | "compact_report_write" | "check_run_write" | "cleanup_end";
56
+ evidence: string;
57
+ }>;
58
+ privacy: {
59
+ storesRawSource: false;
60
+ storesRawDiffs: false;
61
+ storesInstallationToken: false;
62
+ exposesPublicScanner: false;
63
+ };
64
+ }
65
+ export interface HostedSourceCheckoutEvidenceInput {
66
+ requestedAt: string;
67
+ jobKey: string;
68
+ stages: Array<{
69
+ id: HostedSourceCheckoutTrialPlan["stages"][number]["id"];
70
+ ok: boolean;
71
+ at: string;
72
+ }>;
73
+ summaryCounts: Record<string, number>;
74
+ compactFindingCount: number;
75
+ cleanupStatus: "deleted" | "failed" | "unknown";
76
+ rawSource?: string;
77
+ rawDiff?: string;
78
+ checkoutPath?: string;
79
+ installationToken?: string;
80
+ }
81
+ export interface HostedSourceCheckoutEvidence {
82
+ readyForReleaseGate: boolean;
83
+ blockedReasons: Array<"missing_checkout_start" | "missing_token_remove" | "missing_cli_start" | "missing_cli_end" | "missing_compact_report_write" | "missing_check_run_write" | "missing_cleanup_end" | "cleanup_not_deleted">;
84
+ requestedAt: string;
85
+ jobKey: string;
86
+ stageResults: HostedSourceCheckoutEvidenceInput["stages"];
87
+ summaryCounts: Record<string, number>;
88
+ compactFindingCount: number;
89
+ cleanupStatus: "deleted" | "failed" | "unknown";
90
+ privacy: {
91
+ includesRawSource: false;
92
+ includesRawDiffs: false;
93
+ includesPrivateCheckoutPath: false;
94
+ includesInstallationToken: false;
95
+ };
96
+ }
30
97
  export interface HostedReadOnlyCheckoutScanGateInput {
31
98
  requestedAt: string;
32
99
  jobKey: string;
@@ -60,6 +127,41 @@ export interface HostedReadOnlyCheckoutScanGate {
60
127
  claimsCompleteHostedSaas: false;
61
128
  };
62
129
  }
130
+ export interface HostedSourceCheckoutTrialGateInput extends HostedSourceCheckoutTrialPlanInput, Omit<HostedSourceCheckoutEvidenceInput, "requestedAt" | "rawSource" | "rawDiff" | "checkoutPath" | "installationToken">, Omit<HostedReadOnlyCheckoutScanGateInput, "requestedAt" | "jobKey" | "summaryCounts" | "compactFindingCount" | "rawSource" | "rawDiff" | "checkoutPath" | "installationToken"> {
131
+ liveSmokePassed: boolean;
132
+ rollbackTested: boolean;
133
+ monitoringEvidence: boolean;
134
+ incidentOwnerRecorded: boolean;
135
+ rawSource?: string;
136
+ rawDiff?: string;
137
+ checkoutPath?: string;
138
+ installationToken?: string;
139
+ }
140
+ export interface HostedSourceCheckoutTrialGate {
141
+ phase: "phase_3_hosted_source_checkout_trial";
142
+ readyForPhase4Beta: boolean;
143
+ blockedReasons: string[];
144
+ requestedAt: string;
145
+ repositoryFullName: string;
146
+ jobKey: string;
147
+ plan: HostedSourceCheckoutTrialPlan;
148
+ evidence: HostedSourceCheckoutEvidence;
149
+ scanGate: HostedReadOnlyCheckoutScanGate;
150
+ operatorProof: {
151
+ liveSmokePassed: boolean;
152
+ rollbackTested: boolean;
153
+ monitoringEvidence: boolean;
154
+ incidentOwnerRecorded: boolean;
155
+ };
156
+ nextAction: string;
157
+ privacy: {
158
+ includesRawSource: false;
159
+ includesRawDiffs: false;
160
+ includesPrivateCheckoutPath: false;
161
+ includesInstallationToken: false;
162
+ claimsPublicHostedScanner: false;
163
+ };
164
+ }
63
165
  export type HostedReadOnlyCheckoutScanSafeReason = "invalid_worker_plan" | "invalid_repository_full_name" | "invalid_clone_base_url" | "missing_installation_token" | "git_init_failed" | "git_remote_add_failed" | "git_fetch_head_failed" | "git_fetch_base_failed" | "git_checkout_failed" | "cli_scan_failed" | "invalid_cli_output" | "cleanup_failed";
64
166
  export declare class HostedReadOnlyCheckoutScanError extends Error {
65
167
  readonly safeReason: HostedReadOnlyCheckoutScanSafeReason;
@@ -74,5 +176,8 @@ export declare class HostedReadOnlyCheckoutScanError extends Error {
74
176
  constructor(safeReason: HostedReadOnlyCheckoutScanSafeReason);
75
177
  }
76
178
  export declare function createHostedReadOnlyCheckoutScanRunner(options: HostedReadOnlyCheckoutScanRunnerOptions): HostedServiceScanRunner;
179
+ export declare function createHostedSourceCheckoutTrialPlan(input: HostedSourceCheckoutTrialPlanInput): HostedSourceCheckoutTrialPlan;
180
+ export declare function createHostedSourceCheckoutEvidence(input: HostedSourceCheckoutEvidenceInput): HostedSourceCheckoutEvidence;
77
181
  export declare function evaluateHostedReadOnlyCheckoutScanGate(input: HostedReadOnlyCheckoutScanGateInput): HostedReadOnlyCheckoutScanGate;
182
+ export declare function evaluateHostedSourceCheckoutTrialGate(input: HostedSourceCheckoutTrialGateInput): HostedSourceCheckoutTrialGate;
78
183
  export declare function runHostedReadOnlyCheckoutScan(input: HostedServiceScanRunnerInput, options: HostedReadOnlyCheckoutScanRunnerOptions): Promise<HostedServiceScanRunnerResult>;
@@ -28,6 +28,105 @@ export class HostedReadOnlyCheckoutScanError extends Error {
28
28
  export function createHostedReadOnlyCheckoutScanRunner(options) {
29
29
  return (input) => runHostedReadOnlyCheckoutScan(input, options);
30
30
  }
31
+ export function createHostedSourceCheckoutTrialPlan(input) {
32
+ const blockedReasons = [];
33
+ const expectedCommand = [
34
+ "ai-saas-guard",
35
+ "pr-risk",
36
+ "--root",
37
+ "<worker-checkout>",
38
+ "--base",
39
+ "<trusted-base-sha>",
40
+ "--json"
41
+ ];
42
+ if (!input.selectedRepositoryOnly)
43
+ blockedReasons.push("repository_not_selected");
44
+ if (input.permissions.contents !== "read")
45
+ blockedReasons.push("contents_read_required");
46
+ if (input.permissions.checks !== "write")
47
+ blockedReasons.push("checks_write_required");
48
+ if (!arraysEqual(input.command, expectedCommand)) {
49
+ blockedReasons.push("fixed_pr_risk_json_command_required");
50
+ }
51
+ if (input.storesRawSource)
52
+ blockedReasons.push("raw_source_storage_blocked");
53
+ if (input.storesRawDiffs)
54
+ blockedReasons.push("raw_diff_storage_blocked");
55
+ if (input.storesInstallationToken)
56
+ blockedReasons.push("installation_token_storage_blocked");
57
+ if (input.exposesPublicScanner)
58
+ blockedReasons.push("public_scanner_claim_blocked");
59
+ return {
60
+ readyForTrial: blockedReasons.length === 0,
61
+ blockedReasons,
62
+ requestedAt: input.requestedAt,
63
+ repositoryFullName: input.repositoryFullName,
64
+ permissions: {
65
+ contents: "read",
66
+ checks: "write"
67
+ },
68
+ fixedCommand: [
69
+ "ai-saas-guard",
70
+ "pr-risk",
71
+ "--root",
72
+ "<worker-checkout>",
73
+ "--base",
74
+ "<trusted-base-sha>",
75
+ "--json"
76
+ ],
77
+ stages: [
78
+ { id: "checkout_start", evidence: "trusted selected-repository identity and contents:read token requested" },
79
+ { id: "token_remove", evidence: "temporary askpass and token material removed before CLI starts" },
80
+ { id: "cli_start", evidence: "fixed pr-risk --json command starts in temporary worker checkout" },
81
+ { id: "cli_end", evidence: "CLI exits with bounded output and compact JSON only" },
82
+ { id: "compact_report_write", evidence: "compact findings stored without source, diff, secrets, or checkout paths" },
83
+ { id: "check_run_write", evidence: "bounded Check Run written from compact findings only" },
84
+ { id: "cleanup_end", evidence: "worker checkout, generated artifacts, and credential material deleted" }
85
+ ],
86
+ privacy: {
87
+ storesRawSource: false,
88
+ storesRawDiffs: false,
89
+ storesInstallationToken: false,
90
+ exposesPublicScanner: false
91
+ }
92
+ };
93
+ }
94
+ export function createHostedSourceCheckoutEvidence(input) {
95
+ const requiredStages = [
96
+ "checkout_start",
97
+ "token_remove",
98
+ "cli_start",
99
+ "cli_end",
100
+ "compact_report_write",
101
+ "check_run_write",
102
+ "cleanup_end"
103
+ ];
104
+ const observed = new Map(input.stages.map((stage) => [stage.id, stage]));
105
+ const blockedReasons = [];
106
+ for (const stage of requiredStages) {
107
+ if (observed.get(stage)?.ok !== true) {
108
+ blockedReasons.push(`missing_${stage}`);
109
+ }
110
+ }
111
+ if (input.cleanupStatus !== "deleted")
112
+ blockedReasons.push("cleanup_not_deleted");
113
+ return {
114
+ readyForReleaseGate: blockedReasons.length === 0,
115
+ blockedReasons,
116
+ requestedAt: input.requestedAt,
117
+ jobKey: input.jobKey,
118
+ stageResults: input.stages.map((stage) => ({ ...stage })),
119
+ summaryCounts: { ...input.summaryCounts },
120
+ compactFindingCount: input.compactFindingCount,
121
+ cleanupStatus: input.cleanupStatus,
122
+ privacy: {
123
+ includesRawSource: false,
124
+ includesRawDiffs: false,
125
+ includesPrivateCheckoutPath: false,
126
+ includesInstallationToken: false
127
+ }
128
+ };
129
+ }
31
130
  export function evaluateHostedReadOnlyCheckoutScanGate(input) {
32
131
  const requiredStages = [
33
132
  "git_init",
@@ -74,6 +173,72 @@ export function evaluateHostedReadOnlyCheckoutScanGate(input) {
74
173
  }
75
174
  };
76
175
  }
176
+ export function evaluateHostedSourceCheckoutTrialGate(input) {
177
+ const plan = createHostedSourceCheckoutTrialPlan(input);
178
+ const evidence = createHostedSourceCheckoutEvidence({
179
+ requestedAt: input.requestedAt,
180
+ jobKey: input.jobKey,
181
+ stages: input.stages,
182
+ summaryCounts: input.summaryCounts,
183
+ compactFindingCount: input.compactFindingCount,
184
+ cleanupStatus: input.cleanupStatus,
185
+ rawSource: input.rawSource,
186
+ rawDiff: input.rawDiff,
187
+ checkoutPath: input.checkoutPath,
188
+ installationToken: input.installationToken
189
+ });
190
+ const scanGate = evaluateHostedReadOnlyCheckoutScanGate({
191
+ requestedAt: input.requestedAt,
192
+ jobKey: input.jobKey,
193
+ commandStages: input.commandStages,
194
+ summaryCounts: input.summaryCounts,
195
+ compactFindingCount: input.compactFindingCount,
196
+ compactReportStored: input.compactReportStored,
197
+ checkRunPublished: input.checkRunPublished,
198
+ checkoutDeleted: input.checkoutDeleted,
199
+ tokenRemovedBeforeCli: input.tokenRemovedBeforeCli,
200
+ maxOutputBytes: input.maxOutputBytes,
201
+ timeoutMs: input.timeoutMs,
202
+ rawSource: input.rawSource,
203
+ rawDiff: input.rawDiff,
204
+ checkoutPath: input.checkoutPath,
205
+ installationToken: input.installationToken
206
+ });
207
+ const operatorBlockedReasons = operatorProofBlockedReasons(input);
208
+ const blockedReasons = [
209
+ ...plan.blockedReasons,
210
+ ...evidence.blockedReasons,
211
+ ...scanGate.blockedReasons,
212
+ ...operatorBlockedReasons
213
+ ];
214
+ return {
215
+ phase: "phase_3_hosted_source_checkout_trial",
216
+ readyForPhase4Beta: blockedReasons.length === 0,
217
+ blockedReasons,
218
+ requestedAt: input.requestedAt,
219
+ repositoryFullName: input.repositoryFullName,
220
+ jobKey: input.jobKey,
221
+ plan,
222
+ evidence,
223
+ scanGate,
224
+ operatorProof: {
225
+ liveSmokePassed: input.liveSmokePassed,
226
+ rollbackTested: input.rollbackTested,
227
+ monitoringEvidence: input.monitoringEvidence,
228
+ incidentOwnerRecorded: input.incidentOwnerRecorded
229
+ },
230
+ nextAction: blockedReasons.length === 0
231
+ ? "Phase 3 is closed for the selected-repository trial. Continue to Phase 4 beta only with the same privacy boundary."
232
+ : "Do not open hosted beta. Resolve the blocked reasons and rerun the selected-repository source-checkout trial gate.",
233
+ privacy: {
234
+ includesRawSource: false,
235
+ includesRawDiffs: false,
236
+ includesPrivateCheckoutPath: false,
237
+ includesInstallationToken: false,
238
+ claimsPublicHostedScanner: false
239
+ }
240
+ };
241
+ }
77
242
  export async function runHostedReadOnlyCheckoutScan(input, options) {
78
243
  const { plan } = input;
79
244
  const { checkout, cli } = plan;
@@ -240,6 +405,18 @@ function isTrustedFixedReadOnlyPlan(input) {
240
405
  function arraysEqual(left, right) {
241
406
  return left.length === right.length && left.every((value, index) => value === right[index]);
242
407
  }
408
+ function operatorProofBlockedReasons(input) {
409
+ const reasons = [];
410
+ if (!input.liveSmokePassed)
411
+ reasons.push("live_smoke_missing");
412
+ if (!input.rollbackTested)
413
+ reasons.push("rollback_test_missing");
414
+ if (!input.monitoringEvidence)
415
+ reasons.push("monitoring_evidence_missing");
416
+ if (!input.incidentOwnerRecorded)
417
+ reasons.push("incident_owner_missing");
418
+ return reasons;
419
+ }
243
420
  function compactFinding(value) {
244
421
  if (!isRecord(value))
245
422
  return [];
@@ -213,18 +213,18 @@ node dist/cli.js scan --root /path/to/your-saas
213
213
 
214
214
  这个仓库是公开 GitHub 仓库。
215
215
 
216
- CLI 已发布到 npm:`ai-saas-guard@0.40.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.40.0`。
216
+ CLI 已发布到 npm:`ai-saas-guard@0.42.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.42.0`。
217
217
 
218
218
  | 模块 | 状态 |
219
219
  | --- | --- |
220
220
  | 公开 GitHub 仓库 | 已可用 |
221
- | npm CLI | `ai-saas-guard@0.40.0` |
222
- | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.40.0` |
221
+ | npm CLI | `ai-saas-guard@0.42.0` |
222
+ | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.42.0` |
223
223
  | 输出格式 | 上线决策队列、短 summary、Terminal、JSON、SARIF 和 PR markdown |
224
224
  | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
225
225
  | 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
226
- | 当前版本 | `0.40.0` 按上线风险区域分组 hosted Check Run 输出,增加机器可读 hosted smoke evidence,讲清 CLI/Action/Hosted 三条路径选择,并记录下一步 source-checkout 边界 |
227
- | Action 标签 | `v0.40.0`、`v0` |
226
+ | 当前版本 | `0.42.0` 增加统一的 Phase 3 source-checkout trial gate,把 plan、stage evidence、scan proof、live smoke、rollback、monitoring incident owner proof 合并成进入 hosted beta 前的机器可判定门槛 |
227
+ | Action 标签 | `v0.42.0`、`v0` |
228
228
  | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
229
229
  | 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
230
230
  | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;安装和隐私说明见 [hosted-install-privacy.md](hosted-install-privacy.md);提供 `/github/app/install-info`,签名 GitHub App webhook delivery、compact Check Run 和 installation cleanup staging smoke 已通过 |
@@ -383,7 +383,7 @@ GitHub Marketplace wrapper 决策见 [docs/github-marketplace-wrapper-decision.m
383
383
 
384
384
  当前仓库已经包含未来 Hosted GitHub App 的设计文档、纯契约测试、第一个真实 Cloudflare hosted ingress,以及 Node/container read-only checkout scan runner。私有 staging GitHub App `ai-saas-guard-hosted` 已安装到 `zr9959/ai-saas-guard`,Cloudflare 已配置所需的云端凭据绑定。Worker 代码已经能接收签名 webhook、写入 KV 队列、换取 scoped installation token、读取 GitHub PR file metadata、做 compact PR-risk classification,并发布有长度上限的 selected-repository Check Run summary;`/github/app/install-info` 会返回公开安全的安装说明、权限、事件、隐私边界和卸载说明。签名 installation deletion 和 repository removal 事件会删除匹配的 compact records。当前端到端 GitHub App webhook delivery smoke 已通过,证据记录在 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md)。Cloudflare ingress 本身仍不是完整 source checkout scan worker。
385
385
 
386
- 下一步 hosted source checkout 仍然要保持窄边界:把现有 read-only checkout worker 放到同一个 selected-repository identity 后面,继续固定 `pr-risk --json` 命令,只把 compact findings 写入 Check Run,并在扩大 trial 前要求 deployed cleanup、log-boundary 和 rollback evidence。
386
+ 下一步 hosted source checkout 仍然要保持窄边界:把现有 read-only checkout worker 放到同一个 selected-repository identity 后面,继续固定 `pr-risk --json` 命令,只把 compact findings 写入 Check Run,并在扩大 trial 前要求 deployed cleanup、log-boundary 和 rollback evidence。hosted worker export 现在包含 `createHostedSourceCheckoutTrialPlan`、`createHostedSourceCheckoutEvidence` 和 `evaluateHostedSourceCheckoutTrialGate`,用于在进入 Phase 4 beta 前统一检查 checkout start/end、token removal、CLI start/end、compact report write、Check Run write、cleanup status、live smoke、rollback、monitoring 和 incident owner proof。
387
387
 
388
388
  Hosted 安装、权限和隐私边界见 [hosted-install-privacy.md](hosted-install-privacy.md):selected-repository 权限、支持的 GitHub 事件、Check Run 数据边界、卸载清理,以及为什么本地 CLI 仍然是私有/离线路径。
389
389
 
@@ -134,6 +134,6 @@ It does not return:
134
134
 
135
135
  ## Current Status
136
136
 
137
- The repository can now instantiate a Node/container hosted app skeleton, route signed webhooks into the hosted service runtime, process one worker tick through adapters, compose the real read-only checkout scan runner behind a token-provider boundary, expose clamped worker safety budgets, and validate provider adapter references before deployment.
137
+ The repository can now instantiate a Node/container hosted app skeleton, route signed webhooks into the hosted service runtime, process one worker tick through adapters, compose the real read-only checkout scan runner behind a token-provider boundary, expose clamped worker safety budgets, validate provider adapter references before deployment, and evaluate one Phase 3 source-checkout trial gate through `evaluateHostedSourceCheckoutTrialGate`.
138
138
 
139
- A public hosted environment still requires actual platform infrastructure, a public HTTPS webhook URL, platform secrets, durable queue/storage, worker sandboxing, GitHub Checks API credentials at runtime, monitoring, rollback, incident-response evidence, and the hosted operational release gate. Use [hosted-staging-deployment.md](hosted-staging-deployment.md) to plan and block staging exposure until those provider references and evidence exist.
139
+ A public hosted environment still requires actual platform infrastructure, a public HTTPS webhook URL, platform secrets, durable queue/storage, worker sandboxing, GitHub Checks API credentials at runtime, monitoring, rollback, incident-response evidence, and a passing Phase 3 gate. Use [hosted-staging-deployment.md](hosted-staging-deployment.md) to plan and block staging exposure until those provider references and evidence exist.
@@ -170,6 +170,28 @@ Required proof:
170
170
  - failure cleanup covers clone failure, timeout, CLI failure, malformed JSON output, Check Run write failure, cancellation, and process interruption.
171
171
  - cleanup failures create an operator-review event without returning raw source, raw diffs, installation tokens, checkout paths, private URLs, or low-level filesystem errors to users.
172
172
 
173
+ ### Source Checkout Trial Evidence Contract
174
+
175
+ The v0.41 source-checkout trial boundary is executable as a pure contract before public exposure. The `ai-saas-guard/hosted/worker` export now includes:
176
+
177
+ - `createHostedSourceCheckoutTrialPlan`: accepts the proposed selected-repository trial shape and blocks anything wider than `contents: read`, `checks: write`, fixed `ai-saas-guard pr-risk --root <worker-checkout> --base <trusted-base-sha> --json`, no raw source storage, no raw diff storage, no installation-token storage, and no public scanner claim.
178
+ - `createHostedSourceCheckoutEvidence`: records stage-level evidence for `checkout_start`, `token_remove`, `cli_start`, `cli_end`, `compact_report_write`, `check_run_write`, and `cleanup_end`, then blocks release-gate readiness unless every stage passed and cleanup status is `deleted`.
179
+
180
+ The evidence object must stay compact and privacy-safe. It may contain job key, stage IDs, timestamps, summary counts, compact finding count, cleanup status, and safe blocked reasons. It must not include source, diffs, checkout paths, installation tokens, PR-authored commands, private URLs, or low-level filesystem errors.
181
+
182
+ ### Phase 3 Source Checkout Trial Gate
183
+
184
+ The v0.42 hosted worker export adds `evaluateHostedSourceCheckoutTrialGate`. This is the single gate for closing Phase 3 and deciding whether the project may move toward Phase 4 hosted beta.
185
+
186
+ The gate combines:
187
+
188
+ - source-checkout trial plan checks from `createHostedSourceCheckoutTrialPlan`
189
+ - stage evidence checks from `createHostedSourceCheckoutEvidence`
190
+ - read-only checkout scan checks from `evaluateHostedReadOnlyCheckoutScanGate`
191
+ - operator proof for live smoke, rollback, monitoring evidence, and incident owner recording
192
+
193
+ It returns `readyForPhase4Beta: true` only when all four layers pass. Otherwise it returns safe blocked reasons and the next action: do not open hosted beta until the missing proof is rerun. The response must remain compact and privacy-safe: no raw source, raw diffs, checkout paths, installation tokens, public hosted scanner claim, private URLs, or PR-authored commands.
194
+
173
195
  ### Log Boundary Evidence
174
196
 
175
197
  Before exposure, sample ingress, queue, worker, report, and Check Run logs for the release candidate. The sample may contain scan key, installation ID, repository ID, PR number, head SHA, scanner version, duration, summary counts, error class, and cleanup status.
@@ -6,10 +6,18 @@ Passing these checks does not make the project a pentest, certification, or full
6
6
 
7
7
  ## Current Evidence
8
8
 
9
- Recorded on 2026-05-25 from the deployed Cloudflare Worker plus the earlier temporary no-file-change GitHub PR smoke.
9
+ Recorded on 2026-05-25 from the deployed Cloudflare Worker plus temporary GitHub PR smokes.
10
10
 
11
11
  | Check | Evidence | Result |
12
12
  | --- | --- | --- |
13
+ | Cloudflare Worker health, v0.42.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/healthz` returned `ok: true`, routes including `/github/app/install-info`, `checkRunPublisher: "configured"`, `scannerVersion: "0.42.0"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
14
+ | Public install guidance, v0.42.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/github/app/install-info` returned the `ai-saas-guard-hosted` install URL, selected-repository boundary wording, first-slice permissions `checks: write`, `contents: read`, `metadata: read`, `pull_requests: read`, subscribed events `pull_request`, `installation`, and `installation_repositories`, uninstall cleanup wording, `scannerVersion: "0.42.0"`, and no private keys, webhook secrets, installation tokens, source, diffs, or customer payloads | Passed |
15
+ | Deployed Worker version, v0.42.0 | `wrangler deploy` uploaded 38.57 KiB / gzip 9.86 KiB and deployed version `6de0811e-11bf-46a6-9b7b-cbecda409695` at `2026-05-25T13:40:11Z` verification time | Passed |
16
+ | Real hosted PR smoke, v0.42.0 | `node scripts/hosted-pr-smoke.mjs --evidence-file /tmp/ai-saas-guard-hosted-smoke-v0.42.json` opened temporary PR `#89`, waited for Check Run `77721238202` on head SHA `66dfffde2ffa1a563ebc45fe7b22468d2f060e22`, received conclusion `success`, closed the PR, restored the original branch, deleted the local branch, and deleted 9 staging KV records with `remainingSmokeKeys: 0`; `gh pr view 89` returned `state: "CLOSED"`, `git ls-remote --heads origin codex/hosted-smoke-20260525134106` returned no remote branch, and `wrangler kv key list --namespace-id fa5344fbd7944de6a776bf8731d58460 --remote` returned `[]` after cleanup | Passed |
17
+ | Cloudflare Worker health, v0.41.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/healthz` returned `ok: true`, routes including `/github/app/install-info`, `checkRunPublisher: "configured"`, `scannerVersion: "0.41.0"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
18
+ | Public install guidance, v0.41.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/github/app/install-info` returned the `ai-saas-guard-hosted` install URL, selected-repository boundary wording, first-slice permissions `checks: write`, `contents: read`, `metadata: read`, `pull_requests: read`, subscribed events `pull_request`, `installation`, and `installation_repositories`, uninstall cleanup wording, `scannerVersion: "0.41.0"`, and no private keys, webhook secrets, installation tokens, source, diffs, or customer payloads | Passed |
19
+ | Deployed Worker version, v0.41.0 | `wrangler deploy` uploaded 38.57 KiB / gzip 9.86 KiB and deployed version `fb0b4726-ac75-4577-942b-fdeed7752979` at `2026-05-25T13:22:28Z` verification time | Passed |
20
+ | Real hosted PR smoke, v0.41.0 | `node scripts/hosted-pr-smoke.mjs --evidence-file /tmp/ai-saas-guard-hosted-smoke-v0.41.json` opened temporary PR `#87`, waited for Check Run `77718782535` on head SHA `83a341dcba63ad9a30aabdfec1de4f874a3c0b11`, received conclusion `success`, closed the PR, restored the original branch, deleted the local branch, and deleted 9 staging KV records with `remainingSmokeKeys: 0`; `gh pr view 87` returned `state: "CLOSED"`, `git ls-remote --heads origin codex/hosted-smoke-20260525132327` returned no remote branch, and `wrangler kv key list --namespace-id fa5344fbd7944de6a776bf8731d58460 --remote` returned `[]` after cleanup | Passed |
13
21
  | Cloudflare Worker health, v0.40.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/healthz` returned `ok: true`, routes including `/github/app/install-info`, `checkRunPublisher: "configured"`, `scannerVersion: "0.40.0"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
14
22
  | Public install guidance, v0.40.0 | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/github/app/install-info` returned the `ai-saas-guard-hosted` install URL, selected-repository boundary wording, first-slice permissions `checks: write`, `contents: read`, `metadata: read`, `pull_requests: read`, subscribed events `pull_request`, `installation`, and `installation_repositories`, uninstall cleanup wording, `scannerVersion: "0.40.0"`, and no private keys, webhook secrets, installation tokens, source, diffs, or customer payloads | Passed |
15
23
  | Deployed Worker version, v0.40.0 | `wrangler deploy` uploaded 38.99 KiB / gzip 10.01 KiB and deployed version `47e90d1c-0d7b-455f-b1a4-1ec7ee10d58b` at `2026-05-25T12:47:28Z` verification time | Passed |
@@ -5,11 +5,11 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current published version: `0.40.0`
8
+ - Current published version: `0.42.0`
9
9
  - Next source candidate: none
10
10
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
11
11
  - First npm-published version: `0.1.1`
12
- - GitHub Release: `v0.40.0`
12
+ - GitHub Release: `v0.42.0`
13
13
  - Publish workflow: `.github/workflows/npm-publish.yml`
14
14
  - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
15
15
  - Long-lived npm publish token: not required
@@ -18,7 +18,7 @@
18
18
 
19
19
  Use GitHub Actions with npm Trusted Publisher/OIDC:
20
20
 
21
- 1. Create and review a release tag such as `v0.40.0`.
21
+ 1. Create and review a release tag such as `v0.42.0`.
22
22
  2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
23
23
  3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
24
24
  4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
@@ -23,7 +23,7 @@ This Worker is a real hosted ingress with first-slice Check Run publishing code,
23
23
  - `HOSTED_EVENTS`: Cloudflare KV namespace for compact delivery and queued scan records.
24
24
  - `WEBHOOK_SECRET`: Worker secret matching the GitHub App webhook secret.
25
25
  - `GITHUB_APP_PRIVATE_KEY`: Worker secret for the staging GitHub App private key, used only in memory to sign short-lived GitHub App JWTs.
26
- - `SCANNER_VERSION`: public version string, currently `0.40.0`.
26
+ - `SCANNER_VERSION`: public version string, currently `0.42.0`.
27
27
  - `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_INSTALLATION_ID`: public staging identifiers for the private GitHub App installation.
28
28
 
29
29
  ## Deployment
@@ -822,29 +822,19 @@ function summarizeFindings(findings) {
822
822
  function renderCheckRunSummary({ identity, report, scannerVersion }) {
823
823
  const riskAreas = summarizeHostedRiskAreas(report.topRiskyFiles);
824
824
  const lines = [
825
- `Launch-risk gate: ai-saas-guard found ${report.summary.total} PR risk signal(s) for ${identity.repositoryFullName}#${identity.pullRequestNumber}.`,
826
- `Scanner version: ${scannerVersion}.`,
827
- "",
828
- "Review task: inspect the files below before merge.",
825
+ `Launch-risk gate: ${report.summary.total} PR risk signal(s) for ${identity.repositoryFullName}#${identity.pullRequestNumber}.`,
826
+ "Review task: inspect risk areas and files before merge.",
829
827
  "Manual proof: prove changed auth, billing, data, deploy, or tests fail closed.",
830
- "Boundary: selected repository only; not an AI reviewer, pentest, full audit, or certification.",
831
- "",
832
- "Selected-repository hosted check: this App uses checks:write, contents:read, pull_requests:read, and metadata:read for the installed repository only.",
833
828
  "",
834
- "Launch decision queue:",
835
- "- Can a real user get access they should not have?",
836
- "- Can the app claim success when something failed?",
837
- "- Can launch infrastructure do too much damage?",
829
+ "Risk areas:",
830
+ ...(riskAreas.length === 0
831
+ ? ["- None"]
832
+ : riskAreas.slice(0, 5).map((area) => `- ${area.name}: ${area.count} file(s). Proof: ${area.proof}`)),
838
833
  "",
839
- "Privacy: this Check Run stores compact file/category signals only. It does not store webhook payload bodies, PR title/body text, diff contents, source, secrets, checkout paths, or installation tokens."
834
+ "Review queue:"
840
835
  ];
841
836
 
842
837
  if (report.topRiskyFiles.length > 0) {
843
- lines.push("", "Risk areas:");
844
- for (const area of riskAreas.slice(0, 5)) {
845
- lines.push(`- ${area.name}: ${area.count} file(s). Proof: ${area.proof}`);
846
- }
847
- lines.push("", "Review queue:");
848
838
  for (const file of report.topRiskyFiles.slice(0, 5)) {
849
839
  lines.push(`- ${file.path}: ${file.categories.join(", ")} (${file.added}+/${file.removed}-)`);
850
840
  }
@@ -855,8 +845,16 @@ function renderCheckRunSummary({ identity, report, scannerVersion }) {
855
845
  "- Why is this auth, billing, data, deploy, or test decision safe?",
856
846
  "- What manual proof shows it fails closed?"
857
847
  );
848
+ } else {
849
+ lines.push("- None");
858
850
  }
859
851
 
852
+ lines.push(
853
+ "",
854
+ `Boundary: selected repository only; scanner ${scannerVersion}; not an AI reviewer, pentest, full audit, or certification.`,
855
+ "Privacy: compact file/category signals only; no webhook bodies, PR text, source, diffs, secrets, checkout paths, or installation tokens."
856
+ );
857
+
860
858
  if (report.truncated) {
861
859
  lines.push("", "Note: PR file pagination was capped; run the local CLI for a complete review.");
862
860
  }
@@ -7,7 +7,7 @@
7
7
  "enabled": true
8
8
  },
9
9
  "vars": {
10
- "SCANNER_VERSION": "0.40.0",
10
+ "SCANNER_VERSION": "0.42.0",
11
11
  "GITHUB_APP_ID": "3834787",
12
12
  "GITHUB_APP_SLUG": "ai-saas-guard-hosted",
13
13
  "GITHUB_APP_INSTALLATION_ID": "135085075"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Local-first CLI that catches launch blockers in AI-built Next.js/Supabase/Stripe SaaS apps.",
5
5
  "readmeFilename": "README.md",
6
6
  "type": "module",