Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ open build/desktop/Prayu.app
| v100 | 算子价格快照与 Run 金额预算账本(预留/结算/释放) | operator price snapshots and the run monetary budget ledger (reserve/settle/release) |
| v101 | 结构化 Agent 依赖等待与唯一唤醒收据 | structured agent dependency waiting and unique wake receipts |
| v102 | 模型提议的有界 child 任务调度(core/readonly fan-out 分面、去重与准入) | model-proposed bounded child task scheduling (core/read-only fan-out surfaces, dedup, and admission) |
| v103 | 持久浏览器网络隔离证据与操作者 review | durable browser network containment evidence and operator review |

</details>

Expand Down
37 changes: 37 additions & 0 deletions docs/adr/0105-safe-web-readiness-receipt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# ADR 0105: Safe Web Readiness Receipt

Status: accepted

Date: 2026-08-16

## Context

The restricted Safe Web operator entry must stay disabled unless the Windows WFP
production evidence for the exact accepted browser is still valid. The evidence,
its review, and the launch plan are already immutable and fingerprint-bound, but
nothing yet collapses "is Safe Web ready for this browser right now?" into one
bounded, explainable, fail-closed judgment that the CLI, HTTP, and Desktop entry
can share.

## Decision

`BrowserSafeWebReadiness` records a short-lived, fail-closed judgment over one
`BrowserNetworkContainmentEvidence` + `BrowserNetworkContainmentReview` pair. It
binds the evidence, review, executable identity, acceptance, adapter, policy
version, and platform, and carries a single precise blocking reason whenever it
is not ready. A missing, expired, version-mismatched, hash-mismatched,
policy-mismatched, or rejected pair yields a `Ready=false` receipt rather than a
hard error, so the Desktop entry can display the exact blocking cause. The
readiness never authorizes a launch by itself; it only feeds the existing
`AuthorizeSafeWebStart` process-local gate.

## Consequences

The Safe Web entry now has one deterministic readiness predicate with a stable
blocking-reason taxonomy (`evidence_missing`, `evidence_version_mismatch`,
`executable_identity_mismatch`, `acceptance_mismatch`, `policy_version_mismatch`,
`adapter_mismatch`, `platform_mismatch`, `evidence_not_passed`,
`review_missing`, `review_binding_mismatch`, `review_not_accepted`,
`evidence_expired`). Operator-flow wiring (target selection, scope preview,
approval, and CLI/HTTP/Desktop surfaces) consumes this receipt; the receipt
itself changes no launch authority and keeps the product entry fail-closed.
175 changes: 174 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,82 @@
"x-cyberagent-read-only": true
}
},
"/api/v1/browser/safe-web-readiness": {
"get": {
"operationId": "getBrowserSafeWebReadiness",
"summary": "Inspect Safe Web readiness",
"description": "Returns a fail-closed readiness receipt over the latest production containment evidence and operator review for one accepted browser. It never starts a browser and never grants authority.",
"tags": [
"Browser"
],
"parameters": [
{
"name": "product",
"in": "query",
"description": "Accepted browser product",
"schema": {
"enum": [
"chrome",
"edge",
"chromium"
],
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful read",
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"data": {
"$ref": "#/components/schemas/BrowserSafeWebReadiness"
},
"request_id": {
"maxLength": 256,
"minLength": 1,
"type": "string"
},
"version": {
"const": "api.v1",
"type": "string"
}
},
"required": [
"version",
"request_id",
"data"
],
"type": "object"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"414": {
"$ref": "#/components/responses/RequestTooLarge"
},
"429": {
"$ref": "#/components/responses/ResourceExhausted"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
},
"x-cyberagent-read-only": true
}
},
"/api/v1/capabilities": {
"get": {
"operationId": "getRuntimeCapabilities",
Expand Down Expand Up @@ -11495,6 +11571,102 @@
],
"type": "object"
},
"BrowserSafeWebReadiness": {
"additionalProperties": false,
"properties": {
"acceptance_fingerprint": {
"type": "string"
},
"adapter": {
"enum": [
"windows_wfp_dynamic.v1"
],
"type": "string"
},
"architecture": {
"type": "string"
},
"blocking_reason": {
"enum": [
"evidence_missing",
"evidence_version_mismatch",
"executable_identity_mismatch",
"acceptance_mismatch",
"policy_version_mismatch",
"adapter_mismatch",
"platform_mismatch",
"evidence_not_passed",
"review_missing",
"review_binding_mismatch",
"review_not_accepted",
"evidence_expired"
],
"type": "string"
},
"collector_identity": {
"type": "string"
},
"evidence_fingerprint": {
"type": "string"
},
"executable_identity_fingerprint": {
"type": "string"
},
"expires_at": {
"format": "date-time",
"type": "string"
},
"fingerprint": {
"type": "string"
},
"issued_at": {
"format": "date-time",
"type": "string"
},
"operating_system": {
"type": "string"
},
"policy_version": {
"enum": [
"browser_network_containment_policy.v2"
],
"type": "string"
},
"protocol_version": {
"enum": [
"browser_safe_web_readiness.v1"
],
"type": "string"
},
"ready": {
"type": "boolean"
},
"review_fingerprint": {
"type": "string"
},
"reviewer_identity": {
"type": "string"
}
},
"required": [
"protocol_version",
"evidence_fingerprint",
"review_fingerprint",
"executable_identity_fingerprint",
"acceptance_fingerprint",
"adapter",
"policy_version",
"operating_system",
"architecture",
"collector_identity",
"reviewer_identity",
"ready",
"issued_at",
"expires_at",
"fingerprint"
],
"type": "object"
},
"BudgetView": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -19553,7 +19725,8 @@
"approval",
"file_change",
"plan",
"dependency"
"dependency",
"browser"
],
"type": "string"
},
Expand Down
1 change: 1 addition & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func (a *App) printHelp() {
fmt.Fprintln(a.out, "Usage:")
fmt.Fprintln(a.out, " cyberagent version")
fmt.Fprintln(a.out, " cyberagent doctor portable [--json]")
fmt.Fprintln(a.out, " cyberagent doctor browser-readiness --product <edge|chrome|chromium> [--json]")
fmt.Fprintln(a.out, " cyberagent workspace init|list|show|tree|read")
fmt.Fprintln(a.out, " cyberagent script new|run")
fmt.Fprintln(a.out, " cyberagent ctf init|analyze|writeup")
Expand Down
102 changes: 100 additions & 2 deletions internal/app/doctor_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@ import (
"strings"
"time"

"cyberagent-workbench/internal/application"
"cyberagent-workbench/internal/browserruntime"
"cyberagent-workbench/internal/buildinfo"
)

func (a *App) doctorCommand(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New("usage: cyberagent doctor <portable|browser-network-probe>")
return errors.New("usage: cyberagent doctor <portable|browser-network-probe|browser-network-review|browser-readiness>")
}
switch args[0] {
case "portable":
return a.doctorPortableCommand(args[1:])
case "browser-network-probe":
return a.doctorBrowserNetworkProbeCommand(ctx, args[1:])
case "browser-network-review":
return a.doctorBrowserNetworkReviewCommand(ctx, args[1:])
case "browser-readiness":
return a.doctorBrowserReadinessCommand(ctx, args[1:])
default:
return errors.New("usage: cyberagent doctor <portable|browser-network-probe>")
return errors.New("usage: cyberagent doctor <portable|browser-network-probe|browser-network-review|browser-readiness>")
}
}

Expand Down Expand Up @@ -90,6 +95,12 @@ func (a *App) doctorBrowserNetworkProbeCommand(ctx context.Context,
if err != nil {
return err
}
if err := a.ensureStore(); err != nil {
return err
}
if _, err := a.store.RecordBrowserNetworkEvidence(ctx, evidence, evidence.ID); err != nil {
return err
}
if *jsonOutput {
encoder := json.NewEncoder(a.out)
encoder.SetIndent("", " ")
Expand All @@ -114,6 +125,93 @@ func (a *App) doctorBrowserNetworkProbeCommand(ctx context.Context,
return nil
}

func (a *App) doctorBrowserNetworkReviewCommand(ctx context.Context, args []string) error {
flags := newFlagSet("doctor browser-network-review", a.errOut)
product := flags.String("product", string(browserruntime.BrowserProductEdge),
"accepted browser product: edge, chrome, or chromium")
reviewer := flags.String("reviewer", "", "independent reviewer identity")
operationKey := flags.String("operation-key", "", "stable review operation key")
accept := flags.Bool("accept", false, "accept the latest production evidence")
reject := flags.Bool("reject", false, "reject the latest production evidence")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 0 || *accept == *reject ||
strings.TrimSpace(*reviewer) == "" || strings.TrimSpace(*operationKey) == "" {
return errors.New("usage: cyberagent doctor browser-network-review --product <edge|chrome|chromium> --reviewer <operator> --operation-key <key> (--accept|--reject)")
}
selectedProduct, err := parseBrowserNetworkProbeProduct(*product)
if err != nil {
return err
}
identity, acceptance, err := acceptedBrowserNetworkProbeCandidate(selectedProduct)
if err != nil {
return err
}
if err := a.ensureStore(); err != nil {
return err
}
evidence, err := a.store.LoadLatestBrowserNetworkEvidence(ctx, identity.Fingerprint)
if err != nil {
return err
}
if !evidence.Passed {
return fmt.Errorf("latest browser network evidence did not pass: %s", evidence.FailureCode)
}
now := time.Now().UTC()
review, err := browserruntime.BuildBrowserNetworkContainmentReview(evidence, identity,
acceptance, "browser-network-review-"+fmt.Sprintf("%d", now.UnixNano()),
strings.TrimSpace(*reviewer), *accept, now)
if err != nil {
return err
}
record, err := a.store.RecordBrowserNetworkReview(ctx, review, *operationKey)
if err != nil {
return err
}
fmt.Fprintf(a.out, "evidence: %s\naccepted: %t\nreason: %s\nreplayed: %t\nreview_fingerprint: %s\n",
evidence.ID, review.Accepted, review.ReasonCode, record.Replayed, review.Fingerprint)
return nil
}

func (a *App) doctorBrowserReadinessCommand(ctx context.Context, args []string) error {
flags := newFlagSet("doctor browser-readiness", a.errOut)
product := flags.String("product", string(browserruntime.BrowserProductEdge),
"accepted browser product: edge, chrome, or chromium")
jsonOutput := flags.Bool("json", false, "print the readiness receipt as JSON")
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 0 {
return errors.New("usage: cyberagent doctor browser-readiness --product <edge|chrome|chromium> [--json]")
}
selectedProduct, err := parseBrowserNetworkProbeProduct(*product)
if err != nil {
return err
}
identity, acceptance, err := acceptedBrowserNetworkProbeCandidate(selectedProduct)
if err != nil {
return err
}
if err := a.ensureStore(); err != nil {
return err
}
service := application.NewBrowserSafeWebRuntimeService(a.store)
readiness, err := service.Readiness(ctx, identity, acceptance)
if err != nil {
return err
}
if *jsonOutput {
encoder := json.NewEncoder(a.out)
encoder.SetIndent("", " ")
return encoder.Encode(readiness)
}
fmt.Fprintf(a.out, "protocol: %s\nready: %t\nblocking_reason: %s\nadapter: %s\npolicy: %s\nexpires_at: %s\n",
readiness.ProtocolVersion, readiness.Ready, valueOrUnknown(readiness.BlockingReason),
readiness.Adapter, readiness.PolicyVersion, readiness.ExpiresAt.Format(time.RFC3339))
return nil
}

func parseBrowserNetworkProbeProduct(raw string) (browserruntime.BrowserProduct, error) {
product := browserruntime.BrowserProduct(strings.ToLower(strings.TrimSpace(raw)))
switch product {
Expand Down
10 changes: 10 additions & 0 deletions internal/app/doctor_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ func TestDoctorBrowserNetworkProbeFailsBeforeDiscoveryWithoutExactConfirmation(t
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}

func TestDoctorBrowserReadinessRejectsUnknownProduct(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute([]string{"doctor", "browser-readiness", "--product", "firefox"},
&stdout, &stderr)
if code == 0 || stdout.Len() != 0 ||
!strings.Contains(stderr.String(), "edge, chrome, or chromium") {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
}
Loading