diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 301f2cb1..74db9e03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,9 +201,12 @@ jobs: scripts/test-rule-pack-policy.sh scripts/test-swift-warnings.sh scripts/test-codeql-gate.sh + scripts/test-update-cask.sh python3 -I scripts/test_classify_changes.py python3 -I scripts/test_check_site.py - python3 scripts/test_release_checks.py + python3 -I scripts/test_release_checks.py + python3 -I scripts/test_release_artifact.py + python3 -I scripts/test_release_workflow.py # Ad-hoc signing: the entitlements that need a Developer ID profile are only exercised by the release build. - name: Test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 990160c3..293399ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,112 +1,120 @@ name: Release -# A version-tag push builds, signs, notarizes and verifies without publishing. After that dry run passes, dispatch -# this workflow for the existing tag with dry_run=false to publish it. The secrets this needs are listed in -# docs/DEVELOPMENT.md, along with what it means to keep a signing identity in -# GitHub. Without them the job fails at the import step rather than publishing something unsigned. -# -# It runs without waiting for an approval. What gates it instead: the release environment only accepts v* tags, -# only the accounts in RELEASE_ACTORS may start one, and a v* tag can't be moved once pushed. +# A version-tag push prepares the only signed bytes this release may publish. It builds, signs, notarizes, +# verifies, attests, and retains one immutable Actions artifact. A later explicit dispatch names that successful +# preparation run and publishes those exact bytes; the publication job cannot access signing credentials. on: push: tags: ["v*"] workflow_dispatch: inputs: - tag: - description: "Existing tag to build (e.g. v0.2.3). Leave empty to build the current ref." - required: false - dry_run: - description: "Build, sign, notarize and verify — but publish nothing." - type: boolean - default: false + prepare_run_id: + description: "Successful tag-push preparation run whose exact bytes should be published" + required: true + type: string -permissions: - contents: write - checks: read - pull-requests: read +permissions: {} concurrency: - group: release-${{ github.ref }} + group: release-${{ github.ref_name }} cancel-in-progress: false jobs: - release: + prepare: + name: Prepare signed release + if: ${{ github.event_name == 'push' }} runs-on: macos-15 timeout-minutes: 90 - # Gated: the signing secrets live in this environment, it takes a human approval to start, and its policy only - # allows v* tags. A push to a branch — or a workflow someone adds to one — can't reach the certificates. environment: release + permissions: + contents: read + checks: read + issues: read + pull-requests: read + actions: read + id-token: write + attestations: write env: TEAM_ID: ${{ secrets.TEAM_ID }} APP_PROFILE: ${{ vars.APP_PROFILE || 'Flowlight Developer ID' }} EXT_PROFILE: ${{ vars.EXT_PROFILE || 'Flowlight Extension Developer ID' }} - # Job-level so the cask step can ask whether it was provided: the secrets context isn't readable from `if`. - TAP_TOKEN: ${{ secrets.TAP_TOKEN }} steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: - ref: ${{ github.event.inputs.tag || github.ref }} + ref: ${{ github.ref }} fetch-depth: 0 - # The environment no longer asks for a click, so this is what stands in its place: a release may only be - # started by an account on this list. Anyone else pushing a v* tag gets a failed run rather than a signed - # build. Set RELEASE_ACTORS (comma-separated) to change who that is. - name: Check who asked for this release env: ALLOWED: ${{ vars.RELEASE_ACTORS || 'blessdyb' }} + EVENT_ACTOR: ${{ github.actor }} + TRIGGERING_ACTOR: ${{ github.triggering_actor }} run: | set -euo pipefail - actor="${{ github.actor }}" - if ! printf '%s' "$ALLOWED" | tr ',' '\n' | tr -d ' ' | grep -qx "$actor"; then - echo "::error::$actor isn't allowed to publish a release. Allowed: $ALLOWED" - exit 1 - fi - echo "Release requested by $actor." + allowed_list=",${ALLOWED// /}," + for candidate in "$EVENT_ACTOR" "$TRIGGERING_ACTOR"; do + if [[ "$allowed_list" != *",$candidate,"* ]]; then + echo "::error::$candidate isn't allowed to prepare or rerun a release. Allowed: $ALLOWED" + exit 1 + fi + done + echo "Release preparation requested by $EVENT_ACTOR and triggered by $TRIGGERING_ACTOR." - name: Read the version and check it matches the tag id: version + env: + EVENT_TAG: ${{ github.ref_name }} + EVENT_SHA: ${{ github.sha }} run: | set -euo pipefail VERSION=$(sed -n 's/.*MARKETING_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml) BUILD=$(sed -n 's/.*CURRENT_PROJECT_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml) - TAG="${{ github.event.inputs.tag || github.ref_name }}" + TAG="$EVENT_TAG" [[ -n "$VERSION" && -n "$BUILD" ]] || { echo "::error::Missing version metadata in project.yml."; exit 1; } + [[ "$TAG" == "v$VERSION" ]] || { echo "::error::Tag $TAG doesn't match MARKETING_VERSION $VERSION."; exit 1; } + [[ "$(git rev-parse "refs/tags/$TAG^{}")" == "$EVENT_SHA" ]] || { + echo "::error::The tag does not peel to the workflow source SHA." + exit 1 + } echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "build=$BUILD" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" - if [[ "$TAG" != "v$VERSION" ]]; then - echo "::error::Tag $TAG doesn't match MARKETING_VERSION $VERSION in project.yml." - exit 1 - fi - # The release runs at the immutable tag after its pull request gates have passed. Re-running the same test suite - # here used another macOS runner for no new evidence. Resolve the checked-out tag to its commit instead, then - # require the latest trusted CI and CodeQL results plus an open or merged main-targeting pull request at that SHA. - name: Require reviewed checks for the exact release commit env: GH_TOKEN: ${{ github.token }} + RELEASE_REVIEWERS: ${{ vars.RELEASE_REVIEWERS || 'blessdyb' }} run: | set -euo pipefail RELEASE_SHA=$(git rev-parse HEAD) CHECKS="$RUNNER_TEMP/release-checks.json" PULLS="$RUNNER_TEMP/release-pulls.json" + RUNS="$RUNNER_TEMP/release-workflow-runs.json" + COMMENTS="$RUNNER_TEMP/release-review-comments.json" + EVIDENCE="$RUNNER_TEMP/review-evidence.json" gh api --paginate --slurp \ - "repos/${{ github.repository }}/commits/$RELEASE_SHA/check-runs?per_page=100" > "$CHECKS" + "repos/$GITHUB_REPOSITORY/commits/$RELEASE_SHA/check-runs?per_page=100" > "$CHECKS" gh api --paginate --slurp -H 'Accept: application/vnd.github+json' \ - "repos/${{ github.repository }}/commits/$RELEASE_SHA/pulls?per_page=100" > "$PULLS" + "repos/$GITHUB_REPOSITORY/commits/$RELEASE_SHA/pulls?per_page=100" > "$PULLS" + gh api --paginate --slurp \ + "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$RELEASE_SHA&event=pull_request&per_page=100" > "$RUNS" + gh api --paginate --slurp -H 'Accept: application/vnd.github+json' \ + "repos/$GITHUB_REPOSITORY/issues/comments?per_page=100" > "$COMMENTS" python3 -I scripts/check_release_checks.py \ - --repo "${{ github.repository }}" --sha "$RELEASE_SHA" --checks "$CHECKS" --pulls "$PULLS" + --repo "$GITHUB_REPOSITORY" --sha "$RELEASE_SHA" \ + --checks "$CHECKS" --pulls "$PULLS" --runs "$RUNS" --comments "$COMMENTS" \ + --reviewers "$RELEASE_REVIEWERS" --evidence-output "$EVIDENCE" - # The newest Xcode on the image, named in the log so a release says which compiler built it. - name: Select Xcode run: | set -euo pipefail sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)" xcodebuild -version - # The committed project is the exact graph reviewed by CI and selected by Xcode Cloud. Verify its release - # metadata before consuming it so a stale project can never publish under a newer project.yml/tag. - name: Check committed project version + env: + EXPECTED_VERSION: ${{ steps.version.outputs.version }} + EXPECTED_BUILD: ${{ steps.version.outputs.build }} run: | set -euo pipefail for target in Flowlight FlowlightAppStore; do @@ -114,13 +122,12 @@ jobs: xcodebuild -project Flowlight.xcodeproj -target "$target" -configuration Release -showBuildSettings > "$settings" project_version=$(sed -n 's/^[[:space:]]*MARKETING_VERSION = //p' "$settings") project_build=$(sed -n 's/^[[:space:]]*CURRENT_PROJECT_VERSION = //p' "$settings") - if [[ "$project_version" != "${{ steps.version.outputs.version }}" || "$project_build" != "${{ steps.version.outputs.build }}" ]]; then - echo "::error::$target committed project is $project_version/$project_build; project.yml is ${{ steps.version.outputs.version }}/${{ steps.version.outputs.build }}." + if [[ "$project_version" != "$EXPECTED_VERSION" || "$project_build" != "$EXPECTED_BUILD" ]]; then + echo "::error::$target committed project is $project_version/$project_build; project.yml is $EXPECTED_VERSION/$EXPECTED_BUILD." exit 1 fi done - # A keychain of its own, unlocked only for this job, so nothing lands in the runner's login keychain. - name: Import the signing certificates env: APP_CERTIFICATE_P12: ${{ secrets.APP_CERTIFICATE_P12 }} @@ -142,7 +149,6 @@ jobs: -T /usr/bin/codesign -T /usr/bin/productsign -T /usr/bin/productbuild rm -f "$RUNNER_TEMP/$name.p12" done - # Lets codesign use the keys without a UI prompt the runner could never answer. security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null security list-keychains -d user -s "$KEYCHAIN" login.keychain-db security find-identity -v -p codesigning "$KEYCHAIN" @@ -153,7 +159,6 @@ jobs: EXT_PROVISIONING_PROFILE: ${{ secrets.EXT_PROVISIONING_PROFILE }} run: | set -euo pipefail - # Xcode 16 reads this directory; the older location is written too, in case a tool still looks there. for dir in "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" "$HOME/Library/MobileDevice/Provisioning Profiles"; do mkdir -p "$dir" printf '%s' "$APP_PROVISIONING_PROFILE" | base64 --decode > "$dir/flowlight-app.provisionprofile" @@ -162,7 +167,7 @@ jobs: security cms -D -i "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/flowlight-app.provisionprofile" \ | plutil -extract Name raw - - - name: Build, sign and notarize + - name: Build, sign and notarize exactly once env: DMG_SIGN_IDENTITY: ${{ secrets.DMG_SIGN_IDENTITY }} INSTALLER_IDENTITY: ${{ secrets.INSTALLER_IDENTITY }} @@ -174,11 +179,9 @@ jobs: NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }} run: | set -euo pipefail - # Without these the scripts quietly produce an unsigned build, which is worse than failing here. for name in TEAM_ID DMG_SIGN_IDENTITY INSTALLER_IDENTITY; do [[ -n "${!name:-}" ]] || { echo "::error::Secret $name is not set."; exit 1; } done - # Either notarization credential is fine; having neither is not. if [[ -n "${NOTARY_KEY_P8:-}" && -n "${NOTARY_KEY_ID:-}" && -n "${NOTARY_ISSUER:-}" ]]; then export NOTARY_KEY="$RUNNER_TEMP/notary.p8" printf '%s' "$NOTARY_KEY_P8" | base64 --decode > "$NOTARY_KEY" @@ -186,107 +189,413 @@ jobs: elif [[ -n "${NOTARY_APPLE_ID:-}" && -n "${NOTARY_TEAM_ID:-}" && -n "${NOTARY_PASSWORD:-}" ]]; then echo "Notarizing with an Apple ID and app-specific password." else - echo "::error::Set NOTARY_KEY_P8 + NOTARY_KEY_ID + NOTARY_ISSUER, or NOTARY_APPLE_ID + NOTARY_TEAM_ID + NOTARY_PASSWORD." + echo "::error::Set an App Store Connect notarization key or the complete Apple ID credential set." exit 1 fi - # build-dmg.sh runs build-signed.sh itself; build-pkg.sh then reuses build/export/Flowlight.app. scripts/build-dmg.sh REUSE_EXPORT=1 scripts/build-pkg.sh rm -f "${NOTARY_KEY:-}" - - name: Check Gatekeeper accepts what was built + - name: Verify the final signed products + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" + VERSION="$RELEASE_VERSION" + PKG="build/Flowlight-$VERSION.pkg" + [[ -f build/Flowlight.dmg && -f "$PKG" ]] || { echo "::error::Expected DMG or PKG is missing."; exit 1; } + [[ "$(find build -maxdepth 1 -type f -name 'Flowlight*.dmg' | wc -l | tr -d ' ')" == 1 ]] || { + echo "::error::Expected exactly one Flowlight DMG." + exit 1 + } + [[ "$(find build -maxdepth 1 -type f -name 'Flowlight*.pkg' | wc -l | tr -d ' ')" == 1 ]] || { + echo "::error::Expected exactly one Flowlight PKG." + exit 1 + } spctl -a -vvv -t open --context context:primary-signature build/Flowlight.dmg - spctl -a -vvv -t install "build/Flowlight-$VERSION.pkg" - hdiutil attach -nobrowse -quiet build/Flowlight.dmg - spctl -a -vvv /Volumes/Flowlight/Flowlight.app - hdiutil detach /Volumes/Flowlight -quiet - # Plain file names, because this file is published as an asset and the updater looks up the disk - # image by name. `cd build` rather than `sed` so the names in the file are the names on the release. - ( cd build && shasum -a 256 Flowlight.dmg "Flowlight-$VERSION.pkg" | tee SHA256SUMS.txt ) + spctl -a -vvv -t install "$PKG" + xcrun stapler validate build/Flowlight.dmg + xcrun stapler validate "$PKG" + MOUNT="$RUNNER_TEMP/flowlight-release-mount" + mkdir "$MOUNT" + hdiutil attach -nobrowse -readonly -mountpoint "$MOUNT" build/Flowlight.dmg + codesign --verify --deep --strict --verbose=2 "$MOUNT/Flowlight.app" + spctl -a -vvv "$MOUNT/Flowlight.app" + xcrun stapler validate "$MOUNT/Flowlight.app" + hdiutil detach "$MOUNT" -quiet + ( + cd build + shasum -a 256 Flowlight.dmg "Flowlight-$VERSION.pkg" > SHA256SUMS.txt + ) - - name: Publish the release - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }} + - name: Prepare release notes and provenance manifest env: GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + RELEASE_BUILD: ${{ steps.version.outputs.build }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} run: | set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.tag }}" + VERSION="$RELEASE_VERSION" NOTES="packaging/release-notes/$VERSION.md" - ARGS=(--title "Flowlight $VERSION") - # A hand-written file wins; otherwise the notes are built from the entry already written on the - # releases page. Falling through to --generate-notes produces one "Full Changelog" link, which is what - # the Software Update window showed for every release from 0.6.5 to 0.7.1. - if [[ ! -f "$NOTES" ]] && python3 scripts/release_notes.py "$VERSION" > "$RUNNER_TEMP/from-site.md"; then - NOTES="$RUNNER_TEMP/from-site.md" - fi if [[ -f "$NOTES" ]]; then - # The checksums belong in the notes, and they're only known once the artifacts exist. - { cat "$NOTES"; echo; echo '### Checksums'; echo; echo '```'; cat build/SHA256SUMS.txt; echo '```'; } > "$RUNNER_TEMP/notes.md" - ARGS+=(--notes-file "$RUNNER_TEMP/notes.md") + cp "$NOTES" "$RUNNER_TEMP/base-notes.md" + elif python3 -I scripts/release_notes.py "$VERSION" > "$RUNNER_TEMP/base-notes.md"; then + : else - echo "::error::No notes for $VERSION — neither $NOTES nor an entry on the releases page." + echo "::error::No release notes for $VERSION in packaging or the generated site." exit 1 fi - # SHA256SUMS.txt is an asset, not just a paragraph in the notes. The updater refuses a download it - # cannot verify, and until 0.9.0 this file existed only inside the release body — where nothing can - # read it — so the verification was decorative and then, once it started failing closed, fatal. - gh release create "$TAG" "${ARGS[@]}" build/Flowlight.dmg "build/Flowlight-$VERSION.pkg" build/SHA256SUMS.txt + { + cat "$RUNNER_TEMP/base-notes.md" + echo + echo '### Checksums' + echo + echo '```' + cat build/SHA256SUMS.txt + echo '```' + } > build/RELEASE-NOTES.md + WORKFLOW_ID=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/release.yml" --jq .id) + python3 -I scripts/release_artifact.py create-manifest \ + --bundle build \ + --repository "$GITHUB_REPOSITORY" \ + --tag "$RELEASE_TAG" \ + --source-sha "$GITHUB_SHA" \ + --source-tree "$(git rev-parse 'HEAD^{tree}')" \ + --version "$VERSION" \ + --build "$RELEASE_BUILD" \ + --workflow-id "$WORKFLOW_ID" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" \ + --evidence "$RUNNER_TEMP/review-evidence.json" + python3 -I scripts/release_artifact.py verify-bundle \ + --bundle build \ + --repository "$GITHUB_REPOSITORY" \ + --tag "$RELEASE_TAG" \ + --source-sha "$GITHUB_SHA" \ + --version "$VERSION" \ + --build "$RELEASE_BUILD" \ + --workflow-id "$WORKFLOW_ID" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" - # The updater refuses a download whose checksum it can't find, so a release without this file is one - # nobody can install from inside the app. That is exactly what shipped from the beginning until 0.9.0: - # the checksums were written into the release body, where nothing can read them, and the missing asset - # was invisible because verification used to pass when it found nothing. Checked against the published - # release rather than the build directory, because what matters is what people can actually download. - - name: The release must carry the checksums the updater looks for - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }} + - name: Attest the prepared files + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.0.0 + with: + subject-path: | + build/Flowlight.dmg + build/Flowlight-${{ steps.version.outputs.version }}.pkg + build/SHA256SUMS.txt + build/RELEASE-NOTES.md + build/RELEASE-MANIFEST.json + + - name: Preserve the immutable prepared release + id: artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: flowlight-release-${{ steps.version.outputs.tag }}-${{ github.run_id }} + path: | + build/Flowlight.dmg + build/Flowlight-${{ steps.version.outputs.version }}.pkg + build/SHA256SUMS.txt + build/RELEASE-NOTES.md + build/RELEASE-MANIFEST.json + if-no-files-found: error + retention-days: 90 + compression-level: 0 + + - name: Record the publication handoff env: - GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + ARTIFACT_ID: ${{ steps.artifact.outputs.artifact-id }} + ARTIFACT_DIGEST: ${{ steps.artifact.outputs.artifact-digest }} run: | set -euo pipefail - TAG="${{ steps.version.outputs.tag }}" - names=$(gh release view "$TAG" --json assets -q '.assets[].name') - echo "$names" - for required in SHA256SUMS.txt Flowlight.dmg; do - grep -qx "$required" <<< "$names" || { - echo "::error::$TAG has no $required. The in-app updater looks for it by name and refuses a" - echo "::error::download it cannot verify, so this release would be uninstallable from the app." + { + echo "### Signed release prepared once" + echo + echo "- Tag: \`$RELEASE_TAG\`" + echo "- Source: \`$GITHUB_SHA\`" + echo "- Preparation run: \`$GITHUB_RUN_ID\` (attempt $GITHUB_RUN_ATTEMPT)" + echo "- Artifact ID: \`$ARTIFACT_ID\`" + echo "- Artifact archive digest: \`$ARTIFACT_DIGEST\`" + echo + echo '```sh' + echo "gh workflow run Release --ref $RELEASE_TAG -f prepare_run_id=$GITHUB_RUN_ID" + echo '```' + echo + cat build/SHA256SUMS.txt + } >> "$GITHUB_STEP_SUMMARY" + + publish: + name: Publish prepared release + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + environment: release-publish + permissions: + actions: read + attestations: read + contents: write + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + persist-credentials: false + + - name: Check publication identity and exact tag + id: version + env: + ALLOWED: ${{ vars.RELEASE_ACTORS || 'blessdyb' }} + EVENT_ACTOR: ${{ github.actor }} + TRIGGERING_ACTOR: ${{ github.triggering_actor }} + EVENT_REF: ${{ github.ref }} + EVENT_TAG: ${{ github.ref_name }} + EVENT_SHA: ${{ github.sha }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + run: | + set -euo pipefail + allowed_list=",${ALLOWED// /}," + for candidate in "$EVENT_ACTOR" "$TRIGGERING_ACTOR"; do + if [[ "$allowed_list" != *",$candidate,"* ]]; then + echo "::error::$candidate isn't allowed to publish or rerun a release. Allowed: $ALLOWED" exit 1 - } + fi done - # And the file has to name the disk image, not merely exist: the updater looks the name up inside it. - gh release download "$TAG" --pattern SHA256SUMS.txt --dir "$RUNNER_TEMP" --clobber - grep -q " Flowlight.dmg$" "$RUNNER_TEMP/SHA256SUMS.txt" || { - echo "::error::SHA256SUMS.txt has no line for Flowlight.dmg, so the updater can't verify it." + [[ "$EVENT_REF" == refs/tags/v* ]] || { echo "::error::Publication must be dispatched from a v* tag."; exit 1; } + [[ "$PREPARE_RUN_ID" =~ ^[0-9]+$ ]] || { echo "::error::prepare_run_id must contain decimal digits only."; exit 1; } + VERSION=$(sed -n 's/.*MARKETING_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml) + BUILD=$(sed -n 's/.*CURRENT_PROJECT_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml) + TAG="$EVENT_TAG" + RELEASE_SHA=$(git rev-parse HEAD) + [[ "$TAG" == "v$VERSION" ]] || { echo "::error::Tag $TAG doesn't match MARKETING_VERSION $VERSION."; exit 1; } + [[ "$RELEASE_SHA" == "$EVENT_SHA" ]] || { echo "::error::Checkout SHA differs from the dispatch SHA."; exit 1; } + [[ "$(git rev-parse "refs/tags/$TAG^{}")" == "$RELEASE_SHA" ]] || { echo "::error::The tag does not peel to the dispatch SHA."; exit 1; } + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "build=$BUILD" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT" + + - name: Verify the selected preparation run + id: prepared + env: + GH_TOKEN: ${{ github.token }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_SHA: ${{ steps.version.outputs.sha }} + run: | + set -euo pipefail + RUN_ID="$PREPARE_RUN_ID" + RUN_JSON="$RUNNER_TEMP/prepare-run.json" + ARTIFACTS_JSON="$RUNNER_TEMP/prepare-artifacts.json" + RESULT_JSON="$RUNNER_TEMP/prepare-result.json" + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID" > "$RUN_JSON" + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts?per_page=100" > "$ARTIFACTS_JSON" + WORKFLOW_ID=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/release.yml" --jq .id) + python3 -I scripts/release_artifact.py verify-run \ + --run-json "$RUN_JSON" \ + --artifacts-json "$ARTIFACTS_JSON" \ + --repository "$GITHUB_REPOSITORY" \ + --workflow-id "$WORKFLOW_ID" \ + --run-id "$RUN_ID" \ + --tag "$RELEASE_TAG" \ + --source-sha "$RELEASE_SHA" > "$RESULT_JSON" + echo "workflow_id=$WORKFLOW_ID" >> "$GITHUB_OUTPUT" + echo "artifact_id=$(jq -r .artifact_id "$RESULT_JSON")" >> "$GITHUB_OUTPUT" + echo "artifact_digest=$(jq -r .artifact_digest "$RESULT_JSON")" >> "$GITHUB_OUTPUT" + echo "run_attempt=$(jq -r .run_attempt "$RESULT_JSON")" >> "$GITHUB_OUTPUT" + + - name: Download and verify the immutable artifact archive + env: + GH_TOKEN: ${{ github.token }} + ARTIFACT_ID: ${{ steps.prepared.outputs.artifact_id }} + ARTIFACT_DIGEST: ${{ steps.prepared.outputs.artifact_digest }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + ARCHIVE="$RUNNER_TEMP/prepared-release.zip" + PREPARED="$RUNNER_TEMP/prepared-release" + gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID/zip" > "$ARCHIVE" + EXPECTED="$ARTIFACT_DIGEST" + ACTUAL="sha256:$(sha256sum "$ARCHIVE" | cut -d' ' -f1)" + [[ "$ACTUAL" == "$EXPECTED" ]] || { + echo "::error::Artifact archive digest is $ACTUAL, expected $EXPECTED." exit 1 } + python3 -I scripts/release_artifact.py extract-archive \ + --archive "$ARCHIVE" --destination "$PREPARED" --version "$RELEASE_VERSION" + + - name: Verify bundle identity and provenance attestations + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_SHA: ${{ steps.version.outputs.sha }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + RELEASE_BUILD: ${{ steps.version.outputs.build }} + WORKFLOW_ID: ${{ steps.prepared.outputs.workflow_id }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + PREPARE_RUN_ATTEMPT: ${{ steps.prepared.outputs.run_attempt }} + run: | + set -euo pipefail + PREPARED="$RUNNER_TEMP/prepared-release" + python3 -I scripts/release_artifact.py verify-bundle \ + --bundle "$PREPARED" \ + --repository "$GITHUB_REPOSITORY" \ + --tag "$RELEASE_TAG" \ + --source-sha "$RELEASE_SHA" \ + --version "$RELEASE_VERSION" \ + --build "$RELEASE_BUILD" \ + --workflow-id "$WORKFLOW_ID" \ + --run-id "$PREPARE_RUN_ID" \ + --run-attempt "$PREPARE_RUN_ATTEMPT" + for file in "$PREPARED"/*; do + gh attestation verify "$file" \ + --repo "$GITHUB_REPOSITORY" \ + --source-ref "refs/tags/$RELEASE_TAG" \ + --source-digest "$RELEASE_SHA" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" + done - # The cask points at the release that was just published, so this runs last. - - name: Update the Homebrew cask - if: ${{ env.TAP_TOKEN != '' && github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }} + - name: Create or resume the exact release candidate + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail - git clone "https://x-access-token:$TAP_TOKEN@github.com/xinbetween/homebrew-tap.git" "$RUNNER_TEMP/tap" + TAG="$RELEASE_TAG" + VERSION="$RELEASE_VERSION" + PREPARED="$RUNNER_TEMP/prepared-release" + if gh release view "$TAG" >/dev/null 2>&1; then + if [[ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" == true ]]; then + echo "Resuming the existing draft for $TAG." + gh release edit "$TAG" --title "Flowlight $VERSION" --notes-file "$PREPARED/RELEASE-NOTES.md" + gh release upload "$TAG" --clobber \ + "$PREPARED/Flowlight.dmg" \ + "$PREPARED/Flowlight-$VERSION.pkg" \ + "$PREPARED/SHA256SUMS.txt" \ + "$PREPARED/RELEASE-MANIFEST.json" + else + echo "A public release already exists for $TAG; it will be verified without mutation." + fi + else + gh release create "$TAG" \ + --verify-tag \ + --draft \ + --latest \ + --title "Flowlight $VERSION" \ + --notes-file "$PREPARED/RELEASE-NOTES.md" \ + "$PREPARED/Flowlight.dmg" \ + "$PREPARED/Flowlight-$VERSION.pkg" \ + "$PREPARED/SHA256SUMS.txt" \ + "$PREPARED/RELEASE-MANIFEST.json" + fi + + - name: Re-download and verify every release asset + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_SHA: ${{ steps.version.outputs.sha }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + RELEASE_BUILD: ${{ steps.version.outputs.build }} + WORKFLOW_ID: ${{ steps.prepared.outputs.workflow_id }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + PREPARE_RUN_ATTEMPT: ${{ steps.prepared.outputs.run_attempt }} + run: | + set -euo pipefail + TAG="$RELEASE_TAG" + VERSION="$RELEASE_VERSION" + PREPARED="$RUNNER_TEMP/prepared-release" + CANDIDATE="$RUNNER_TEMP/release-candidate" + mkdir "$CANDIDATE" + gh release view "$TAG" --json assets -q '.assets[].name' > "$RUNNER_TEMP/candidate-assets.txt" + sort "$RUNNER_TEMP/candidate-assets.txt" > "$RUNNER_TEMP/candidate-assets-sorted.txt" + { + echo "Flowlight-$VERSION.pkg" + echo "Flowlight.dmg" + echo "RELEASE-MANIFEST.json" + echo "SHA256SUMS.txt" + } > "$RUNNER_TEMP/expected-assets.txt" + cmp "$RUNNER_TEMP/expected-assets.txt" "$RUNNER_TEMP/candidate-assets-sorted.txt" + gh release download "$TAG" --dir "$CANDIDATE" + python3 -I scripts/release_artifact.py verify-bundle \ + --bundle "$CANDIDATE" \ + --public \ + --repository "$GITHUB_REPOSITORY" \ + --tag "$TAG" \ + --source-sha "$RELEASE_SHA" \ + --version "$VERSION" \ + --build "$RELEASE_BUILD" \ + --workflow-id "$WORKFLOW_ID" \ + --run-id "$PREPARE_RUN_ID" \ + --run-attempt "$PREPARE_RUN_ATTEMPT" + cmp "$PREPARED/Flowlight.dmg" "$CANDIDATE/Flowlight.dmg" + cmp "$PREPARED/Flowlight-$VERSION.pkg" "$CANDIDATE/Flowlight-$VERSION.pkg" + cmp "$PREPARED/SHA256SUMS.txt" "$CANDIDATE/SHA256SUMS.txt" + cmp "$PREPARED/RELEASE-MANIFEST.json" "$CANDIDATE/RELEASE-MANIFEST.json" + gh release view "$TAG" --json body --jq .body > "$RUNNER_TEMP/candidate-body.md" + python3 -I - "$PREPARED/RELEASE-NOTES.md" "$RUNNER_TEMP/candidate-body.md" <<'PY' + import pathlib, sys + expected, actual = (pathlib.Path(value).read_text().rstrip("\n") for value in sys.argv[1:]) + if actual != expected: + raise SystemExit("release body differs from the prepared release notes") + PY + + - name: Publish the verified draft + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + TAG="$RELEASE_TAG" + if [[ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" == true ]]; then + gh release edit "$TAG" --draft=false --latest + fi + [[ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" == false ]] || { + echo "::error::$TAG is still a draft after publication." + exit 1 + } + + - name: Update Homebrew from the verified DMG + env: + TAP_TOKEN: ${{ secrets.TAP_TOKEN }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + if [[ -z "$TAP_TOKEN" ]]; then + echo "TAP_TOKEN is not configured in release-publish; GitHub publication is complete and Homebrew must be updated manually." + exit 0 + fi + VERSION="$RELEASE_VERSION" + PREPARED="$RUNNER_TEMP/prepared-release" + mkdir build + cp "$PREPARED/Flowlight.dmg" build/Flowlight.dmg + export GH_TOKEN="$TAP_TOKEN" + gh auth setup-git + gh repo clone xinbetween/homebrew-tap "$RUNNER_TEMP/tap" git -C "$RUNNER_TEMP/tap" config user.name "github-actions[bot]" git -C "$RUNNER_TEMP/tap" config user.email "41898282+github-actions[bot]@users.noreply.github.com" - TAP="$RUNNER_TEMP/tap" scripts/update-cask.sh + VERSION="$VERSION" REQUIRE_LOCAL_DMG=1 TAP="$RUNNER_TEMP/tap" scripts/update-cask.sh + EXPECTED=$(jq -r '.assets[] | select(.name == "Flowlight.dmg") | .sha256' "$PREPARED/RELEASE-MANIFEST.json") + ACTUAL=$(sed -n 's/^ sha256 "\(.*\)"/\1/p' "$RUNNER_TEMP/tap/Casks/flowlight.rb") + [[ "$ACTUAL" == "$EXPECTED" ]] || { echo "::error::Homebrew checksum differs from the prepared manifest."; exit 1; } git -C "$RUNNER_TEMP/tap" push - - name: Say what a dry run produced - if: ${{ github.event_name == 'push' || github.event.inputs.dry_run == 'true' }} + - name: Record the published identity + env: + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_SHA: ${{ steps.version.outputs.sha }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + ARTIFACT_DIGEST: ${{ steps.prepared.outputs.artifact_digest }} run: | set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" { - echo "### Dry run: nothing was published" + echo "### Published the prepared release" echo - echo "Signed, notarized and accepted by Gatekeeper:" - echo '```' - cat build/SHA256SUMS.txt - echo '```' - echo "Dispatch this workflow for \`${{ steps.version.outputs.tag }}\` with **dry_run=false** to publish." + echo "- Tag: \`$RELEASE_TAG\`" + echo "- Source: \`$RELEASE_SHA\`" + echo "- Preparation run: \`$PREPARE_RUN_ID\`" + echo "- Prepared artifact digest: \`$ARTIFACT_DIGEST\`" + echo "- No build, signing, notarization, or Apple credential step ran during publication." } >> "$GITHUB_STEP_SUMMARY" - echo "Dry run complete — Flowlight $VERSION built and verified, nothing published." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b023153a..6a5f2f1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ Internals are documented in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). Every release follows this non-negotiable sequence: ```text -feature branch → release branch → reviewed green PR → immutable tag → signed dry run → publish and verify → merge to main +feature branch → release branch → reviewed green PR → immutable tag → signed preparation → publish exact bytes and verify → merge to main ``` `main` is what has shipped, not what is being built. Start each feature from it and rebase it onto it; never merge @@ -65,28 +65,32 @@ git merge --ff-only feature/relaunch-through-proxy git push -u origin release/0.13.4 gh pr create --base main --title "Flowlight 0.13.4" -# wait for review and every required PR check to pass +# after reviewing the final head, a login configured in RELEASE_REVIEWERS must post +# `Reviewed-Commit: ` as a PR comment; the PR author's editable body is not evidence +# wait for that authenticated receipt and every required PR check to pass git tag -a v0.13.4 -m "Flowlight 0.13.4" -git push origin v0.13.4 # automatically runs the signed dry run; it cannot publish -# inspect that successful signed/notarized/Gatekeeper run, then publish from the same tag: -gh workflow run Release --ref v0.13.4 -f tag=v0.13.4 -# verify the GitHub release, DMG, PKG and SHA256SUMS.txt before the final merge +git push origin v0.13.4 # prepares, attests and retains the only bytes publication may use +# inspect that successful preparation, then publish its exact artifact from the same tag: +gh workflow run Release --ref v0.13.4 -f prepare_run_id= +# verify the GitHub release, DMG, PKG, SHA256SUMS.txt and RELEASE-MANIFEST.json before the final merge gh pr merge --merge --delete-branch ``` -**Tags are immutable.** Never retag, force-push or move a version after it has been pushed. If a tag is wrong or a -release needs another change, bump the patch version and begin a new release branch and tag. +**Release-tag creation is restricted and tags are immutable.** Only the designated release account may create a +`v*` tag. Never retag, force-push, or move a version after it has been pushed. If a tag is wrong or a release needs +another change, bump the patch version and begin a new release branch and tag. **The PR precedes the tag, and the merge follows publication.** Review and green checks protect the exact tree that will be signed. Publishing before merging keeps the GitHub Pages site from announcing a download that does not exist: `docs/` is served from `main`, its Download button resolves to the latest GitHub release, CI requires `docs/` to match `site/`, and the site builder reads `MARKETING_VERSION`. -A tag push starts `release.yml` in dry-run-only mode; push-triggered runs cannot publish. It must prove tests, signing, -notarization and Gatekeeper verification before an explicit production dispatch for that immutable tag. After production -succeeds, verify the release page and downloaded DMG, PKG and -`SHA256SUMS.txt` checksums, then merge the release PR into `main`. +A tag push starts `release.yml` in preparation-only mode; push-triggered runs cannot publish. It verifies reviewed +exact-head evidence, builds/signs/notarizes once, passes Gatekeeper, records a manifest, attests every prepared file, and +retains one immutable Actions artifact. The explicit publication dispatch names that successful run, has no signing or +notarization credentials, and can only verify and publish those bytes. After it succeeds, verify the release page and +downloaded DMG, PKG, `SHA256SUMS.txt`, and `RELEASE-MANIFEST.json`, then merge the release PR into `main`. [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#release-branches) documents the signing inputs and workflow internals. diff --git a/Flowlight.xcodeproj/project.pbxproj b/Flowlight.xcodeproj/project.pbxproj index 085efa62..9da38ed0 100644 --- a/Flowlight.xcodeproj/project.pbxproj +++ b/Flowlight.xcodeproj/project.pbxproj @@ -2252,7 +2252,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -2267,7 +2267,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.17.3; + MARKETING_VERSION = 0.17.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2313,7 +2313,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 38RJUJHKZS; ENABLE_HARDENED_RUNTIME = YES; @@ -2328,7 +2328,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.17.3; + MARKETING_VERSION = 0.17.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2393,7 +2393,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -2414,7 +2414,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.17.3; + MARKETING_VERSION = 0.17.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/Flowlight/App/AppNavigation.swift b/Flowlight/App/AppNavigation.swift index fc5e1617..2fcdf3c7 100644 --- a/Flowlight/App/AppNavigation.swift +++ b/Flowlight/App/AppNavigation.swift @@ -24,7 +24,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .reports: return "worth-a-look" case .visualize: return "visualize" case .alerts: return "alerts" - case .rules: return "rules" + case .rules: return "policies" case .ask: return "ask" case .inspect: return "inspection" case .sources: return "sources" @@ -40,7 +40,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .reports: return "History" case .visualize: return "Connections" case .alerts: return "Alerts" - case .rules: return "Rules" + case .rules: return "Policies" case .ask: return "Ask" case .inspect: return "Inspect" case .sources: return "Sources" @@ -54,7 +54,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .reports: return L("History") case .visualize: return L("Connections") case .alerts: return L("Alerts") - case .rules: return L("Rules") + case .rules: return L("Policies") case .ask: return L("Ask") case .inspect: return L("Inspect") case .sources: return L("Sources") @@ -143,8 +143,8 @@ enum SidebarSection: String, CaseIterable, Identifiable { } } -/// The three operational views behind the single Sources destination. Keeping this in shared navigation lets a -/// status notice open the exact recovery surface without putting three configuration screens back in the sidebar. +/// The three sections in the single Sources destination. The cases remain navigation state so restored windows, +/// screenshot arguments and links can scroll to the exact operational section without hiding the other two. enum SourcesTab: String, CaseIterable, Identifiable, Sendable { case collector, devices, coverage @@ -157,6 +157,19 @@ enum SourcesTab: String, CaseIterable, Identifiable, Sendable { case .coverage: return L("Coverage") } } + + var anchorID: String { "sources-\(rawValue)" } + + /// `devices` and `coverage` were once standalone screens, then visible tabs. Both old forms still restore to + /// the corresponding section; an explicit modern section always wins over the legacy screen value. + static func restored(explicitRawValue: String?, legacyScreenRawValue: String?) -> SourcesTab { + if let explicitRawValue, let explicit = SourcesTab(rawValue: explicitRawValue) { return explicit } + switch legacyScreenRawValue { + case "devices": return .devices + case "coverage": return .coverage + default: return .collector + } + } } /// Alert review and its detection tuning share a destination, but app-wide alert links must still be able to select @@ -168,20 +181,56 @@ enum AlertsTab: String, CaseIterable, Identifiable { @MainActor var title: String { self == .alerts ? L("Alerts") : L("Detection") } } -/// The three views inside Rules. This lives with the shared navigation state so an app-wide notice can take someone -/// straight to a changed pack, while ordinary rule links always land back on the rule list. +/// The two views inside Policies. The raw `rules` value is deliberately retained: restored windows and preferences +/// written before the product rename must keep landing on the inventory rather than silently resetting. enum RulesTab: String, CaseIterable, Identifiable { - case rules, packs, activity + case rules, activity var id: String { rawValue } @MainActor var title: String { switch self { - case .rules: return L("Rules") - case .packs: return L("Packs") - case .activity: return L("What they did") + case .rules: return L("Policies") + case .activity: return L("Activity") } } + + /// `packs` was a visible tab through 0.17.3. It now restores to the unified inventory, where managed packs have + /// their own section. Unknown future values take the same conservative path. + static func restored(from rawValue: String?) -> RulesTab { + rawValue == RulesTab.activity.rawValue ? .activity : .rules + } + + static func restoresManagedPackFocus(from rawValue: String?) -> Bool { + rawValue == "packs" + } +} + +enum AgentPolicySection: Equatable, Sendable { case allowlist, guardrails } + +struct AgentPolicyRequest: Equatable, Sendable { + var agentID: String + var section: AgentPolicySection + var id = UUID() +} + +enum InspectPolicySection: Equatable, Sendable { case mocks, requestRewrites, responseTransforms } + +/// A precise request into the policy inventory. It is intentionally source-shaped rather than carrying an editor: +/// each destination still owns its consent, validation, and save behavior. +enum PolicyDestination: Equatable, Sendable { + case rule(UUID) + case allowlist(String) + case managedPack(String?) + case guardrail(id: UUID, agentID: String) + case mock(UUID) + case requestRewrite(UUID) + case responseTransform(UUID) +} + +struct PolicyRequest: Equatable, Sendable { + var destination: PolicyDestination + var id = UUID() } /// A rule the user started writing somewhere else — a table row, an alert — waiting for the Rules screen to open @@ -215,22 +264,23 @@ final class AppNavigation: ObservableObject { }() @Published var sourcesTab: SourcesTab = { let defaults = UserDefaults.standard - if let explicit = SourcesTab(rawValue: defaults.string(forKey: "FLSourcesTab") ?? "") { - return explicit - } - switch defaults.string(forKey: "FLScreen") { - case "devices": return .devices - case "coverage": return .coverage - default: return .collector - } + return SourcesTab.restored(explicitRawValue: defaults.string(forKey: "FLSourcesTab"), + legacyScreenRawValue: defaults.string(forKey: "FLScreen")) }() + /// A separate pulse makes a repeated link to the already-selected section scroll again. + @Published private(set) var sourcesRequestID = UUID() @Published var alertsTab: AlertsTab = .alerts @Published var rulesTab: RulesTab = { - RulesTab(rawValue: UserDefaults.standard.string(forKey: "FLRulesTab") ?? "") ?? .rules + RulesTab.restored(from: UserDefaults.standard.string(forKey: "FLRulesTab")) }() @Published var reportRequest: ReportRequest? @Published var inspectRequest: InspectRequest? @Published var ruleRequest: RuleRequest? + @Published var policyRequest: PolicyRequest? = { + RulesTab.restoresManagedPackFocus(from: UserDefaults.standard.string(forKey: "FLRulesTab")) + ? PolicyRequest(destination: .managedPack(nil)) : nil + }() + @Published var agentPolicyRequest: AgentPolicyRequest? private struct TrailEntry { var item: SidebarItem @@ -289,12 +339,34 @@ final class AppNavigation: ObservableObject { func showSources(_ tab: SourcesTab = .collector) { sourcesTab = tab + sourcesRequestID = UUID() show(.sources) } func showPackReviews() { show(.rules) - rulesTab = .packs + policyRequest = PolicyRequest(destination: .managedPack(nil)) + } + + func showPolicyPack(_ packID: String) { + show(.rules) + policyRequest = PolicyRequest(destination: .managedPack(packID)) + } + + func showPolicy(_ destination: PolicyDestination) { + show(.rules) + policyRequest = PolicyRequest(destination: destination) + } + + func showAgentPolicy(agentID: String, section: AgentPolicySection) { + agentPolicyRequest = AgentPolicyRequest(agentID: agentID, section: section) + followLink(to: .agents) + } + + func showInspectionPolicy(_ section: InspectPolicySection) { + guard SidebarItem.availableCases.contains(.inspect) else { return } + inspectRequest = InspectRequest(search: "", policySection: section) + followLink(to: .inspect) } func showReport(filter: TrafficFilter, granularity: Granularity? = nil, @@ -378,5 +450,7 @@ struct InspectRequest: Equatable { var mcpServer: String? = nil /// Explicitly displayed and removable in Inspect; Reports uses it for the potential-harm drill-in. var potentialHarmOnly = false + /// Opens setup on the contextual editor that owns this kind of policy. + var policySection: InspectPolicySection? = nil var id = UUID() } diff --git a/Flowlight/Ask/AskProvider.swift b/Flowlight/Ask/AskProvider.swift index bb1c97d5..fd8f209d 100644 --- a/Flowlight/Ask/AskProvider.swift +++ b/Flowlight/Ask/AskProvider.swift @@ -103,7 +103,7 @@ struct AskRecordedCall: Identifiable, Equatable, Sendable { var chart: AskChart? /// Which app screen clicking should open when Reports cannot represent this result. var screen: SidebarItem? - /// The exact operational tab when that destination is Sources. + /// The exact section to focus when that destination is Sources. var sourcesTab: SourcesTab? = nil } diff --git a/Flowlight/Ask/AskQueryRunner.swift b/Flowlight/Ask/AskQueryRunner.swift index d7b0a804..530ebc64 100644 --- a/Flowlight/Ask/AskQueryRunner.swift +++ b/Flowlight/Ask/AskQueryRunner.swift @@ -22,7 +22,7 @@ enum AskQueryRunner { var chart: AskChart? /// Where the answer points for a question about the app rather than about traffic. var screen: SidebarItem? - /// The exact operational tab when that screen is Sources. + /// The exact section to focus when that screen is Sources. var sourcesTab: SourcesTab? = nil } diff --git a/Flowlight/Ask/FeatureGuide.swift b/Flowlight/Ask/FeatureGuide.swift index c0d7a7ac..97f4b054 100644 --- a/Flowlight/Ask/FeatureGuide.swift +++ b/Flowlight/Ask/FeatureGuide.swift @@ -19,7 +19,7 @@ struct FeatureGuide: Equatable, Sendable, Identifiable { var steps: [String] /// Where it lives, so an answer can offer to go there. var screen: SidebarItem? - /// The operational tab behind Sources, when the destination needs more precision than one sidebar item. + /// The section to focus in Sources, when the destination needs more precision than one sidebar item. var sourcesTab: SourcesTab? = nil /// What it can't do. Left out of an answer, these are the things people discover the hard way. var caveats: [String] = [] @@ -47,7 +47,7 @@ extension FeatureGuide { FeatureGuide( id: "capture", title: "Capture source", summary: "Where traffic comes from. The Network Extension is a content filter that sits in the data path and attributes every flow to its app; the nettop sampler needs no entitlement but samples once a second and misses flows shorter than that.", - steps: ["Open Sources and choose Collector.", "Pick Network Extension or nettop sampler.", + steps: ["Open Sources and go to Collector.", "Pick Network Extension or nettop sampler.", "For the extension, press Install & enable filter and approve it in System Settings."], screen: .sources, sourcesTab: .collector, @@ -58,7 +58,7 @@ extension FeatureGuide { FeatureGuide( id: "hostnames", title: "Hostnames", summary: "Turning IP addresses into names. The extension reads TLS SNI and DNS answers itself. On the sampler, a one-time packet-capture setup is what lets Flowlight see those names.", - steps: ["Open Sources and choose Collector.", "Turn on Read hostnames from the network.", "Give the helper permission when macOS asks."], + steps: ["Open Sources and go to Collector.", "Turn on Read hostnames from the network.", "Give the helper permission when macOS asks."], screen: .sources, sourcesTab: .collector, caveats: ["Without it the sampler falls back to reverse DNS, which is often missing or wrong."], @@ -99,14 +99,14 @@ extension FeatureGuide { keywords: ["allowlist", "allow", "whitelist", "block agent", "enforce"]), FeatureGuide( - id: "rules", title: "Rules", - summary: "Block or allow anything: an app, an agent, a destination, a URL, or a pairing of those — forever, until a time, until Flowlight quits, or between chosen hours. The most specific rule wins and a tie goes to the block.", - steps: ["Open Rules and press +, or right-click any row in Live, History, AI Agents or Inspect and choose Block.", - "Name an app or a destination, pick block or allow, and choose how long it lasts.", - "Pause Blocking in the toolbar stands every rule down for a while."], + id: "rules", title: "Policies", + summary: "See ordinary rules, agent allowlists, managed packs and inspection policies together, with a truthful state and activity trail for each. Ordinary rules can block or allow an app, agent, destination, URL or pairing for a chosen duration.", + steps: ["Open Policies and press + to write an ordinary rule, or right-click a row in Live, History, AI Agents or Inspect and choose Block.", + "Select any existing policy to open the contextual editor that owns it.", + "Use Activity to review decisions, and Pause Blocking when you need a temporary escape hatch."], screen: .rules, - caveats: ["A rule that names a path can only be carried out by HTTPS inspection; one that names a host needs the Network Extension. A rule the current setup can't enforce says so in the list."], - keywords: ["block", "refuse", "deny", "firewall", "stop app", "allow once", "pause"]), + caveats: ["A URL rule needs HTTPS inspection; a host rule needs the Network Extension. Policies distinguishes enforcing, reporting, watching, waiting and unavailable controls rather than calling every enabled item active."], + keywords: ["policy", "rules", "allowlist", "managed pack", "block", "refuse", "deny", "firewall", "stop app", "allow once", "pause"]), FeatureGuide( id: "guardrails", title: "Agent guardrails", @@ -176,7 +176,7 @@ extension FeatureGuide { FeatureGuide( id: "devices", title: "Devices: camera, Bluetooth and USB", summary: "Whether each camera is active, which Bluetooth and USB devices are paired or attached, and when that changed. Every channel is off until you turn it on.", - steps: ["Open Sources and choose Devices.", "Turn on Watch camera activity, Watch Bluetooth, or Watch USB and external storage."], + steps: ["Open Sources and go to Devices.", "Turn on Watch camera activity, Watch Bluetooth, or Watch USB and external storage."], screen: .sources, sourcesTab: .devices, caveats: ["Camera activity names the camera, not the app using it; Flowlight never reads frames or requests camera access.", @@ -223,7 +223,7 @@ extension FeatureGuide { guard guide.id == "capture" else { return guide } var storeGuide = guide storeGuide.summary = "Where traffic comes from. The Mac App Store build uses Flowlight's content-filter extension to attribute every flow to its app." - storeGuide.steps = ["Open Sources and choose Collector.", "Under Content filter, press Enable Filter and approve it in System Settings."] + storeGuide.steps = ["Open Sources and go to Collector.", "Under Content filter, press Enable Filter and approve it in System Settings."] storeGuide.caveats = ["macOS runs one content filter at a time. If a VPN or a security agent holds that slot, Flowlight cannot receive traffic."] storeGuide.keywords = ["network extension", "content filter", "source", "no traffic", "nothing showing"] return storeGuide diff --git a/Flowlight/Inspection/InspectionProxy.swift b/Flowlight/Inspection/InspectionProxy.swift index b46e3d7e..e1f17583 100644 --- a/Flowlight/Inspection/InspectionProxy.swift +++ b/Flowlight/Inspection/InspectionProxy.swift @@ -32,6 +32,9 @@ protocol ProxyObserver: AnyObject { /// The request whose bytes come next left with something taken out of it — a guardrail removing tools an /// agent is not allowed to use. What is recorded is what the server was sent, which is what happened. func flow(_ flow: ProxyFlow, guardedBy note: String) + /// A configured rewrite changed the request that left for the server. Kept separate from guardrail metadata so + /// Activity can say whether policy removed an agent capability or edited an HTTP request. + func flow(_ flow: ProxyFlow, rewrittenBy note: String) /// The upstream response bytes that come next were changed before the client received them. This is deliberately /// distinct from a mock (which never contacted the origin) and a guardrail (which changed the request). func flow(_ flow: ProxyFlow, responseTransformedBy note: String) @@ -414,7 +417,7 @@ final class InspectionProxy: @unchecked Sendable { observer?.flow(flow, clientSent: bytes) case .rewritten(let bytes, let note): // Recorded as what left, not as what arrived: Inspect shows the request the server was sent. - observer?.flow(flow, guardedBy: note) + observer?.flow(flow, rewrittenBy: note) observer?.flow(flow, clientSent: bytes) onward.append(bytes) case .answer(let rule, let bytes, let head): diff --git a/Flowlight/Inspection/InspectionRecorder.swift b/Flowlight/Inspection/InspectionRecorder.swift index 5067bd1c..10344df0 100644 --- a/Flowlight/Inspection/InspectionRecorder.swift +++ b/Flowlight/Inspection/InspectionRecorder.swift @@ -44,6 +44,9 @@ struct HTTPExchange: Identifiable, Equatable, Sendable { /// What a guardrail took out of this request before it left. Its own field for the same reason: the exchange /// is real and was really sent, but it is not quite what the agent wrote, and that has to be readable. var guardrail: String? + /// What a configured request rewrite changed before the request left. Earlier builds put this text in + /// `guardrail`; new recordings keep the two effects distinct without altering either enforcement path. + var requestRewrite: String? = nil /// The rule that changed a real upstream response before the client received it. A mock is intentionally not /// reused here: a transformed exchange really reached its origin. var responseTransform: String? = nil @@ -111,7 +114,15 @@ enum SocketOwner { /// them to the process (and agent) behind the connection, reads tool calls, redacts credentials, and hands each /// finished exchange to `onExchange`. final class InspectionRecorder: ProxyObserver, @unchecked Sendable { - private struct Pending { var head: HTTPHead; var body: HTTPBody; var started: Date; var mock: String?; var guardrail: String?; var responseTransform: String? } + private struct Pending { + var head: HTTPHead + var body: HTTPBody + var started: Date + var mock: String? + var guardrail: String? + var requestRewrite: String? + var responseTransform: String? + } private final class FlowState { let request: HTTPStreamParser @@ -120,6 +131,7 @@ final class InspectionRecorder: ProxyObserver, @unchecked Sendable { /// Set by the proxy just before the bytes that complete a request it answers itself. var nextMock: String? var nextGuardrail: String? + var nextRequestRewrite: String? /// Marked by the proxy immediately before the transformed response is fed here. The oldest pending request /// is the response being delivered, including on a pipelined keep-alive connection. var nextResponseTransform: String? @@ -162,9 +174,11 @@ final class InspectionRecorder: ProxyObserver, @unchecked Sendable { state.request.onHead = { response.requestMethods.append($0.method) } state.request.onMessage = { [weak state] head, body in state?.queue.append(Pending(head: head, body: body, started: Date(), mock: state?.nextMock, - guardrail: state?.nextGuardrail, responseTransform: nil)) + guardrail: state?.nextGuardrail, requestRewrite: state?.nextRequestRewrite, + responseTransform: nil)) state?.nextMock = nil state?.nextGuardrail = nil + state?.nextRequestRewrite = nil } state.response.onMessage = { [weak self, weak state] head, body in guard let self, let state, !state.queue.isEmpty else { return } @@ -195,6 +209,7 @@ final class InspectionRecorder: ProxyObserver, @unchecked Sendable { func flow(_ flow: ProxyFlow, serverSent data: Data) { flows[flow.id]?.response.feed(data) } func flow(_ flow: ProxyFlow, mockedBy rule: String) { flows[flow.id]?.nextMock = rule } func flow(_ flow: ProxyFlow, guardedBy note: String) { flows[flow.id]?.nextGuardrail = note } + func flow(_ flow: ProxyFlow, rewrittenBy note: String) { flows[flow.id]?.nextRequestRewrite = note } func flow(_ flow: ProxyFlow, responseTransformedBy note: String) { flows[flow.id]?.nextResponseTransform = note } func flowEnded(_ flow: ProxyFlow, note: String?) { @@ -204,7 +219,8 @@ final class InspectionRecorder: ProxyObserver, @unchecked Sendable { // Nothing was decrypted; record why, so the user sees which app and host were passed through. let head = HTTPHead(startLine: "CONNECT \(flow.host):\(flow.port) HTTP/1.1", headers: []) emit(flow: flow, state: state, - request: Pending(head: head, body: HTTPBody(), started: flow.started, mock: nil, guardrail: nil), + request: Pending(head: head, body: HTTPBody(), started: flow.started, mock: nil, guardrail: nil, + requestRewrite: nil, responseTransform: nil), responseHead: nil, responseBody: HTTPBody(), note: note) } } @@ -256,7 +272,8 @@ final class InspectionRecorder: ProxyObserver, @unchecked Sendable { contentType: request.head.value("Content-Type") ?? responseHead?.value("Content-Type") ?? "", pid: owner.pid, bundleID: owner.bundleID, appName: owner.appName, agent: owner.agent, agentName: owner.agentName, mcpServer: owner.mcpServer, toolCalls: calls, toolResults: results, mcp: mcp, llm: llm, note: notes.isEmpty ? nil : notes.joined(separator: " "), - mockRule: request.mock, guardrail: request.guardrail, responseTransform: request.responseTransform) + mockRule: request.mock, guardrail: request.guardrail, requestRewrite: request.requestRewrite, + responseTransform: request.responseTransform) onExchange(exchange) } } diff --git a/Flowlight/Localization/de.lproj/Localizable.strings b/Flowlight/Localization/de.lproj/Localizable.strings index b5a39be4..ae28b6de 100644 --- a/Flowlight/Localization/de.lproj/Localizable.strings +++ b/Flowlight/Localization/de.lproj/Localizable.strings @@ -752,8 +752,6 @@ "Seen as it happened, named, and readable." = "Beim Entstehen gesehen, benannt und lesbar."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Nicht überwacht"; "Read the device list again now (⌘R)" = "Geräteliste jetzt neu lesen (⌘R)"; "Watch Bluetooth" = "Bluetooth überwachen"; "Watch USB and external storage" = "USB und externe Speicher überwachen"; @@ -1234,13 +1232,8 @@ "Edit as normal rule…" = "Als normale Regel bearbeiten…"; /* UI/RulesView.swift */ -"Packs" = "Pakete"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "Regelentscheidungen und zusammengefasste Paketblockierungen erscheinen hier. Paketaktivitäten werden gruppiert, damit eine Seite voller Werbung nicht zu einer Seite voller Warnungen wird."; "Managed packs" = "Verwaltete Pakete"; -"What they did" = "Wirkung"; "Paused until %@" = "Pausiert bis %@"; -"%lld in force" = "%lld in Kraft"; -"%1$lld of %2$lld in force" = "%1$lld von %2$lld in Kraft"; "Templates" = "Vorlagen"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Regeln, die sich lohnen, nach Zweck gruppiert und mit den Hosts schon eingetragen"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Eine Regel schreiben: App, Agent, Ziel oder URL, blockiert oder erlaubt"; @@ -1254,8 +1247,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld Regeln werden beobachtet, aber nicht durchgesetzt. Nur die Netzwerkerweiterung sitzt im Datenpfad und kann eine Verbindung verweigern; der nettop-Sampler zählt Datenverkehr im Nachhinein. Wählen Sie unter „Quellen › Collector“ die Netzwerkerweiterung, um diese Regeln durchzusetzen."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "Eine Regel wird beobachtet, aber nicht durchgesetzt – eine Regel, die einen Pfad benennt, lässt sich nur mit HTTPS-Inspektion abgleichen, und die ist aus."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld Regeln werden beobachtet, aber nicht durchgesetzt – eine Regel, die einen Pfad benennt, lässt sich nur mit HTTPS-Inspektion abgleichen, und die ist aus."; -"In force" = "In Kraft"; -"Not in force" = "Nicht in Kraft"; "Nothing is being refused until %@." = "Bis %@ wird nichts verweigert."; "Resume now" = "Jetzt fortsetzen"; "No rules yet" = "Noch keine Regeln"; @@ -1265,7 +1256,6 @@ "Nothing yet" = "Noch nichts"; "Today" = "Heute"; "Yesterday" = "Gestern"; -"A rule since deleted" = "Eine inzwischen gelöschte Regel"; "Edit…" = "Bearbeiten…"; "Reset Count" = "Zähler zurücksetzen"; "Delete" = "Löschen"; @@ -1887,3 +1877,70 @@ "Restart or reinstall the filter to finish the update" = "Filter neu starten oder neu installieren, um die Aktualisierung abzuschließen"; "Return to the previous history scope" = "Zum vorherigen Verlaufsbereich zurückkehren"; "Selected" = "Ausgewählt"; + +/* 0.17.4 vereinheitlichte Richtlinien und Quellen */ +"Policies" = "Richtlinien"; +"Activity" = "Aktivität"; +"Choose the traffic source and verify that data is arriving." = "Wähle die Datenquelle aus und prüfe, ob Daten eintreffen."; +"Choose which local device channels Flowlight watches." = "Wähle aus, welche lokalen Gerätekanäle Flowlight überwacht."; +"Check what Flowlight can name, attribute, and inspect." = "Prüfe, was Flowlight benennen, zuordnen und inspizieren kann."; +"Network rules" = "Netzwerkregeln"; +"Agent allowlists" = "Agenten-Erlaubnislisten"; +"Agent guardrails" = "Schutzregeln für Agenten"; +"Request rewrites" = "Anfrageumschreibungen"; +"Response transforms" = "Antworttransformationen"; +"In effect" = "Wirksam"; +"Reporting only" = "Nur Meldungen"; +"Waiting" = "Wartet"; +"Unavailable" = "Nicht verfügbar"; +"Not applying" = "Wird nicht angewendet"; +"HTTPS inspection is off." = "Die HTTPS-Inspektion ist ausgeschaltet."; +"HTTPS inspection is not running." = "Die HTTPS-Inspektion läuft nicht."; +"This policy is switched off." = "Diese Richtlinie ist ausgeschaltet."; +"This rule is switched off." = "Diese Regel ist ausgeschaltet."; +"Network blocking is paused." = "Die Netzwerkblockierung ist pausiert."; +"This one-off rule has already been used." = "Diese einmalige Regel wurde bereits verwendet."; +"This rule is outside its scheduled time." = "Diese Regel liegt außerhalb ihrer geplanten Zeit."; +"Every destination" = "Jedes Ziel"; +"No listed destinations" = "Keine aufgeführten Ziele"; +"1 listed destination" = "1 aufgeführtes Ziel"; +"%lld listed destinations" = "%lld aufgeführte Ziele"; +"This allowlist is switched off." = "Diese Erlaubnisliste ist ausgeschaltet."; +"Unlisted destinations raise alerts but are not blocked." = "Nicht aufgeführte Ziele lösen Warnungen aus, werden aber nicht blockiert."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "Blockieren ist nicht verfügbar; nicht aufgeführte Ziele lösen weiterhin Warnungen aus."; +"Review the changed pack before it can keep applying." = "Prüfe das geänderte Paket, bevor es weiter angewendet werden kann."; +"This pack is switched off." = "Dieses Paket ist ausgeschaltet."; +"The pack is matching destinations but cannot block them here." = "Das Paket erkennt passende Ziele, kann sie hier aber nicht blockieren."; +"The pack is waiting for the active filter to apply it." = "Das Paket wartet darauf, vom aktiven Filter angewendet zu werden."; +"The managed-pack engine is not applying this configuration." = "Die Engine für verwaltete Pakete wendet diese Konfiguration nicht an."; +"No longer in the official catalog" = "Nicht mehr im offiziellen Katalog"; +"Blocking stopped automatically. Acknowledge this change after review." = "Die Blockierung wurde automatisch beendet. Bestätige diese Änderung nach der Prüfung."; +"Every agent" = "Jeder Agent"; +"A policy since deleted" = "Eine inzwischen gelöschte Richtlinie"; +"%@ · %lld connections" = "%@ · %lld Verbindungen"; +"%lld policies · none currently effective" = "%lld Richtlinien · derzeit keine wirksam"; +"%1$lld policies · %2$lld currently effective" = "%1$lld Richtlinien · %2$lld derzeit wirksam"; +"Everything Flowlight can apply" = "Alles, was Flowlight anwenden kann"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "Netzwerkentscheidungen, Agentenbeschränkungen, verwaltete Ziellisten und Richtlinien für die Entwicklerinspektion sind hier zusammengefasst. Jede Richtlinie öffnet sich weiterhin in dem Editor, der ihr Verhalten und ihre Einwilligung verwaltet."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "Netzwerkentscheidungen, Agentenbeschränkungen und verwaltete Ziellisten sind hier zusammengefasst. Jede Richtlinie öffnet sich weiterhin in dem Editor, der ihr Verhalten und ihre Einwilligung verwaltet."; +"%lld destinations · %@" = "%lld Ziele · %@"; +"every app" = "jede App"; +"Official · pack v%@ · %@" = "Offiziell · Paket v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · Signaturgeber %@"; +"Catalog" = "Katalog"; +"Sequence %1$lld · digest %2$@" = "Sequenz %1$lld · Prüfsumme %2$@"; +"Last verified" = "Zuletzt verifiziert"; +"Next check" = "Nächste Prüfung"; +"Official catalog details" = "Details zum offiziellen Katalog"; +"Not checked" = "Nicht geprüft"; +"Verified" = "Verifiziert"; +"Stale" = "Veraltet"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Blockierungen, Ausnahmen, Verstöße gegen Erlaubnislisten, Schutzregeln, Mock-Antworten sowie Änderungen an Anfragen oder Antworten erscheinen hier, ohne ihre ursprünglichen Einträge zu verändern."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Netzwerkblockierungen, Ausnahmen, Treffer verwalteter Pakete und Verstöße gegen Erlaubnislisten erscheinen hier, ohne ihre ursprünglichen Einträge zu verändern."; +"Blocked connection or request" = "Blockierte Verbindung oder Anfrage"; +"Blocked by a managed pack" = "Von einem verwalteten Paket blockiert"; +"Outside an agent allowlist" = "Außerhalb einer Agenten-Erlaubnisliste"; +"Capability removed by a guardrail" = "Funktion durch eine Schutzregel entfernt"; +"Answered by a mock" = "Mit einer Mock-Antwort beantwortet"; +"Request changed before sending" = "Anfrage vor dem Senden geändert"; +"Response changed before delivery" = "Antwort vor der Zustellung geändert"; diff --git a/Flowlight/Localization/en.lproj/Localizable.strings b/Flowlight/Localization/en.lproj/Localizable.strings index 89b0a0f2..24811f9f 100644 --- a/Flowlight/Localization/en.lproj/Localizable.strings +++ b/Flowlight/Localization/en.lproj/Localizable.strings @@ -724,8 +724,6 @@ "Seen as it happened, named, and readable." = "Seen as it happened, named, and readable."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Not watching"; "Read the device list again now (⌘R)" = "Read the device list again now (⌘R)"; "Watch Bluetooth" = "Watch Bluetooth"; "Watch USB and external storage" = "Watch USB and external storage"; @@ -1166,13 +1164,8 @@ "Edit as normal rule…" = "Edit as normal rule…"; /* UI/RulesView.swift */ -"Packs" = "Packs"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts."; "Managed packs" = "Managed packs"; -"What they did" = "What they did"; "Paused until %@" = "Paused until %@"; -"%lld in force" = "%lld in force"; -"%1$lld of %2$lld in force" = "%1$lld of %2$lld in force"; "Templates" = "Templates"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Rules worth having, grouped by what they are for, with the hosts already written out"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Write a rule: an app, an agent, a destination or a URL, blocked or allowed"; @@ -1186,8 +1179,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off."; -"In force" = "In force"; -"Not in force" = "Not in force"; "Nothing is being refused until %@." = "Nothing is being refused until %@."; "Resume now" = "Resume now"; "No rules yet" = "No rules yet"; @@ -1197,7 +1188,6 @@ "Nothing yet" = "Nothing yet"; "Today" = "Today"; "Yesterday" = "Yesterday"; -"A rule since deleted" = "A rule since deleted"; "Edit…" = "Edit…"; "Reset Count" = "Reset Count"; "Delete" = "Delete"; @@ -1886,3 +1876,70 @@ "Restart or reinstall the filter to finish the update" = "Restart or reinstall the filter to finish the update"; "Return to the previous history scope" = "Return to the previous history scope"; "Selected" = "Selected"; + +/* 0.17.4 unified policies and sources */ +"Policies" = "Policies"; +"Activity" = "Activity"; +"Choose the traffic source and verify that data is arriving." = "Choose the traffic source and verify that data is arriving."; +"Choose which local device channels Flowlight watches." = "Choose which local device channels Flowlight watches."; +"Check what Flowlight can name, attribute, and inspect." = "Check what Flowlight can name, attribute, and inspect."; +"Network rules" = "Network rules"; +"Agent allowlists" = "Agent allowlists"; +"Agent guardrails" = "Agent guardrails"; +"Request rewrites" = "Request rewrites"; +"Response transforms" = "Response transforms"; +"In effect" = "In effect"; +"Reporting only" = "Reporting only"; +"Waiting" = "Waiting"; +"Unavailable" = "Unavailable"; +"Not applying" = "Not applying"; +"HTTPS inspection is off." = "HTTPS inspection is off."; +"HTTPS inspection is not running." = "HTTPS inspection is not running."; +"This policy is switched off." = "This policy is switched off."; +"This rule is switched off." = "This rule is switched off."; +"Network blocking is paused." = "Network blocking is paused."; +"This one-off rule has already been used." = "This one-off rule has already been used."; +"This rule is outside its scheduled time." = "This rule is outside its scheduled time."; +"Every destination" = "Every destination"; +"No listed destinations" = "No listed destinations"; +"1 listed destination" = "1 listed destination"; +"%lld listed destinations" = "%lld listed destinations"; +"This allowlist is switched off." = "This allowlist is switched off."; +"Unlisted destinations raise alerts but are not blocked." = "Unlisted destinations raise alerts but are not blocked."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "Blocking is unavailable; unlisted destinations still raise alerts."; +"Review the changed pack before it can keep applying." = "Review the changed pack before it can keep applying."; +"This pack is switched off." = "This pack is switched off."; +"The pack is matching destinations but cannot block them here." = "The pack is matching destinations but cannot block them here."; +"The pack is waiting for the active filter to apply it." = "The pack is waiting for the active filter to apply it."; +"The managed-pack engine is not applying this configuration." = "The managed-pack engine is not applying this configuration."; +"No longer in the official catalog" = "No longer in the official catalog"; +"Blocking stopped automatically. Acknowledge this change after review." = "Blocking stopped automatically. Acknowledge this change after review."; +"Every agent" = "Every agent"; +"A policy since deleted" = "A policy since deleted"; +"%@ · %lld connections" = "%@ · %lld connections"; +"%lld policies · none currently effective" = "%lld policies · none currently effective"; +"%1$lld policies · %2$lld currently effective" = "%1$lld policies · %2$lld currently effective"; +"Everything Flowlight can apply" = "Everything Flowlight can apply"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent."; +"%lld destinations · %@" = "%lld destinations · %@"; +"every app" = "every app"; +"Official · pack v%@ · %@" = "Official · pack v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · signer %@"; +"Catalog" = "Catalog"; +"Sequence %1$lld · digest %2$@" = "Sequence %1$lld · digest %2$@"; +"Last verified" = "Last verified"; +"Next check" = "Next check"; +"Official catalog details" = "Official catalog details"; +"Not checked" = "Not checked"; +"Verified" = "Verified"; +"Stale" = "Stale"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records."; +"Blocked connection or request" = "Blocked connection or request"; +"Blocked by a managed pack" = "Blocked by a managed pack"; +"Outside an agent allowlist" = "Outside an agent allowlist"; +"Capability removed by a guardrail" = "Capability removed by a guardrail"; +"Answered by a mock" = "Answered by a mock"; +"Request changed before sending" = "Request changed before sending"; +"Response changed before delivery" = "Response changed before delivery"; diff --git a/Flowlight/Localization/es.lproj/Localizable.strings b/Flowlight/Localization/es.lproj/Localizable.strings index 3cda2c08..b788eca2 100644 --- a/Flowlight/Localization/es.lproj/Localizable.strings +++ b/Flowlight/Localization/es.lproj/Localizable.strings @@ -752,8 +752,6 @@ "Seen as it happened, named, and readable." = "Visto en el momento, con nombre y legible."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Sin vigilar"; "Read the device list again now (⌘R)" = "Volver a leer la lista de dispositivos ahora (⌘R)"; "Watch Bluetooth" = "Vigilar el Bluetooth"; "Watch USB and external storage" = "Vigilar el USB y el almacenamiento externo"; @@ -1234,13 +1232,8 @@ "Edit as normal rule…" = "Editar como regla normal…"; /* UI/RulesView.swift */ -"Packs" = "Paquetes"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "Aquí aparecen las decisiones de las reglas y los bloqueos resumidos de los paquetes. La actividad de los paquetes se agrupa para que una página llena de anuncios no se convierta en una página llena de alertas."; "Managed packs" = "Paquetes gestionados"; -"What they did" = "Qué han hecho"; "Paused until %@" = "En pausa hasta %@"; -"%lld in force" = "%lld en vigor"; -"%1$lld of %2$lld in force" = "%1$lld de %2$lld en vigor"; "Templates" = "Plantillas"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Reglas que merece la pena tener, agrupadas por su propósito y con los hosts ya escritos"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Escribir una regla: una app, un agente, un destino o una URL, bloqueado o permitido"; @@ -1254,8 +1247,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld reglas se están vigilando, pero no se aplican. Solo la extensión de red está en el camino de los datos y puede rechazar una conexión; el muestreador nettop cuenta el tráfico a posteriori. Selecciona la extensión de red en Fuentes › Colector para aplicar estas reglas."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "Una regla se está vigilando, pero no se aplica: una regla que nombra una ruta solo puede compararse con la inspección HTTPS, y está desactivada."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld reglas se están vigilando, pero no se aplican: una regla que nombra una ruta solo puede compararse con la inspección HTTPS, y está desactivada."; -"In force" = "En vigor"; -"Not in force" = "Sin vigor"; "Nothing is being refused until %@." = "No se rechazará nada hasta %@."; "Resume now" = "Reanudar ahora"; "No rules yet" = "Todavía no hay reglas"; @@ -1265,7 +1256,6 @@ "Nothing yet" = "Todavía nada"; "Today" = "Hoy"; "Yesterday" = "Ayer"; -"A rule since deleted" = "Una regla ya eliminada"; "Edit…" = "Editar…"; "Reset Count" = "Poner el contador a cero"; "Delete" = "Eliminar"; @@ -1887,3 +1877,70 @@ "Restart or reinstall the filter to finish the update" = "Reinicia o reinstala el filtro para terminar la actualización"; "Return to the previous history scope" = "Volver al ámbito anterior del historial"; "Selected" = "Seleccionado"; + +/* 0.17.4 políticas y fuentes unificadas */ +"Policies" = "Políticas"; +"Activity" = "Actividad"; +"Choose the traffic source and verify that data is arriving." = "Elige la fuente de tráfico y comprueba que estén llegando datos."; +"Choose which local device channels Flowlight watches." = "Elige qué canales de dispositivos locales vigila Flowlight."; +"Check what Flowlight can name, attribute, and inspect." = "Comprueba qué puede nombrar, atribuir e inspeccionar Flowlight."; +"Network rules" = "Reglas de red"; +"Agent allowlists" = "Listas de permitidos de agentes"; +"Agent guardrails" = "Salvaguardas de agentes"; +"Request rewrites" = "Reescrituras de peticiones"; +"Response transforms" = "Transformaciones de respuestas"; +"In effect" = "En vigor"; +"Reporting only" = "Solo informa"; +"Waiting" = "En espera"; +"Unavailable" = "No disponible"; +"Not applying" = "Sin aplicar"; +"HTTPS inspection is off." = "La inspección HTTPS está desactivada."; +"HTTPS inspection is not running." = "La inspección HTTPS no está en marcha."; +"This policy is switched off." = "Esta política está desactivada."; +"This rule is switched off." = "Esta regla está desactivada."; +"Network blocking is paused." = "El bloqueo de red está en pausa."; +"This one-off rule has already been used." = "Esta regla de un solo uso ya se ha utilizado."; +"This rule is outside its scheduled time." = "Esta regla está fuera de su horario programado."; +"Every destination" = "Todos los destinos"; +"No listed destinations" = "Ningún destino en la lista"; +"1 listed destination" = "1 destino en la lista"; +"%lld listed destinations" = "%lld destinos en la lista"; +"This allowlist is switched off." = "Esta lista de permitidos está desactivada."; +"Unlisted destinations raise alerts but are not blocked." = "Los destinos que no estén en la lista generan alertas, pero no se bloquean."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "El bloqueo no está disponible; los destinos que no estén en la lista siguen generando alertas."; +"Review the changed pack before it can keep applying." = "Revisa el paquete modificado para que pueda seguir aplicándose."; +"This pack is switched off." = "Este paquete está desactivado."; +"The pack is matching destinations but cannot block them here." = "El paquete detecta destinos coincidentes, pero aquí no puede bloquearlos."; +"The pack is waiting for the active filter to apply it." = "El paquete está esperando a que el filtro activo lo aplique."; +"The managed-pack engine is not applying this configuration." = "El motor de paquetes gestionados no está aplicando esta configuración."; +"No longer in the official catalog" = "Ya no figura en el catálogo oficial"; +"Blocking stopped automatically. Acknowledge this change after review." = "El bloqueo se detuvo automáticamente. Confirma este cambio después de revisarlo."; +"Every agent" = "Todos los agentes"; +"A policy since deleted" = "Una política ya eliminada"; +"%@ · %lld connections" = "%@ · %lld conexiones"; +"%lld policies · none currently effective" = "%lld políticas · ninguna en vigor ahora"; +"%1$lld policies · %2$lld currently effective" = "%1$lld políticas · %2$lld en vigor ahora"; +"Everything Flowlight can apply" = "Todo lo que Flowlight puede aplicar"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "Aquí se reúnen las decisiones de red, los límites de agentes, las listas de destinos gestionadas y las políticas de inspección para desarrolladores. Cada política sigue abriéndose en el editor que controla su comportamiento y su consentimiento."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "Aquí se reúnen las decisiones de red, los límites de agentes y las listas de destinos gestionadas. Cada política sigue abriéndose en el editor que controla su comportamiento y su consentimiento."; +"%lld destinations · %@" = "%lld destinos · %@"; +"every app" = "todas las apps"; +"Official · pack v%@ · %@" = "Oficial · paquete v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · firmante %@"; +"Catalog" = "Catálogo"; +"Sequence %1$lld · digest %2$@" = "Secuencia %1$lld · resumen %2$@"; +"Last verified" = "Última verificación"; +"Next check" = "Próxima comprobación"; +"Official catalog details" = "Detalles del catálogo oficial"; +"Not checked" = "Sin comprobar"; +"Verified" = "Verificado"; +"Stale" = "Obsoleto"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Aquí aparecen bloqueos, excepciones, incumplimientos de listas de permitidos, salvaguardas, simulaciones y cambios en peticiones o respuestas sin alterar sus registros originales."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Aquí aparecen bloqueos de red, excepciones, coincidencias de paquetes gestionados e incumplimientos de listas de permitidos sin alterar sus registros originales."; +"Blocked connection or request" = "Conexión o petición bloqueada"; +"Blocked by a managed pack" = "Bloqueado por un paquete gestionado"; +"Outside an agent allowlist" = "Fuera de la lista de permitidos de un agente"; +"Capability removed by a guardrail" = "Capacidad retirada por una salvaguarda"; +"Answered by a mock" = "Respondido por una simulación"; +"Request changed before sending" = "Petición modificada antes de enviarla"; +"Response changed before delivery" = "Respuesta modificada antes de entregarla"; diff --git a/Flowlight/Localization/fr.lproj/Localizable.strings b/Flowlight/Localization/fr.lproj/Localizable.strings index 77d49d96..2d1c6685 100644 --- a/Flowlight/Localization/fr.lproj/Localizable.strings +++ b/Flowlight/Localization/fr.lproj/Localizable.strings @@ -752,8 +752,6 @@ "Seen as it happened, named, and readable." = "Vu au moment où cela s'est produit, nommé et lisible."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Non surveillé"; "Read the device list again now (⌘R)" = "Relire la liste des appareils maintenant (⌘R)"; "Watch Bluetooth" = "Surveiller le Bluetooth"; "Watch USB and external storage" = "Surveiller l'USB et le stockage externe"; @@ -1234,13 +1232,8 @@ "Edit as normal rule…" = "Modifier comme règle normale…"; /* UI/RulesView.swift */ -"Packs" = "Packs"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "Les décisions des règles et les blocages résumés des packs apparaissent ici. L’activité des packs est regroupée afin qu’une page remplie de publicités ne devienne pas une page remplie d’alertes."; "Managed packs" = "Packs gérés"; -"What they did" = "Ce qu'elles ont fait"; "Paused until %@" = "Suspendu jusqu'à %@"; -"%lld in force" = "%lld en vigueur"; -"%1$lld of %2$lld in force" = "%1$lld sur %2$lld en vigueur"; "Templates" = "Modèles"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Des règles utiles, groupées par usage, avec les hôtes déjà renseignés"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Écrire une règle : une app, un agent, une destination ou une URL, bloquée ou autorisée"; @@ -1254,8 +1247,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld règles sont surveillées mais pas appliquées. Seule l'extension réseau se trouve sur le trajet des données et peut refuser une connexion ; l'échantillonneur nettop compte le trafic après coup. Sélectionner l'extension réseau dans Sources › Collecteur pour appliquer ces règles."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "Une règle est surveillée mais pas appliquée — une règle qui nomme un chemin ne peut être mise en correspondance que par l'inspection HTTPS, et celle-ci est désactivée."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld règles sont surveillées mais pas appliquées — une règle qui nomme un chemin ne peut être mise en correspondance que par l'inspection HTTPS, et celle-ci est désactivée."; -"In force" = "En vigueur"; -"Not in force" = "Pas en vigueur"; "Nothing is being refused until %@." = "Rien n'est refusé jusqu'à %@."; "Resume now" = "Reprendre maintenant"; "No rules yet" = "Aucune règle pour l'instant"; @@ -1265,7 +1256,6 @@ "Nothing yet" = "Rien pour l'instant"; "Today" = "Aujourd'hui"; "Yesterday" = "Hier"; -"A rule since deleted" = "Une règle depuis supprimée"; "Edit…" = "Modifier…"; "Reset Count" = "Réinitialiser le compteur"; "Delete" = "Supprimer"; @@ -1887,3 +1877,70 @@ "Restart or reinstall the filter to finish the update" = "Redémarrer ou réinstaller le filtre pour terminer la mise à jour"; "Return to the previous history scope" = "Revenir à la plage précédente de l'historique"; "Selected" = "Sélectionné"; + +/* 0.17.4 politiques et sources unifiées */ +"Policies" = "Politiques"; +"Activity" = "Activité"; +"Choose the traffic source and verify that data is arriving." = "Choisissez la source de trafic et vérifiez que les données arrivent."; +"Choose which local device channels Flowlight watches." = "Choisissez les canaux d’appareils locaux que Flowlight surveille."; +"Check what Flowlight can name, attribute, and inspect." = "Vérifiez ce que Flowlight peut nommer, attribuer et inspecter."; +"Network rules" = "Règles réseau"; +"Agent allowlists" = "Listes d’autorisation des agents"; +"Agent guardrails" = "Garde-fous des agents"; +"Request rewrites" = "Réécritures de requêtes"; +"Response transforms" = "Transformations de réponses"; +"In effect" = "En vigueur"; +"Reporting only" = "Signalement uniquement"; +"Waiting" = "En attente"; +"Unavailable" = "Indisponible"; +"Not applying" = "Non appliqué"; +"HTTPS inspection is off." = "L’inspection HTTPS est désactivée."; +"HTTPS inspection is not running." = "L’inspection HTTPS ne fonctionne pas."; +"This policy is switched off." = "Cette politique est désactivée."; +"This rule is switched off." = "Cette règle est désactivée."; +"Network blocking is paused." = "Le blocage réseau est suspendu."; +"This one-off rule has already been used." = "Cette règle à usage unique a déjà été utilisée."; +"This rule is outside its scheduled time." = "Cette règle est en dehors de sa plage horaire."; +"Every destination" = "Toutes les destinations"; +"No listed destinations" = "Aucune destination listée"; +"1 listed destination" = "1 destination listée"; +"%lld listed destinations" = "%lld destinations listées"; +"This allowlist is switched off." = "Cette liste d’autorisation est désactivée."; +"Unlisted destinations raise alerts but are not blocked." = "Les destinations non listées déclenchent des alertes, mais ne sont pas bloquées."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "Le blocage est indisponible ; les destinations non listées déclenchent toujours des alertes."; +"Review the changed pack before it can keep applying." = "Examinez le pack modifié avant qu’il puisse continuer à s’appliquer."; +"This pack is switched off." = "Ce pack est désactivé."; +"The pack is matching destinations but cannot block them here." = "Le pack reconnaît des destinations, mais ne peut pas les bloquer ici."; +"The pack is waiting for the active filter to apply it." = "Le pack attend que le filtre actif l’applique."; +"The managed-pack engine is not applying this configuration." = "Le moteur des packs gérés n’applique pas cette configuration."; +"No longer in the official catalog" = "Ne figure plus dans le catalogue officiel"; +"Blocking stopped automatically. Acknowledge this change after review." = "Le blocage s’est arrêté automatiquement. Confirmez ce changement après vérification."; +"Every agent" = "Tous les agents"; +"A policy since deleted" = "Une politique désormais supprimée"; +"%@ · %lld connections" = "%@ · %lld connexions"; +"%lld policies · none currently effective" = "%lld politiques · aucune actuellement en vigueur"; +"%1$lld policies · %2$lld currently effective" = "%1$lld politiques · %2$lld actuellement en vigueur"; +"Everything Flowlight can apply" = "Tout ce que Flowlight peut appliquer"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "Les décisions réseau, les limites des agents, les listes de destinations gérées et les politiques d’inspection pour développeurs sont réunies ici. Chaque politique s’ouvre toujours dans l’éditeur qui gère son comportement et son consentement."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "Les décisions réseau, les limites des agents et les listes de destinations gérées sont réunies ici. Chaque politique s’ouvre toujours dans l’éditeur qui gère son comportement et son consentement."; +"%lld destinations · %@" = "%lld destinations · %@"; +"every app" = "toutes les apps"; +"Official · pack v%@ · %@" = "Officiel · pack v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · signataire %@"; +"Catalog" = "Catalogue"; +"Sequence %1$lld · digest %2$@" = "Séquence %1$lld · empreinte %2$@"; +"Last verified" = "Dernière vérification"; +"Next check" = "Prochaine vérification"; +"Official catalog details" = "Détails du catalogue officiel"; +"Not checked" = "Non vérifié"; +"Verified" = "Vérifié"; +"Stale" = "Obsolète"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Les blocages, exceptions, violations de listes d’autorisation, garde-fous, simulations et modifications de requêtes ou de réponses apparaissent ici sans altérer leurs enregistrements d’origine."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Les blocages réseau, exceptions, correspondances de packs gérés et violations de listes d’autorisation apparaissent ici sans altérer leurs enregistrements d’origine."; +"Blocked connection or request" = "Connexion ou requête bloquée"; +"Blocked by a managed pack" = "Bloqué par un pack géré"; +"Outside an agent allowlist" = "Hors de la liste d’autorisation d’un agent"; +"Capability removed by a guardrail" = "Capacité retirée par un garde-fou"; +"Answered by a mock" = "Traité par une simulation"; +"Request changed before sending" = "Requête modifiée avant l’envoi"; +"Response changed before delivery" = "Réponse modifiée avant la remise"; diff --git a/Flowlight/Localization/it.lproj/Localizable.strings b/Flowlight/Localization/it.lproj/Localizable.strings index eebba212..f530a272 100644 --- a/Flowlight/Localization/it.lproj/Localizable.strings +++ b/Flowlight/Localization/it.lproj/Localizable.strings @@ -752,8 +752,6 @@ "Seen as it happened, named, and readable." = "Visto mentre accadeva, con un nome e leggibile."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Non monitorato"; "Read the device list again now (⌘R)" = "Rileggi subito l'elenco dei dispositivi (⌘R)"; "Watch Bluetooth" = "Monitora il Bluetooth"; "Watch USB and external storage" = "Monitora USB e archiviazione esterna"; @@ -1234,13 +1232,8 @@ "Edit as normal rule…" = "Modifica come regola normale…"; /* UI/RulesView.swift */ -"Packs" = "Pacchetti"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "Qui compaiono le decisioni delle regole e i blocchi riepilogati dei pacchetti. L’attività dei pacchetti viene raggruppata, così una pagina piena di pubblicità non diventa una pagina piena di avvisi."; "Managed packs" = "Pacchetti gestiti"; -"What they did" = "Cosa hanno fatto"; "Paused until %@" = "In pausa fino a %@"; -"%lld in force" = "%lld in vigore"; -"%1$lld of %2$lld in force" = "%1$lld di %2$lld in vigore"; "Templates" = "Modelli"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Regole utili, raggruppate per scopo, con gli host già scritti"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Scrivi una regola: un'app, un agente, una destinazione o un URL, bloccato o consentito"; @@ -1254,8 +1247,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld regole vengono osservate ma non applicate. Solo l'estensione di rete si trova sul percorso dei dati e può rifiutare una connessione; il campionatore nettop conta il traffico a posteriori. Seleziona l'estensione di rete in Origini › Collector per applicare queste regole."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "Una regola viene osservata ma non applicata: una regola che indica un percorso può essere confrontata solo dall'ispezione HTTPS, che è disattivata."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld regole vengono osservate ma non applicate: una regola che indica un percorso può essere confrontata solo dall'ispezione HTTPS, che è disattivata."; -"In force" = "In vigore"; -"Not in force" = "Non in vigore"; "Nothing is being refused until %@." = "Nulla viene rifiutato fino a %@."; "Resume now" = "Riprendi adesso"; "No rules yet" = "Ancora nessuna regola"; @@ -1265,7 +1256,6 @@ "Nothing yet" = "Ancora nulla"; "Today" = "Oggi"; "Yesterday" = "Ieri"; -"A rule since deleted" = "Una regola poi eliminata"; "Edit…" = "Modifica…"; "Reset Count" = "Azzera il conteggio"; "Delete" = "Elimina"; @@ -1887,3 +1877,70 @@ "Restart or reinstall the filter to finish the update" = "Riavvia o reinstalla il filtro per completare l'aggiornamento"; "Return to the previous history scope" = "Torna all'ambito precedente della cronologia"; "Selected" = "Selezionato"; + +/* 0.17.4 criteri unificati e origini */ +"Policies" = "Criteri"; +"Activity" = "Attività"; +"Choose the traffic source and verify that data is arriving." = "Scegli l'origine del traffico e verifica che i dati stiano arrivando."; +"Choose which local device channels Flowlight watches." = "Scegli quali canali dei dispositivi locali deve monitorare Flowlight."; +"Check what Flowlight can name, attribute, and inspect." = "Verifica che cosa Flowlight può nominare, attribuire e ispezionare."; +"Network rules" = "Regole di rete"; +"Agent allowlists" = "Elenchi consentiti degli agenti"; +"Agent guardrails" = "Protezioni degli agenti"; +"Request rewrites" = "Riscritture delle richieste"; +"Response transforms" = "Trasformazioni delle risposte"; +"In effect" = "Effettivo"; +"Reporting only" = "Solo segnalazione"; +"Waiting" = "In attesa"; +"Unavailable" = "Non disponibile"; +"Not applying" = "Non applicato"; +"HTTPS inspection is off." = "L'ispezione HTTPS è disattivata."; +"HTTPS inspection is not running." = "L'ispezione HTTPS non è in esecuzione."; +"This policy is switched off." = "Questo criterio è disattivato."; +"This rule is switched off." = "Questa regola è disattivata."; +"Network blocking is paused." = "Il blocco della rete è in pausa."; +"This one-off rule has already been used." = "Questa regola monouso è già stata utilizzata."; +"This rule is outside its scheduled time." = "Questa regola è fuori dall'orario programmato."; +"Every destination" = "Tutte le destinazioni"; +"No listed destinations" = "Nessuna destinazione elencata"; +"1 listed destination" = "1 destinazione elencata"; +"%lld listed destinations" = "%lld destinazioni elencate"; +"This allowlist is switched off." = "Questo elenco consentiti è disattivato."; +"Unlisted destinations raise alerts but are not blocked." = "Le destinazioni non elencate generano avvisi ma non vengono bloccate."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "Il blocco non è disponibile; le destinazioni non elencate continuano a generare avvisi."; +"Review the changed pack before it can keep applying." = "Esamina il pacchetto modificato prima che possa continuare a essere applicato."; +"This pack is switched off." = "Questo pacchetto è disattivato."; +"The pack is matching destinations but cannot block them here." = "Il pacchetto trova corrispondenze tra le destinazioni ma qui non può bloccarle."; +"The pack is waiting for the active filter to apply it." = "Il pacchetto attende che il filtro attivo lo applichi."; +"The managed-pack engine is not applying this configuration." = "Il motore dei pacchetti gestiti non sta applicando questa configurazione."; +"No longer in the official catalog" = "Non è più nel catalogo ufficiale"; +"Blocking stopped automatically. Acknowledge this change after review." = "Il blocco è stato interrotto automaticamente. Dopo aver esaminato la modifica, confermala."; +"Every agent" = "Tutti gli agenti"; +"A policy since deleted" = "Un criterio poi eliminato"; +"%@ · %lld connections" = "%@ · %lld connessioni"; +"%lld policies · none currently effective" = "%lld criteri · nessuno attualmente applicato"; +"%1$lld policies · %2$lld currently effective" = "%1$lld criteri · %2$lld attualmente applicati"; +"Everything Flowlight can apply" = "Tutto ciò che Flowlight può applicare"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "Qui trovi le decisioni di rete, i limiti degli agenti, gli elenchi gestiti di destinazioni e i criteri di ispezione per sviluppatori. Ogni criterio continua ad aprirsi nell'editor responsabile del relativo comportamento e consenso."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "Qui trovi le decisioni di rete, i limiti degli agenti e gli elenchi gestiti di destinazioni. Ogni criterio continua ad aprirsi nell'editor responsabile del relativo comportamento e consenso."; +"%lld destinations · %@" = "%lld destinazioni · %@"; +"every app" = "ogni app"; +"Official · pack v%@ · %@" = "Ufficiale · pacchetto v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · firmatario %@"; +"Catalog" = "Catalogo"; +"Sequence %1$lld · digest %2$@" = "Sequenza %1$lld · digest %2$@"; +"Last verified" = "Ultima verifica"; +"Next check" = "Prossimo controllo"; +"Official catalog details" = "Dettagli del catalogo ufficiale"; +"Not checked" = "Non controllato"; +"Verified" = "Verificato"; +"Stale" = "Obsoleto"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Blocchi, eccezioni, violazioni degli elenchi consentiti, protezioni, simulazioni e modifiche a richieste o risposte compaiono qui senza alterare i record originali."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Blocchi di rete, eccezioni, corrispondenze dei pacchetti gestiti e violazioni degli elenchi consentiti compaiono qui senza alterare i record originali."; +"Blocked connection or request" = "Connessione o richiesta bloccata"; +"Blocked by a managed pack" = "Bloccata da un pacchetto gestito"; +"Outside an agent allowlist" = "Fuori dall'elenco consentiti di un agente"; +"Capability removed by a guardrail" = "Funzionalità rimossa da una protezione"; +"Answered by a mock" = "Risposta fornita da una simulazione"; +"Request changed before sending" = "Richiesta modificata prima dell'invio"; +"Response changed before delivery" = "Risposta modificata prima della consegna"; diff --git a/Flowlight/Localization/ja.lproj/Localizable.strings b/Flowlight/Localization/ja.lproj/Localizable.strings index 4f3e1460..aa4b175a 100644 --- a/Flowlight/Localization/ja.lproj/Localizable.strings +++ b/Flowlight/Localization/ja.lproj/Localizable.strings @@ -752,8 +752,6 @@ "Seen as it happened, named, and readable." = "発生した時点で見えており、名前も付き、中身も読めています。"; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "監視していません"; "Read the device list again now (⌘R)" = "デバイスのリストをいま読み直します(⌘R)"; "Watch Bluetooth" = "Bluetooth を監視"; "Watch USB and external storage" = "USB と外部ストレージを監視"; @@ -1234,13 +1232,8 @@ "Edit as normal rule…" = "通常のルールとして編集…"; /* UI/RulesView.swift */ -"Packs" = "パック"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "ルールの判断とパックによるブロックの要約がここに表示されます。広告だらけのページがアラートだらけにならないよう、パックのアクティビティはまとめられます。"; "Managed packs" = "管理対象パック"; -"What they did" = "判断の記録"; "Paused until %@" = "%@ まで一時停止中"; -"%lld in force" = "%lld 件が有効"; -"%1$lld of %2$lld in force" = "%2$lld 件中 %1$lld 件が有効"; "Templates" = "テンプレート"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "用意しておく価値のあるルールを用途別にまとめました。ホストはすでに書き込まれています"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "ルールを作成します。アプリ、エージェント、接続先、URL のいずれかを、ブロックまたは許可します"; @@ -1254,8 +1247,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld 件のルールは監視されているだけで、実行されていません。データ経路に入って接続を拒否できるのはネットワーク機能拡張だけで、nettop サンプラーは事後にトラフィックを数えます。これらのルールを実行するには、ソース › コレクターでネットワーク機能拡張を選んでください。"; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "1 件のルールは監視されているだけで、実行されていません。パスを指定したルールを照合できるのは HTTPS インスペクションだけで、それがオフになっています。"; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld 件のルールは監視されているだけで、実行されていません。パスを指定したルールを照合できるのは HTTPS インスペクションだけで、それがオフになっています。"; -"In force" = "有効"; -"Not in force" = "無効"; "Nothing is being refused until %@." = "%@ まで、何も拒否しません。"; "Resume now" = "今すぐ再開"; "No rules yet" = "ルールがありません"; @@ -1265,7 +1256,6 @@ "Nothing yet" = "まだ何もありません"; "Today" = "今日"; "Yesterday" = "昨日"; -"A rule since deleted" = "すでに削除されたルール"; "Edit…" = "編集…"; "Reset Count" = "カウントをリセット"; "Delete" = "削除"; @@ -1887,3 +1877,70 @@ "Restart or reinstall the filter to finish the update" = "フィルタのアップデートを完了するには再起動または再インストールしてください"; "Return to the previous history scope" = "前の履歴範囲に戻る"; "Selected" = "選択中"; + +/* 0.17.4 unified policies and sources */ +"Policies" = "ポリシー"; +"Activity" = "アクティビティ"; +"Choose the traffic source and verify that data is arriving." = "トラフィックソースを選択し、データが届いていることを確認します。"; +"Choose which local device channels Flowlight watches." = "Flowlight が監視するローカルデバイスのチャンネルを選択します。"; +"Check what Flowlight can name, attribute, and inspect." = "Flowlight が名前を付け、関連付け、インスペクトできる範囲を確認します。"; +"Network rules" = "ネットワークルール"; +"Agent allowlists" = "エージェントの許可リスト"; +"Agent guardrails" = "エージェントのガードレール"; +"Request rewrites" = "リクエストの書き換え"; +"Response transforms" = "レスポンスの変換"; +"In effect" = "適用中"; +"Reporting only" = "報告のみ"; +"Waiting" = "待機中"; +"Unavailable" = "利用不可"; +"Not applying" = "未適用"; +"HTTPS inspection is off." = "HTTPS インスペクションはオフです。"; +"HTTPS inspection is not running." = "HTTPS インスペクションは稼働していません。"; +"This policy is switched off." = "このポリシーはオフです。"; +"This rule is switched off." = "このルールはオフです。"; +"Network blocking is paused." = "ネットワークのブロックは一時停止中です。"; +"This one-off rule has already been used." = "この一度きりのルールはすでに使用済みです。"; +"This rule is outside its scheduled time." = "このルールは現在、設定された時間帯の外です。"; +"Every destination" = "すべての接続先"; +"No listed destinations" = "登録された接続先なし"; +"1 listed destination" = "登録された接続先 1 件"; +"%lld listed destinations" = "登録された接続先 %lld 件"; +"This allowlist is switched off." = "この許可リストはオフです。"; +"Unlisted destinations raise alerts but are not blocked." = "リストにない接続先はアラートを出しますが、ブロックされません。"; +"Blocking is unavailable; unlisted destinations still raise alerts." = "ブロックは利用できませんが、リストにない接続先は引き続きアラートを出します。"; +"Review the changed pack before it can keep applying." = "変更されたパックを確認すると、再び適用できるようになります。"; +"This pack is switched off." = "このパックはオフです。"; +"The pack is matching destinations but cannot block them here." = "このパックは接続先に一致していますが、ここではブロックできません。"; +"The pack is waiting for the active filter to apply it." = "このパックは、有効なフィルタによる適用を待っています。"; +"The managed-pack engine is not applying this configuration." = "管理対象パックのエンジンは、この設定を適用していません。"; +"No longer in the official catalog" = "公式カタログから削除済み"; +"Blocking stopped automatically. Acknowledge this change after review." = "ブロックは自動的に停止しました。確認後、この変更を承認してください。"; +"Every agent" = "すべてのエージェント"; +"A policy since deleted" = "すでに削除されたポリシー"; +"%@ · %lld connections" = "%@ · 接続 %lld 件"; +"%lld policies · none currently effective" = "ポリシー %lld 件 · 現在適用中のものはありません"; +"%1$lld policies · %2$lld currently effective" = "ポリシー %1$lld 件 · 現在 %2$lld 件が適用中"; +"Everything Flowlight can apply" = "Flowlight が適用できるすべての制御"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "ネットワークの判断、エージェントの制限、管理対象の接続先リスト、開発者向けインスペクションポリシーをここにまとめます。各ポリシーを選ぶと、その動作と同意を管理する本来のエディタが開きます。"; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "ネットワークの判断、エージェントの制限、管理対象の接続先リストをここにまとめます。各ポリシーを選ぶと、その動作と同意を管理する本来のエディタが開きます。"; +"%lld destinations · %@" = "接続先 %lld 件 · %@"; +"every app" = "すべてのアプリ"; +"Official · pack v%@ · %@" = "公式 · パック v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · 署名者 %@"; +"Catalog" = "カタログ"; +"Sequence %1$lld · digest %2$@" = "シーケンス %1$lld · ダイジェスト %2$@"; +"Last verified" = "最終検証"; +"Next check" = "次回確認"; +"Official catalog details" = "公式カタログの詳細"; +"Not checked" = "未確認"; +"Verified" = "検証済み"; +"Stale" = "古い"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "ブロック、例外、許可リスト違反、ガードレール、モック、リクエストやレスポンスの変更を、元の記録を変えずにここへ表示します。"; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "ネットワークのブロック、例外、管理対象パックへの一致、許可リスト違反を、元の記録を変えずにここへ表示します。"; +"Blocked connection or request" = "ブロックされた接続またはリクエスト"; +"Blocked by a managed pack" = "管理対象パックによるブロック"; +"Outside an agent allowlist" = "エージェントの許可リスト外"; +"Capability removed by a guardrail" = "ガードレールによって取り除かれた機能"; +"Answered by a mock" = "モックによる応答"; +"Request changed before sending" = "送信前に変更されたリクエスト"; +"Response changed before delivery" = "受け渡し前に変更されたレスポンス"; diff --git a/Flowlight/Localization/ko.lproj/Localizable.strings b/Flowlight/Localization/ko.lproj/Localizable.strings index 6ad2abc5..7f4ad3bd 100644 --- a/Flowlight/Localization/ko.lproj/Localizable.strings +++ b/Flowlight/Localization/ko.lproj/Localizable.strings @@ -775,8 +775,6 @@ "Seen as it happened, named, and readable." = "일어난 순간에 포착되고, 이름이 붙고, 내용도 읽혔습니다."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "감시하지 않음"; "Read the device list again now (⌘R)" = "지금 기기 목록 다시 읽기 (⌘R)"; "Watch Bluetooth" = "Bluetooth 감시"; "Watch USB and external storage" = "USB 및 외장 저장 장치 감시"; @@ -1257,13 +1255,8 @@ "Edit as normal rule…" = "일반 규칙으로 편집…"; /* UI/RulesView.swift */ -"Packs" = "팩"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "규칙 판단과 팩 차단 요약이 여기에 표시됩니다. 광고가 많은 페이지 때문에 알림이 가득 차지 않도록 팩 활동은 묶어서 표시됩니다."; "Managed packs" = "관리형 팩"; -"What they did" = "동작 내역"; "Paused until %@" = "%@까지 일시 중지됨"; -"%lld in force" = "%lld개 적용 중"; -"%1$lld of %2$lld in force" = "%2$lld개 중 %1$lld개 적용 중"; "Templates" = "템플릿"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "쓸 만한 규칙을 용도별로 모아 두었고, 호스트도 이미 채워져 있습니다"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "규칙 작성: 앱, 에이전트, 목적지 또는 URL을 차단하거나 허용합니다"; @@ -1277,8 +1270,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "규칙 %lld개가 관찰만 되고 실행되지는 않습니다. 데이터 경로에 자리해 연결을 거부할 수 있는 것은 네트워크 확장 프로그램뿐이며, nettop 샘플러는 트래픽을 사후에 집계합니다. 이 규칙을 적용하려면 소스 › 컬렉터에서 네트워크 확장 프로그램을 선택하세요."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "규칙 1개가 관찰만 되고 실행되지는 않습니다. 경로를 지정한 규칙은 HTTPS 검사로만 일치시킬 수 있는데, 검사가 꺼져 있습니다."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "규칙 %lld개가 관찰만 되고 실행되지는 않습니다. 경로를 지정한 규칙은 HTTPS 검사로만 일치시킬 수 있는데, 검사가 꺼져 있습니다."; -"In force" = "적용 중"; -"Not in force" = "적용되지 않음"; "Nothing is being refused until %@." = "%@까지는 아무것도 거부하지 않습니다."; "Resume now" = "지금 다시 시작"; "No rules yet" = "아직 규칙이 없습니다"; @@ -1288,7 +1279,6 @@ "Nothing yet" = "아직 없음"; "Today" = "오늘"; "Yesterday" = "어제"; -"A rule since deleted" = "이후 삭제된 규칙"; "Edit…" = "편집…"; "Reset Count" = "횟수 재설정"; "Delete" = "삭제"; @@ -1886,3 +1876,70 @@ "Restart or reinstall the filter to finish the update" = "업데이트를 완료하려면 필터를 재시작하거나 다시 설치하세요"; "Return to the previous history scope" = "이전 기록 범위로 돌아가기"; "Selected" = "선택됨"; + +/* 0.17.4 unified policies and sources */ +"Policies" = "정책"; +"Activity" = "활동"; +"Choose the traffic source and verify that data is arriving." = "트래픽 소스를 선택하고 데이터가 들어오는지 확인합니다."; +"Choose which local device channels Flowlight watches." = "Flowlight가 감시할 로컬 기기 채널을 선택합니다."; +"Check what Flowlight can name, attribute, and inspect." = "Flowlight가 이름을 붙이고, 출처를 연결하고, 검사할 수 있는 범위를 확인합니다."; +"Network rules" = "네트워크 규칙"; +"Agent allowlists" = "에이전트 허용 목록"; +"Agent guardrails" = "에이전트 가드레일"; +"Request rewrites" = "요청 다시 쓰기"; +"Response transforms" = "응답 변환"; +"In effect" = "적용 중"; +"Reporting only" = "보고만 함"; +"Waiting" = "대기 중"; +"Unavailable" = "사용할 수 없음"; +"Not applying" = "적용되지 않음"; +"HTTPS inspection is off." = "HTTPS 검사가 꺼져 있습니다."; +"HTTPS inspection is not running." = "HTTPS 검사가 실행 중이 아닙니다."; +"This policy is switched off." = "이 정책은 꺼져 있습니다."; +"This rule is switched off." = "이 규칙은 꺼져 있습니다."; +"Network blocking is paused." = "네트워크 차단이 일시 중지되었습니다."; +"This one-off rule has already been used." = "이 일회성 규칙은 이미 사용되었습니다."; +"This rule is outside its scheduled time." = "이 규칙은 현재 지정된 시간대 밖에 있습니다."; +"Every destination" = "모든 목적지"; +"No listed destinations" = "등록된 목적지 없음"; +"1 listed destination" = "등록된 목적지 1개"; +"%lld listed destinations" = "등록된 목적지 %lld개"; +"This allowlist is switched off." = "이 허용 목록은 꺼져 있습니다."; +"Unlisted destinations raise alerts but are not blocked." = "목록에 없는 목적지는 알림을 띄우지만 차단하지 않습니다."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "차단을 사용할 수 없지만, 목록에 없는 목적지는 계속 알림을 띄웁니다."; +"Review the changed pack before it can keep applying." = "변경된 팩을 검토해야 다시 적용할 수 있습니다."; +"This pack is switched off." = "이 팩은 꺼져 있습니다."; +"The pack is matching destinations but cannot block them here." = "이 팩은 목적지를 찾아내고 있지만 여기서는 차단할 수 없습니다."; +"The pack is waiting for the active filter to apply it." = "이 팩은 활성 필터가 적용하기를 기다리고 있습니다."; +"The managed-pack engine is not applying this configuration." = "관리형 팩 엔진이 이 구성을 적용하지 않고 있습니다."; +"No longer in the official catalog" = "공식 카탈로그에서 삭제됨"; +"Blocking stopped automatically. Acknowledge this change after review." = "차단이 자동으로 중지되었습니다. 검토한 뒤 이 변경을 확인하세요."; +"Every agent" = "모든 에이전트"; +"A policy since deleted" = "이후 삭제된 정책"; +"%@ · %lld connections" = "%@ · 연결 %lld개"; +"%lld policies · none currently effective" = "정책 %lld개 · 현재 적용 중인 정책 없음"; +"%1$lld policies · %2$lld currently effective" = "정책 %1$lld개 · 현재 %2$lld개 적용 중"; +"Everything Flowlight can apply" = "Flowlight가 적용할 수 있는 모든 제어"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "네트워크 판정, 에이전트 제한, 관리형 목적지 목록, 개발자용 검사 정책을 한곳에서 봅니다. 각 정책을 선택하면 그 동작과 동의를 관리하는 원래 편집기가 열립니다."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "네트워크 판정, 에이전트 제한, 관리형 목적지 목록을 한곳에서 봅니다. 각 정책을 선택하면 그 동작과 동의를 관리하는 원래 편집기가 열립니다."; +"%lld destinations · %@" = "목적지 %lld개 · %@"; +"every app" = "모든 앱"; +"Official · pack v%@ · %@" = "공식 · 팩 v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · 서명자 %@"; +"Catalog" = "카탈로그"; +"Sequence %1$lld · digest %2$@" = "순번 %1$lld · 다이제스트 %2$@"; +"Last verified" = "마지막 검증"; +"Next check" = "다음 확인"; +"Official catalog details" = "공식 카탈로그 세부 정보"; +"Not checked" = "확인하지 않음"; +"Verified" = "검증됨"; +"Stale" = "오래됨"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "차단, 예외, 허용 목록 위반, 가드레일, 모의 응답, 요청·응답 변경을 원래 기록은 그대로 둔 채 여기에 표시합니다."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "네트워크 차단, 예외, 관리형 팩 일치, 허용 목록 위반을 원래 기록은 그대로 둔 채 여기에 표시합니다."; +"Blocked connection or request" = "차단된 연결 또는 요청"; +"Blocked by a managed pack" = "관리형 팩으로 차단됨"; +"Outside an agent allowlist" = "에이전트 허용 목록 밖"; +"Capability removed by a guardrail" = "가드레일이 제거한 기능"; +"Answered by a mock" = "모의 응답으로 처리됨"; +"Request changed before sending" = "전송 전에 변경된 요청"; +"Response changed before delivery" = "전달 전에 변경된 응답"; diff --git a/Flowlight/Localization/pt-PT.lproj/Localizable.strings b/Flowlight/Localization/pt-PT.lproj/Localizable.strings index 324de56f..d44dc9f6 100644 --- a/Flowlight/Localization/pt-PT.lproj/Localizable.strings +++ b/Flowlight/Localization/pt-PT.lproj/Localizable.strings @@ -775,8 +775,6 @@ "Seen as it happened, named, and readable." = "Visto no momento em que aconteceu, nomeado e legível."; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "Não vigiado"; "Read the device list again now (⌘R)" = "Ler a lista de dispositivos outra vez agora (⌘R)"; "Watch Bluetooth" = "Vigiar o Bluetooth"; "Watch USB and external storage" = "Vigiar o USB e o armazenamento externo"; @@ -1257,13 +1255,8 @@ "Edit as normal rule…" = "Editar como regra normal…"; /* UI/RulesView.swift */ -"Packs" = "Pacotes"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "As decisões das regras e os bloqueios resumidos dos pacotes aparecem aqui. A atividade dos pacotes é agrupada para que uma página cheia de anúncios não se torne numa página cheia de alertas."; "Managed packs" = "Pacotes geridos"; -"What they did" = "O que fizeram"; "Paused until %@" = "Em pausa até %@"; -"%lld in force" = "%lld em vigor"; -"%1$lld of %2$lld in force" = "%1$lld de %2$lld em vigor"; "Templates" = "Modelos"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "Regras que vale a pena ter, agrupadas pelo seu propósito, com os anfitriões já escritos"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "Escrever uma regra: uma app, um agente, um destino ou um URL, bloqueado ou permitido"; @@ -1277,8 +1270,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld regras estão a ser vigiadas mas não aplicadas. Só a extensão de rede está no caminho dos dados e pode recusar uma ligação; o amostrador nettop conta o tráfego depois do facto. Selecione a extensão de rede em Origens › Coletor para aplicar estas regras."; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "Uma regra está a ser vigiada mas não aplicada — uma regra que nomeia um caminho só pode ser correspondida pela inspeção HTTPS, e ela está desativada."; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld regras estão a ser vigiadas mas não aplicadas — uma regra que nomeia um caminho só pode ser correspondida pela inspeção HTTPS, e ela está desativada."; -"In force" = "Em vigor"; -"Not in force" = "Sem vigor"; "Nothing is being refused until %@." = "Nada está a ser recusado até %@."; "Resume now" = "Retomar agora"; "No rules yet" = "Ainda sem regras"; @@ -1288,7 +1279,6 @@ "Nothing yet" = "Ainda nada"; "Today" = "Hoje"; "Yesterday" = "Ontem"; -"A rule since deleted" = "Uma regra entretanto eliminada"; "Edit…" = "Editar…"; "Reset Count" = "Reiniciar a contagem"; "Delete" = "Eliminar"; @@ -1886,3 +1876,70 @@ "Restart or reinstall the filter to finish the update" = "Reinicie ou reinstale o filtro para concluir a atualização"; "Return to the previous history scope" = "Voltar ao âmbito anterior do histórico"; "Selected" = "Selecionado"; + +/* 0.17.4 políticas e origens unificadas */ +"Policies" = "Políticas"; +"Activity" = "Atividade"; +"Choose the traffic source and verify that data is arriving." = "Escolha a origem do tráfego e verifique se os dados estão a chegar."; +"Choose which local device channels Flowlight watches." = "Escolha os canais de dispositivos locais que o Flowlight vigia."; +"Check what Flowlight can name, attribute, and inspect." = "Verifique o que o Flowlight consegue nomear, atribuir e inspecionar."; +"Network rules" = "Regras de rede"; +"Agent allowlists" = "Listas de permissões dos agentes"; +"Agent guardrails" = "Proteções dos agentes"; +"Request rewrites" = "Reescritas de pedidos"; +"Response transforms" = "Transformações de respostas"; +"In effect" = "Em vigor"; +"Reporting only" = "Apenas a comunicar"; +"Waiting" = "À espera"; +"Unavailable" = "Indisponível"; +"Not applying" = "Não aplicado"; +"HTTPS inspection is off." = "A inspeção HTTPS está desativada."; +"HTTPS inspection is not running." = "A inspeção HTTPS não está em execução."; +"This policy is switched off." = "Esta política está desativada."; +"This rule is switched off." = "Esta regra está desativada."; +"Network blocking is paused." = "O bloqueio da rede está em pausa."; +"This one-off rule has already been used." = "Esta regra de utilização única já foi usada."; +"This rule is outside its scheduled time." = "Esta regra está fora do horário agendado."; +"Every destination" = "Todos os destinos"; +"No listed destinations" = "Nenhum destino listado"; +"1 listed destination" = "1 destino listado"; +"%lld listed destinations" = "%lld destinos listados"; +"This allowlist is switched off." = "Esta lista de permissões está desativada."; +"Unlisted destinations raise alerts but are not blocked." = "Os destinos não listados geram alertas, mas não são bloqueados."; +"Blocking is unavailable; unlisted destinations still raise alerts." = "O bloqueio não está disponível; os destinos não listados continuam a gerar alertas."; +"Review the changed pack before it can keep applying." = "Reveja o pacote alterado antes de este poder continuar a ser aplicado."; +"This pack is switched off." = "Este pacote está desativado."; +"The pack is matching destinations but cannot block them here." = "O pacote encontra destinos correspondentes, mas não os consegue bloquear aqui."; +"The pack is waiting for the active filter to apply it." = "O pacote aguarda que o filtro ativo o aplique."; +"The managed-pack engine is not applying this configuration." = "O motor de pacotes geridos não está a aplicar esta configuração."; +"No longer in the official catalog" = "Já não consta do catálogo oficial"; +"Blocking stopped automatically. Acknowledge this change after review." = "O bloqueio parou automaticamente. Depois de rever esta alteração, confirme-a."; +"Every agent" = "Todos os agentes"; +"A policy since deleted" = "Uma política entretanto eliminada"; +"%@ · %lld connections" = "%@ · %lld ligações"; +"%lld policies · none currently effective" = "%lld políticas · nenhuma atualmente em vigor"; +"%1$lld policies · %2$lld currently effective" = "%1$lld políticas · %2$lld atualmente em vigor"; +"Everything Flowlight can apply" = "Tudo o que o Flowlight pode aplicar"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "As decisões de rede, os limites dos agentes, as listas geridas de destinos e as políticas de inspeção para programadores estão aqui. Cada política continua a abrir no editor responsável pelo respetivo comportamento e consentimento."; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "As decisões de rede, os limites dos agentes e as listas geridas de destinos estão aqui. Cada política continua a abrir no editor responsável pelo respetivo comportamento e consentimento."; +"%lld destinations · %@" = "%lld destinos · %@"; +"every app" = "todas as apps"; +"Official · pack v%@ · %@" = "Oficial · pacote v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · signatário %@"; +"Catalog" = "Catálogo"; +"Sequence %1$lld · digest %2$@" = "Sequência %1$lld · resumo %2$@"; +"Last verified" = "Última verificação"; +"Next check" = "Próxima verificação"; +"Official catalog details" = "Detalhes do catálogo oficial"; +"Not checked" = "Não verificado"; +"Verified" = "Verificado"; +"Stale" = "Desatualizado"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "Bloqueios, exceções, violações de listas de permissões, proteções, simulações e alterações a pedidos ou respostas aparecem aqui sem alterar os registos originais."; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "Bloqueios de rede, exceções, correspondências de pacotes geridos e violações de listas de permissões aparecem aqui sem alterar os registos originais."; +"Blocked connection or request" = "Ligação ou pedido bloqueado"; +"Blocked by a managed pack" = "Bloqueado por um pacote gerido"; +"Outside an agent allowlist" = "Fora da lista de permissões de um agente"; +"Capability removed by a guardrail" = "Capacidade removida por uma proteção"; +"Answered by a mock" = "Respondido por uma simulação"; +"Request changed before sending" = "Pedido alterado antes do envio"; +"Response changed before delivery" = "Resposta alterada antes da entrega"; diff --git a/Flowlight/Localization/zh-Hans.lproj/Localizable.strings b/Flowlight/Localization/zh-Hans.lproj/Localizable.strings index b0dafb5f..9ca6f05c 100644 --- a/Flowlight/Localization/zh-Hans.lproj/Localizable.strings +++ b/Flowlight/Localization/zh-Hans.lproj/Localizable.strings @@ -775,8 +775,6 @@ "Seen as it happened, named, and readable." = "在发生时即被看到,有名称,内容也可读。"; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "未监视"; "Read the device list again now (⌘R)" = "立即重新读取设备列表(⌘R)"; "Watch Bluetooth" = "监视蓝牙"; "Watch USB and external storage" = "监视 USB 和外置存储"; @@ -1257,13 +1255,8 @@ "Edit as normal rule…" = "作为普通规则编辑…"; /* UI/RulesView.swift */ -"Packs" = "包"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "规则判定和包的阻止摘要会显示在这里。包活动会被分组,以免满是广告的页面变成满是警报的页面。"; "Managed packs" = "托管包"; -"What they did" = "它们做了什么"; "Paused until %@" = "已暂停至 %@"; -"%lld in force" = "%lld 条生效中"; -"%1$lld of %2$lld in force" = "%2$lld 条中有 %1$lld 条生效中"; "Templates" = "模板"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "值得拥有的规则,按用途分组,主机已经写好"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "写一条规则:应用、代理、目标或 URL,阻止或允许"; @@ -1277,8 +1270,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "%lld 条规则只是被监视,而没有被执行。只有网络扩展位于数据路径上,能够拒绝连接;nettop 采样器是事后计数。请在“来源 › 收集器”中选择网络扩展,以执行这些规则。"; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "有一条规则只是被监视,而没有被执行 —— 指明路径的规则只能由 HTTPS 检查匹配,而它已关闭。"; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "%lld 条规则只是被监视,而没有被执行 —— 指明路径的规则只能由 HTTPS 检查匹配,而它已关闭。"; -"In force" = "生效中"; -"Not in force" = "未生效"; "Nothing is being refused until %@." = "在 %@ 之前不会拒绝任何连接。"; "Resume now" = "立即恢复"; "No rules yet" = "还没有规则"; @@ -1288,7 +1279,6 @@ "Nothing yet" = "暂无内容"; "Today" = "今天"; "Yesterday" = "昨天"; -"A rule since deleted" = "一条已被删除的规则"; "Edit…" = "编辑…"; "Reset Count" = "重置计数"; "Delete" = "删除"; @@ -1886,3 +1876,70 @@ "Restart or reinstall the filter to finish the update" = "重新启动或重新安装过滤器以完成更新"; "Return to the previous history scope" = "返回上一个历史记录范围"; "Selected" = "已选择"; + +/* 0.17.4 策略与整合后的来源 */ +"Policies" = "策略"; +"Activity" = "活动"; +"Choose the traffic source and verify that data is arriving." = "选择流量来源,并确认数据正在到达。"; +"Choose which local device channels Flowlight watches." = "选择 Flowlight 要监视的本地设备通道。"; +"Check what Flowlight can name, attribute, and inspect." = "检查 Flowlight 能够命名、归属和检查哪些内容。"; +"Network rules" = "网络规则"; +"Agent allowlists" = "代理允许列表"; +"Agent guardrails" = "代理护栏"; +"Request rewrites" = "请求改写"; +"Response transforms" = "响应转换"; +"In effect" = "生效中"; +"Reporting only" = "仅报告"; +"Waiting" = "等待中"; +"Unavailable" = "不可用"; +"Not applying" = "未在执行"; +"HTTPS inspection is off." = "HTTPS 检查已关闭。"; +"HTTPS inspection is not running." = "HTTPS 检查未在运行。"; +"This policy is switched off." = "此策略已关闭。"; +"This rule is switched off." = "此规则已关闭。"; +"Network blocking is paused." = "网络阻止已暂停。"; +"This one-off rule has already been used." = "这条一次性规则已经用过。"; +"This rule is outside its scheduled time." = "此规则当前不在预定时段内。"; +"Every destination" = "所有目标"; +"No listed destinations" = "没有列出的目标"; +"1 listed destination" = "列出了 1 个目标"; +"%lld listed destinations" = "列出了 %lld 个目标"; +"This allowlist is switched off." = "此允许列表已关闭。"; +"Unlisted destinations raise alerts but are not blocked." = "未列出的目标会触发警报,但不会被阻止。"; +"Blocking is unavailable; unlisted destinations still raise alerts." = "阻止功能不可用;未列出的目标仍会触发警报。"; +"Review the changed pack before it can keep applying." = "请先检查已更改的包,之后它才能继续生效。"; +"This pack is switched off." = "此包已关闭。"; +"The pack is matching destinations but cannot block them here." = "此包正在匹配目标,但无法在这里阻止它们。"; +"The pack is waiting for the active filter to apply it." = "此包正在等待活动的过滤器应用它。"; +"The managed-pack engine is not applying this configuration." = "托管包引擎没有执行此配置。"; +"No longer in the official catalog" = "已不在官方目录中"; +"Blocking stopped automatically. Acknowledge this change after review." = "阻止已自动停止。检查后请确认此项变更。"; +"Every agent" = "所有代理"; +"A policy since deleted" = "一项已删除的策略"; +"%@ · %lld connections" = "%@ · %lld 个连接"; +"%lld policies · none currently effective" = "%lld 项策略 · 当前均未生效"; +"%1$lld policies · %2$lld currently effective" = "%1$lld 项策略 · 当前 %2$lld 项生效"; +"Everything Flowlight can apply" = "Flowlight 可以执行的所有策略"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "网络判定、代理限制、托管目标列表和开发者检查策略都集中在这里。每项策略仍会在负责其行为与授权的编辑器中打开。"; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "网络判定、代理限制和托管目标列表都集中在这里。每项策略仍会在负责其行为与授权的编辑器中打开。"; +"%lld destinations · %@" = "%lld 个目标 · %@"; +"every app" = "所有应用"; +"Official · pack v%@ · %@" = "官方 · 包 v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · 签名者 %@"; +"Catalog" = "目录"; +"Sequence %1$lld · digest %2$@" = "序列 %1$lld · 摘要 %2$@"; +"Last verified" = "上次验证"; +"Next check" = "下次检查"; +"Official catalog details" = "官方目录详情"; +"Not checked" = "未检查"; +"Verified" = "已验证"; +"Stale" = "已过期"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "阻止、例外、允许列表违规、护栏、模拟响应以及请求或响应改动都会显示在这里,原始记录不会被更改。"; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "网络阻止、例外、托管包匹配和允许列表违规都会显示在这里,原始记录不会被更改。"; +"Blocked connection or request" = "连接或请求已被阻止"; +"Blocked by a managed pack" = "被托管包阻止"; +"Outside an agent allowlist" = "不在代理的允许列表中"; +"Capability removed by a guardrail" = "能力已被护栏移除"; +"Answered by a mock" = "由模拟响应作答"; +"Request changed before sending" = "请求在发送前已更改"; +"Response changed before delivery" = "响应在送达前已更改"; diff --git a/Flowlight/Localization/zh-Hant.lproj/Localizable.strings b/Flowlight/Localization/zh-Hant.lproj/Localizable.strings index 9fb87054..6946fcee 100644 --- a/Flowlight/Localization/zh-Hant.lproj/Localizable.strings +++ b/Flowlight/Localization/zh-Hant.lproj/Localizable.strings @@ -775,8 +775,6 @@ "Seen as it happened, named, and readable." = "在發生時就看到、有名稱,內容也讀得到。"; /* UI/DevicesView.swift */ -"USB" = "USB"; -"Not watching" = "未監看"; "Read the device list again now (⌘R)" = "立即重新讀取裝置列表(⌘R)"; "Watch Bluetooth" = "監看藍牙"; "Watch USB and external storage" = "監看 USB 與外接儲存裝置"; @@ -1257,13 +1255,8 @@ "Edit as normal rule…" = "編輯為一般規則…"; /* UI/RulesView.swift */ -"Packs" = "套件"; -"Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts." = "規則決定和套件的封鎖摘要會顯示在這裡。套件活動會被分組,以免滿是廣告的頁面變成滿是警示的頁面。"; "Managed packs" = "受管理套件"; -"What they did" = "它們做了什麼"; "Paused until %@" = "已暫停至 %@"; -"%lld in force" = "%lld 條生效中"; -"%1$lld of %2$lld in force" = "%2$lld 條中有 %1$lld 條生效中"; "Templates" = "範本"; "Rules worth having, grouped by what they are for, with the hosts already written out" = "值得一用的規則,依用途分組,主機都已經寫好"; "Write a rule: an app, an agent, a destination or a URL, blocked or allowed" = "寫一條規則:指定 App、代理、目標或 URL,並選擇封鎖或允許"; @@ -1277,8 +1270,6 @@ "%lld rules are being watched but not carried out. Only the Network Extension sits in the data path and can refuse a connection; the nettop sampler counts traffic after the fact. Select the Network Extension in Sources › Collector to enforce these rules." = "有 %lld 條規則只被監看,沒有實際執行。只有網路延伸功能位在資料路徑上,能夠拒絕連線;nettop 取樣器是事後計數流量的。請在「來源 › 收集器」中選擇網路延伸功能來實際執行這些規則。"; "One rule is being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "有 1 條規則只被監看,沒有實際執行 — 指名路徑的規則只有 HTTPS 檢查才比對得到,而它已關閉。"; "%lld rules are being watched but not carried out — a rule that names a path can only be matched by HTTPS inspection, and it is off." = "有 %lld 條規則只被監看,沒有實際執行 — 指名路徑的規則只有 HTTPS 檢查才比對得到,而它已關閉。"; -"In force" = "生效中"; -"Not in force" = "未生效"; "Nothing is being refused until %@." = "在 %@ 之前不會拒絕任何連線。"; "Resume now" = "立即恢復"; "No rules yet" = "還沒有任何規則"; @@ -1288,7 +1279,6 @@ "Nothing yet" = "還沒有任何項目"; "Today" = "今天"; "Yesterday" = "昨天"; -"A rule since deleted" = "一條之後被刪除的規則"; "Edit…" = "編輯…"; "Reset Count" = "重設計數"; "Delete" = "刪除"; @@ -1886,3 +1876,70 @@ "Restart or reinstall the filter to finish the update" = "重新啟動或重新安裝過濾器以完成更新"; "Return to the previous history scope" = "返回上一個歷史記錄範圍"; "Selected" = "已選取"; + +/* 0.17.4 政策與整合後的來源 */ +"Policies" = "政策"; +"Activity" = "活動"; +"Choose the traffic source and verify that data is arriving." = "選擇流量來源,並確認資料正在傳入。"; +"Choose which local device channels Flowlight watches." = "選擇 Flowlight 要監看的本機裝置通道。"; +"Check what Flowlight can name, attribute, and inspect." = "檢查 Flowlight 能為哪些內容命名、判定歸屬和進行檢查。"; +"Network rules" = "網路規則"; +"Agent allowlists" = "代理允許清單"; +"Agent guardrails" = "代理防護機制"; +"Request rewrites" = "請求改寫"; +"Response transforms" = "回應轉換"; +"In effect" = "生效中"; +"Reporting only" = "僅回報"; +"Waiting" = "等待中"; +"Unavailable" = "無法使用"; +"Not applying" = "未套用"; +"HTTPS inspection is off." = "HTTPS 檢查已關閉。"; +"HTTPS inspection is not running." = "HTTPS 檢查未在執行。"; +"This policy is switched off." = "此政策已關閉。"; +"This rule is switched off." = "此規則已關閉。"; +"Network blocking is paused." = "網路封鎖已暫停。"; +"This one-off rule has already been used." = "這條一次性規則已經用過。"; +"This rule is outside its scheduled time." = "此規則目前不在排定時段內。"; +"Every destination" = "所有目標"; +"No listed destinations" = "沒有列出的目標"; +"1 listed destination" = "列出 1 個目標"; +"%lld listed destinations" = "列出 %lld 個目標"; +"This allowlist is switched off." = "此允許清單已關閉。"; +"Unlisted destinations raise alerts but are not blocked." = "未列出的目標會發出警示,但不會被封鎖。"; +"Blocking is unavailable; unlisted destinations still raise alerts." = "封鎖功能無法使用;未列出的目標仍會發出警示。"; +"Review the changed pack before it can keep applying." = "請先檢閱變更後的套件,它才能繼續套用。"; +"This pack is switched off." = "此套件已關閉。"; +"The pack is matching destinations but cannot block them here." = "此套件正在比對目標,但無法在這裡封鎖它們。"; +"The pack is waiting for the active filter to apply it." = "此套件正在等待作用中的過濾器套用它。"; +"The managed-pack engine is not applying this configuration." = "受管理套件引擎沒有套用這項設定。"; +"No longer in the official catalog" = "已不在官方目錄中"; +"Blocking stopped automatically. Acknowledge this change after review." = "封鎖已自動停止。檢閱後請確認這項變更。"; +"Every agent" = "所有代理"; +"A policy since deleted" = "一項已刪除的政策"; +"%@ · %lld connections" = "%@ · %lld 個連線"; +"%lld policies · none currently effective" = "%lld 項政策 · 目前均未生效"; +"%1$lld policies · %2$lld currently effective" = "%1$lld 項政策 · 目前 %2$lld 項生效"; +"Everything Flowlight can apply" = "Flowlight 可以套用的所有政策"; +"Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent." = "網路判定、代理限制、受管理目標清單和開發者檢查政策都集中在這裡。每項政策仍會在負責其行為與授權的編輯器中開啟。"; +"Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent." = "網路判定、代理限制和受管理目標清單都集中在這裡。每項政策仍會在負責其行為與授權的編輯器中開啟。"; +"%lld destinations · %@" = "%lld 個目標 · %@"; +"every app" = "所有 App"; +"Official · pack v%@ · %@" = "官方 · 套件 v%@ · %@"; +"Flowlight · signer %@" = "Flowlight · 簽署者 %@"; +"Catalog" = "目錄"; +"Sequence %1$lld · digest %2$@" = "序號 %1$lld · 摘要 %2$@"; +"Last verified" = "上次驗證"; +"Next check" = "下次檢查"; +"Official catalog details" = "官方目錄詳細資料"; +"Not checked" = "尚未檢查"; +"Verified" = "已驗證"; +"Stale" = "已過期"; +"Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records." = "封鎖、例外、允許清單違規、防護機制、模擬回應,以及請求或回應變更都會顯示在這裡,而不會改動原始記錄。"; +"Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records." = "網路封鎖、例外、受管理套件比對和允許清單違規都會顯示在這裡,而不會改動原始記錄。"; +"Blocked connection or request" = "連線或請求已被封鎖"; +"Blocked by a managed pack" = "被受管理套件封鎖"; +"Outside an agent allowlist" = "不在代理的允許清單中"; +"Capability removed by a guardrail" = "功能已被防護機制移除"; +"Answered by a mock" = "由模擬回應作答"; +"Request changed before sending" = "請求在送出前已變更"; +"Response changed before delivery" = "回應在送達前已變更"; diff --git a/Flowlight/Storage/TrafficDatabase.swift b/Flowlight/Storage/TrafficDatabase.swift index da133f1c..1baa3c86 100644 --- a/Flowlight/Storage/TrafficDatabase.swift +++ b/Flowlight/Storage/TrafficDatabase.swift @@ -655,6 +655,11 @@ final class TrafficDatabase: @unchecked Sendable { if !exchangeColumns.contains("guardrail") { try conn.execute("ALTER TABLE http_exchanges ADD COLUMN guardrail TEXT NOT NULL DEFAULT ''") } + // Request rewrites used the guardrail column before central policy activity needed to distinguish their + // effects. Keep old rows readable as they were and give new recordings truthful metadata. + if !exchangeColumns.contains("request_rewrite") { + try conn.execute("ALTER TABLE http_exchanges ADD COLUMN request_rewrite TEXT NOT NULL DEFAULT ''") + } // Unlike a mock, this names a response the origin really sent before Flowlight changed it for the client. if !exchangeColumns.contains("response_transform") { try conn.execute("ALTER TABLE http_exchanges ADD COLUMN response_transform TEXT NOT NULL DEFAULT ''") @@ -2440,8 +2445,9 @@ final class TrafficDatabase: @unchecked Sendable { try conn.run(""" INSERT INTO http_exchanges (ts, duration, scheme, host, port, method, path, status, req_headers, req_body, req_size, req_truncated, resp_headers, resp_body, resp_size, resp_truncated, content_type, pid, bundle_id, app_name, agent, - agent_name, mcp_server, tool_calls, note, tool_results, mcp, llm, mock_rule, guardrail, response_transform) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + agent_name, mcp_server, tool_calls, note, tool_results, mcp, llm, mock_rule, guardrail, request_rewrite, + response_transform) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, [.double(e.started.timeIntervalSince1970), .double(e.duration), .text(e.scheme), .text(e.host), .int(Int64(e.port)), .text(e.method), .text(e.path), e.status.map { .int(Int64($0)) } ?? .null, .text(json(e.requestHeaders)), .blob(e.requestBody), .int(Int64(e.requestSize)), .int(e.requestTruncated ? 1 : 0), @@ -2449,7 +2455,8 @@ final class TrafficDatabase: @unchecked Sendable { .text(e.contentType), .int(Int64(e.pid)), .text(e.bundleID), .text(e.appName), .text(e.agent ?? ""), .text(e.agentName ?? ""), .text(e.mcpServer ?? ""), .text(e.toolCalls.isEmpty ? "" : json(e.toolCalls)), .text(e.note ?? ""), .text(e.toolResults.isEmpty ? "" : json(e.toolResults)), .text(e.mcp.isEmpty ? "" : json(e.mcp)), - .text(e.llm.map(json) ?? ""), .text(e.mockRule ?? ""), .text(e.guardrail ?? ""), .text(e.responseTransform ?? "")]) + .text(e.llm.map(json) ?? ""), .text(e.mockRule ?? ""), .text(e.guardrail ?? ""), + .text(e.requestRewrite ?? ""), .text(e.responseTransform ?? "")]) insertedID = try conn.query("SELECT last_insert_rowid()", map: { $0.int(0) }).first ?? 0 var stored = e stored.id = insertedID @@ -2753,7 +2760,7 @@ final class TrafficDatabase: @unchecked Sendable { return try conn.query(""" SELECT id, ts, duration, scheme, host, port, method, path, status, req_headers, req_size, req_truncated, resp_headers, resp_size, resp_truncated, content_type, pid, bundle_id, app_name, agent, agent_name, mcp_server, tool_calls, note, - tool_results, mcp, llm, mock_rule, guardrail, response_transform + tool_results, mcp, llm, mock_rule, guardrail, request_rewrite, response_transform FROM http_exchanges WHERE ts >= ? AND (? = '' OR host LIKE ? OR path LIKE ? OR app_name LIKE ? OR agent_name LIKE ? OR tool_calls LIKE ? OR mcp LIKE ? OR CAST(req_body AS TEXT) LIKE ? OR CAST(resp_body AS TEXT) LIKE ?)\(focusClause)\(scopeClause)\(riskClause) ORDER BY ts DESC LIMIT ? @@ -2775,7 +2782,7 @@ final class TrafficDatabase: @unchecked Sendable { mcp: mcp.isEmpty ? [] : ((try? decoder.decode([MCPActivity].self, from: Data(mcp.utf8))) ?? []), llm: llm.isEmpty ? nil : try? decoder.decode(LLMFacts.self, from: Data(llm.utf8)), note: row.text(23).nilIfEmpty, mockRule: row.text(27).nilIfEmpty, guardrail: row.text(28).nilIfEmpty, - responseTransform: row.text(29).nilIfEmpty) + requestRewrite: row.text(29).nilIfEmpty, responseTransform: row.text(30).nilIfEmpty) } } diff --git a/Flowlight/UI/AgentsView.swift b/Flowlight/UI/AgentsView.swift index c0fa3da4..f8451db9 100644 --- a/Flowlight/UI/AgentsView.swift +++ b/Flowlight/UI/AgentsView.swift @@ -1,5 +1,18 @@ import SwiftUI +struct AgentPolicyDeepLinkState: Equatable { + var request: AgentPolicyRequest? + + func section(for agentID: String) -> AgentPolicySection? { + guard request?.agentID == agentID else { return nil } + return request?.section + } + + mutating func consume() { + request = nil + } +} + enum AgentWindow: String, CaseIterable, Identifiable { case hour, day, week var id: String { rawValue } @@ -32,6 +45,7 @@ struct AgentsView: View { @State private var inspectedAgents: Set = [] @ObservedObject private var workspaceStore = AgentWorkspaceStore.shared @State private var selection: AgentSummary.ID? + @State private var policyDeepLink = AgentPolicyDeepLinkState() @State private var sortOrder = [KeyPathComparator(\AgentSummary.riskScore, order: .reverse)] @State private var loaded = false @@ -93,15 +107,24 @@ struct AgentsView: View { activity: toolActivity[selected.bundleID] ?? [], servers: mcpServers[selected.bundleID] ?? [], profile: profiles[selected.bundleID], inspected: inspectedAgents.contains(selected.bundleID), workspaces: workspaceStore.workspaces(bundleID: selected.bundleID, name: selected.name), - policy: policies[selected.bundleID] ?? AgentPolicy(agentID: selected.bundleID, enabled: false)) { policy in + policy: policies[selected.bundleID] ?? AgentPolicy(agentID: selected.bundleID, enabled: false), + requestedPolicySection: policyDeepLink.section(for: selected.bundleID), + consumeRequestedPolicySection: { policyDeepLink.consume() }) { policy in policies[policy.agentID] = policy monitor.savePolicy(policy) } + .id(selected.id) } } } .padding() .navigationTitle(L("AI Agents")) + .onChange(of: nav.agentPolicyRequest, initial: true) { _, request in + guard let request else { return } + selection = request.agentID + policyDeepLink.request = request + nav.agentPolicyRequest = nil + } .task(id: LoadKey(window: window, version: monitor.dataVersion, focus: focus.scope)) { await load() } } @@ -530,12 +553,18 @@ struct AgentDetail: View { var inspected = false var workspaces: [AgentWorkspace] = [] var policy: AgentPolicy + var requestedPolicySection: AgentPolicySection? + var consumeRequestedPolicySection: () -> Void = {} var save: (AgentPolicy) -> Void @State private var tab = Tab.fromLaunchArgument enum Tab: Hashable { case destinations, calls, tools, servers, guardrails + static func policyLanding(_ section: AgentPolicySection) -> Tab { + section == .guardrails ? .guardrails : .destinations + } + /// `-FLAgentTab tools`, for the screenshots. The default is what someone actually opens on. static var fromLaunchArgument: Tab { switch UserDefaults.standard.string(forKey: "FLAgentTab") { @@ -739,6 +768,13 @@ struct AgentDetail: View { } .lineLimit(1) } + .onChange(of: requestedPolicySection, initial: true) { _, section in + guard let section else { return } + // The allowlist is always visible in the left column; returning the right column to Destinations keeps + // it in the normal overview instead of preserving a Guardrails tab from a previous deep link. + tab = Tab.policyLanding(section) + consumeRequestedPolicySection() + } } /// Tools declared to the model, plus everything configured on disk. diff --git a/Flowlight/UI/CaptureView.swift b/Flowlight/UI/CaptureView.swift index 815dbd01..7f4bebb7 100644 --- a/Flowlight/UI/CaptureView.swift +++ b/Flowlight/UI/CaptureView.swift @@ -1,177 +1,81 @@ import SwiftUI -/// Pure recovery-state decisions keep the Sources checklist honest and easy to exercise without constructing views. -enum SourceSetupPolicy { - enum CoverageHealth: Equatable { case empty, good, warning } - - static func coverageHealth(named: Double, hasTraffic: Bool) -> CoverageHealth { - guard hasTraffic else { return .empty } - return named > 0.8 ? .good : .warning - } - - static func collectorConfigured(mode: CaptureMode, extensionState: ExtensionManager.State, - extensionFellBack: Bool) -> Bool { - if mode == .nettop { return true } - return extensionState == .enabled && !extensionFellBack - } - - static func setupComplete(mode: CaptureMode, extensionState: ExtensionManager.State, - extensionFellBack: Bool, isReceiving: Bool) -> Bool { - collectorConfigured(mode: mode, extensionState: extensionState, - extensionFellBack: extensionFellBack) && isReceiving - } -} - -/// Collector setup, device channels and coverage belong to one operational destination. The compact health strip -/// answers "is Flowlight working?" before someone has to know which of the three tabs contains the recovery action. +/// Collector setup, device channels and coverage form one operational document. A requested section is a scroll +/// target, not a visibility filter: links and restored windows establish context while the full workflow remains. struct SourcesView: View { @Binding var selection: SourcesTab - @EnvironmentObject var monitor: TrafficMonitor - @EnvironmentObject var extensionManager: ExtensionManager + var requestID: UUID + @AccessibilityFocusState private var accessibilityFocus: SourcesTab? + @State private var highlightedSection: SourcesTab? + @State private var focusGeneration = UUID() var body: some View { - VStack(spacing: 0) { - VStack(alignment: .leading, spacing: 10) { - Picker(L("Sources"), selection: $selection) { - ForEach(SourcesTab.allCases) { tab in Text(tab.title).tag(tab) } - } - .pickerStyle(.segmented) - .labelsHidden() - .frame(maxWidth: 420) - - HStack(spacing: 10) { - healthCard(.collector, icon: "antenna.radiowaves.left.and.right", - title: L("Collector"), detail: collectorDetail, - tint: monitor.isReceiving ? FL.good : FL.warning) - healthCard(.coverage, icon: "circle.dashed.inset.filled", - title: L("Coverage"), detail: coverageDetail, - tint: coverageTint) - healthCard(.devices, icon: "dot.radiowaves.left.and.right", - title: L("Devices"), detail: deviceDetail, - tint: monitor.devices.isWatchingAnything ? FL.good : .secondary) - } - - if !sourceSetupComplete { sourceRecoveryChecklist } - } - .padding(.horizontal, Measure.gutter) - .padding(.vertical, 12) - .background(.bar) - .overlay(alignment: .bottom) { Divider() } - - Group { - switch selection { - case .collector: CaptureView() - case .devices: DevicesView() - case .coverage: CoverageView() + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 32) { + sourcesSection(.collector, + icon: "antenna.radiowaves.left.and.right", + detail: L("Choose the traffic source and verify that data is arriving.")) { + CaptureView(embedded: true) + } + sourcesSection(.devices, + icon: "dot.radiowaves.left.and.right", + detail: L("Choose which local device channels Flowlight watches.")) { + DevicesView(embedded: true) + } + sourcesSection(.coverage, + icon: "circle.dashed.inset.filled", + detail: L("Check what Flowlight can name, attribute, and inspect.")) { + CoverageView(embedded: true) + } } + .padding(Measure.gutter) } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { focus(selection, in: proxy) } + .onChange(of: selection) { _, section in focus(section, in: proxy) } + .onChange(of: requestID) { focus(selection, in: proxy) } } .navigationTitle(L("Sources")) - .navigationSubtitle(selection.title) - } - - private var collectorDetail: String { - if monitor.extensionFellBack { - #if APP_STORE - // The Store product has no sampler fallback; its fail-closed source is unavailable, not nettop. - return L("Nothing is arriving") - #else - return L("nettop sampler") - #endif - } - return monitor.isReceiving ? L("Data is arriving") : L("Nothing is arriving") - } - - private var coverageHealth: SourceSetupPolicy.CoverageHealth { - SourceSetupPolicy.coverageHealth(named: monitor.coverage.named, - hasTraffic: monitor.coverage.hasTraffic) - } - - private var coverageDetail: String { - guard coverageHealth != .empty else { return L("Nothing recorded in the last hour") } - return "\(monitor.coverage.named.formatted(.percent.precision(.fractionLength(0)))) \(L("Named"))" - } - - private var coverageTint: Color { - switch coverageHealth { - case .empty: return .secondary - case .good: return FL.good - case .warning: return FL.warning - } } - private var deviceDetail: String { - guard monitor.devices.isWatchingAnything else { return L("Not watching") } - var channels: [String] = [] - if monitor.devices.watchingBluetooth { channels.append(L("Bluetooth")) } - if monitor.devices.watchingUSB { channels.append(L("USB")) } - if monitor.devices.watchingCamera { channels.append(L("Camera")) } - return channels.joined(separator: " · ") - } - - private var collectorConfigured: Bool { - SourceSetupPolicy.collectorConfigured(mode: monitor.mode, extensionState: extensionManager.state, - extensionFellBack: monitor.extensionFellBack) - } - - private var sourceSetupComplete: Bool { - SourceSetupPolicy.setupComplete(mode: monitor.mode, extensionState: extensionManager.state, - extensionFellBack: monitor.extensionFellBack, - isReceiving: monitor.isReceiving) - } - - /// A short recovery path stays beside the source status. It reports completed steps instead of presenting one - /// generic error, and the action opens the one place where every unfinished step can be repaired. - private var sourceRecoveryChecklist: some View { - HStack(alignment: .top, spacing: 14) { - VStack(alignment: .leading, spacing: 7) { - Text(monitor.isReceiving ? L("Collector") : L("Nothing is arriving")) - .font(.caption.weight(.semibold)) - checklistRow(monitor.mode == .networkExtension ? L("Network Extension") : L("nettop sampler"), - complete: collectorConfigured) - checklistRow(L("Data is arriving"), complete: monitor.isReceiving) - } - Spacer(minLength: 8) - Button { selection = .collector } label: { - Label(L("Collector"), systemImage: "chevron.right") - .labelStyle(.titleAndIcon) - } - .buttonStyle(.bordered) - .controlSize(.small) - } - .padding(10) - .background(FL.warning.opacity(0.10), in: RoundedRectangle(cornerRadius: 9)) - .overlay(RoundedRectangle(cornerRadius: 9).stroke(FL.warning.opacity(0.25))) - } - - private func checklistRow(_ title: String, complete: Bool) -> some View { - Label(title, systemImage: complete ? "checkmark.circle.fill" : "circle") - .font(.caption) - .foregroundStyle(complete ? FL.good : .secondary) - } - - private func healthCard(_ tab: SourcesTab, icon: String, title: String, - detail: String, tint: Color) -> some View { - Button { selection = tab } label: { - HStack(alignment: .top, spacing: 9) { + private func sourcesSection(_ section: SourcesTab, icon: String, detail: String, + @ViewBuilder content: () -> Content) -> some View { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { Image(systemName: icon) - .foregroundStyle(tint) - .frame(width: 22, height: 22) - VStack(alignment: .leading, spacing: 2) { - Text(title).font(.caption.weight(.semibold)) - Text(detail).font(.caption2).foregroundStyle(.secondary).lineLimit(1) + .font(.title3) + .foregroundStyle(FL.accent) + .frame(width: 26) + .accessibilityHidden(true) + VStack(alignment: .leading, spacing: 3) { + Text(section.title).font(.title2.weight(.semibold)) + Text(detail).font(.callout).foregroundStyle(.secondary).prose() } - Spacer(minLength: 0) } - .padding(10) - .frame(maxWidth: .infinity, alignment: .leading) - .background(selection == tab ? FL.accent.opacity(0.10) : Color.secondary.opacity(0.07), + .padding(8) + .background(highlightedSection == section ? FL.accent.opacity(0.12) : .clear, in: RoundedRectangle(cornerRadius: 9)) - .contentShape(Rectangle()) + .accessibilityFocused($accessibilityFocus, equals: section) + + content() + } + .id(section.anchorID) + } + + @MainActor + private func focus(_ section: SourcesTab, in proxy: ScrollViewProxy) { + selection = section + proxy.scrollTo(section.anchorID, anchor: .top) + accessibilityFocus = section + highlightedSection = section + + let generation = UUID() + focusGeneration = generation + Task { @MainActor in + try? await Task.sleep(for: .seconds(1.4)) + guard focusGeneration == generation else { return } + highlightedSection = nil } - .buttonStyle(.plain) - .accessibilityAddTraits(selection == tab ? .isSelected : []) } } @@ -182,6 +86,7 @@ struct SourcesView: View { /// explanations are all still here and all still true, but they are not what someone opens this panel for, so they /// sit behind a summary row instead of in front of the controls. struct CaptureView: View { + var embedded = false @EnvironmentObject var monitor: TrafficMonitor @EnvironmentObject var extensionManager: ExtensionManager @State private var showExtensionSetup = false @@ -198,27 +103,32 @@ struct CaptureView: View { monitor.mode == .networkExtension || showExtensionSetup } - var body: some View { - ScrollView { - LazyVStack(alignment: .leading, spacing: 18) { - statusCard - CaptureWarningBanner() - if !otherFilters.isEmpty { OtherFiltersNotice(filters: otherFilters) } - sourceSection - filterSection + @ViewBuilder var body: some View { + if embedded { + content #if !APP_STORE - if monitor.mode == .nettop { HostnameSection() } + // Which other content filters exist decides whether ours can work at all, so it's read on arrival. + .task { otherFilters = SystemExtensionScan.competingFilters(in: await SystemExtensionScan.read()) } + #endif + } else { + ScrollView { content.padding(Measure.gutter) } + #if !APP_STORE + .task { otherFilters = SystemExtensionScan.competingFilters(in: await SystemExtensionScan.read()) } #endif - } - // A settings panel is read, not scanned, and a line of prose stretched across a maximised window is - // unreadable. The column stops at a comfortable measure and stays against the sidebar rather than - // floating in the middle of the window. - .padding(Measure.gutter) } - #if !APP_STORE - // Which other content filters exist decides whether ours can work at all, so it's read when the screen opens. - .task { otherFilters = SystemExtensionScan.competingFilters(in: await SystemExtensionScan.read()) } - #endif + } + + private var content: some View { + LazyVStack(alignment: .leading, spacing: 18) { + statusCard + CaptureWarningBanner() + if !otherFilters.isEmpty { OtherFiltersNotice(filters: otherFilters) } + sourceSection + filterSection + #if !APP_STORE + if monitor.mode == .nettop { HostnameSection() } + #endif + } } // MARK: Source diff --git a/Flowlight/UI/ContentView.swift b/Flowlight/UI/ContentView.swift index 39685c3c..e5cedb21 100644 --- a/Flowlight/UI/ContentView.swift +++ b/Flowlight/UI/ContentView.swift @@ -92,7 +92,7 @@ struct ContentView: View { InspectView() #endif case .ask: AskView() - case .sources: SourcesView(selection: $nav.sourcesTab) + case .sources: SourcesView(selection: $nav.sourcesTab, requestID: nav.sourcesRequestID) } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Flowlight/UI/CoverageView.swift b/Flowlight/UI/CoverageView.swift index 8900c456..aaecbe7b 100644 --- a/Flowlight/UI/CoverageView.swift +++ b/Flowlight/UI/CoverageView.swift @@ -6,44 +6,63 @@ import SwiftUI /// the question a monitoring tool usually leaves to its documentation — and which decides how much the other /// screens are worth. struct CoverageView: View { + var embedded = false @EnvironmentObject var monitor: TrafficMonitor - var body: some View { CoverageContent(inspection: monitor.inspection) } + var body: some View { CoverageContent(inspection: monitor.inspection, embedded: embedded) } } private struct CoverageContent: View { @ObservedObject var inspection: InspectionController + var embedded: Bool @EnvironmentObject var monitor: TrafficMonitor @EnvironmentObject var nav: AppNavigation @AppStorage("coverage.window") private var window: AgentWindow = .day @State private var rows: [AppCoverage] = [] @State private var loaded = false - var body: some View { - ScrollView { - LazyVStack(alignment: .leading, spacing: 18) { - summary - if rows.isEmpty && loaded { - ContentUnavailableView { - Label(L("Nothing recorded yet"), systemImage: "chart.bar.doc.horizontal") - } description: { - Text(L("Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment.")) + @ViewBuilder var body: some View { + if embedded { + VStack(alignment: .leading, spacing: 14) { + windowPicker.labelsHidden().frame(maxWidth: 320, alignment: .leading) + content + } + .task(id: LoadKey(window: window, version: monitor.dataVersion, inspecting: inspection.enabled)) { + await load() + } + } else { + ScrollView { content.padding(Measure.gutter) } + .toolbar { + ToolbarItem(placement: .primaryAction) { + windowPicker.labelsHidden() } - } else { - table } - } - .padding(Measure.gutter) + .task(id: LoadKey(window: window, version: monitor.dataVersion, inspecting: inspection.enabled)) { + await load() + } } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Picker(L("Window"), selection: $window) { - ForEach(AgentWindow.allCases) { Text($0.title).tag($0) } + } + + private var windowPicker: some View { + Picker(L("Window"), selection: $window) { + ForEach(AgentWindow.allCases) { Text($0.title).tag($0) } + } + .pickerStyle(.segmented) + } + + private var content: some View { + LazyVStack(alignment: .leading, spacing: 18) { + summary + if rows.isEmpty && loaded { + ContentUnavailableView { + Label(L("Nothing recorded yet"), systemImage: "chart.bar.doc.horizontal") + } description: { + Text(L("Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment.")) } - .pickerStyle(.segmented).labelsHidden() + } else { + table } } - .task(id: LoadKey(window: window, version: monitor.dataVersion, inspecting: inspection.enabled)) { await load() } } private struct LoadKey: Equatable { var window: AgentWindow; var version: Int; var inspecting: Bool } diff --git a/Flowlight/UI/DevicesView.swift b/Flowlight/UI/DevicesView.swift index 17936168..e9feaeb0 100644 --- a/Flowlight/UI/DevicesView.swift +++ b/Flowlight/UI/DevicesView.swift @@ -7,47 +7,77 @@ import SwiftUI /// that changed. It does not report bytes, because macOS does not account for these per app and a number invented /// here would be worse than no number. struct DevicesView: View { + var embedded = false @EnvironmentObject var monitor: TrafficMonitor var body: some View { - DevicesContent(store: monitor.devices) + DevicesContent(store: monitor.devices, embedded: embedded) } } private struct DevicesContent: View { @ObservedObject var store: DeviceStore + var embedded: Bool - var body: some View { - Group { - if store.isWatchingAnything { - content + @ViewBuilder var body: some View { + if embedded { + VStack(alignment: .leading, spacing: 12) { + embeddedControls + deviceContent + } + } else { + deviceContent.toolbar { toolbar } + } + } + + @ViewBuilder + private var deviceContent: some View { + if store.isWatchingAnything { + if embedded { + sections } else { - offer + ScrollView { sections.padding(Measure.gutter) } } + } else { + offer } - .toolbar { toolbar } } @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItemGroup { - if store.isWatchingAnything { - Button { store.refresh() } label: { Label(L("Refresh"), systemImage: "arrow.clockwise") } - .keyboardShortcut("r", modifiers: .command) - .help(L("Read the device list again now (⌘R)")) - } - Menu { - Toggle(L("Watch Bluetooth"), isOn: Binding(get: { store.watchingBluetooth }, - set: { store.watchingBluetooth = $0 })) - Toggle(L("Watch USB and external storage"), isOn: Binding(get: { store.watchingUSB }, - set: { store.watchingUSB = $0 })) - Toggle(L("Watch camera activity"), isOn: Binding(get: { store.watchingCamera }, - set: { store.watchingCamera = $0 })) - } label: { - Label(L("Channels"), systemImage: "switch.2") - } - .help(L("Choose which channels Flowlight watches")) + if store.isWatchingAnything { refreshButton } + channelMenu + } + } + + private var embeddedControls: some View { + HStack(spacing: 8) { + channelMenu + if store.isWatchingAnything { refreshButton } + Spacer(minLength: 0) } + .controlSize(.small) + } + + private var refreshButton: some View { + Button { store.refresh() } label: { Label(L("Refresh"), systemImage: "arrow.clockwise") } + .keyboardShortcut("r", modifiers: .command) + .help(L("Read the device list again now (⌘R)")) + } + + private var channelMenu: some View { + Menu { + Toggle(L("Watch Bluetooth"), isOn: Binding(get: { store.watchingBluetooth }, + set: { store.watchingBluetooth = $0 })) + Toggle(L("Watch USB and external storage"), isOn: Binding(get: { store.watchingUSB }, + set: { store.watchingUSB = $0 })) + Toggle(L("Watch camera activity"), isOn: Binding(get: { store.watchingCamera }, + set: { store.watchingCamera = $0 })) + } label: { + Label(L("Channels"), systemImage: "switch.2") + } + .help(L("Choose which channels Flowlight watches")) } /// Nothing is on yet. The offer says what each channel can and can't tell you before it is switched on, rather @@ -66,16 +96,13 @@ private struct DevicesContent: View { // MARK: The screen - private var content: some View { - ScrollView { - LazyVStack(alignment: .leading, spacing: 18) { - accounting - if store.watchingBluetooth { bluetoothSection } - if store.watchingUSB { usbSection } - if store.watchingCamera { cameraSection } - if !store.events.isEmpty { historySection } - } - .padding(Measure.gutter) + private var sections: some View { + LazyVStack(alignment: .leading, spacing: 18) { + accounting + if store.watchingBluetooth { bluetoothSection } + if store.watchingUSB { usbSection } + if store.watchingCamera { cameraSection } + if !store.events.isEmpty { historySection } } } diff --git a/Flowlight/UI/InspectView.swift b/Flowlight/UI/InspectView.swift index 4bc3ee03..1e1b294e 100644 --- a/Flowlight/UI/InspectView.swift +++ b/Flowlight/UI/InspectView.swift @@ -98,15 +98,17 @@ private struct InspectContent: View { @State private var rewriteDraft: RewriteRuleDraft? /// A response-transform rule is created only after its selected exchange's stored response has been read. @State private var responseTransformDraft: ResponseTransformDraft? - /// Setup was opened to look at the mock rules, so it opens on them. + /// Setup was opened to look at one contextual policy editor, so it opens on that section. @State private var openMocks = false + @State private var openRewrites = false @State private var openResponseTransforms = false var body: some View { VStack(alignment: .leading, spacing: 12) { if (!inspection.enabled || showSetup) && !DemoData.isEnabled { ScrollView { - InspectionSetup(inspection: inspection, openMocks: openMocks, openResponseTransforms: openResponseTransforms, done: { showSetup = false }) + InspectionSetup(inspection: inspection, openMocks: openMocks, openRewrites: openRewrites, + openResponseTransforms: openResponseTransforms, done: { showSetup = false }) .padding(.bottom, 20) } } else { @@ -163,6 +165,12 @@ private struct InspectContent: View { scopeAgentParent = request.agentParent scopeMCPServer = request.mcpServer potentialHarmOnly = request.potentialHarmOnly + if let section = request.policySection { + showSetup = true + openMocks = section == .mocks + openRewrites = section == .requestRewrites + openResponseTransforms = section == .responseTransforms + } nav.inspectRequest = nil } .onChange(of: monitor.localRiskVersion) { @@ -510,6 +518,7 @@ private struct InspectContent: View { private struct InspectionSetup: View { @ObservedObject var inspection: InspectionController var openMocks = false + var openRewrites = false var openResponseTransforms = false var done: () -> Void @State private var newPattern = "" @@ -685,6 +694,7 @@ private struct InspectionSetup: View { .onAppear { inspection.refreshStatus() showMocks = showMocks || openMocks + showRewrites = showRewrites || openRewrites showResponseTransforms = showResponseTransforms || openResponseTransforms } } diff --git a/Flowlight/UI/ReportsView.swift b/Flowlight/UI/ReportsView.swift index f60f974c..31e18ee8 100644 --- a/Flowlight/UI/ReportsView.swift +++ b/Flowlight/UI/ReportsView.swift @@ -146,6 +146,11 @@ enum ReportChartHoverLayout { : lineX - gap - cardWidth return min(max(0, proposed), max(0, chartWidth - cardWidth)) } + + static func contains(_ location: CGPoint, in plotFrame: CGRect) -> Bool { + location.x > plotFrame.minX && location.x < plotFrame.maxX + && location.y > plotFrame.minY && location.y < plotFrame.maxY + } } enum ReportChartKeyboardDirection { @@ -161,6 +166,17 @@ enum ReportChartKeyboardSelection { case .right: return dates[min(dates.count - 1, index + 1)] } } + + static func reconcile(_ current: Date?, chartFocused: Bool, dates: [Date]) -> Date? { + guard chartFocused, let current, !dates.contains(current) else { return current } + return move(from: nil, direction: .right, dates: dates) + } +} + +enum ReportChartHighlightSelection { + static func date(hovered: Date?, keyboard: Date?, chartFocused: Bool) -> Date? { + hovered ?? (chartFocused ? keyboard : nil) + } } struct ReportNavigationState: Equatable { @@ -252,10 +268,12 @@ struct ReportsView: View { /// From the series, not the table rows, so shares stay right even if the table is capped. private var grandTotal: Int64 { max(1, series.reduce(0) { $0 + $1.total }) } private var highlightedPoint: SeriesPoint? { - if chartFocused, let keyboardBucketDate { - return series.first { $0.date == keyboardBucketDate } - } - return hovered + let date = ReportChartHighlightSelection.date( + hovered: hovered?.date, + keyboard: keyboardBucketDate, + chartFocused: chartFocused + ) + return series.first { $0.date == date } } var body: some View { @@ -701,9 +719,12 @@ struct ReportsView: View { case .active(let location): hoverDismissID = UUID() tooltipHovered = false - hovered = point(at: location, proxy: proxy, geo: geo) - hoverLineX = hovered.flatMap { lineX(for: $0, proxy: proxy, geo: geo) } - ?? location.x + if let point = point(at: location, proxy: proxy, geo: geo) { + hovered = point + hoverLineX = lineX(for: point, proxy: proxy, geo: geo) ?? location.x + } else { + clearTransientChartHover() + } case .ended: scheduleHoverDismissal() } @@ -713,17 +734,12 @@ struct ReportsView: View { .focusable() .focusEffectDisabled() .focused($chartFocused) - .onChange(of: chartFocused) { _, focused in - guard focused else { return } - keyboardBucketDate = ReportChartKeyboardSelection.move( - from: keyboardBucketDate, - direction: .right, - dates: series.map(\.date) - ) - } .onChange(of: series.map(\.date)) { _, dates in - guard chartFocused, !dates.contains(keyboardBucketDate ?? .distantPast) else { return } - keyboardBucketDate = ReportChartKeyboardSelection.move(from: nil, direction: .right, dates: dates) + keyboardBucketDate = ReportChartKeyboardSelection.reconcile( + keyboardBucketDate, + chartFocused: chartFocused, + dates: dates + ) } .onMoveCommand { direction in let keyboardDirection: ReportChartKeyboardDirection @@ -770,6 +786,9 @@ struct ReportsView: View { } } } + .onHover { inside in + if !inside { clearTransientChartHover() } + } .frame(height: 300) HistoryNavigator(points: overview, contextPoints: navigatorDetail, available: availableRange, selection: window, onCommit: apply) @@ -1107,8 +1126,9 @@ struct ReportsView: View { private func point(at location: CGPoint, proxy: ChartProxy, geo: GeometryProxy) -> SeriesPoint? { guard let plot = proxy.plotFrame else { return nil } - let origin = geo[plot].origin - guard let date: Date = proxy.value(atX: location.x - origin.x) else { return nil } + let frame = geo[plot] + guard ReportChartHoverLayout.contains(location, in: frame), + let date: Date = proxy.value(atX: location.x - frame.origin.x) else { return nil } return series.last { $0.date <= date } ?? series.first } @@ -1118,14 +1138,20 @@ struct ReportsView: View { return geo[plot].origin.x + x } + private func clearTransientChartHover() { + hoverDismissID = UUID() + tooltipHovered = false + hovered = nil + hoverLineX = nil + } + private func scheduleHoverDismissal() { let requestID = UUID() hoverDismissID = requestID Task { @MainActor in try? await Task.sleep(for: .milliseconds(180)) guard hoverDismissID == requestID, !tooltipHovered else { return } - hovered = nil - hoverLineX = nil + clearTransientChartHover() } } diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift index 1b45d6cd..54386f57 100644 --- a/Flowlight/UI/RulePacksView.swift +++ b/Flowlight/UI/RulePacksView.swift @@ -297,7 +297,7 @@ private struct RulePacksContent: View { } } -private struct RulePackEditor: View { +struct RulePackEditor: View { let pack: RulePackDefinition let receipt: RulePackReviewReceipt? @EnvironmentObject var monitor: TrafficMonitor diff --git a/Flowlight/UI/RulesView.swift b/Flowlight/UI/RulesView.swift index 94d6323d..128d45de 100644 --- a/Flowlight/UI/RulesView.swift +++ b/Flowlight/UI/RulesView.swift @@ -1,6 +1,326 @@ import SwiftUI -/// Every rule Flowlight has been given, what each one is doing, and what they have refused. +struct PolicyInventoryCapabilities: Equatable, Sendable { + var inspectionPolicies: Bool + + static var current: PolicyInventoryCapabilities { + #if APP_STORE + PolicyInventoryCapabilities(inspectionPolicies: false) + #else + PolicyInventoryCapabilities(inspectionPolicies: true) + #endif + } +} + +enum PolicyInventoryKind: String, CaseIterable, Sendable { + case networkRule = "rule" + case agentAllowlist = "allowlist" + case managedPack = "pack" + case guardrail + case mock + case requestRewrite = "request-rewrite" + case responseTransform = "response-transform" + + var requiresInspection: Bool { + switch self { + case .guardrail, .mock, .requestRewrite, .responseTransform: return true + case .networkRule, .agentAllowlist, .managedPack: return false + } + } + + @MainActor var sectionTitle: String { + switch self { + case .networkRule: return L("Network rules") + case .agentAllowlist: return L("Agent allowlists") + case .managedPack: return L("Managed packs") + case .guardrail: return L("Agent guardrails") + case .mock: return L("Mock responses") + case .requestRewrite: return L("Request rewrites") + case .responseTransform: return L("Response transforms") + } + } + + var symbol: String { + switch self { + case .networkRule: return "hand.raised" + case .agentAllowlist: return "checklist" + case .managedPack: return "shippingbox" + case .guardrail: return "wrench.and.screwdriver" + case .mock: return "wand.and.stars" + case .requestRewrite: return "slider.horizontal.3" + case .responseTransform: return "arrow.left.arrow.right" + } + } +} + +enum PolicyInventoryID { + static func make(_ kind: PolicyInventoryKind, sourceID: String) -> String { + "\(kind.rawValue):\(sourceID)" + } +} + +enum PolicyEffectState: Equatable, Sendable { + case effective + case reportingOnly + case watchingOnly + case waiting + case off + case needsReview + case unavailable + case failed + + var isEffective: Bool { + switch self { + case .effective, .reportingOnly, .watchingOnly: return true + case .waiting, .off, .needsReview, .unavailable, .failed: return false + } + } + + @MainActor var title: String { + switch self { + case .effective: return L("In effect") + case .reportingOnly: return L("Reporting only") + case .watchingOnly: return L("Watching only") + case .waiting: return L("Waiting") + case .off: return L("Off") + case .needsReview: return L("Needs review") + case .unavailable: return L("Unavailable") + case .failed: return L("Not applying") + } + } +} + +struct PolicyInventoryItem: Identifiable, Equatable, Sendable { + var kind: PolicyInventoryKind + var sourceID: String + var title: String + var detail: String + /// The persisted switch or configuration intent. This is not evidence that the policy can currently act. + var isEnabled: Bool + var effect: PolicyEffectState + var reason: String? + var destination: PolicyDestination + var id: String { PolicyInventoryID.make(kind, sourceID: sourceID) } + + func isVisible(in capabilities: PolicyInventoryCapabilities) -> Bool { + capabilities.inspectionPolicies || !kind.requiresInspection + } +} + +enum PolicyInventoryBuilder { + @MainActor + static func build(rules: [Rule], allowlists: [String: AgentPolicy], packs: [RulePackDefinition], + unavailablePacks: [RulePackConfiguration] = [], + packConfiguration: (String) -> RulePackConfiguration, + packNeedsReview: (RulePackDefinition) -> Bool = { _ in false }, + packStatus: RulePackStore.ApplyStatus = .off, + guardrails: [Guardrail], mocks: [MockRule], rewrites: [RewriteRule], + transforms: [ResponseTransformRule], extensionRunning: Bool = true, + canBlock: Bool = true, inspectionEnabled: Bool = true, inspectionRunning: Bool = true, + rulesPaused: Bool = false, now: Date = Date(), ruleSession: String = RuleStore.session, + capabilities: PolicyInventoryCapabilities = .current) -> [PolicyInventoryItem] { + let inspectionReason = !inspectionEnabled ? L("HTTPS inspection is off.") : L("HTTPS inspection is not running.") + func inspectionEffect(_ enabled: Bool) -> (PolicyEffectState, String?) { + guard enabled else { return (.off, L("This policy is switched off.")) } + return inspectionEnabled && inspectionRunning ? (.effective, nil) : (.waiting, inspectionReason) + } + + var items = rules.map { rule -> PolicyInventoryItem in + let effect: PolicyEffectState + let reason: String? + if !rule.enabled { + (effect, reason) = (.off, L("This rule is switched off.")) + } else if rulesPaused { + (effect, reason) = (.waiting, L("Network blocking is paused.")) + } else if !rule.isUsable { + (effect, reason) = (.waiting, L("This one-off rule has already been used.")) + } else if !rule.schedule.isActive(at: now, session: ruleSession) { + (effect, reason) = (.waiting, L("This rule is outside its scheduled time.")) + } else if let limitation = RuleWords.limitation(rule, extensionRunning: extensionRunning, + inspecting: inspectionEnabled && inspectionRunning) { + (effect, reason) = (.watchingOnly, limitation) + } else { + (effect, reason) = (.effective, nil) + } + return PolicyInventoryItem(kind: .networkRule, sourceID: rule.id.uuidString.lowercased(), + title: RuleWords.title(rule), + detail: rule.destination.isEmpty ? L("Every destination") : rule.destination, + isEnabled: rule.enabled, effect: effect, reason: reason, + destination: .rule(rule.id)) + } + items += allowlists.keys.sorted().compactMap { agentID in + guard let policy = allowlists[agentID] else { return nil } + let detail = policy.patterns.isEmpty ? L("No listed destinations") + : (policy.patterns.count == 1 ? L("1 listed destination") : L("%lld listed destinations", policy.patterns.count)) + let effect: PolicyEffectState + let reason: String? + if !policy.enabled { + (effect, reason) = (.off, L("This allowlist is switched off.")) + } else if !policy.enforce { + (effect, reason) = (.reportingOnly, L("Unlisted destinations raise alerts but are not blocked.")) + } else if canBlock { + (effect, reason) = (.effective, nil) + } else { + (effect, reason) = (.reportingOnly, L("Blocking is unavailable; unlisted destinations still raise alerts.")) + } + return PolicyInventoryItem(kind: .agentAllowlist, sourceID: agentID, title: agentID, detail: detail, + isEnabled: policy.enabled, effect: effect, reason: reason, + destination: .allowlist(agentID)) + } + items += packs.map { pack in + let configuration = packConfiguration(pack.id) + let effect: PolicyEffectState + let reason: String? + if packNeedsReview(pack) { + (effect, reason) = (.needsReview, L("Review the changed pack before it can keep applying.")) + } else if !configuration.enabled { + (effect, reason) = (.off, L("This pack is switched off.")) + } else { + switch packStatus { + case .enforcing where canBlock: (effect, reason) = (.effective, nil) + case .watchingOnly, .enforcing: + (effect, reason) = (.watchingOnly, L("The pack is matching destinations but cannot block them here.")) + case .applying, .ready: + (effect, reason) = (.waiting, L("The pack is waiting for the active filter to apply it.")) + case .incompatible(let message), .failed(let message): + (effect, reason) = (.failed, message) + case .off: + (effect, reason) = (.waiting, L("The managed-pack engine is not applying this configuration.")) + } + } + return PolicyInventoryItem(kind: .managedPack, sourceID: pack.id, title: L(pack.manifest.titleKey), + detail: L("%lld destinations", pack.entries.count), + isEnabled: configuration.enabled, effect: effect, reason: reason, + destination: .managedPack(pack.id)) + } + let knownPackIDs = Set(packs.map(\.id)) + items += unavailablePacks.filter { !knownPackIDs.contains($0.packID) }.map { configuration in + PolicyInventoryItem(kind: .managedPack, sourceID: configuration.packID, title: configuration.packID, + detail: L("No longer in the official catalog"), isEnabled: configuration.enabled, + effect: .unavailable, + reason: L("Blocking stopped automatically. Acknowledge this change after review."), + destination: .managedPack(configuration.packID)) + } + items += guardrails.map { guardrail in + let (effect, reason) = inspectionEffect(guardrail.enabled) + return PolicyInventoryItem(kind: .guardrail, sourceID: guardrail.id.uuidString.lowercased(), + title: guardrail.localizedTitle, + detail: guardrail.agent.isEmpty ? L("Every agent") : guardrail.agent, + isEnabled: guardrail.enabled, effect: effect, reason: reason, + destination: .guardrail(id: guardrail.id, agentID: guardrail.agent)) + } + items += mocks.map { rule in + let (effect, reason) = inspectionEffect(rule.enabled) + return PolicyInventoryItem(kind: .mock, sourceID: rule.id.uuidString.lowercased(), title: rule.title, + detail: "\(rule.method.isEmpty ? "ANY" : rule.method.uppercased()) \(rule.host)\(rule.path)", + isEnabled: rule.enabled, effect: effect, reason: reason, + destination: .mock(rule.id)) + } + items += rewrites.map { rule in + let (effect, reason) = inspectionEffect(rule.enabled) + return PolicyInventoryItem(kind: .requestRewrite, sourceID: rule.id.uuidString.lowercased(), title: rule.title, + detail: "\(rule.method.isEmpty ? "ANY" : rule.method.uppercased()) \(rule.host)\(rule.path)", + isEnabled: rule.enabled, effect: effect, reason: reason, + destination: .requestRewrite(rule.id)) + } + items += transforms.map { rule in + let (effect, reason) = inspectionEffect(rule.enabled) + return PolicyInventoryItem(kind: .responseTransform, sourceID: rule.id.uuidString.lowercased(), title: rule.title, + detail: "\(rule.method.isEmpty ? "ANY" : rule.method.uppercased()) \(rule.host)\(rule.path)", + isEnabled: rule.enabled, effect: effect, reason: reason, + destination: .responseTransform(rule.id)) + } + return items.filter { $0.isVisible(in: capabilities) } + } +} + +enum PolicyActivityEffect: String, Equatable, Hashable, Sendable { + case blocked + case allowed + case packBlocked = "pack-blocked" + case allowlistViolation = "allowlist-violation" + case guardrailApplied = "guardrail-applied" + case mocked + case requestRewritten = "request-rewritten" + case responseTransformed = "response-transformed" +} + +struct PolicyActivityItem: Identifiable, Equatable, Sendable { + var id: String + var timestamp: Date + var kind: PolicyInventoryKind + var effect: PolicyActivityEffect + var title: String + var detail: String + var policyID: String? +} + +enum PolicyActivityBuilder { + static func build(ruleEvents: [RuleEventRecord], packEvents: [RulePackEventRecord], alerts: [AlertRecord], + exchanges: [HTTPExchange], rules: [Rule], guardrails: [Guardrail], packs: [RulePackDefinition], + mocks: [MockRule], rewrites: [RewriteRule], transforms: [ResponseTransformRule], + capabilities: PolicyInventoryCapabilities = .current) -> [PolicyActivityItem] { + let guardrailIDs = Set(guardrails.map(\.id)) + var activity = ruleEvents.map { event -> PolicyActivityItem in + let isGuardrail = guardrailIDs.contains(event.ruleID) + let title = rules.first(where: { $0.id == event.ruleID }).map(RuleWords.title) + ?? guardrails.first(where: { $0.id == event.ruleID })?.localizedTitle ?? L("A policy since deleted") + return PolicyActivityItem( + id: "rule-event:\(event.id)", timestamp: event.timestamp, + kind: isGuardrail ? .guardrail : .networkRule, + effect: isGuardrail ? .guardrailApplied : (event.action == .block ? .blocked : .allowed), + title: title, detail: event.path.isEmpty ? "\(event.destination):\(event.port)" : "\(event.method) \(event.destination)\(event.path)", + policyID: PolicyInventoryID.make(isGuardrail ? .guardrail : .networkRule, + sourceID: event.ruleID.uuidString.lowercased())) + } + activity += packEvents.map { event in + let title = packs.first(where: { $0.id == event.packID }).map { L($0.manifest.titleKey) } ?? event.packID + return PolicyActivityItem(id: "pack-event:\(event.id)", timestamp: event.lastAt, kind: .managedPack, + effect: .packBlocked, + title: title, detail: L("%@ · %lld connections", event.host, event.count), + policyID: PolicyInventoryID.make(.managedPack, sourceID: event.packID)) + } + activity += alerts.filter { $0.kind == AnomalyEngine.Kind.allowlistViolation.rawValue }.map { alert in + PolicyActivityItem(id: "alert:\(alert.id)", timestamp: alert.timestamp, kind: .agentAllowlist, + effect: .allowlistViolation, + title: alert.appName, detail: alert.detail, + policyID: PolicyInventoryID.make(.agentAllowlist, sourceID: alert.bundleID)) + } + for exchange in exchanges { + let source = exchange.id.map(String.init) ?? "\(exchange.started.timeIntervalSince1970)" + let detail = "\(exchange.method) \(exchange.host)\(exchange.path)" + if let value = exchange.guardrail { + let policy = guardrails.first { $0.localizedTitle == value || $0.title == value } + activity.append(PolicyActivityItem(id: "exchange:\(source):guardrail", timestamp: exchange.started, + kind: .guardrail, effect: .guardrailApplied, title: value, detail: detail, + policyID: policy.map { PolicyInventoryID.make(.guardrail, sourceID: $0.id.uuidString.lowercased()) })) + } + if let value = exchange.mockRule, let policy = mocks.first(where: { $0.title == value }) { + // Request-level network blocks use the same answer machinery but already have a RuleEvent. Only a + // saved mock belongs in this adapter, otherwise one refusal would appear twice with two effects. + activity.append(PolicyActivityItem(id: "exchange:\(source):mock", timestamp: exchange.started, + kind: .mock, effect: .mocked, title: value, detail: detail, + policyID: PolicyInventoryID.make(.mock, sourceID: policy.id.uuidString.lowercased()))) + } + if let value = exchange.requestRewrite { + let policy = rewrites.first { $0.title == value } + activity.append(PolicyActivityItem(id: "exchange:\(source):request-rewrite", timestamp: exchange.started, + kind: .requestRewrite, effect: .requestRewritten, title: value, detail: detail, + policyID: policy.map { PolicyInventoryID.make(.requestRewrite, sourceID: $0.id.uuidString.lowercased()) })) + } + if let value = exchange.responseTransform { + let policy = transforms.first { $0.title == value } + activity.append(PolicyActivityItem(id: "exchange:\(source):response-transform", timestamp: exchange.started, + kind: .responseTransform, effect: .responseTransformed, title: value, detail: detail, + policyID: policy.map { PolicyInventoryID.make(.responseTransform, sourceID: $0.id.uuidString.lowercased()) })) + } + } + return activity.filter { capabilities.inspectionPolicies || !$0.kind.requiresInspection } + .sorted { ($0.timestamp, $0.id) > ($1.timestamp, $1.id) } + } +} + +/// Every policy Flowlight has been given, what each one is doing, and what it has changed. /// /// The screen answers three questions, in the order people ask them: what have I said, is it actually happening, /// and why is this getting through? The last two are why the list carries a limitation beside a rule the current @@ -11,17 +331,24 @@ struct RulesView: View { @ObservedObject private var templates = RuleTemplateRequests.shared @State private var sheet: Sheet? @State private var now = Date() + @State private var allowlists: [String: AgentPolicy] = [:] + @State private var alerts: [AlertRecord] = [] + @State private var exchanges: [HTTPExchange] = [] + @State private var focusPolicyID: String? + @State private var packSaveErrors: [String: String] = [:] /// One sheet at a time, named rather than implied by two booleans — the editor and the library are alternatives /// and the type should say so. private enum Sheet: Identifiable { case editor(Rule, isNew: Bool) case library(RuleTemplate.ID?, RuleTemplate.Subject) + case pack(RulePackDefinition, RulePackEditorDraft.Intent) var id: String { switch self { case .editor(let rule, _): return "editor-\(rule.id)" case .library(let focus, _): return "library-\(focus ?? "all")" + case .pack(let pack, let intent): return "pack-\(pack.id)-\(intent)" } } } @@ -31,15 +358,11 @@ struct RulesView: View { var body: some View { Group { switch nav.rulesTab { - case .rules: - if store.rules.isEmpty { empty } else { ruleList } - case .packs: - RulePacksView() - case .activity: - activityFeed + case .rules: inventory + case .activity: activityFeed } } - .navigationTitle(L("Rules")) + .navigationTitle(L("Policies")) // The title bar carries the state that would otherwise need a trip to the screen to discover: how many // rules are live, and whether they are standing down. .navigationSubtitle(subtitle) @@ -55,6 +378,10 @@ struct RulesView: View { RuleTemplateLibrary(focus: focus, subject: subject) { rules in for rule in rules { store.save(rule) } } + case .pack(let pack, let intent): + RulePackEditor(pack: pack, receipt: monitor.rulePacks.reviewReceipt(for: pack), + saved: monitor.rulePacks.configuration(for: pack.id), intent: intent, + packs: monitor.rulePacks, rules: store) } } .onChange(of: nav.ruleRequest) { _, request in @@ -63,6 +390,12 @@ struct RulesView: View { nav.ruleRequest = nil } .onChange(of: templates.pending?.id) { _, _ in takeTemplateRequest() } + .onChange(of: nav.policyRequest, initial: true) { _, request in + guard let request else { return } + handle(request.destination) + nav.policyRequest = nil + } + .task(id: monitor.dataVersion) { await loadPolicySources() } .onAppear { if let request = nav.ruleRequest { sheet = .editor(request.rule, isNew: true) @@ -81,28 +414,79 @@ struct RulesView: View { templates.pending = nil } + private func handle(_ destination: PolicyDestination) { + nav.rulesTab = .rules + switch destination { + case .rule(let id): + if let rule = store.rules.first(where: { $0.id == id }) { sheet = .editor(rule, isNew: false) } + case .managedPack(let packID): + focusPolicyID = packID.map { PolicyInventoryID.make(.managedPack, sourceID: $0) } ?? "managed-packs" + if let packID, let pack = monitor.rulePacks.definitions.first(where: { $0.id == packID }) { + sheet = .pack(pack, .review) + } + case .allowlist(let agentID): + nav.showAgentPolicy(agentID: agentID, section: .allowlist) + case .guardrail(_, let agentID): + guard !agentID.isEmpty else { return } + nav.showAgentPolicy(agentID: agentID, section: .guardrails) + case .mock: + nav.showInspectionPolicy(.mocks) + case .requestRewrite: + nav.showInspectionPolicy(.requestRewrites) + case .responseTransform: + nav.showInspectionPolicy(.responseTransforms) + } + } + + private func loadPolicySources() async { + let loaded = try? await monitor.read { db in + (try db.loadPolicies(), + try db.alerts(limit: 2_000).filter { $0.kind == AnomalyEngine.Kind.allowlistViolation.rawValue }, + try db.exchanges(since: Date().addingTimeInterval(-30 * 86_400), limit: 2_000)) + } + guard let loaded else { return } + allowlists = loaded.0 + alerts = loaded.1 + exchanges = loaded.2 + } + + private var inventoryItems: [PolicyInventoryItem] { + PolicyInventoryBuilder.build(rules: store.rules, allowlists: allowlists, + packs: monitor.rulePacks.definitions, + unavailablePacks: monitor.rulePacks.unavailableReviewConfigurations, + packConfiguration: { monitor.rulePacks.configuration(for: $0) }, + packNeedsReview: monitor.rulePacks.needsReview, + packStatus: monitor.rulePacks.status, + guardrails: monitor.guardrails.guardrails, + mocks: monitor.inspection.mockRules, + rewrites: monitor.inspection.rewriteRules, + transforms: monitor.inspection.responseTransformRules, + extensionRunning: monitor.canBlock, canBlock: monitor.canBlock, + inspectionEnabled: monitor.inspection.enabled, + inspectionRunning: monitor.inspection.running, + rulesPaused: store.isPaused, now: now) + } + private var subtitle: String { if store.isPaused, let until = store.pausedUntil { return L("Paused until %@", until.formatted(date: .omitted, time: .shortened)) } - let live = store.liveRules.count - guard !store.rules.isEmpty else { return "" } - return live == store.rules.count - ? L("%lld in force", live) : L("%1$lld of %2$lld in force", live, store.rules.count) + let effective = inventoryItems.filter { $0.effect.isEffective }.count + guard !inventoryItems.isEmpty else { return "" } + return effective == 0 + ? L("%lld policies · none currently effective", inventoryItems.count) + : L("%1$lld policies · %2$lld currently effective", inventoryItems.count, effective) } // MARK: Toolbar @ToolbarContentBuilder private var toolbar: some ToolbarContent { - // Nothing to switch between until there is something in one of them. - if !store.rules.isEmpty || !store.events.isEmpty || monitor.rulePacks.hasPackNavigation { - ToolbarItem(placement: .principal) { - Picker("", selection: $nav.rulesTab) { - ForEach(RulesTab.allCases) { Text($0.title).tag($0) } - } - .pickerStyle(.segmented).labelsHidden().frame(width: 310) + ToolbarItem(placement: .principal) { + Picker("", selection: $nav.rulesTab) { + ForEach(RulesTab.allCases) { Text($0.title).tag($0) } } + .pickerStyle(.segmented).labelsHidden().frame(width: 220) } ToolbarItemGroup { pauseControl @@ -181,26 +565,273 @@ struct RulesView: View { store.rules.filter { !($0.enabled && $0.isUsable && $0.schedule.isActive(at: now, session: RuleStore.session)) } } - private var ruleList: some View { - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - if !resting.isEmpty && !inForce.isEmpty { heading(L("In force")) } - ForEach(inForce) { rule in row(rule) } - if !resting.isEmpty { - heading(L("Not in force")).padding(.top, inForce.isEmpty ? 0 : 12) - ForEach(resting) { rule in row(rule) } + private var inventory: some View { + ScrollViewReader { scroll in + ScrollView { + LazyVStack(alignment: .leading, spacing: 18) { + inventoryIntroduction + if inventoryItems.isEmpty { empty } + ForEach(PolicyInventoryKind.allCases, id: \.self) { kind in + let items = inventoryItems.filter { $0.kind == kind } + if !items.isEmpty { inventorySection(kind, items: items) } + } + } + .padding(Measure.gutter) + } + .onChange(of: focusPolicyID) { _, target in + guard let target else { return } + withAnimation { scroll.scrollTo(target, anchor: .center) } + focusPolicyID = nil + } + .onAppear { + guard let target = focusPolicyID else { return } + DispatchQueue.main.async { + scroll.scrollTo(target, anchor: .center) + focusPolicyID = nil } } - .padding(Measure.gutter) } .safeAreaInset(edge: .top, spacing: 0) { banner } } + private var inventoryIntroduction: some View { + VStack(alignment: .leading, spacing: 4) { + Text(L("Everything Flowlight can apply")).font(.headline) + Text(PolicyInventoryCapabilities.current.inspectionPolicies + ? L("Network decisions, agent limits, managed destination lists, and developer inspection policies live here. Each policy still opens in the editor that owns its behavior and consent.") + : L("Network decisions, agent limits, and managed destination lists live here. Each policy still opens in the editor that owns its behavior and consent.")) + .font(.callout).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) + } + } + + private func inventorySection(_ kind: PolicyInventoryKind, items: [PolicyInventoryItem]) -> some View { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 7) { + Image(systemName: kind.symbol).font(.caption).foregroundStyle(.tertiary) + Text(kind.sectionTitle).font(.caption.weight(.semibold)).foregroundStyle(.secondary) + Text("\(items.count)").font(.caption2.monospacedDigit()).foregroundStyle(.tertiary) + Spacer() + if kind == .managedPack { packCatalogControls } + } + .id(kind == .managedPack ? "managed-packs" : "section-\(kind.rawValue)") + if kind == .managedPack { packCatalogProvenance } + VStack(spacing: 0) { + ForEach(Array(items.enumerated()), id: \.element.id) { index, item in + if index > 0 { Divider().padding(.leading, 40) } + inventoryRow(item).id(item.id) + } + } + .background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 10)) + } + } + + @ViewBuilder + private func inventoryRow(_ item: PolicyInventoryItem) -> some View { + if item.kind == .networkRule, + let rule = store.rules.first(where: { $0.id.uuidString.caseInsensitiveCompare(item.sourceID) == .orderedSame }) { + row(rule) + } else if item.kind == .managedPack, + let pack = monitor.rulePacks.definitions.first(where: { $0.id == item.sourceID }) { + packRow(pack) + } else if item.kind == .managedPack, + let configuration = monitor.rulePacks.unavailableReviewConfigurations.first(where: { $0.packID == item.sourceID }) { + unavailablePackRow(configuration) + } else { + Button { handle(item.destination) } label: { + HStack(alignment: .top, spacing: 10) { + Image(systemName: item.kind.symbol) + .font(.caption.weight(.semibold)) + .foregroundStyle(item.isEnabled ? FL.accent : .secondary) + .frame(width: 26, height: 26) + .background((item.isEnabled ? FL.accent : Color.secondary).opacity(0.12), + in: RoundedRectangle(cornerRadius: 7)) + VStack(alignment: .leading, spacing: 2) { + Text(item.title).font(.callout.weight(.medium)).lineLimit(1) + Text(item.detail).font(.caption).foregroundStyle(.secondary).lineLimit(2) + if let reason = item.reason { + Text(reason).font(.caption2).foregroundStyle(.tertiary).lineLimit(2) + } + } + Spacer(minLength: 8) + Text(item.effect.title) + .font(.caption.weight(.medium)) + .foregroundStyle(policyStatusTint(item.effect)) + Image(systemName: "chevron.right").font(.caption2).foregroundStyle(.tertiary).padding(.top, 3) + } + .padding(.horizontal, 10).padding(.vertical, 8) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + + private func policyStatusTint(_ effect: PolicyEffectState) -> AnyShapeStyle { + switch effect { + case .effective: return AnyShapeStyle(FL.good) + case .reportingOnly, .watchingOnly, .waiting, .needsReview: return AnyShapeStyle(FL.warning) + case .failed, .unavailable: return AnyShapeStyle(FL.critical) + case .off: return AnyShapeStyle(.tertiary) + } + } + + private func packRow(_ pack: RulePackDefinition) -> some View { + let configuration = monitor.rulePacks.configuration(for: pack.id) + let item = inventoryItems.first { $0.kind == .managedPack && $0.sourceID == pack.id } + return HStack(alignment: .top, spacing: 10) { + Image(systemName: "shippingbox") + .font(.caption.weight(.semibold)) + .foregroundStyle(configuration.enabled ? FL.critical : .secondary) + .frame(width: 26, height: 26) + .background(.quaternary, in: RoundedRectangle(cornerRadius: 7)) + VStack(alignment: .leading, spacing: 2) { + Text(L(pack.manifest.titleKey)).font(.callout.weight(.medium)) + Text(L("%lld destinations · %@", pack.entries.count, + configuration.scope == .allApps ? L("every app") : L("%lld selected apps", configuration.apps.count))) + .font(.caption).foregroundStyle(.secondary) + if let reason = item?.reason { + Text(reason).font(.caption2).foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + } + Text(L("Official · pack v%@ · %@", pack.manifest.version, pack.manifest.license)) + .font(.caption2).foregroundStyle(.tertiary) + if let error = packSaveErrors[pack.id] { + Text(error).font(.caption).foregroundStyle(FL.critical) + } + } + Spacer(minLength: 8) + VStack(alignment: .trailing, spacing: 6) { + if let item { + Text(item.effect.title).font(.caption.weight(.medium)) + .foregroundStyle(policyStatusTint(item.effect)) + } + HStack(spacing: 8) { + Toggle("", isOn: Binding(get: { configuration.enabled }, set: { enabled in + if enabled { + sheet = .pack(pack, .activate) + } else { + var changed = configuration + changed.enabled = false + packSaveErrors[pack.id] = nil + monitor.rulePacks.save(changed) { success in + if !success { packSaveErrors[pack.id] = monitor.rulePacks.failureMessage ?? L("Failed: %@", L("Save")) } + } + } + })) + .toggleStyle(.switch).controlSize(.mini).labelsHidden() + Button(L("Review…")) { sheet = .pack(pack, .review) }.controlSize(.small) + } + } + } + .padding(.horizontal, 10).padding(.vertical, 8) + } + + private func unavailablePackRow(_ configuration: RulePackConfiguration) -> some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: "shippingbox.and.arrow.backward") + .font(.caption.weight(.semibold)).foregroundStyle(FL.warning) + .frame(width: 26, height: 26).background(FL.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 7)) + VStack(alignment: .leading, spacing: 2) { + Text(configuration.packID).font(.callout.weight(.medium)) + Text(L("This previously enabled official pack is no longer in the catalog. Blocking stopped automatically.")) + .font(.caption).foregroundStyle(.secondary) + if let error = packSaveErrors[configuration.packID] { + Text(error).font(.caption).foregroundStyle(FL.critical) + } + } + Spacer(minLength: 8) + VStack(alignment: .trailing, spacing: 6) { + Text(PolicyEffectState.unavailable.title).font(.caption.weight(.medium)) + .foregroundStyle(policyStatusTint(.unavailable)) + Button(L("Acknowledge")) { + packSaveErrors[configuration.packID] = nil + monitor.rulePacks.acknowledgeUnavailableReview(configuration.packID) { success in + if !success { + packSaveErrors[configuration.packID] = monitor.rulePacks.failureMessage ?? L("Failed: %@", L("Save")) + } + } + } + .controlSize(.small) + } + } + .padding(.horizontal, 10).padding(.vertical, 8) + } + + private var packCatalogProvenance: some View { + DisclosureGroup { + VStack(alignment: .leading, spacing: 4) { + LabeledContent(L("Source")) { + Text(RulePackCatalogVerifier.sourceURL).font(.caption2.monospaced()) + .foregroundStyle(.secondary).textSelection(.enabled) + } + if let receipt = monitor.rulePackCatalog.receipt { + LabeledContent(L("Publisher")) { + Text(L("Flowlight · signer %@", receipt.signerKeyID)) + .font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled) + } + LabeledContent(L("Catalog")) { + Text(L("Sequence %1$lld · digest %2$@", receipt.sequence, String(receipt.digest.prefix(12)))) + .font(.caption2.monospaced()).foregroundStyle(.secondary).textSelection(.enabled) + } + } + if let success = monitor.rulePackCatalog.catalogState.update.lastSuccess { + LabeledContent(L("Last verified")) { + Text(success.formatted(date: .abbreviated, time: .shortened)) + .font(.caption2).foregroundStyle(.secondary) + } + } + if monitor.rulePackCatalog.automaticRefresh, + let next = monitor.rulePackCatalog.catalogState.update.nextEligibleAttempt { + LabeledContent(L("Next check")) { + Text(next.formatted(date: .abbreviated, time: .shortened)) + .font(.caption2).foregroundStyle(.secondary) + } + } + } + .padding(.top, 5) + } label: { + Label(L("Official catalog details"), systemImage: "checkmark.seal") + .font(.caption).foregroundStyle(.secondary) + } + .padding(.horizontal, 10).padding(.vertical, 7) + .background(.quaternary.opacity(0.22), in: RoundedRectangle(cornerRadius: 8)) + } + + private var packCatalogControls: some View { + HStack(spacing: 8) { + Text(packCatalogStatus).font(.caption2).foregroundStyle(.tertiary) + Menu { + Toggle(L("Automatic updates"), isOn: Binding( + get: { monitor.rulePackCatalog.automaticRefresh }, + set: { enabled in Task { await monitor.rulePackCatalog.setAutomaticRefresh(enabled) } } + )) + Button(L("Check Now")) { Task { await monitor.rulePackCatalog.checkNow() } } + .disabled(monitor.rulePackCatalog.status == .checking) + } label: { + Image(systemName: "arrow.triangle.2.circlepath") + } + .menuStyle(.borderlessButton).menuIndicator(.hidden).fixedSize() + .help(L("Official pack updates")) + } + } + + private var packCatalogStatus: String { + switch monitor.rulePackCatalog.status { + case .idle: return L("Not checked") + case .checking: return L("Checking…") + case .current: return L("Verified") + case .stale: return L("Stale") + case .recovery: return L("Recovery required") + case .failed: return L("Check failed") + } + } + private func row(_ rule: Rule) -> some View { - RuleRow(rule: rule, store: store, now: now, - extensionRunning: monitor.mode == .networkExtension, - inspecting: monitor.inspection.enabled, - events: store.events(for: rule).count) { + let policy = inventoryItems.first { $0.kind == .networkRule && $0.sourceID == rule.id.uuidString.lowercased() } + return RuleRow(rule: rule, store: store, now: now, + extensionRunning: monitor.canBlock, + inspecting: monitor.inspection.enabled && monitor.inspection.running, + events: store.events(for: rule).count, + effect: policy?.effect ?? .off, effectReason: policy?.reason) { sheet = .editor(rule, isNew: false) } // A rule that has just been written, or has just crossed between in force and not, is a change of state @@ -258,11 +889,13 @@ struct RulesView: View { private var activityFeed: some View { Group { - if store.events.isEmpty && monitor.rulePacks.events.isEmpty { + if policyActivity.isEmpty { ContentUnavailableView { Label(L("Nothing yet"), systemImage: "list.bullet.rectangle") } description: { - Text(L("Rule decisions and summarized pack blocks land here. Pack activity is grouped so a page full of ads does not become a page full of alerts.")) + Text(PolicyInventoryCapabilities.current.inspectionPolicies + ? L("Blocks, exceptions, allowlist violations, guardrails, mocks, and request or response changes appear here without changing their original records.") + : L("Network blocks, exceptions, managed-pack matches, and allowlist violations appear here without changing their original records.")) } } else { feed @@ -270,71 +903,112 @@ struct RulesView: View { } } - /// The decisions, as the rest of the app reads: a day at a time, newest first. A table would sort and resize, - /// neither of which anybody asked of a feed — what is asked is "what happened, and which rule did it". + /// The existing sources, adapted into one read-only chronology. The original records remain the source of truth; + /// this list only gives their effects a shared vocabulary. private var feed: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 18) { - if !monitor.rulePacks.events.isEmpty { - VStack(alignment: .leading, spacing: 6) { - heading(L("Managed packs")) - VStack(spacing: 0) { - ForEach(Array(monitor.rulePacks.events.enumerated()), id: \.element.id) { index, event in - if index > 0 { Divider().padding(.leading, 34) } - RulePackEventRow(event: event) - } - } - .background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 10)) - } - } - ForEach(feedDays) { day in + ForEach(activityDays) { day in VStack(alignment: .leading, spacing: 6) { heading(day.title) VStack(spacing: 0) { ForEach(Array(day.events.enumerated()), id: \.element.id) { index, event in - if index > 0 { Divider().padding(.leading, 34) } - EventRow(event: event, rule: name(of: event)) { - if let rule = store.rules.first(where: { $0.id == event.ruleID }) { - sheet = .editor(rule, isNew: false) - } + if index > 0 { Divider().padding(.leading, 38) } + PolicyActivityRow(event: event) { + guard let policyID = event.policyID, + let item = inventoryItems.first(where: { $0.id == policyID }) else { return } + handle(item.destination) } } } .background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 10)) } - .entrance() } } .padding(Measure.gutter) } } - private struct FeedDay: Identifiable { + private struct ActivityDay: Identifiable { let id: Date let title: String - let events: [RuleEventRecord] + let events: [PolicyActivityItem] + } + + private var policyActivity: [PolicyActivityItem] { + PolicyActivityBuilder.build(ruleEvents: store.events, packEvents: monitor.rulePacks.events, + alerts: alerts, exchanges: exchanges, rules: store.rules, + guardrails: monitor.guardrails.guardrails, packs: monitor.rulePacks.definitions, + mocks: monitor.inspection.mockRules, rewrites: monitor.inspection.rewriteRules, + transforms: monitor.inspection.responseTransformRules) } - private var feedDays: [FeedDay] { + private var activityDays: [ActivityDay] { let calendar = Calendar.current - let byDay = Dictionary(grouping: store.events) { calendar.startOfDay(for: $0.timestamp) } + let byDay = Dictionary(grouping: policyActivity) { calendar.startOfDay(for: $0.timestamp) } return byDay.keys.sorted(by: >).map { day in let title = calendar.isDateInToday(day) ? L("Today") : calendar.isDateInYesterday(day) ? L("Yesterday") : day.formatted(date: .abbreviated, time: .omitted) - return FeedDay(id: day, title: title, - events: (byDay[day] ?? []).sorted { $0.timestamp > $1.timestamp }) + return ActivityDay(id: day, title: title, events: byDay[day] ?? []) } } +} + +private struct PolicyActivityRow: View { + let event: PolicyActivityItem + let showPolicy: () -> Void - /// A decision can come from a network rule or from a guardrail — the same question asked about a destination - /// or about a tool — so the feed looks in both lists before giving up on it. - private func name(of event: RuleEventRecord) -> String { - if let rule = store.rules.first(where: { $0.id == event.ruleID }) { return RuleWords.title(rule) } - if let guardrail = monitor.guardrails.guardrails.first(where: { $0.id == event.ruleID }) { - return guardrail.localizedTitle + var body: some View { + Button(action: showPolicy) { + HStack(alignment: .top, spacing: 10) { + Image(systemName: symbol) + .font(.caption).foregroundStyle(tint).frame(width: 18).padding(.top, 2) + VStack(alignment: .leading, spacing: 2) { + Text(event.title).font(.callout.weight(.medium)).lineLimit(1) + Text(event.detail).font(.caption).foregroundStyle(.secondary).lineLimit(2) + Text(effectTitle).font(.caption2).foregroundStyle(.tertiary) + } + Spacer(minLength: 8) + Text(event.timestamp.formatted(date: .omitted, time: .standard)) + .font(.caption.monospacedDigit()).foregroundStyle(.secondary) + } + .padding(.horizontal, 10).padding(.vertical, 8) + .contentShape(Rectangle()) } - return L("A rule since deleted") + .buttonStyle(.plain) + .disabled(event.policyID == nil) + } + + private var effectTitle: String { + switch event.effect { + case .blocked: return L("Blocked connection or request") + case .allowed: return L("Allowed by an exception") + case .packBlocked: return L("Blocked by a managed pack") + case .allowlistViolation: return L("Outside an agent allowlist") + case .guardrailApplied: return L("Capability removed by a guardrail") + case .mocked: return L("Answered by a mock") + case .requestRewritten: return L("Request changed before sending") + case .responseTransformed: return L("Response changed before delivery") + } + } + + private var symbol: String { + switch event.effect { + case .allowed: return "checkmark.shield.fill" + case .mocked: return "wand.and.stars" + case .requestRewritten: return "slider.horizontal.3" + case .responseTransformed: return "arrow.left.arrow.right" + case .guardrailApplied: return "wrench.and.screwdriver.fill" + case .allowlistViolation: return "checklist.unchecked" + case .blocked, .packBlocked: return "hand.raised.fill" + } + } + + private var tint: Color { + event.effect == .allowed ? FL.good + : (event.effect == .mocked || event.effect == .requestRewritten || event.effect == .responseTransformed + ? FL.tool : FL.critical) } } @@ -350,6 +1024,8 @@ private struct RuleRow: View { let extensionRunning: Bool let inspecting: Bool let events: Int + let effect: PolicyEffectState + let effectReason: String? let edit: () -> Void private var limitation: String? { @@ -380,6 +1056,8 @@ private struct RuleRow: View { meta } Spacer(minLength: 8) + Text(effect.title).font(.caption.weight(.medium)).foregroundStyle(effectTint) + .padding(.top, 3) Toggle("", isOn: Binding(get: { rule.enabled }, set: { store.setEnabled(rule, $0) })) .toggleStyle(.switch).controlSize(.mini).labelsHidden() .accessibilityLabel(L("Enable %@", RuleWords.title(rule))) @@ -399,6 +1077,8 @@ private struct RuleRow: View { } if let limitation { note(limitation, icon: "eye", tint: FL.warning) + } else if let effectReason, rule.isUsable { + note(effectReason, icon: effect == .off ? "power" : "clock", tint: .secondary) } if !rule.isUsable { note(L("Used up — a one-off allowance, already spent."), icon: "checkmark.circle", tint: .secondary) @@ -417,6 +1097,15 @@ private struct RuleRow: View { } } + private var effectTint: AnyShapeStyle { + switch effect { + case .effective: return AnyShapeStyle(FL.good) + case .reportingOnly, .watchingOnly, .waiting, .needsReview: return AnyShapeStyle(FL.warning) + case .failed, .unavailable: return AnyShapeStyle(FL.critical) + case .off: return AnyShapeStyle(.tertiary) + } + } + /// The verb, as a symbol. A rule that is only being watched says so here as well as in the strip below, because /// this is the mark the eye lands on first and a red hand over a rule that refuses nothing would be a lie. private var glyph: some View { diff --git a/FlowlightTests/HelpTests.swift b/FlowlightTests/HelpTests.swift index 6a22965f..3cce5866 100644 --- a/FlowlightTests/HelpTests.swift +++ b/FlowlightTests/HelpTests.swift @@ -24,7 +24,7 @@ final class HelpTests: XCTestCase { } func testTheHelpURLIsBuiltFromTheAnchorRatherThanGuessed() { - XCTAssertEqual(Help.forScreen(.rules).absoluteString, "https://flowlight.xinbetween.com/docs/#rules") + XCTAssertEqual(Help.forScreen(.rules).absoluteString, "https://flowlight.xinbetween.com/docs/#policies") XCTAssertEqual(Help.forScreen(.inspect).absoluteString, "https://flowlight.xinbetween.com/docs/#inspection") } diff --git a/FlowlightTests/InsightsTests.swift b/FlowlightTests/InsightsTests.swift index 5d8eba91..6acb5452 100644 --- a/FlowlightTests/InsightsTests.swift +++ b/FlowlightTests/InsightsTests.swift @@ -161,6 +161,24 @@ final class InsightsTests: XCTestCase { XCTAssertEqual(ReportChartHoverLayout.cardX(lineX: 10, chartWidth: 200), 0) } + func testReportHoverRejectsPointersOutsideThePlotArea() { + let plot = CGRect(x: 40, y: 20, width: 600, height: 240) + + XCTAssertTrue(ReportChartHoverLayout.contains(CGPoint(x: 320, y: 100), in: plot)) + XCTAssertFalse(ReportChartHoverLayout.contains(CGPoint(x: 20, y: 100), in: plot), + "the y-axis and leading chart furniture are not time buckets") + XCTAssertFalse(ReportChartHoverLayout.contains(CGPoint(x: 320, y: 280), in: plot), + "leaving the plot must clear transient hover") + XCTAssertFalse(ReportChartHoverLayout.contains(CGPoint(x: plot.minX, y: 100), in: plot), + "a hover clamped to the leading edge means the pointer has left the plot") + XCTAssertFalse(ReportChartHoverLayout.contains(CGPoint(x: plot.maxX, y: 100), in: plot), + "a hover clamped to the trailing edge means the pointer has left the plot") + XCTAssertTrue(ReportChartHoverLayout.contains(CGPoint(x: plot.minX + 1, y: 100), in: plot), + "the first bucket remains inspectable inside the plot") + XCTAssertTrue(ReportChartHoverLayout.contains(CGPoint(x: plot.maxX - 1, y: 100), in: plot), + "the last bucket remains inspectable inside the plot") + } + func testReportChartKeyboardSelectionMovesAcrossBuckets() { let dates = [10.0, 20.0, 30.0].map(Date.init(timeIntervalSince1970:)) @@ -171,6 +189,30 @@ final class InsightsTests: XCTestCase { XCTAssertNil(ReportChartKeyboardSelection.move(from: nil, direction: .right, dates: [])) } + func testReportChartFocusDoesNotCreateAnUpperLeftSelection() { + let dates = [10.0, 20.0, 30.0].map(Date.init(timeIntervalSince1970:)) + + XCTAssertNil(ReportChartKeyboardSelection.reconcile(nil, chartFocused: true, dates: dates), + "focus alone must not synthesize a first-bucket tooltip") + XCTAssertEqual( + ReportChartHighlightSelection.date(hovered: dates[1], keyboard: dates[0], chartFocused: true), + dates[1], + "pointer inspection takes precedence over a previous keyboard selection" + ) + XCTAssertNil(ReportChartHighlightSelection.date(hovered: nil, keyboard: nil, chartFocused: true), + "leaving the chart after pointer inspection must not reveal a synthetic keyboard tooltip") + XCTAssertEqual( + ReportChartHighlightSelection.date(hovered: nil, keyboard: dates[0], chartFocused: true), + dates[0], + "leaving pointer inspection restores an intentional keyboard selection" + ) + XCTAssertEqual( + ReportChartKeyboardSelection.reconcile(dates[2], chartFocused: true, dates: Array(dates.prefix(2))), + dates[0], + "an intentional keyboard selection remains usable when the series changes" + ) + } + func testHistoryPresetUsesAnIndependentVisibleWindowAndResolution() { let end = Date(timeIntervalSince1970: 2_000_000) let available = Date(timeIntervalSince1970: 0)...end diff --git a/FlowlightTests/InspectNavigationTests.swift b/FlowlightTests/InspectNavigationTests.swift index ec765c97..64116240 100644 --- a/FlowlightTests/InspectNavigationTests.swift +++ b/FlowlightTests/InspectNavigationTests.swift @@ -30,14 +30,15 @@ final class InspectNavigationTests: XCTestCase { XCTAssertEqual(SidebarItem.inspect.section, .investigate) } - func testPackReviewNavigationOpensThePacksTab() { + func testPackReviewNavigationFocusesManagedPacksInPolicies() { let nav = AppNavigation() nav.show(.live) nav.showPackReviews() XCTAssertEqual(nav.selection, .rules) - XCTAssertEqual(nav.rulesTab, .packs) + XCTAssertEqual(nav.rulesTab, .rules) + XCTAssertEqual(nav.policyRequest?.destination, .managedPack(nil)) } func testOrdinaryRulesNavigationReturnsToTheRulesTab() { @@ -60,4 +61,137 @@ final class InspectNavigationTests: XCTestCase { XCTAssertEqual(nav.rulesTab, .rules) XCTAssertNotNil(nav.ruleRequest) } + + func testLegacyRulesTabsRestoreIntoTwoTabPoliciesUI() { + XCTAssertEqual(RulesTab.rules.rawValue, "rules") + XCTAssertEqual(RulesTab.restored(from: "rules"), .rules) + XCTAssertEqual(RulesTab.restored(from: "packs"), .rules) + XCTAssertTrue(RulesTab.restoresManagedPackFocus(from: "packs")) + XCTAssertFalse(RulesTab.restoresManagedPackFocus(from: "rules")) + XCTAssertEqual(RulesTab.restored(from: "activity"), .activity) + XCTAssertEqual(RulesTab.allCases, [.rules, .activity]) + } + + func testPolicyInventoryIDsAreStableAndNamespaced() { + let source = "8f183e44-895b-4f05-b221-9d845e772def" + XCTAssertEqual(PolicyInventoryID.make(.networkRule, sourceID: source), "rule:\(source)") + XCTAssertEqual(PolicyInventoryID.make(.networkRule, sourceID: source), + PolicyInventoryID.make(.networkRule, sourceID: source)) + XCTAssertNotEqual(PolicyInventoryID.make(.networkRule, sourceID: source), + PolicyInventoryID.make(.guardrail, sourceID: source)) + } + + func testStoreCapabilitiesOmitEveryInspectionDependentPolicyKind() { + let capabilities = PolicyInventoryCapabilities(inspectionPolicies: false) + let items = PolicyInventoryKind.allCases.map { kind in + PolicyInventoryItem(kind: kind, sourceID: kind.rawValue, title: kind.rawValue, detail: "", + isEnabled: true, effect: .effective, reason: nil, destination: .managedPack(nil)) + } + let visible = items.filter { $0.isVisible(in: capabilities) }.map(\.kind) + + XCTAssertEqual(visible, [.networkRule, .agentAllowlist, .managedPack]) + XCTAssertFalse(visible.contains(.guardrail)) + XCTAssertFalse(visible.contains(.mock)) + XCTAssertFalse(visible.contains(.requestRewrite)) + XCTAssertFalse(visible.contains(.responseTransform)) + } + + func testInventorySeparatesConfiguredStateFromCurrentEffect() throws { + let rule = Rule(action: .block, destination: "example.com") + let allowlist = AgentPolicy(agentID: "agent", enabled: true, patterns: ["example.com"], enforce: false) + let guardrail = Guardrail(agent: "agent", tool: "bash") + let pack = try XCTUnwrap(BuiltInRulePacks.all.first) + let retired = RulePackConfiguration(packID: "retired.pack", enabled: true) + + let items = PolicyInventoryBuilder.build( + rules: [rule], allowlists: ["agent": allowlist], packs: [pack], unavailablePacks: [retired], + packConfiguration: { RulePackConfiguration(packID: $0, enabled: true) }, + packNeedsReview: { _ in true }, packStatus: .enforcing, + guardrails: [guardrail], mocks: [], rewrites: [], transforms: [], + extensionRunning: false, canBlock: true, inspectionEnabled: false, inspectionRunning: false) + + let network = try XCTUnwrap(items.first { $0.kind == .networkRule }) + XCTAssertTrue(network.isEnabled) + XCTAssertEqual(network.effect, .watchingOnly) + XCTAssertNotNil(network.reason) + + let agent = try XCTUnwrap(items.first { $0.kind == .agentAllowlist }) + XCTAssertTrue(agent.isEnabled) + XCTAssertEqual(agent.effect, .reportingOnly) + XCTAssertNotNil(agent.reason) + + let developer = try XCTUnwrap(items.first { $0.kind == .guardrail }) + XCTAssertTrue(developer.isEnabled) + XCTAssertEqual(developer.effect, .waiting) + XCTAssertNotNil(developer.reason) + + let managed = try XCTUnwrap(items.first { $0.sourceID == pack.id }) + XCTAssertTrue(managed.isEnabled) + XCTAssertEqual(managed.effect, .needsReview) + + let unavailable = try XCTUnwrap(items.first { $0.sourceID == "retired.pack" }) + XCTAssertTrue(unavailable.isEnabled) + XCTAssertEqual(unavailable.effect, .unavailable) + XCTAssertNotNil(unavailable.reason) + } + + func testPackFocusCarriesExactPackIdentity() { + let nav = AppNavigation() + nav.showPolicyPack("official.trackers") + + XCTAssertEqual(nav.selection, .rules) + XCTAssertEqual(nav.rulesTab, .rules) + XCTAssertEqual(nav.policyRequest?.destination, .managedPack("official.trackers")) + } + + func testPolicyDeepLinksKeepContextualEditorsAsOwners() { + let nav = AppNavigation() + nav.show(.rules) + nav.showAgentPolicy(agentID: "claude", section: .guardrails) + XCTAssertEqual(nav.selection, .agents) + XCTAssertEqual(nav.agentPolicyRequest?.agentID, "claude") + XCTAssertEqual(nav.agentPolicyRequest?.section, .guardrails) + + nav.showInspectionPolicy(.requestRewrites) + XCTAssertEqual(nav.selection, .inspect) + XCTAssertEqual(nav.inspectRequest?.policySection, .requestRewrites) + } + + func testAgentPolicyDeepLinkIsScopedAndConsumedOnce() { + let request = AgentPolicyRequest(agentID: "claude", section: .guardrails) + var state = AgentPolicyDeepLinkState(request: request) + + XCTAssertEqual(state.section(for: "claude"), .guardrails) + XCTAssertNil(state.section(for: "codex")) + XCTAssertEqual(AgentDetail.Tab.policyLanding(.guardrails), .guardrails) + XCTAssertEqual(AgentDetail.Tab.policyLanding(.allowlist), .destinations) + + state.consume() + XCTAssertNil(state.section(for: "claude")) + } + + func testUnifiedActivityKeepsGuardrailAndRewriteEffectsDistinct() { + let exchange = HTTPExchange( + id: 42, started: Date(timeIntervalSince1970: 100), duration: 0.2, scheme: "https", + host: "api.example.com", port: 443, method: "POST", path: "/v1/run", status: 200, + requestHeaders: [], requestBody: Data(), requestSize: 0, requestTruncated: false, + responseHeaders: [], responseBody: Data(), responseSize: 0, responseTruncated: false, + contentType: "application/json", pid: 1, bundleID: "agent", appName: "Agent", + agent: "agent", agentName: "Agent", mcpServer: nil, toolCalls: [], toolResults: [], mcp: [], llm: nil, + note: nil, mockRule: nil, guardrail: "Removed shell", requestRewrite: "set X-Test", + responseTransform: nil) + + let activity = PolicyActivityBuilder.build(ruleEvents: [], packEvents: [], alerts: [], exchanges: [exchange], + rules: [], guardrails: [], packs: [], mocks: [], rewrites: [], + transforms: []) + + XCTAssertEqual(Set(activity.map(\.effect)), [.guardrailApplied, .requestRewritten]) + XCTAssertEqual(Set(activity.map(\.id)), ["exchange:42:guardrail", "exchange:42:request-rewrite"]) + + let storeActivity = PolicyActivityBuilder.build( + ruleEvents: [], packEvents: [], alerts: [], exchanges: [exchange], rules: [], guardrails: [], + packs: [], mocks: [], rewrites: [], transforms: [], + capabilities: PolicyInventoryCapabilities(inspectionPolicies: false)) + XCTAssertTrue(storeActivity.isEmpty, "Store activity must omit retained Developer-only effects") + } } diff --git a/FlowlightTests/VisualizeNavigationTests.swift b/FlowlightTests/VisualizeNavigationTests.swift index ff99d071..22cade8a 100644 --- a/FlowlightTests/VisualizeNavigationTests.swift +++ b/FlowlightTests/VisualizeNavigationTests.swift @@ -43,31 +43,36 @@ final class VisualizeNavigationTests: XCTestCase { XCTAssertTrue(SidebarItem.sidebarOrder.allSatisfy { $0.shortcut != nil }) } - func testSourcesCoverageDistinguishesNoEvidenceFromZeroPercentNamed() { - XCTAssertEqual(SourceSetupPolicy.coverageHealth(named: 0, hasTraffic: false), .empty) - XCTAssertEqual(SourceSetupPolicy.coverageHealth(named: 0, hasTraffic: true), .warning) - XCTAssertEqual(SourceSetupPolicy.coverageHealth(named: 0.8, hasTraffic: true), .warning) - XCTAssertEqual(SourceSetupPolicy.coverageHealth(named: 0.81, hasTraffic: true), .good) + func testSourcesRestoresExplicitAndLegacySectionTargets() { + XCTAssertEqual(SourcesTab.restored(explicitRawValue: "coverage", legacyScreenRawValue: "devices"), + .coverage, "the modern section argument must win") + XCTAssertEqual(SourcesTab.restored(explicitRawValue: nil, legacyScreenRawValue: "devices"), .devices) + XCTAssertEqual(SourcesTab.restored(explicitRawValue: nil, legacyScreenRawValue: "coverage"), .coverage) + XCTAssertEqual(SourcesTab.restored(explicitRawValue: "unknown", legacyScreenRawValue: "capture"), + .collector) } - func testSourcesRecoveryChecklistRequiresAConfiguredCollectorAndArrivingData() { - XCTAssertTrue(SourceSetupPolicy.collectorConfigured(mode: .nettop, extensionState: .notInstalled, - extensionFellBack: false), - "the sampler needs no separate filter setup") - XCTAssertFalse(SourceSetupPolicy.collectorConfigured(mode: .networkExtension, - extensionState: .notInstalled, - extensionFellBack: false)) - XCTAssertTrue(SourceSetupPolicy.collectorConfigured(mode: .networkExtension, - extensionState: .enabled, - extensionFellBack: false)) - XCTAssertFalse(SourceSetupPolicy.collectorConfigured(mode: .networkExtension, - extensionState: .enabled, - extensionFellBack: true), - "fallback is working but is not the configured collector") - XCTAssertFalse(SourceSetupPolicy.setupComplete(mode: .nettop, extensionState: .unknown, - extensionFellBack: false, isReceiving: false)) - XCTAssertTrue(SourceSetupPolicy.setupComplete(mode: .nettop, extensionState: .unknown, - extensionFellBack: false, isReceiving: true)) + func testSourcesSectionsHaveStableDistinctScrollTargets() { + XCTAssertEqual(SourcesTab.allCases, [.collector, .devices, .coverage]) + XCTAssertEqual(SourcesTab.allCases.map(\.anchorID), + ["sources-collector", "sources-devices", "sources-coverage"]) + XCTAssertEqual(Set(SourcesTab.allCases.map(\.anchorID)).count, SourcesTab.allCases.count) + } + + func testRepeatedSourcesLinkCreatesANewSectionFocusRequest() { + let nav = AppNavigation() + let initialRequest = nav.sourcesRequestID + + nav.showSources(.devices) + let firstRequest = nav.sourcesRequestID + XCTAssertEqual(nav.selection, .sources) + XCTAssertEqual(nav.sourcesTab, .devices) + XCTAssertNotEqual(firstRequest, initialRequest) + + nav.showSources(.devices) + XCTAssertEqual(nav.sourcesTab, .devices) + XCTAssertNotEqual(nav.sourcesRequestID, firstRequest, + "a repeated link must scroll even when its section value did not change") } func testFocusNoticeClaimsFilteringOnlyWhereTheWholeScopeApplies() { @@ -94,6 +99,12 @@ final class VisualizeNavigationTests: XCTestCase { XCTAssertEqual(SidebarItem.visualize.englishTitle, "Connections") } + func testPoliciesKeepsTheCompatibleRulesRouteButUsesCurrentHelpAnchor() { + XCTAssertEqual(SidebarItem.rules.rawValue, "rules") + XCTAssertEqual(SidebarItem.rules.helpAnchor, "policies") + XCTAssertEqual(SidebarItem.rules.englishTitle, "Policies") + } + func testConnectionsLoadingAlwaysOutranksAStaleSnapshotAndYieldsToEmptyState() { XCTAssertTrue(ConnectionsRenderPolicy.showsLoading(isLoading: true, hasSnapshot: true)) XCTAssertTrue(ConnectionsRenderPolicy.showsLoading(isLoading: true, hasSnapshot: false)) @@ -170,7 +181,8 @@ final class VisualizeNavigationTests: XCTestCase { nav.showPackReviews() XCTAssertEqual(nav.selection, .rules) - XCTAssertEqual(nav.rulesTab, .packs) + XCTAssertEqual(nav.rulesTab, .rules) + XCTAssertEqual(nav.policyRequest?.destination, .managedPack(nil)) XCTAssertNil( nav.backDestination, "pack review navigation starts a new task and must not retain an unrelated investigation trail" diff --git a/README.md b/README.md index 8aa6a92b..1a35cda6 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,12 @@ traffic, DNS, essential Apple system services and Flowlight's own connections ar after the app stops running. When HTTPS inspection proxies an app, destination rules and enabled managed packs are decided against the original app before Flowlight opens the origin connection; agent allowlists still apply only on the direct Network Extension path. -### Rules -An allowlist is one agent's idea of where it may go. A **rule** is the general version: it names a subject — an app, -an agent, a destination, a URL, or a pairing of two of those — and says block or allow. Right-click any row in Live, -History, AI Agents or Inspect to write one about what you're looking at; all of them land in the same editor on the -**Rules** screen. +### Policies +**Policies** is the inventory for every control Flowlight can apply: ordinary allow/block rules, agent allowlists, +managed packs, and—where HTTPS inspection is available—guardrails, mocks, request rewrites, and response transforms. +Each row reports whether it is actually enforcing, reporting, watching, waiting, scheduled, paused, or unavailable, and +opens the contextual editor that owns it. An allowlist is one agent's idea of where it may go; an ordinary **rule** is +the general version, naming an app, agent, destination, URL, or pairing and saying block or allow. - **The most specific match decides**, and a tie goes to the block. An exception written underneath a broad block works; one broader than what it excepts doesn't quietly widen it. @@ -148,15 +149,17 @@ History, AI Agents or Inspect to write one about what you're looking at; all of - **A rule says where it bites.** The extension refuses whole connections and knows nothing about paths; HTTPS inspection refuses one request and answers with a status the agent can read; the sampler refuses nothing. A rule the current setup can't carry out says so in the list instead of looking enforced. -- A **violations feed** beside the list: every connection a rule decided, refusals and exceptions alike. +- A unified read-only **Activity** feed beside the inventory: rule decisions, allowlist violations, managed-pack events, + and distinct inspection-policy effects without pretending different policy types share one persistence model. ### Managed ad and tracker packs Flowlight includes three official hostname lists: **Advertising**, **Cross-site tracking**, and **Privacy and telemetry**. Their baseline is built into the signed app for offline use and the app checks one fixed HTTPS source for newer -publisher-signed catalogs. Automatic checks are on by default and can be switched off in **Rules › Packs**; no traffic, -pack settings, or device identifier is sent. The server sees the Mac's IP address and `Flowlight/` user agent. +publisher-signed catalogs. Automatic checks are on by default and can be switched off under **Policies › Managed packs**; +no traffic, pack settings, or device identifier is sent. The server sees the Mac's IP address and `Flowlight/` +user agent. -All three packs are visible in **Rules › Packs** and off by default. A valid signature authenticates content but never +All three packs are visible under **Policies › Managed packs** and off by default. A valid signature authenticates content but never grants enforcement consent: new packs remain off, and changed destinations disable that pack until you review the exact new version and press Save. A verified accepted catalog remains available as visible stale last-known-good during an outage; rollback, equivocation, revoked trust, or corrupt cached bytes fail closed. Enable a pack for every app or only @@ -217,11 +220,13 @@ Flowlight says *Extension connection invalidated*, and neither changes its mind. Flowlight climbs out of that on its own: redial twice, then ask macOS which build it has and reinstall the extension if it isn't this app's, then redial again — and if none of it works, capture with the nettop sampler so there is still data. It says which rung it is on and how long until the next one. Your chosen source isn't -rewritten, blocking is switched off while the sampler stands in (the sampler can't refuse anything, and Rules +rewritten, blocking is switched off while the sampler stands in (the sampler can't refuse anything, and Policies shouldn't claim otherwise), and **Try the extension again** starts over. -### Sources: collector, coverage and devices -**Sources** keeps collector health and recovery, hostname coverage, and opt-in device channels in one destination. Its health summary shows whether traffic is arriving, how much is named, and which device channels are active. +### Sources: collector, devices and coverage +**Sources** is one scrollable operational page: Collector first, then Devices and Coverage as clearly headed sections. +Status notices, Ask answers, restored windows, and screenshot links scroll to and briefly focus the relevant section +without hiding the others. A Mac sends and receives over more than TCP and UDP. **Sources › Devices** covers the non-network channels, with every channel off until you turn it on because watching more of your Mac should be a decision rather than something an update did. @@ -317,7 +322,7 @@ No account, no cloud, no telemetry. Everything stays in a local SQLite database. answers and TLS ClientHellos (a kernel filter drops everything else), and **packet contents are never stored**. Naming who owns an IP with no known hostname is **on by default**: it sends those public IPs (never private ones) to Team Cymru's DNS service, and you can turn it off in Sources › Collector. Release and official rule-pack checks make standard HTTPS -requests with no traffic or pack settings; each has its own off switch in Settings or Rules › Packs. To attribute tools +requests with no traffic or pack settings; each has its own off switch in Settings or Policies › Managed packs. To attribute tools and MCP servers, Flowlight reads process command lines and your agents' MCP config files on your Mac; none of it leaves your Mac. HTTPS inspection is off unless you turn it on, and what it records stays in the same local database. @@ -484,7 +489,7 @@ Shipped: - [x] AI visualization: a separate mode for proved agent clients, attributed tools and MCP servers, LLM providers, and every other destination they reached (0.16.0). Tool traffic folds into its parent agent without erasing the - selectable tool node; provider grouping keeps exact hosts underneath for honest Reports, Inspect and Rules + selectable tool node; provider grouping keeps exact hosts underneath for honest History, Inspect and Policies navigation. Process lineage is labelled “works for,” never an invocation count without inspected evidence. The full evidence, data, status and acceptance contract lives in [docs/VISUALIZATION.md](docs/VISUALIZATION.md). @@ -526,20 +531,23 @@ Releases are deliberately not ordinary merges. `main` represents published softw new version and generated site until a downloadable, verified build exists. Every release follows this order: ```text -feature branch → release branch → reviewed green PR → immutable tag → signed dry run → publish and verify → merge to main +feature branch → release branch → reviewed green PR → immutable tag → signed preparation → publish exact bytes and verify → merge to main ``` 1. Branch a feature from `main`, test it, and rebase it onto current `main` rather than merging `main` into it. 2. Fast-forward finished features into `release/` and make one final release commit containing the version bump, release entry in `site/pages/releases.html`, and regenerated `docs/` website output. -3. Push the release branch and open a PR to `main`. Do not tag until it has been reviewed and every required check is - green. -4. Create and push an annotated `v` tag on that exact release commit. Tags are immutable: never retag, move or +3. Push the release branch and open a PR to `main`. After reviewing the final head, a login configured in the + `RELEASE_REVIEWERS` repository variable must post `Reviewed-Commit: ` as a PR comment. The editable PR + body is not review evidence. Do not tag until that authenticated receipt and every required check are green. +4. The designated release account creates and pushes an annotated `v` tag on that exact release commit; the + active ruleset rejects release-tag creation by other write collaborators. Tags are immutable: never retag, move, or force-push one. If it is wrong, issue the next patch release instead. -5. The tag push automatically runs the signed/notarized/Gatekeeper **dry run** and cannot publish. After it passes, - explicitly dispatch production for that same tag. -6. Verify the GitHub release and its DMG, PKG and `SHA256SUMS.txt` assets/checksums. Only then merge the release PR into - `main`, allowing GitHub Pages to advertise the downloadable release. +5. The tag push builds, signs, notarizes, Gatekeeper-verifies, attests, and retains one immutable preparation artifact; + it cannot publish. After it passes, explicitly dispatch publication with that preparation run ID. +6. Publication verifies the run, archive digest, manifest, checksums, and provenance, then creates and re-downloads a draft + before making those exact bytes public. Verify the DMG, PKG, `SHA256SUMS.txt`, and `RELEASE-MANIFEST.json`; only then + merge the release PR into `main`, allowing GitHub Pages to advertise the downloadable release. The full commands, signing prerequisites and CI behavior live in [CONTRIBUTING.md](CONTRIBUTING.md#branches-and-releases) and [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#release-branches). diff --git a/SECURITY.md b/SECURITY.md index 8c5b37aa..0811f964 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -44,7 +44,17 @@ what Flowlight does defend against. ## Signed releases -Releases are signed with a Developer ID and notarized by Apple, and every release publishes `SHA256SUMS.txt`. +Releases are signed with a Developer ID and notarized by Apple, and every release publishes `SHA256SUMS.txt` plus +`RELEASE-MANIFEST.json`. A tag-push preparation builds and signs once, passes Gatekeeper, attests the DMG, PKG, +checksums, notes, and manifest, and retains those exact bytes. The separate publication job has no signing or Apple +credentials: it verifies the selected run, source tag/SHA, archive digest, manifest, checksums, and GitHub provenance, +then re-downloads a draft before making those same bytes public. For example: + +```sh +gh attestation verify Flowlight.dmg --repo xinbetween/flowlight \ + --signer-workflow xinbetween/flowlight/.github/workflows/release.yml +``` + The in-app updater refuses a download whose checksum it cannot verify, and refuses to install an app that is not signed by the same team as the copy asking for the update. diff --git a/docs/404.html b/docs/404.html index 0d6bebcd..20e3ee68 100644 --- a/docs/404.html +++ b/docs/404.html @@ -86,7 +86,7 @@

That page isn't here

Try the home page,

diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7d39f944..e655bcef 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -400,7 +400,7 @@ TEAM_ID=ABCDE12345 \ DMG_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" \ NOTARY_PROFILE=flowlight-notary scripts/build-dmg.sh # build/Flowlight.dmg, signed + notarized INSTALLER_IDENTITY="Developer ID Installer: Your Name (ABCDE12345)" \ -NOTARY_PROFILE=flowlight-notary TEAM_ID=ABCDE12345 scripts/build-pkg.sh +NOTARY_PROFILE=flowlight-notary TEAM_ID=ABCDE12345 REUSE_EXPORT=1 scripts/build-pkg.sh gh release create v0.1.0 build/Flowlight.dmg build/Flowlight-0.1.0.pkg --title "Flowlight 0.1.0" --generate-notes scripts/update-cask.sh # Homebrew cask → new version + checksum git -C "$(brew --repository xinbetween/tap)" push # publishes it @@ -408,8 +408,9 @@ git -C "$(brew --repository xinbetween/tap)" push # publishes it - Keep the DMG asset named exactly `Flowlight.dmg`: the website and README link to `releases/latest/download/Flowlight.dmg`, and so does the Homebrew cask. -- `scripts/update-cask.sh` runs after the release exists on GitHub: with no local build it checksums the published - DMG. It lints the cask with `brew style` and `brew audit` in the tap, since Homebrew won't lint one outside a tap. +- `scripts/update-cask.sh` prefers the local `build/Flowlight.dmg`; with no local build it can checksum the published + DMG. CI sets `REQUIRE_LOCAL_DMG=1`, so publication cannot silently replace its verified prepared DMG with a download. + It lints the cask with `brew style` and `brew audit` in the tap, since Homebrew won't lint one outside a tap. - `INSTALLER_IDENTITY` is what signs the `.pkg`. Leave it out and `build-pkg.sh` produces an unsigned package that Gatekeeper refuses, without saying so. - Without `DMG_SIGN_IDENTITY` and `NOTARY_PROFILE` the DMG contains an ad-hoc signed app. That's fine for testing, @@ -434,7 +435,7 @@ cannot be downloaded until signing and notarization finish. `docs/` also has to commit (CI checks it) and `build_site.py` reads `MARKETING_VERSION`, so the version bump and the rebuilt site cannot be separated. Keeping both off `main` until the release exists is what the branch is for. -Every release follows the mandatory sequence **feature branch → release branch → reviewed green PR → immutable tag → signed dry run → publish and verify → merge to main**: +Every release follows the mandatory sequence **feature branch → release branch → reviewed green PR → immutable tag → signed preparation → publish exact bytes and verify → merge to main**: ```sh git switch -c feature/relaunch-through-proxy main @@ -446,13 +447,15 @@ git merge --ff-only feature/relaunch-through-proxy git push -u origin release/0.13.4 gh pr create --base main --title "Flowlight 0.13.4" -# wait for review and every required PR check to pass +# after reviewing the final head, a configured trusted reviewer posts this authenticated receipt: +gh pr comment --body "Reviewed-Commit: $(git rev-parse HEAD)" +# wait for that receipt and every required PR check to pass git tag -a v0.13.4 -m "Flowlight 0.13.4" -git push origin v0.13.4 # automatically runs the signed dry run; it cannot publish -# inspect that successful signed/notarized/Gatekeeper run, then publish from the same tag: -gh workflow run Release --ref v0.13.4 -f tag=v0.13.4 -# verify the GitHub release, DMG, PKG and SHA256SUMS.txt before the final merge +git push origin v0.13.4 # prepares, attests and retains the only bytes publication may use +# inspect that successful preparation, then publish its exact artifact from the same tag: +gh workflow run Release --ref v0.13.4 -f prepare_run_id= +# verify DMG, PKG, SHA256SUMS.txt and RELEASE-MANIFEST.json before the final merge gh pr merge --merge --delete-branch ``` @@ -461,17 +464,29 @@ comes after publication so the site can only ever describe something downloadabl force-push or move a pushed version. If a tag is wrong or a release needs another change, bump the patch version and begin a new release branch and tag. -A tag push starts `release.yml` automatically in dry-run-only mode; push-triggered runs cannot publish. It first -requires an open or merged `main` pull request and the latest successful CI and CodeQL checks at the exact tagged -commit, rather than running the same test suite a second time. It must then build, sign, notarize and pass Gatekeeper -verification before an explicit production dispatch for the immutable tag. After production succeeds, verify the -GitHub release page and downloaded DMG, PKG and `SHA256SUMS.txt` checksums before merging the release PR. +A tag push starts `release.yml` automatically in preparation-only mode; push-triggered runs cannot publish. It first +requires an open or merged `main` pull request and an exact-SHA `Reviewed-Commit: ` comment posted by a +login configured in the `RELEASE_REVIEWERS` repository variable. The commenter must be an owner, member, or +collaborator; the editable PR body is never accepted as review evidence. The release manifest retains the authenticated +comment ID, URL, reviewer, repository association, PR number, and reviewed SHA. Preparation also requires the latest +successful CI and CodeQL checks from their trusted workflows at that exact tagged commit. Every Actions run must also +identify the same authenticated, exact-head `main` PR; checks from another PR at the same commit are rejected. It then +builds, signs, notarizes, passes Gatekeeper, +records checksums and a source/review manifest, attests all five prepared files, and retains them for 90 days in one +immutable Actions artifact. An explicit publication dispatch names that run ID. Its separate job has no certificates, +provisioning profiles, Xcode, or notarization credentials: it verifies and publishes the retained bytes only. After +publication succeeds, verify the GitHub release page and downloaded DMG, PKG, `SHA256SUMS.txt`, and +`RELEASE-MANIFEST.json` before merging the release PR. ## Releasing from CI `.github/workflows/release.yml` checks the tag matches `MARKETING_VERSION`, verifies the exact commit already passed -the required PR CI and CodeQL checks, then signs, notarizes and verifies with `spctl`. A tag push always stops there; -an explicit dispatch for that tag publishes the release and updates the Homebrew cask. +the required PR CI and CodeQL checks, then signs, notarizes, verifies with `spctl`, creates the release manifest and +notes, attests every file, and uploads one immutable artifact. A tag push always stops there. An explicit dispatch for +that tag and preparation run ID verifies the run metadata, archive digest, manifest, checksums, and attestations; creates +or safely resumes a draft from those exact files; re-downloads and compares every release asset and the release body; +then publishes. If `TAP_TOKEN` is configured, Homebrew is updated idempotently from the already verified DMG; otherwise +GitHub publication succeeds and the cask is updated manually. Pull-request verification jobs explicitly check out the PR head SHA associated with their checks, not GitHub's synthetic merge ref. App-changing PRs also receive one required prospective-merge build of the synthetic tree, so exact @@ -485,22 +500,26 @@ requires every applicable job and preserves an immutable shadow receipt containi fingerprint, workflow schema, and conclusions. A cache is never release evidence. ```sh -scripts/ci-secrets.sh # upload the secrets once -git tag -a v0.3.0 -m "Flowlight 0.3.0" && git push origin v0.3.0 # automatic dry run -gh workflow run Release --ref v0.3.0 -f tag=v0.3.0 # after it passes: publish +scripts/ci-secrets.sh # upload preparation secrets once +git tag -a v0.3.0 -m "Flowlight 0.3.0" && git push origin v0.3.0 # automatic preparation +gh workflow run Release --ref v0.3.0 -f prepare_run_id= ``` `--ref` has to name a tag whose tree *contains* the workflow: `gh` reads the workflow file from the ref you give it, so tags older than `.github/workflows/release.yml` fail with "Workflow does not have 'workflow_dispatch' -trigger". The environment only accepts `v*` tags, so dispatching from a branch is refused at the approval gate — -which is the point of it. +trigger". Both environments accept only `v*` tags, so branch dispatches cannot reach signing or publication credentials. -A tag push—and an explicit dispatch with **dry_run=true**—builds, signs, notarizes and asks Gatekeeper for a verdict, -then stops without creating a release or touching the cask. Production requires an explicit dispatch with the tag input -and **dry_run=false**. +A tag push performs the only build/sign/notarize pass and stops after preserving its attested preparation. Production +requires an explicit dispatch with that successful push run's numeric ID. `scripts/release_artifact.py` rejects a run +from another event, repository, workflow, tag, SHA, or conclusion; an expired, missing, or duplicate artifact; changed +bytes; malformed checksums; manifest disagreement; and unexpected files. Its tested archive extractor also rejects +unsafe paths, duplicate names, non-regular entries, substituted package names, corruption, and oversized expansion before +writing prepared files. Publication verifies GitHub provenance for every file and publishes only after re-downloading and +comparing the draft successfully. A rerun resumes a draft, or verifies an already +public immutable release without mutating its assets, so a transient Homebrew failure does not require another signature. Release notes come from `packaging/release-notes/.md` when that file exists (checksums are appended), and -from generated notes when it doesn't. +from the matching generated site release entry when it does not. A release with neither source fails preparation. `scripts/ci-secrets.sh` finds the two provisioning profiles by name in Xcode's folder, so the only things it asks for are the certificate exports and how to notarize. Prefer the App Store Connect key where you can: it can only @@ -518,33 +537,47 @@ notarize, so revoking it breaks nothing else, whereas an app-specific password a | `NOTARY_APPLE_ID`, `NOTARY_PASSWORD`, `NOTARY_TEAM_ID` | *the other way:* an Apple ID and an app-specific password | | `TEAM_ID` | e.g. `38RJUJHKZS` | | `DMG_SIGN_IDENTITY`, `INSTALLER_IDENTITY` | the identity names, e.g. `Developer ID Application: Your Name (TEAMID)` | -| `TAP_TOKEN` | *optional.* Fine-grained token with Contents: write on `xinbetween/homebrew-tap`, so the cask updates itself | + +All values above belong only to the `release` preparation environment. `TAP_TOKEN` is the sole secret in the separate +`release-publish` environment: an optional fine-grained token with Contents: write on `xinbetween/homebrew-tap`. Never +put certificates or notarization credentials in `release-publish`, and never put the tap token in `release`. ### What protects it -A signing identity in GitHub Actions is a signing identity outside your Mac, so the release path is fenced in four -ways. Each one is a setting on the repository; they aren't in this file, so this is also the record of what to -check if the setup is ever rebuilt. - -1. **The secrets live in the `release` environment, not the repository.** Only a job that declares - `environment: release` can read them. A workflow added to a branch cannot. -2. **Only listed accounts can start a release.** The environment no longer waits for a click — a release by the - maintainer shouldn't need the maintainer to approve themselves. In its place the job refuses to run unless - `github.actor` is in `RELEASE_ACTORS` (a repository variable, `blessdyb` by default), so a tag pushed by - anyone else fails instead of producing a signed build. This is weaker than an approval: it trusts the account - rather than a person at the keyboard, so it stands on the account's own protection. Put the reviewers back - (Settings › Environments › release) if that trade stops being the right one. -3. **The environment only accepts `v*` tags.** Its deployment branch policy is a single tag rule, so a run from a - branch — or from a tag with any other name — can't reach the secrets even with approval. -4. **Release tags can't move.** A ruleset over `refs/tags/v*` blocks deletion, updates and force-pushes, so a - published tag always points at the commit that was reviewed. `main` is likewise protected from force-pushes +A signing identity in GitHub Actions is a signing identity outside your Mac, so the release path is fenced in six +ways. The repository settings among them are not stored in this file, so this is also the record of what to check if the +setup is ever rebuilt. + +1. **Signing secrets live only in `release`; publication credentials live only in `release-publish`.** The preparation + job has read-only Contents permission and cannot create a release. The publication job has no signing or Apple input, + and its static contract rejects any build, signing, provisioning, or notarization command. +2. **Only listed accounts can start or rerun a release.** The environments do not need a self-approval click — a release + by the maintainer should not require the maintainer to approve themselves. In its place both jobs require + `github.actor` and `github.triggering_actor` to appear in `RELEASE_ACTORS` (a repository variable, `blessdyb` by + default). This also prevents an unlisted account from inheriting the original actor's authority by rerunning a job. + This is weaker than an approval: it trusts the allowed account rather than a person at the keyboard, so it stands on + that account's own protection. Put the reviewers back in Settings › Environments if that trade stops being right. +3. **A trusted account authenticates the exact-head review receipt.** `RELEASE_REVIEWERS` is a repository variable + containing comma- or whitespace-separated GitHub logins. One of those logins, with owner/member/collaborator + association to this repository, must post the exact `Reviewed-Commit: ` line on the associated PR. Preparation + reads authenticated issue-comment metadata and retains its immutable ID and URL; a contributor-controlled PR body or + comment from any other account cannot satisfy this gate. +4. **Both environments accept only `v*` tags.** Each deployment branch policy is a single tag rule, so a run from a + branch — or from a tag with any other name — cannot reach either credential set. +5. **Only the designated release account can create release tags, and release tags can't move.** The active ruleset + over `refs/tags/v*` restricts creation with an always-bypass entry for the release account, then blocks deletion, + updates, and force-pushes for everyone. A collaborator therefore cannot tag attacker-controlled workflow content, + and a published tag always points at the commit that was reviewed. `main` is likewise protected from force-pushes and deletion. +6. **Published releases are immutable.** Repository immutable releases are enabled. Draft assets remain replaceable + only while they are being verified; making the draft public locks the tag and assets. The retained Actions artifact + and GitHub attestations independently bind the same files to the source SHA. -Third-party actions are pinned to a commit SHA rather than a moving tag, so a compromised action release can't -change what runs here. +Third-party actions are pinned to a commit SHA rather than a moving tag, so a compromised action release cannot change +what runs here. Repository-enforced SHA pinning is also enabled; every current action uses a full commit SHA. -**What's still true after all that.** Anyone who can approve a release can sign software as you, and anyone with -admin access can change who that is — a compromise of the GitHub account is a compromise of the certificate. The +**What's still true after all that.** Any allowed release account can cause CI to sign software as you, and anyone with +admin access can change that allowlist or the environment controls — compromising either path compromises the certificate. The App Store Connect key only notarizes and can't sign, so rotating it is cheap; the Developer ID certificates are the ones that matter, and they're revocable in Apple's developer portal. If that trade is ever the wrong one, the local recipe above still works and the workflow can sit unused. diff --git a/docs/about/index.html b/docs/about/index.html index 5ede6eb9..4c3f0dbd 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -153,7 +153,7 @@

Thanks

diff --git a/docs/assets/screenshots/agent-detail.png b/docs/assets/screenshots/agent-detail.png index c6a37dd6..82e8749a 100644 Binary files a/docs/assets/screenshots/agent-detail.png and b/docs/assets/screenshots/agent-detail.png differ diff --git a/docs/assets/screenshots/agent-tools.png b/docs/assets/screenshots/agent-tools.png index e5cb760f..e4eb2d96 100644 Binary files a/docs/assets/screenshots/agent-tools.png and b/docs/assets/screenshots/agent-tools.png differ diff --git a/docs/assets/screenshots/agents.png b/docs/assets/screenshots/agents.png index 2bf4cba4..0bfb51e3 100644 Binary files a/docs/assets/screenshots/agents.png and b/docs/assets/screenshots/agents.png differ diff --git a/docs/assets/screenshots/alerts.png b/docs/assets/screenshots/alerts.png index 916069e8..7d8ab0ea 100644 Binary files a/docs/assets/screenshots/alerts.png and b/docs/assets/screenshots/alerts.png differ diff --git a/docs/assets/screenshots/always-monitor.png b/docs/assets/screenshots/always-monitor.png index 38ad7579..dcff9ef2 100644 Binary files a/docs/assets/screenshots/always-monitor.png and b/docs/assets/screenshots/always-monitor.png differ diff --git a/docs/assets/screenshots/inspect.png b/docs/assets/screenshots/inspect.png index 8ac1f8c1..80803897 100644 Binary files a/docs/assets/screenshots/inspect.png and b/docs/assets/screenshots/inspect.png differ diff --git a/docs/assets/screenshots/live.png b/docs/assets/screenshots/live.png index 283deb0d..02e14ab6 100644 Binary files a/docs/assets/screenshots/live.png and b/docs/assets/screenshots/live.png differ diff --git a/docs/assets/screenshots/manifest.json b/docs/assets/screenshots/manifest.json index 28dcbdb6..5308ad30 100644 --- a/docs/assets/screenshots/manifest.json +++ b/docs/assets/screenshots/manifest.json @@ -3,62 +3,62 @@ "screenshots": { "agent-detail.png": { "height": 1988, - "sha256": "24d5911b28de614100c70a38db5b73c7678b68750e4635d958153e2d2f7b4345", - "version": "0.17.3", + "sha256": "3112dfc92b9c96f4260fa879219027a6abbd15e30fe6b14f06a6239c1e1b1279", + "version": "0.17.4", "width": 3200 }, "agent-tools.png": { "height": 1424, - "sha256": "ea6450cad546bc142e8eeaf4cac051f1b9d7b253bb336ee7d079278c647414c5", - "version": "0.17.3", + "sha256": "fdac9e90c0b68e4fc4d21f837b13b7b825927f8b62f67571bdd4244f915016d5", + "version": "0.17.4", "width": 3200 }, "agents.png": { "height": 1988, - "sha256": "9ab6ca6a8536b48a98a4f771bc22070e773a66dc4beea96183d452dd4976d55d", - "version": "0.17.3", + "sha256": "9283586bfa8bee4e9f89057613d1132e1e6c77d2ac030e7a010bd3a170b17d8e", + "version": "0.17.4", "width": 3200 }, "alerts.png": { "height": 1988, - "sha256": "e24902bc31cdf83513b4c908dd8a57b62ee5887712c359dc65f1c63df9263733", - "version": "0.17.3", + "sha256": "c0b5409635d8d214b06a7b5ae4d76092b6e71a6bf48e6c0ed84d35dff9f2c317", + "version": "0.17.4", "width": 3200 }, "always-monitor.png": { "height": 1988, - "sha256": "e21d673f2ab978ca767b7b6628aad3005387e8e12ff31972f1dc2c616cda207d", - "version": "0.17.3", + "sha256": "9d78773c1ef51978edb3f78bb4746c4255349daf70197b589f137ce4df5a5128", + "version": "0.17.4", "width": 3200 }, "inspect.png": { "height": 1988, - "sha256": "c74fd8d50fda010f05fbabcc4e5dfcdbf8a1207b16f1b79b3a9cc2314ef260d7", - "version": "0.17.3", + "sha256": "6dd77b55204b317e5bdef30b7ab2b8e550bb111582417ca9fc3b5fed36536518", + "version": "0.17.4", "width": 3200 }, "live.png": { "height": 1988, - "sha256": "023b487c1d3fada57c6afd800c245b01656e7790b486c67510ff848a7537eaa8", - "version": "0.17.3", + "sha256": "c1170b0027969a69107d473f0aefe227a496e54fe6536073b89a4976aa02585e", + "version": "0.17.4", "width": 3200 }, "mcp-servers.png": { "height": 1424, - "sha256": "5753c21b921d8b16904ecd819bac062238c46534dec8ffabc70e8c7cdec3d4d9", - "version": "0.17.3", + "sha256": "b3c9879845e0838c0d51b60687198191cc10d3759af2ebc6b435a9b05fe33a66", + "version": "0.17.4", "width": 3200 }, "reports.png": { "height": 1988, - "sha256": "921a83c0e6a0fa34db78223e38f6e43bfab3e476c2fc6eb748afd4eceb7b84a0", - "version": "0.17.3", + "sha256": "e7c313707a596c00b7fdc92835c89aecc0d53a0d5ed632b1b4a8b639bcc67594", + "version": "0.17.4", "width": 3200 }, "visualize.png": { "height": 1988, - "sha256": "6d140321d16f096326fd5deb3d5430f6d7327a9ac9390cf95959d29db25da132", - "version": "0.17.3", + "sha256": "76a0de61bc483aceb5cbfec1c4447ea8a09814e397369a5e7d19bc34fe42d183", + "version": "0.17.4", "width": 3200 } } diff --git a/docs/assets/screenshots/mcp-servers.png b/docs/assets/screenshots/mcp-servers.png index 6ffc9c12..05dd0a39 100644 Binary files a/docs/assets/screenshots/mcp-servers.png and b/docs/assets/screenshots/mcp-servers.png differ diff --git a/docs/assets/screenshots/reports.png b/docs/assets/screenshots/reports.png index 15998659..2c628da3 100644 Binary files a/docs/assets/screenshots/reports.png and b/docs/assets/screenshots/reports.png differ diff --git a/docs/assets/screenshots/visualize.png b/docs/assets/screenshots/visualize.png index 6d94d685..da8e96bc 100644 Binary files a/docs/assets/screenshots/visualize.png and b/docs/assets/screenshots/visualize.png differ diff --git a/docs/de/about/index.html b/docs/de/about/index.html index 529c9357..2e646d80 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -152,7 +152,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index 038f0cae..a91c79d5 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -76,7 +76,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.17.3, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.17.4, macOS 15 oder neuer.

@@ -170,11 +170,11 @@

Die AnsichtenWarnungen⌘5Alles, was ungewöhnlich aussah, mit App, Ziel und dem Wert, der die Regel ausgelöst hat. Fragen⌘6Fragen zu deinem eigenen Datenverkehr in normaler Sprache, beantwortet aus der aufgezeichneten Historie von einem lokalen oder einem gehosteten Modell. Inspizieren⌘7 · Developer IDOptional: entschlüsselte Anfragen der Agenten, die du über Flowlight leitest, und die Tool-Aufrufe dahinter. Die Suche reicht in Anfrage- und Antwort-Bodys hinein. - Regeln⌘8 · Developer ID
⌘7 · App StoreJede Erlaubnisliste und jede Verweigerung, die du gesetzt hast, was jede davon abdeckt und was sie tatsächlich gestoppt hat. - Quellen⌘9 · Developer ID
⌘8 · App StoreCollector-Zustand, Gerätekanäle und Benennungsabdeckung in drei Tabs. + Richtlinien⌘8 · Developer ID
⌘7 · App StoreJede konfigurierte Richtlinie, ob sie tatsächlich wirksam ist, und ein einheitlicher schreibgeschützter Aktivitätsverlauf. + Quellen⌘9 · Developer ID
⌘8 · App StoreZustand und Wiederherstellung des Collectors, optionale Gerätekanäle und Abdeckung auf einer einzigen scrollbaren Betriebsseite. -

Die Kurzbefehle folgen der sichtbaren Reihenfolge der Seitenleiste. Developer-ID-Versionen enthalten Inspizieren auf ⌘7; App-Store-Versionen lassen es weg, daher rücken Regeln und Quellen auf ⌘7 und ⌘8.

-
Die Kurzbefehle folgen der sichtbaren Reihenfolge der Seitenleiste. Developer-ID-Versionen enthalten Inspizieren auf ⌘7; App-Store-Versionen lassen es weg, daher rücken Richtlinien und Quellen auf ⌘7 und ⌘8.

+
Der Bildschirm Live: eine Liste von Anwendungen mit ihren aktuellen Sende- und Empfangsraten und ihrem meistgenutzten Ziel.
Live, der Bildschirm, mit dem Flowlight startet.

Bewege in Verlauf den Zeiger über einen Balken, um eine mitlaufende, scrollbare Karte mit den beteiligten Apps zu öffnen; die Balken zoomen beim Klicken nicht. Doppelklicke eine Zeile, um den Verlauf darauf einzugrenzen.

@@ -216,7 +216,7 @@

Tools & MCP-Server

eine nur konfigurierte Domain nie erreicht wurde und eine nur beobachtete nirgends aufgeschrieben stand, wo du sie hättest lesen können.

Jede Domain bietet zwei Verweigerungen an: für diesen Agenten, was den Agenten und die von ihm gestarteten Tools abdeckt, oder überall, was jede App auf dem Mac abdeckt. Beide schreiben eine ganz gewöhnliche Regel, sodass der - Verstoß-Verlauf sie meldet und der Regeleditor sie erklärt – und weil es gewöhnliche Regeln sind, sagt dir die Simulation im Editor, was + schreibgeschützte Aktivität sie meldet und der Regeleditor sie erklärt – und weil es gewöhnliche Regeln sind, sagt dir die Simulation im Editor, was eine davon verweigert hätte, bevor du sie einschaltest.

Eine Art von Domain hat keine Knöpfe. Ein Connector, den der Modellanbieter von sich aus erreicht – im Body der Anfrage deklariert und von der Seite des Anbieters aus kontaktiert –, berührt diesen Mac überhaupt nicht, also kann keine Regel hier ihn erreichen. Die Zeile sagt @@ -407,16 +407,16 @@

Mock-Antworten

verbunden: Ein Mock ändert die Antwort, nicht die Frage, ob die Verbindung zustande kommt. „Die API liefert 500“ funktioniert also, „die API ist überhaupt nicht erreichbar“ nicht.

-

Regeln

-

Eine Erlaubnisliste ist die Vorstellung eines Agenten davon, wohin er gehen darf. Eine Regel ist die allgemeine Fassung: Sie nennt ein +

Richtlinien

+

Unter Richtlinien sind alle Kontrollen, die Flowlight anwenden kann, in einem gemeinsamen Verzeichnis zusammengefasst: gewöhnliche Erlaubnis- und Sperrregeln, Agenten-Erlaubnislisten, verwaltete Pakete und – wo die HTTPS-Inspektion verfügbar ist – Schutzregeln, Mock-Antworten, Anfrageumschreibungen und Antworttransformationen. Jede Zeile unterscheidet den konfigurierten Zustand von der tatsächlichen Wirkung: wirksam, nur meldend, beobachtend, wartend, geplant, pausiert, verbraucht, in Anwendung, Prüfung erforderlich, nicht verfügbar, inkompatibel oder fehlgeschlagen. Wenn du eine Zeile auswählst, öffnet sich der zugehörige Editor dieser Richtlinie; die gemeinsame Ansicht ersetzt oder vermischt nicht deren unterschiedliche Einwilligungs- und Speichermodelle.

+

Eine Erlaubnisliste ist die Vorstellung eines Agenten davon, wohin er gehen darf. Eine gewöhnliche Regel ist die allgemeine Fassung: Sie nennt ein Subjekt – eine App, einen Agenten, ein Ziel, eine URL oder ein Paar aus zwei davon – und sagt blockieren oder erlauben. Rechtsklicke eine Zeile in Live, Verlauf, KI-Agenten oder Inspizieren, um eine Regel über das zu schreiben, was du gerade ansiehst; - jede davon landet im selben Editor auf dem Bildschirm Regeln, denn eine Regel, die im Verborgenen greift, - wäre eine Regel, die du nie wiederfindest.

+ jede bleibt unter Richtlinien auffindbar und öffnet sich in ihrem maßgeblichen Editor.

Vorlagen. Zweiundzwanzig, in fünf Gruppen – einen Agenten von Paketregistern oder anderen Modellanbietern fernhalten, die üblichen Wege schließen, auf denen Daten abfließen (Paste-Dienste, Webhook-Fänger, anonyme Datei-Ablagen, Tunnel, DNS over HTTPS), Produktanalyse und Crash-Berichte, Regeln für Arbeitszeiten und das einmalige „blockiere das eine Stunde lang, bis ich - herausgefunden habe, was es ist“. Durchsuche sie über die Symbolleiste von Regeln, oder rechtsklicke irgendwo in Live, Verlauf, KI-Agenten oder Inspizieren eine Zeile und + herausgefunden habe, was es ist“. Durchsuche sie über die Symbolleiste von Richtlinien, oder rechtsklicke irgendwo in Live, Verlauf, KI-Agenten oder Inspizieren eine Zeile und wende eine auf das an, was du ansiehst.

Jede Vorlage sagt, was sie tun wird, bevor sie es tut. Alles, was mehr als eine Regel schreibt, öffnet eine Karte mit jedem Host und jedem Zeitplan; alles Dauerhafte öffnet den vorausgefüllten Editor; und nur Regeln, die sich selbst aufheben – nach einer @@ -437,8 +437,7 @@

Regeln

Adresse und Port, nichts über Pfade, und das nur im Erweiterungsmodus. Die HTTPS-Inspektion verweigert eine einzelne Anfrage, kann also eine URL treffen und mit einem Status antworten, den der Agent lesen kann, statt mit einem Socket, der ohne Erklärung starb. Der nettop-Sampler kann überhaupt nichts verweigern. Eine Regel, die die aktuelle Einrichtung nicht umsetzen kann, sagt das in der Liste, statt dort durchgesetzt auszusehen.

-

Der zweite Tab ist der Verstoß-Verlauf: jede Verbindung, über die eine Regel entschieden hat, mit der App, dem Ziel, welche - Regel entschied und ob das eine Verweigerung war oder eine Ausnahme, die sie durchließ.

+

Die zweite Ansicht ist die schreibgeschützte Aktivität: gewöhnliche Regelentscheidungen, Verstöße gegen Erlaubnislisten, Ereignisse verwalteter Pakete und eigenständige Wirkungen von Inspektionsrichtlinien in einer gemeinsamen Chronologie. Sie bereitet die bestehenden maßgeblichen Einträge auf, statt den Verlauf in ein einheitliches allgemeines Ereignismodell umzuschreiben; App-Store-Versionen lassen von der Inspektion abhängige Richtlinienarten und deren aufbewahrte Aktivität weg.

Was sie bewirkt hätte. Eine Regel, die blockiert, ist leicht zu schreiben und beängstigend einzuschalten, denn das Erste, was man normalerweise über eine solche Regel erfährt, ist, was sie kaputt gemacht hat – und man erfährt es später, @@ -460,7 +459,7 @@

Regeln

getestet wurde, ist gegen zwei Stunden Aufzeichnung getestet.

Verwaltete Werbe- und Tracker-Pakete

-

Regeln › Pakete enthält drei offizielle Hostnamenlisten: Werbung, Websiteübergreifendes Tracking und Datenschutz und Telemetrie. Sie sind von Anfang an sichtbar und standardmäßig alle deaktiviert. Zum Blockieren ist die Netzwerkerweiterung erforderlich; der nettop-Sampler kann nur beobachten.

+

Richtlinien › Verwaltete Pakete enthält drei offizielle Hostnamenlisten: Werbung, Websiteübergreifendes Tracking und Datenschutz und Telemetrie. Sie sind von Anfang an sichtbar und standardmäßig alle deaktiviert. Zum Blockieren ist die Netzwerkerweiterung erforderlich; der nettop-Sampler kann nur beobachten.

Ausschließlich offizielle, signierte Updates. Flowlight prüft eine einzige festgelegte HTTPS-Quelle auf neuere, vom Herausgeber signierte Kataloge. Automatische Prüfungen sind standardmäßig aktiviert und können hier deaktiviert werden. Eine Signatur authentifiziert den Inhalt, aktiviert ihn aber nicht: Ein neues Paket bleibt deaktiviert, und bei geänderten Zielen wird ein aktiviertes Paket nicht mehr durchgesetzt, bis du die genaue neue Version geprüft und auf Sichern geklickt hast. Akzeptierte Inhalte können während eines Ausfalls als sichtbar veralteter letzter bekannter funktionsfähiger Stand erhalten bleiben. Dies sind Netzwerksperren auf Hostnamenebene: Sie können keine Lücken im DOM oder Layout, Browser-Overlays, skriptgesteuerten Pop-ups oder Fenster nativer Apps entfernen.

Wenn die HTTPS-Inspektion eine App durch Flowlights Proxy leitet, entscheidet er anhand der ursprünglichen App-Identität über gewöhnliche Zielregeln und aktivierte verwaltete Pakete, bevor eine Verbindung zum Ursprungsserver geöffnet wird. Bei angepinnten oder nie entschlüsselten CONNECT-Tunneln funktioniert das anhand des Hostnamens; Agenten-Erlaubnislisten bleiben auf den direkten Weg über die Netzwerkerweiterung beschränkt. Ist der Eigentümer oder Hostname unbekannt, wird die Verbindung im Zweifel zugelassen.

@@ -509,7 +508,8 @@

Schutzregeln für Agenten: ein Tool, einen Server oder eine

Das ist keine Sandbox. Ein verweigertes Tool wird nicht mehr angeboten und nicht mehr beantwortet. Ein Agent, der noch eine Shell hat, kann von Hand tun, was das Tool getan hätte. Flowlight zeichnet das auf; es tut nicht so, als würde es das verhindern.

-

Quellen: Collector und Engines

+

Quellen: Collector, Geräte und Abdeckung

+

Quellen ist eine einzige scrollbare Betriebsseite: zuerst Collector, dann Geräte und Abdeckung als klar bezeichnete Abschnitte. Statushinweise, Antworten unter Fragen, wiederhergestellte Fenster und Links aus Screenshots lassen die Seite zum passenden Abschnitt scrollen und heben ihn kurz hervor, ohne die anderen auszublenden.

Flowlight zeichnet Datenverkehr auf eine von zwei Weisen auf, wählbar unter Quellen › Collector. Sie unterscheiden sich darin, was sie verpassen können, und das lohnt sich zu verstehen, bevor du dich auf eine verlässt.

@@ -706,7 +706,7 @@

Fragen & Antworten

Mit geschlossenem Fenster tut Flowlight fast nichts: Es hält die Raten in der Menüleiste und schreibt eine Zeile pro Sekunde. Die Netzwerkerweiterung kostet weniger als der Sampler als Rückfallebene, der jede Sekunde einen kurzlebigen nettop-Prozess startet.
Kann es Verbindungen blockieren?
-
Ja, mit der Netzwerkerweiterung. Die Erlaubnisliste eines Agenten kann verweigern und nicht nur warnen, eine Regel kann eine App, +
Ja, mit der Netzwerkerweiterung. Die Erlaubnisliste eines Agenten kann verweigern und nicht nur warnen, eine Regel kann eine App, ein Ziel oder eine URL so lange blockieren, wie du sagst, und eine Schutzregel kann einem Agenten ein Tool wegnehmen, bevor sein Modell es angeboten bekommt. Nichts blockiert, bis du es verlangst, und jede Verweigerung führt ihren Ausweg mit.
Brauche ich die Netzwerkerweiterung?
@@ -761,7 +761,7 @@

Grenzen

diff --git a/docs/de/index.html b/docs/de/index.html index 00e84079..047a924b 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.17.4macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -127,7 +127,7 @@

Sieh, was deine Apps im Netz tun.
-
Flowlights Karte der KI-Aktivität. Agentenkarten führen über zugeordnete Werkzeuge und konfigurierte Gateways; die beobachtete Route von magpie über Flowlight verzweigt sich anschließend zu LLM-Diensten und Domains. Die Bandbreite zeigt das Verkehrsvolumen des Zeitraums.

Sieh die tatsächliche KI-Route: Agenten, zugeordnete Werkzeuge, konfigurierte Gateways, Flowlights beobachtete Proxy-Stufe und exakte Ziele. Die Bandbreite zeigt empfangene plus gesendete Daten im gewählten Zeitraum. Mit Demodaten.

@@ -135,7 +135,7 @@

Sieh, was deine Apps im Netz tun.
-
Flowlights Ansicht KI-Agenten. Claude Code, Cursor, Codex und ein Python-Skript mit ihren KI-Anbietern, dem woanders hin gesendeten Datenverkehr und Risikomarkierungen für SMTP, SSH, FTP, einen großen Upload und eine reine IP.

Die Ansicht KI-Agenten: der Modellanbieter jedes Agenten – und alles andere, mit dem er gesprochen hat. Mit Demodaten gezeigt.

@@ -179,11 +179,11 @@

Verlauf

Wo ist alles hingegangen? Sekunde bis Jahr, gruppiert nach Ap

Warnungen

Was sah falsch aus? Nachvollziehbare Warnungen pro App, mit Ziel und Schwellenwert.

⌘5
-

Regeln

Was habe ich verweigert – und passiert es? Jede Regel, was sie blockiert hat, und ein Verlauf jeder Entscheidung.

⌘8 · Developer ID / ⌘7 · App Store
+

Richtlinien

Welche Kontrollen gibt es, und sind sie wirksam? Jede Richtlinie, ihr tatsächlicher Zustand und ein einheitlicher Aktivitätsverlauf.

⌘8 · Developer ID / ⌘7 · App Store

Fragen

Fragen in normaler Sprache, beantwortet aus dem Verlauf – mit jeder ausgeführten Abfrage darunter.

⌘6
-

Quellen

Kommen Daten an, wie viel ist benannt und welche Gerätekanäle sind aktiv? Collector, Geräte und Abdeckung an einem Ort.

⌘9 · Developer ID / ⌘8 · App Store
+

Quellen

Kommen Daten an, welche Gerätekanäle sind aktiv, und was kann Flowlight erfassen? Eine Seite vom Collector bis zur Abdeckung.

⌘9 · Developer ID / ⌘8 · App Store

Inspizieren

Optional. Was hat der Agent wirklich gesendet, und welcher Tool-Aufruf hat es ausgelöst? Entschlüsselte Anfragen, Tool-Aufrufe, Ergebnisse.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

Unterstützte Tool-Aufrufe mit Netzwerkanfragen verknüpfen

Mac. Starte einen Agenten aus einem inspizierten Terminal, und der unterstützte HTTP-Verkehr des Agenten und seiner Tools wird entschlüsselt – mit den Tool-Aufrufen, die das Modell angefordert hat, und den Ergebnissen, die zurückgingen.

-
Flowlights Ansicht Inspizieren: eine Liste entschlüsselter Anfragen von Claude Code mit ihren Tool-Aufrufen und das Detail des curl-Uploads zu paste.example, ausgelöst vom Bash-Tool-Aufruf, mit den hochgeladenen Notizen im Anfrage-Body
Wo sind meine Notizen gelandet? Der curl-Upload zu paste.example, zurückverfolgt bis zum Bash-Befehl, den das Modell angefordert hat, mit dem gesendeten Body.
-
Der Tab Tools für Claude Code: Bash, Read und zwei MCP-Tools mit Beschreibungen und der Häufigkeit, mit der das Modell sie aufgerufen hat
Jedes Tool, das ein Agent dem Modell anbietet. Agenten deklarieren ihre Tools bei jedem Zug, also listet Flowlight alle auf – mit denen, die das Modell tatsächlich genutzt hat, und denen, die es nie angefasst hat.
-
Der Tab MCP-Server für Codex: ein sentry-Server, markiert als über den Anbieter, seine URL, Freigabe auf nie gesetzt, erlaubte Tools und die Tools, die er anbietet
Auch die, die du auf der Leitung nicht siehst. Bittet ein Agent den Anbieter, sich mit einem MCP-Server zu verbinden, erreicht dieser Datenverkehr deinen Mac nie. Seine Anfrage schon – also tauchen der Server, seine URL, seine Freigabeeinstellung und seine Tools trotzdem auf.
@@ -460,7 +460,7 @@

Ein Wächter, den du nicht bewachen musst

-
Das Detail von Claude Code in Flowlight: Tools und MCP-Server mit Aufrufzahlen und einem Fehlschlag, eine Erlaubnisliste und der Tab Tool-Aufrufe mit einem fehlgeschlagenen git push, einem curl-Upload zu paste.example samt Antwort und Anfrage, npm install, einem GitHub-Issue, einer Doku-Suche und einem Datei-Lesevorgang, jeweils mit Ergebnis
Ein Agent, jeder Tool-Aufruf. Der Datenverkehr der git-, curl- und MCP-Server, die Claude Code gestartet hat, zählt als Datenverkehr von Claude Code. Mit eingeschalteter HTTPS-Inspektion zeigt der Tab Tool-Aufrufe, was das Modell angefordert hat, was zurückkam und welche Anfragen jedes Tool gestellt hat: @@ -470,10 +470,10 @@

Ein Wächter, den du nicht bewachen musst

-
Die Ansicht Verlauf mit stündlichen Balken für empfangene und gesendete Daten und Ringdiagrammen für Apps, Ziele und Protokolle
Verlauf. Zoom von der Sekunde bis zum Jahr, drei Gruppierungen, Diagramme, die auch bei hunderten Apps lesbar bleiben.
-
Die Ansicht Warnungen mit Warnungen zu Agenten-Exfiltration, E-Mail, FTP, Aktivität bei Abwesenheit und einer Verkehrsspitze
Warnungen. Jede nennt die App, das Ziel und den Wert, der sie ausgelöst hat.
@@ -589,7 +589,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index 4879b58a..74aaba64 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -125,7 +125,7 @@

Netzwerkanfragen, die die App stellt

- + @@ -189,7 +189,7 @@

Kontakt

diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html index c00bcb1b..8fa673c1 100644 --- a/docs/de/threat-model/index.html +++ b/docs/de/threat-model/index.html @@ -225,7 +225,7 @@

Eine Schwachstelle melden

diff --git a/docs/docs/index.html b/docs/docs/index.html index c070431a..40a5b602 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -76,7 +76,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.17.3, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.17.4, macOS 15 or later.

@@ -170,11 +170,11 @@

The views

- - + +
AnfrageGesendet anWas sie enthältWann
Update-Prüfungapi.github.comEine normale HTTPS-Anfrage nach der neuesten Version; GitHub sieht deine IP-Adresse und den User-Agent Flowlight/<version>Beim Start und täglich, ab 0.1.2. Abschalten in Einstellungen › Allgemein
Update-Downloadgithub.comDas Image und seine PrüfsummendateiNur wenn du auf „Herunterladen & öffnen“ klickst
Offizielle Regelpaket-Aktualisierungflowlight.xinbetween.comEine standardmäßige HTTPS-Anfrage an den festgelegten offiziellen Feed. Der Server sieht deine IP-Adresse, den User-Agent Flowlight/<version> und bedingte Cache-Validatoren; er erhält weder Datenverkehr noch Paketeinstellungen oder eine GerätekennungNach dem Start, sobald eine Prüfung fällig ist, und danach bei störungsfreiem Betrieb höchstens einmal täglich. Standardmäßig aktiviert; Ausschalter und manuelle Option Jetzt suchen unter Regeln › Pakete
Offizielle Regelpaket-Aktualisierungflowlight.xinbetween.comEine standardmäßige HTTPS-Anfrage an den festgelegten offiziellen Feed. Der Server sieht deine IP-Adresse, den User-Agent Flowlight/<version> und bedingte Cache-Validatoren; er erhält weder Datenverkehr noch Paketeinstellungen oder eine GerätekennungNach dem Start, sobald eine Prüfung fällig ist, und danach bei störungsfreiem Betrieb höchstens einmal täglich. Standardmäßig aktiviert; Ausschalter und manuelle Option Jetzt suchen unter Richtlinien › Verwaltete Pakete
Reverse-DNSDer von dir eingerichtete DNS-ResolverEine Abfrage des registrierten Namens einer IP-Adresse, wenn kein Hostname gesehen wurdeNach Bedarf, im Erfassungsmodus des Samplers, der Standard ist, bis die Netzwerkerweiterung läuft. Kein Schalter zum Abschalten: ohne sie kann der Sampler die meisten Verbindungen nicht benennen. Die Netzwerkerweiterung benennt sie aus dem Datenverkehr selbst und macht diese Abfrage nicht
Abfrage des NetzinhabersTeam Cymru, über DNSÖffentliche IP-Adressen ohne bekannten Hostnamen. Private und reservierte Adressen verlassen deinen Mac nieStandardmäßig ein. Abschalten in Erfassung › Netzinhaber bestimmen
Fragen mit einem gehosteten ModellDer Anbieter, den du wählst: Anthropic, OpenAI, Google Gemini oder ein kompatibler EndpunktDeine Frage und die Ergebnisse der nur lesenden Flowlight-Abfragen, die das Modell ausgewählt hat. Die Verkehrsdatenbank selbst wird nicht hochgeladenNur wenn du einen gehosteten Anbieter wählst und eine Frage abschickst. Der Bildschirm Fragen zeigt die gesendeten Anfragen
Alerts⌘5Everything that looked unusual, with the app, the destination and the number that tripped the rule.
Ask⌘6Questions about your own traffic in plain language, answered from the recorded history by a local or a hosted model.
Inspect⌘7 · Developer IDOptional: decrypted requests from the agents you route through Flowlight, and the tool calls behind them. Search reaches into request and response bodies.
Rules⌘8 · Developer ID
⌘7 · App Store
Every allowlist and refusal you have set, what each one covers, and what it has actually stopped.
Sources⌘9 · Developer ID
⌘8 · App Store
Collector health and recovery, hostname coverage, and opt-in camera, Bluetooth, USB and storage activity in three tabs.
Policies⌘8 · Developer ID
⌘7 · App Store
Every configured policy, whether it is actually effective, and a unified read-only activity trail.
Sources⌘9 · Developer ID
⌘8 · App Store
Collector health and recovery, opt-in device channels, and coverage as one scrollable operational page.
-

The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Rules and Sources move to ⌘7 and ⌘8.

-
The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Policies and Sources move to ⌘7 and ⌘8.

+
The Live screen: a list of applications with their current send and receive rates and busiest destination.
Live, the screen Flowlight opens on.

In History, choose 15 min, 1h, 6h, 1d, 1w, 1 mo, 6 mo, 1y, or All. Hover-only bars open a cursor-following, scrollable card with the bucket totals and contributing apps; they never change the selected range. The navigator below the chart starts with Nearby context and can switch to All history: drag the brush to pan the selected window, or drag either edge to resize it. Double-click any breakdown row to narrow History to it.

@@ -216,7 +216,7 @@

Tools & MCP servers

in a configuration file and actually contacted says more than either alone, while a domain only ever configured has never been reached, and one only ever observed was never written down anywhere you could have read it.

Each domain offers two refusals: for this agent, which covers the agent and the tools it starts, or - everywhere, which covers every app on the Mac. Both write an ordinary rule, so the violations feed reports + everywhere, which covers every app on the Mac. Both write an ordinary rule, so Activity reports them and the rule editor explains them — and because they are ordinary rules, the simulation in the editor will tell you what one would have refused before you switch it on.

One kind of domain has no buttons. A connector the model provider reaches on its own — declared in the request body and @@ -415,16 +415,20 @@

Mock responses

connected to: a mock changes the answer, not whether the connection can be made, so "the API returns 500" works while "the API is entirely unreachable" does not.

-

Rules

-

An allowlist is one agent's idea of where it may go. A rule is the general version: it names a +

Policies

+

Policies is one inventory for every control Flowlight can apply: ordinary allow/block rules, agent + allowlists, managed packs, and—where HTTPS inspection is available—guardrails, mocks, request rewrites, and response + transforms. Each row distinguishes configured state from actual effect: enforcing, reporting, watching, waiting, + scheduled, paused, spent, applying, review required, unavailable, incompatible, or failed. Selecting a row opens the + contextual editor that owns that policy; Policies does not replace or blur those separate consent and persistence models.

+

An allowlist is one agent's idea of where it may go. An ordinary rule is the general version: it names a subject — an app, an agent, a destination, a URL, or a pairing of two of those — and says block or allow. Right-click any row in Live, History, AI Agents or Inspect to write one about what you're looking at; - every one of them lands in the same editor on the Rules screen, because a rule that took effect out of sight - would be a rule you could never find again.

+ every one remains findable in Policies and opens in its authoritative editor.

Templates. Twenty-two of them, in five groups — stopping an agent reaching package registries or other model providers, closing the common ways data leaves (paste sites, webhook catchers, anonymous file drops, tunnels, DNS over HTTPS), product analytics and crash reporting, working-hours policies, and one-off "block this for an hour while I work out - what it is". Browse them from the Rules toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and + what it is". Browse them from the Policies toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and apply one to what you're looking at.

Each template says what it will do before it does it. Anything that writes more than one rule opens a card listing every host and schedule; anything permanent opens the editor pre-filled; and only rules that undo themselves — an @@ -445,8 +449,9 @@

Rules

address and the port, nothing about paths, and only in extension mode. HTTPS inspection refuses one request, so it can match a URL and answer with a status the agent can read instead of a socket that died without saying why. The nettop sampler cannot refuse anything at all. A rule the current setup can't carry out says so in the list rather than sitting there looking enforced.

-

The second tab is the violations feed: every connection a rule decided, with the app, where it was headed, which - rule decided it and whether that was a refusal or an exception letting it through.

+

The second view is read-only Activity: ordinary rule decisions, allowlist violations, managed-pack events, + and distinct inspection-policy effects in one chronology. It adapts the existing authoritative records rather than rewriting + history into one generic event model; Store builds omit inspection-dependent policy kinds and their retained activity.

What it would have done. A blocking rule is easy to write and frightening to switch on, because the first thing you normally learn about one is what it broke — and you learn it later, from something failing somewhere else. The rule @@ -466,7 +471,7 @@

Rules

history has been tested against two hours of history.

Managed ad and tracker packs

-

Rules › Packs contains three official lists built into Flowlight: Advertising covers known +

Policies › Managed packs contains three official lists built into Flowlight: Advertising covers known advertising delivery and auction hosts; Cross-site tracking covers common analytics, session replay and cross-site measurement hosts; and Privacy and telemetry covers crash reporting, install attribution, and audited Apple advertising and analytics hosts. The packs are visible from the start and all are off by default.

@@ -551,8 +556,10 @@

Agent guardrails: block a tool, a server or a resource

This is not a sandbox. A refused tool stops being offered and stops being answered. An agent that still has a shell can do by hand what the tool would have done. Flowlight will record that; it does not pretend to prevent it.

-

Sources: collector, coverage and devices

-

Sources keeps operational setup in one place. Its health summary shows whether traffic is arriving, how much is named, and which optional device channels are active; any unfinished collector step links directly to Sources › Collector.

+

Sources: collector, devices and coverage

+

Sources is one scrollable operational page: Collector first, then Devices and Coverage as clearly headed + sections. Status notices, Ask answers, restored windows, and screenshot links scroll to and briefly focus the relevant section + without hiding the others.

Flowlight records traffic one of two ways, chosen under Sources › Collector. They differ in what they can miss, which is worth understanding before you rely on either.

@@ -755,7 +762,7 @@

Questions & answers

With the window closed Flowlight does almost nothing: it keeps the menu bar rates and writes one row a second. The Network Extension costs less than the fallback sampler, which starts a short-lived nettop process every second.
Can it block connections?
-
Yes, with the Network Extension. An agent's allowlist can refuse as well as warn, a rule can block an app, +
Yes, with the Network Extension. An agent's allowlist can refuse as well as warn, a rule can block an app, a destination or a URL for as long as you say, and a guardrail can take a tool away from an agent before its model is offered it. Nothing blocks until you ask it to, and every refusal carries its way out.
Do I need the Network Extension?
@@ -811,7 +818,7 @@

Limitations

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index 3b7c8cdd..c0f1d8d3 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -152,7 +152,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 02340914..d8b405a3 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -76,7 +76,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.17.3, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.17.4, macOS 15 o posterior.

@@ -170,11 +170,11 @@

Las vistas

- - + +
Alertas⌘5Todo lo que ha resultado inusual, con la app, el destino y la cifra que activó la regla.
Preguntar⌘6Preguntas sobre tu propio tráfico en lenguaje corriente, respondidas a partir del historial registrado por un modelo local o alojado.
Inspeccionar⌘7 · Developer IDOpcional: las peticiones descifradas de los agentes que encamines por Flowlight, y las llamadas de herramientas que hay detrás. La búsqueda llega a los cuerpos de las peticiones y las respuestas.
Reglas⌘8 · Developer ID
⌘7 · App Store
Cada lista de permitidos y cada rechazo que hayas puesto, qué abarca cada uno y qué ha detenido realmente.
Fuentes⌘9 · Developer ID
⌘8 · App Store
Estado del colector, canales de dispositivos y cobertura de nombres en tres pestañas.
Políticas⌘8 · Developer ID
⌘7 · App Store
Cada política configurada, si está surtiendo efecto de verdad y un historial de actividad unificado de solo lectura.
Fuentes⌘9 · Developer ID
⌘8 · App Store
Estado y recuperación del colector, canales de dispositivos opcionales y cobertura en una sola página operativa desplazable.
-

Los atajos siguen el orden visible de la barra lateral. Las versiones Developer ID incluyen Inspeccionar en ⌘7; las de App Store lo omiten, así que Reglas y Fuentes pasan a ⌘7 y ⌘8.

-
Los atajos siguen el orden visible de la barra lateral. Las versiones Developer ID incluyen Inspeccionar en ⌘7; las de App Store lo omiten, así que Políticas y Fuentes pasan a ⌘7 y ⌘8.

+
La pantalla En vivo: una lista de aplicaciones con sus tasas actuales de envío y recepción y su destino más activo.
En vivo, la pantalla con la que se abre Flowlight.

En Historial, pasa el cursor sobre una barra para abrir una tarjeta desplazable que sigue al puntero y muestra las apps que contribuyeron; las barras no amplían al hacer clic. Haz doble clic en cualquier fila para limitar el historial a ella.

@@ -215,7 +215,7 @@

Herramientas y servidores MCP

separado, mientras que un dominio solo configurado nunca se ha alcanzado, y uno solo observado no estaba escrito en ningún sitio donde pudieras haberlo leído.

Cada dominio ofrece dos rechazos: para este agente, que cubre al agente y a las herramientas que arranca, o en - todas partes, que cubre todas las apps del Mac. Ambos escriben una regla corriente, así que el flujo de incumplimientos los informa + todas partes, que cubre todas las apps del Mac. Ambos escriben una regla corriente, así que Actividad los registra y el editor de reglas los explica — y, por ser reglas corrientes, la simulación del editor te dirá qué habría rechazado uno de ellos antes de que lo actives.

Hay un tipo de dominio que no trae botones. Un conector al que el proveedor del modelo llega por su cuenta —declarado en el cuerpo de la @@ -404,16 +404,16 @@

Respuestas simuladas

una simulación cambia la respuesta, no si la conexión puede hacerse, así que «la API devuelve 500» funciona y «la API es del todo inalcanzable» no.

-

Reglas

-

Una lista de permitidos es la idea que tiene un agente de adónde puede ir. Una regla es la versión general: nombra un +

Políticas

+

Políticas reúne en un solo inventario todos los controles que Flowlight puede aplicar: reglas normales de permiso o bloqueo, listas de permitidos de agentes, paquetes gestionados y, donde esté disponible la inspección HTTPS, salvaguardas, simulaciones, reescrituras de peticiones y transformaciones de respuestas. Cada fila distingue entre el estado configurado y el efecto real: en vigor, solo informando, vigilando, en espera, programada, en pausa, agotada, en proceso de aplicación, pendiente de revisión, no disponible, incompatible o fallida. Al seleccionar una fila se abre el editor contextual al que pertenece esa política; Políticas no sustituye ni confunde sus distintos modelos de consentimiento y persistencia.

+

Una lista de permitidos es la idea que tiene un agente de adónde puede ir. Una regla normal es la versión general: nombra un sujeto —una app, un agente, un destino, una URL o una pareja de dos de ellos— y dice bloquear o permitir. Haz clic derecho en cualquier fila de En vivo, Historial, Agentes de IA o Inspeccionar para escribir una sobre lo que estás mirando; - todas acaban en el mismo editor de la pantalla Reglas, porque una regla que surtiera efecto fuera de la vista - sería una regla que ya nunca podrías encontrar.

+ todas siguen localizables en Políticas y se abren en su editor de referencia.

Plantillas. Veintidós, en cinco grupos: impedir que un agente llegue a registros de paquetes o a otros proveedores de modelos, cerrar las vías habituales por las que salen los datos (sitios de pegado, receptores de webhooks, servicios anónimos de subida de archivos, túneles, DNS over HTTPS), analítica de producto e informes de errores, políticas de horario laboral, y el «bloquea esto una hora mientras averiguo - qué es» puntual. Explóralas desde la barra de herramientas de Reglas, o haz clic derecho en una fila de En vivo, Historial, Agentes de IA o Inspeccionar y + qué es» puntual. Explóralas desde la barra de herramientas de Políticas, o haz clic derecho en una fila de En vivo, Historial, Agentes de IA o Inspeccionar y aplica una a lo que estés mirando.

Cada plantilla dice qué va a hacer antes de hacerlo. Todo lo que escriba más de una regla abre una tarjeta con la lista de cada host y cada horario; todo lo permanente abre el editor ya rellenado; y solo las reglas que se deshacen solas —una @@ -434,8 +434,7 @@

Reglas

dirección y el puerto, nada de rutas, y solo en el modo de extensión. La inspección HTTPS rechaza una petición concreta, así que puede casar con una URL y responder con un estado que el agente pueda leer en lugar de un socket que murió sin explicar por qué. El muestreador nettop no puede rechazar nada en absoluto. Una regla que la configuración actual no puede aplicar lo dice en la lista en vez de quedarse ahí aparentando estar en vigor.

-

La segunda pestaña es el flujo de incumplimientos: cada conexión que decidió una regla, con la app, adónde iba, qué - regla lo decidió y si fue un rechazo o una excepción que la dejó pasar.

+

La segunda vista es Actividad, de solo lectura: reúne en una cronología las decisiones de reglas normales, los incumplimientos de listas de permitidos, los eventos de paquetes gestionados y los efectos diferenciados de las políticas de inspección. Adapta los registros de referencia existentes en vez de reescribir el historial en un modelo de eventos genérico; las versiones de la App Store omiten los tipos de políticas que dependen de la inspección y su actividad conservada.

Qué habría hecho. Una regla de bloqueo es fácil de escribir y da miedo activarla, porque lo primero que normalmente averiguas sobre una regla así es qué rompió —y lo averiguas más tarde, porque algo falla en otro sitio—. El editor de reglas @@ -454,7 +453,7 @@

Reglas

probado con dos horas de historial.

Paquetes gestionados de publicidad y rastreadores

-

Reglas › Paquetes contiene tres listas oficiales de nombres de host: Publicidad, Seguimiento entre sitios y Privacidad y telemetría. Son visibles desde el principio y todas están desactivadas de forma predeterminada. El bloqueo requiere la Extensión de red; el muestreador nettop solo puede observar.

+

Políticas › Paquetes gestionados contiene tres listas oficiales de nombres de host: Publicidad, Seguimiento entre sitios y Privacidad y telemetría. Son visibles desde el principio y todas están desactivadas de forma predeterminada. El bloqueo requiere la Extensión de red; el muestreador nettop solo puede observar.

Solo actualizaciones oficiales y firmadas. Flowlight consulta una única fuente HTTPS fija para buscar catálogos más recientes firmados por el editor. Las comprobaciones automáticas están activadas de forma predeterminada y pueden desactivarse aquí. Una firma autentica el contenido, pero no lo activa: un paquete nuevo permanece desactivado y, si cambian los destinos, un paquete activado deja de aplicarse hasta que revises la nueva versión exacta y pulses Guardar. Durante una interrupción, el contenido aceptado puede conservarse como la última versión válida conocida, marcada visiblemente como obsoleta. Estos son bloqueos de red a nivel de nombre de host: no pueden eliminar huecos del DOM o del diseño, superposiciones del navegador, ventanas emergentes generadas por scripts ni ventanas de aplicaciones nativas.

Cuando la inspección HTTPS encamina una app por el proxy de Flowlight, este decide las reglas de destino normales y los paquetes gestionados activados usando la identidad original de la app antes de abrir ninguna conexión con el servidor de origen. En los túneles CONNECT con certificado fijado o que nunca se descifran, esto funciona por nombre de host; las listas de permitidos de agentes siguen limitadas al camino directo por la Extensión de red. Si no se conoce el propietario o el nombre de host, la conexión se permite.

@@ -503,7 +502,8 @@

Salvaguardas de agente: bloquear una herramienta, un servido

Esto no es un sandbox. Una herramienta rechazada deja de ofrecerse y deja de responderse. Un agente que siga teniendo un shell puede hacer a mano lo que habría hecho la herramienta. Flowlight lo registrará; no pretende impedirlo.

-

Fuentes: colector y motores

+

Fuentes: colector, dispositivos y cobertura

+

Fuentes es una sola página operativa desplazable: primero Colector y después Dispositivos y Cobertura como secciones claramente encabezadas. Los avisos de estado, las respuestas de Preguntar, las ventanas restauradas y los enlaces de capturas de pantalla hacen que la página se desplace hasta la sección correspondiente y la resaltan brevemente sin ocultar las demás.

Flowlight registra el tráfico de una de dos maneras, que se eligen en Fuentes › Colector. Se diferencian en lo que pueden perderse, y conviene entenderlo antes de confiar en cualquiera de las dos.

@@ -699,7 +699,7 @@

Preguntas y respuestas

Con la ventana cerrada, Flowlight casi no hace nada: mantiene las tasas de la barra de menús y escribe una fila por segundo. La extensión de red cuesta menos que el muestreador de reserva, que arranca un proceso nettop efímero cada segundo.
¿Puede bloquear conexiones?
-
Sí, con la extensión de red. La lista de permitidos de un agente puede rechazar además de avisar, una regla puede bloquear una app, +
Sí, con la extensión de red. La lista de permitidos de un agente puede rechazar además de avisar, una regla puede bloquear una app, un destino o una URL el tiempo que digas, y una salvaguarda puede quitarle una herramienta a un agente antes de que se le ofrezca a su modelo. Nada se bloquea hasta que tú lo pides, y cada rechazo lleva consigo su salida.
¿Necesito la extensión de red?
@@ -754,7 +754,7 @@

Limitaciones

diff --git a/docs/es/index.html b/docs/es/index.html index 1b4e7b85..31ca2192 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.17.4macOS 15+UniversalGPL-3.0Sin telemetría

@@ -127,7 +127,7 @@

Observa la actividad de red de tus apps.
-
El mapa de actividad de IA de Flowlight. Las tarjetas de agentes se conectan mediante herramientas atribuidas y puertas de enlace configuradas; la ruta observada de magpie a Flowlight se ramifica después hacia servicios LLM y dominios. El ancho de las cintas muestra el tráfico del período.

Ve la ruta real de IA: agentes, herramientas atribuidas, puertas de enlace configuradas, la etapa de proxy observada por Flowlight y destinos exactos. El ancho muestra el total recibido y enviado en el período seleccionado. Con datos de demostración.

@@ -135,7 +135,7 @@

Observa la actividad de red de tus apps.
-
La vista Agentes de IA de Flowlight. Claude Code, Cursor, Codex y un script de Python aparecen con sus proveedores de IA, el tráfico enviado a otros sitios y distintivos de riesgo por SMTP, SSH, FTP, una subida grande y una IP directa.

La vista Agentes de IA: el proveedor de modelo de cada agente y todo lo demás con lo que habló. Con datos de demostración.

@@ -179,11 +179,11 @@

Historial

¿Adónde ha ido todo? Del segundo al año, agrupado por ap

Alertas

¿Qué ha pintado mal? Alertas por app, explicables, con el destino y el umbral.

⌘5
-

Reglas

¿Qué he rechazado y se está cumpliendo? Cada regla, lo que ha bloqueado y un flujo con todas las decisiones.

⌘8 · Developer ID / ⌘7 · App Store
+

Políticas

¿Qué controles hay y están surtiendo efecto? Cada política, su estado real y un historial de actividad unificado.

⌘8 · Developer ID / ⌘7 · App Store

Preguntar

Preguntas en lenguaje corriente, respondidas con el historial, y debajo cada consulta que ejecutó.

⌘6
-

Fuentes

¿Están llegando datos, cuánto tiene nombre y qué canales de dispositivos están activos? Colector, Dispositivos y Cobertura en un solo lugar.

⌘9 · Developer ID / ⌘8 · App Store
+

Fuentes

¿Están llegando datos, qué canales de dispositivos están activos y qué puede contabilizar Flowlight? Una página desde Colector hasta Cobertura.

⌘9 · Developer ID / ⌘8 · App Store

Inspeccionar

Opcional. ¿Qué envió el agente en realidad y qué llamada de herramienta lo provocó? Peticiones descifradas, llamadas de herramientas, resultados.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

Correlaciona las llamadas de herramientas compatibles con las peticiones de Mac. Arranca un agente desde un Terminal inspeccionado y el tráfico HTTP compatible del agente y de sus herramientas se descifra, con las llamadas de herramientas que pidió el modelo y los resultados que volvieron.

-
La vista Inspeccionar de Flowlight: una lista de peticiones descifradas de Claude Code con sus llamadas de herramientas, y el detalle de la subida de curl a paste.example, hecha por la llamada de herramienta Bash que la ejecutó, con las notas subidas en el cuerpo de la petición
¿Adónde han ido mis notas? La subida de curl a paste.example, rastreada hasta el comando Bash que pidió el modelo, con el cuerpo que envió.
-
La pestaña Herramientas de Claude Code: Bash, Read y dos herramientas MCP con sus descripciones y cuántas veces las llamó el modelo
Todas las herramientas que un agente ofrece al modelo. Los agentes declaran sus herramientas en cada turno, así que Flowlight las lista todas, señalando las que el modelo usó de verdad y las que no tocó nunca.
-
La pestaña Servidores MCP de Codex: un servidor sentry marcado como vía proveedor, su URL, la aprobación fijada en nunca, las herramientas permitidas y las que ofrece
Incluidos los que no se ven en el cable. Cuando un agente pide al proveedor que conecte con un servidor MCP, ese tráfico nunca llega a tu Mac. Su petición sí, así que el servidor, su URL, su ajuste de aprobación y sus herramientas siguen apareciendo.
@@ -460,7 +460,7 @@

Un monitor que no tienes que vigilar

-
El detalle de Claude Code en Flowlight: herramientas y servidores MCP con su número de llamadas y un fallo, una lista de permitidos y la pestaña Llamadas de herramientas con un git push fallido, una subida de curl a paste.example con su respuesta y su petición, un npm install, una incidencia de GitHub, una búsqueda en la documentación y una lectura de archivo, cada una con su resultado
Un agente, todas sus llamadas de herramientas. El tráfico del git, del curl y de los servidores MCP que arrancó Claude Code cuenta como de Claude Code. Con la inspección HTTPS activada, la pestaña Llamadas de herramientas muestra qué pidió el modelo, qué volvió y las peticiones que hizo cada herramienta: @@ -470,10 +470,10 @@

Un monitor que no tienes que vigilar

-
La vista Historial con barras por hora de datos recibidos y enviados y gráficos de anillo de apps, destinos y protocolos
Historial. Zoom del segundo al año, tres agrupaciones y gráficos que siguen legibles con cientos de apps.
-
La vista Alertas con alertas de exfiltración por un agente, de correo, de FTP, de actividad mientras no estás y de picos de tráfico
Alertas. Cada una nombra la app, el destino y la cifra que la activó.
@@ -589,7 +589,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 3b18c063..22114120 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -125,7 +125,7 @@

Peticiones de red que hace la app

- + @@ -189,7 +189,7 @@

Contacto

diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html index cd6249f0..0ac4d702 100644 --- a/docs/es/threat-model/index.html +++ b/docs/es/threat-model/index.html @@ -221,7 +221,7 @@

Informar de una vulnerabilidad

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index 6a1adea2..a2b71e8a 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -152,7 +152,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index bdff2bac..414d4a1a 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -76,7 +76,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.17.3, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.17.4, macOS 15 ou version ultérieure.

@@ -170,11 +170,11 @@

Les vues

- - + +
PeticiónEnviada aQué contieneCuándo
Comprobación de actualizacionesapi.github.comUna petición HTTPS normal de la última versión; GitHub ve tu dirección IP y el agente de usuario Flowlight/<version>Al arrancar y a diario, desde la 0.1.2. Interruptor para desactivarla en Ajustes › General
Descarga de la actualizacióngithub.comLa imagen de disco y su archivo de suma de verificaciónSolo cuando pulsas Descargar y abrir
Actualización oficial del paquete de reglasflowlight.xinbetween.comUna solicitud HTTPS estándar al feed oficial fijo. El servidor ve tu dirección IP, el agente de usuario Flowlight/<version> y los validadores condicionales de caché; no recibe tráfico, ajustes de paquetes ni ningún identificador del dispositivoDespués del inicio, cuando corresponda, y luego como máximo una vez al día mientras todo funcione correctamente. Activada de forma predeterminada; interruptor para desactivarla y opción manual Buscar ahora en Reglas › Paquetes
Actualización oficial del paquete de reglasflowlight.xinbetween.comUna solicitud HTTPS estándar al feed oficial fijo. El servidor ve tu dirección IP, el agente de usuario Flowlight/<version> y los validadores condicionales de caché; no recibe tráfico, ajustes de paquetes ni ningún identificador del dispositivoDespués del inicio, cuando corresponda, y luego como máximo una vez al día mientras todo funcione correctamente. Activada de forma predeterminada; interruptor para desactivarla y opción manual Buscar ahora en Políticas › Paquetes gestionados
DNS inversoEl servidor DNS que tengas configuradoUna consulta del nombre registrado de una dirección IP, cuando no se ha visto ningún nombre de hostSegún haga falta, en el modo de captura del muestreador, que es el predeterminado hasta que la extensión de red esté en marcha. Sin interruptor para desactivarla: sin ella el muestreador no puede nombrar la mayoría de las conexiones. La extensión de red las nombra a partir del propio tráfico y no hace esta consulta
Consulta del propietario de la redTeam Cymru, por DNSDirecciones IP públicas sin nombre de host conocido. Las direcciones privadas y reservadas no salen nunca de tu MacActivada por defecto. Interruptor para desactivarla en Captura › Identificar a los propietarios de la red
Preguntar con un modelo alojadoEl proveedor que elijas: Anthropic, OpenAI, Google Gemini o un endpoint compatibleTu pregunta y los resultados que devuelvan las consultas de solo lectura de Flowlight que elija el modelo. La base de datos de tráfico en sí no se subeSolo cuando eliges un proveedor alojado y envías una pregunta. La pantalla Preguntar muestra las peticiones transmitidas
Alertes⌘5Tout ce qui a paru inhabituel, avec l’app, la destination et le chiffre qui a déclenché la règle.
Demander⌘6Des questions sur votre propre trafic en langage courant, auxquelles un modèle local ou hébergé répond à partir de l’historique enregistré.
Inspecter⌘7 · Developer IDFacultatif : les requêtes déchiffrées des agents que vous faites passer par Flowlight, et les appels d’outils qui les ont causées. La recherche va jusque dans les corps de requête et de réponse.
Règles⌘8 · Developer ID
⌘7 · App Store
Chaque liste d’autorisation et chaque refus que vous avez posés, ce que chacun couvre, et ce qu’il a réellement arrêté.
Sources⌘9 · Developer ID
⌘8 · App Store
La santé et la récupération du collecteur, la couverture des noms d’hôte, ainsi que l’activité facultative des caméras, du Bluetooth, de l’USB et du stockage dans trois onglets.
Politiques⌘8 · Developer ID
⌘7 · App Store
Chaque politique configurée, son effet réel et un historique d’activité unifié en lecture seule.
Sources⌘9 · Developer ID
⌘8 · App Store
La santé et la récupération du collecteur, les canaux d’appareils facultatifs et la couverture sur une seule page opérationnelle déroulante.
-

Les raccourcis suivent l’ordre visible de la barre latérale. Les versions Developer ID incluent Inspecter en ⌘7 ; les versions App Store l’omettent, donc Règles et Sources passent à ⌘7 et ⌘8.

-
Les raccourcis suivent l’ordre visible de la barre latérale. Les versions Developer ID incluent Inspecter en ⌘7 ; les versions App Store l’omettent, donc Politiques et Sources passent à ⌘7 et ⌘8.

+
L’écran En direct : une liste d’applications avec leurs débits d’envoi et de réception actuels et leur destination la plus active.
En direct, l’écran sur lequel Flowlight s’ouvre.

Dans Historique, survolez une barre pour ouvrir une fiche défilante qui suit le pointeur et affiche les apps contributrices ; les barres ne zooment pas au clic. Double-cliquez une ligne pour y restreindre l’historique.

@@ -215,7 +215,7 @@

Outils & serveurs MCP

dit plus que l’un ou l’autre seul, tandis qu’un domaine seulement configuré n’a jamais été joint, et qu’un domaine seulement observé n’était écrit nulle part où vous auriez pu le lire.

Chaque domaine propose deux refus : pour cet agent, ce qui couvre l’agent et les outils qu’il lance, ou - partout, ce qui couvre toutes les apps du Mac. Les deux écrivent une règle ordinaire, si bien que le flux des violations + partout, ce qui couvre toutes les apps du Mac. Les deux écrivent une règle ordinaire, si bien qu’Activité les signale et que l’éditeur de règles les explique — et, parce que ce sont des règles ordinaires, la simulation de l’éditeur vous dira ce que l’une d’elles aurait refusé avant que vous ne l’activiez.

Un type de domaine n’a pas de boutons. Un connecteur que le fournisseur de modèle joint de lui-même — déclaré dans le corps de la requête @@ -406,16 +406,16 @@

Réponses simulées

contactée : une simulation change la réponse, pas la possibilité d’établir la connexion, donc « l’API renvoie 500 » fonctionne alors que « l’API est complètement injoignable » non.

-

Règles

-

Une liste d’autorisation, c’est l’idée qu’un agent se fait des endroits où il peut aller. Une règle en est la version générale : elle nomme un +

Politiques

+

Politiques réunit tous les contrôles que Flowlight peut appliquer dans un même inventaire : règles ordinaires d’autorisation ou de blocage, listes d’autorisation des agents, packs gérés et, lorsque l’inspection HTTPS est disponible, garde-fous, simulations, réécritures de requêtes et transformations de réponses. Chaque ligne distingue l’état configuré de l’effet réel : appliquée, signalement uniquement, surveillée, en attente, programmée, suspendue, utilisée, en cours d’application, à vérifier, indisponible, incompatible ou en échec. La sélection d’une ligne ouvre l’éditeur contextuel auquel cette politique appartient ; Politiques ne remplace ni ne confond leurs différents modèles de consentement et de conservation.

+

Une liste d’autorisation, c’est l’idée qu’un agent se fait des endroits où il peut aller. Une règle ordinaire en est la version générale : elle nomme un sujet — une app, un agent, une destination, une URL, ou un couple formé de deux d’entre eux — et dit bloquer ou autoriser. Faites un clic droit sur n’importe quelle ligne dans En direct, Historique, Agents IA ou Inspecter pour en écrire une sur ce que vous regardez ; - toutes atterrissent dans le même éditeur, sur l’écran Règles, parce qu’une règle qui prendrait effet hors de votre vue - serait une règle que vous ne pourriez plus jamais retrouver.

+ chacune reste accessible dans Politiques et s’ouvre dans son éditeur de référence.

Des modèles. Vingt-deux, en cinq groupes — empêcher un agent d’atteindre des registres de paquets ou d’autres fournisseurs de modèles, fermer les voies habituelles par lesquelles les données partent (sites de partage de texte, collecteurs de webhooks, dépôts de fichiers anonymes, tunnels, DNS over HTTPS), l’analytique produit et les rapports de plantage, les politiques d’horaires de travail, et le « bloque-moi ça une heure, le temps que je comprenne - ce que c’est ». Parcourez-les depuis la barre d’outils de Règles, ou faites un clic droit sur une ligne n’importe où dans En direct, Historique, Agents IA ou Inspecter et + ce que c’est ». Parcourez-les depuis la barre d’outils de Politiques, ou faites un clic droit sur une ligne n’importe où dans En direct, Historique, Agents IA ou Inspecter et appliquez-en un à ce que vous regardez.

Chaque modèle annonce ce qu’il va faire avant de le faire. Tout ce qui écrit plus d’une règle ouvre une fiche listant chaque hôte et chaque horaire ; tout ce qui est permanent ouvre l’éditeur prérempli ; et seules les règles qui se défont d’elles-mêmes — une @@ -436,8 +436,7 @@

Règles

l’adresse et le port, rien des chemins, et uniquement en mode extension. L’inspection HTTPS refuse une requête : elle peut donc viser une URL et répondre par un statut que l’agent sait lire, au lieu d’une socket morte sans explication. L’échantillonneur nettop ne peut rien refuser du tout. Une règle que la configuration actuelle ne peut pas appliquer le dit dans la liste, plutôt que de rester là en faisant semblant.

-

Le second onglet est le flux des violations : chaque connexion qu’une règle a tranchée, avec l’app, sa destination, quelle - règle a tranché et s’il s’agissait d’un refus ou d’une exception qui l’a laissée passer.

+

La seconde vue est Activité, en lecture seule : elle réunit dans une même chronologie les décisions des règles ordinaires, les violations de listes d’autorisation, les événements des packs gérés et les effets distincts des politiques d’inspection. Elle adapte les enregistrements de référence existants au lieu de réécrire l’historique dans un modèle d’événement générique ; les versions App Store omettent les types de politiques dépendant de l’inspection et leur activité conservée.

Ce qu’elle aurait fait. Une règle de blocage est facile à écrire et effrayante à activer, parce que la première chose que l’on apprend d’ordinaire à son sujet, c’est ce qu’elle a cassé — et on l’apprend plus tard, parce que quelque chose @@ -458,7 +457,7 @@

Règles

d’historique a été testée sur deux heures d’historique.

Packs gérés de publicité et de traqueurs

-

Règles › Packs contient trois listes officielles de noms d’hôte : Publicité, Suivi intersites et Confidentialité et télémétrie. Elles sont visibles dès le départ et toutes désactivées par défaut. Le blocage nécessite l’Extension réseau ; l’échantillonneur nettop ne peut qu’observer.

+

Politiques › Packs gérés contient trois listes officielles de noms d’hôte : Publicité, Suivi intersites et Confidentialité et télémétrie. Elles sont visibles dès le départ et toutes désactivées par défaut. Le blocage nécessite l’Extension réseau ; l’échantillonneur nettop ne peut qu’observer.

Uniquement des mises à jour officielles et signées. Flowlight consulte une source HTTPS fixe unique pour rechercher des catalogues plus récents signés par l’éditeur. Les vérifications automatiques sont activées par défaut et peuvent être désactivées ici. Une signature authentifie le contenu, mais ne l’active pas : un nouveau pack reste désactivé et, si les destinations changent, l’application d’un pack activé est suspendue jusqu’à ce que vous examiniez la nouvelle version exacte et appuyiez sur Enregistrer. Pendant une panne, le contenu accepté peut rester disponible comme dernière version valide connue, visiblement signalée comme obsolète. Il s’agit de blocages réseau au niveau des noms d’hôte : ils ne peuvent pas supprimer les vides dans le DOM ou la mise en page, les superpositions du navigateur, les fenêtres contextuelles générées par des scripts ni les fenêtres d’applications natives.

Quand l’inspection HTTPS achemine une app par le proxy de Flowlight, celui-ci décide des règles de destination ordinaires et des packs gérés activés d’après l’identité d’origine de l’app, avant d’ouvrir toute connexion au serveur d’origine. Pour les tunnels CONNECT épinglés ou jamais déchiffrés, cela fonctionne par nom d’hôte ; les listes d’autorisation des agents restent limitées au chemin direct par l’Extension réseau. Si le propriétaire ou le nom d’hôte est inconnu, la connexion est autorisée.

@@ -507,8 +506,8 @@

Garde-fous des agents : bloquer un outil, un serveur ou

Ce n’est pas un bac à sable. Un outil refusé cesse d’être proposé et cesse d’obtenir une réponse. Un agent qui a encore un shell peut faire à la main ce que l’outil aurait fait. Flowlight l’enregistrera ; il ne prétend pas l’empêcher.

-

Sources : collecteur, couverture et appareils

-

Sources réunit la configuration opérationnelle. Son résumé de santé indique si le trafic arrive, quelle part est nommée et quels canaux d’appareils facultatifs sont actifs ; toute étape inachevée mène directement à Sources › Collecteur.

+

Sources : collecteur, appareils et couverture

+

Sources est une seule page opérationnelle déroulante : d’abord Collecteur, puis Appareils et Couverture dans des sections clairement titrées. Les avis d’état, les réponses de Demander, les fenêtres restaurées et les liens des captures d’écran font défiler jusqu’à la section concernée et la mettent brièvement en évidence sans masquer les autres.

Flowlight enregistre le trafic de deux façons, au choix dans Sources › Collecteur. Elles diffèrent par ce qu’elles peuvent manquer, ce qu’il vaut la peine de comprendre avant de compter sur l’une ou l’autre.

@@ -706,7 +705,7 @@

Questions & réponses

Fenêtre fermée, Flowlight ne fait presque rien : il tient à jour les débits de la barre des menus et écrit une ligne par seconde. L’extension réseau coûte moins que l’échantillonneur de secours, qui lance un processus nettop éphémère chaque seconde.
Peut-il bloquer des connexions ?
-
Oui, avec l’extension réseau. La liste d’autorisation d’un agent peut refuser autant qu’avertir, une règle peut bloquer une app, +
Oui, avec l’extension réseau. La liste d’autorisation d’un agent peut refuser autant qu’avertir, une règle peut bloquer une app, une destination ou une URL aussi longtemps que vous le dites, et un garde-fou peut retirer un outil à un agent avant que son modèle ne le voie. Rien ne bloque avant que vous le demandiez, et chaque refus porte sa sortie de secours.
Ai-je besoin de l’extension réseau ?
@@ -761,7 +760,7 @@

Limites

diff --git a/docs/fr/index.html b/docs/fr/index.html index bd083475..605ee2b9 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.17.4macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -127,7 +127,7 @@

Voyez l’activité réseau de vos apps.
-
La carte d’activité IA de Flowlight. Les cartes des agents passent par des outils attribués et des passerelles configurées ; la route observée de magpie vers Flowlight se ramifie ensuite vers les services LLM et les domaines. La largeur des rubans indique le trafic de la période.

Voyez la route IA réelle : agents, outils attribués, passerelles configurées, étape proxy observée par Flowlight et destinations exactes. La largeur indique le total reçu et envoyé pendant la période sélectionnée. Avec des données de démonstration.

@@ -135,7 +135,7 @@

Voyez l’activité réseau de vos apps.
-
La vue Agents IA de Flowlight. Claude Code, Cursor, Codex et un script Python y figurent avec leurs fournisseurs d’IA, le trafic envoyé ailleurs et des badges de risque pour SMTP, SSH, FTP, un envoi volumineux et une IP brute.

La vue Agents IA : le fournisseur de modèle de chaque agent, et tout le reste de ce qu’il a contacté. Avec des données de démonstration.

@@ -179,11 +179,11 @@

Historique

Où tout cela est-il parti ? De la seconde à l’ann

Alertes

Qu’est-ce qui clochait ? Des alertes explicables, par app, avec la destination et le seuil.

⌘5
-

Règles

Qu’ai-je refusé, et est-ce bien appliqué ? Chaque règle, ce qu’elle a bloqué, et le flux de toutes les décisions.

⌘8 · Developer ID / ⌘7 · App Store
+

Politiques

Quels contrôles existent, et sont-ils effectifs ? Chaque politique, son état réel et un historique d’activité unifié.

⌘8 · Developer ID / ⌘7 · App Store

Demander

Des questions en langage courant, auxquelles l’historique répond — avec la liste des requêtes exécutées juste en dessous.

⌘6
-

Sources

Les données arrivent-elles, quelle part est nommée et quels canaux d’appareils sont actifs ? Collecteur, Appareils et Couverture au même endroit.

⌘9 · Developer ID / ⌘8 · App Store
+

Sources

Les données arrivent-elles, quels canaux d’appareils sont actifs et de quoi Flowlight peut-il rendre compte ? Une seule page, du Collecteur à la Couverture.

⌘9 · Developer ID / ⌘8 · App Store

Inspecter

Facultatif. Qu’a réellement envoyé l’agent, et quel appel d’outil en est la cause ? Requêtes déchiffrées, appels d’outils, résultats.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

Reliez les appels d’outils pris en charge aux requêtes réseau

Mac. Lancez un agent depuis un Terminal inspecté et le trafic HTTP pris en charge de l’agent et de ses outils est déchiffré, avec les appels d’outils que le modèle a demandés et les résultats qui lui sont revenus.

-
La vue Inspecter de Flowlight : une liste de requêtes Claude Code déchiffrées avec leurs appels d’outils, et le détail de l’envoi de curl vers paste.example, effectué par l’appel d’outil Bash qui l’a lancé, avec les notes envoyées dans le corps de la requête
Où sont parties mes notes ? L’envoi de curl vers paste.example, remonté jusqu’à la commande Bash demandée par le modèle, avec le corps transmis.
-
L’onglet Outils de Claude Code : Bash, Read et deux outils MCP avec leurs descriptions et le nombre d’appels du modèle pour chacun
Tous les outils qu’un agent propose au modèle. Les agents déclarent leurs outils à chaque tour, donc Flowlight les liste tous, en distinguant ceux que le modèle a vraiment utilisés de ceux qu’il n’a jamais touchés.
-
L’onglet Serveurs MCP de Codex : un serveur sentry marqué via le fournisseur, son URL, l’approbation réglée sur jamais, les outils autorisés et les outils qu’il propose
Y compris ceux qu’on ne voit pas passer sur le réseau. Quand un agent demande au fournisseur de se connecter à un serveur MCP, ce trafic ne passe jamais par votre Mac. Sa requête, si : le serveur, son URL, son réglage d’approbation et ses outils apparaissent quand même.
@@ -460,7 +460,7 @@

Un moniteur que vous n’avez pas à surveiller

-
Le détail de Claude Code dans Flowlight : Outils et serveurs MCP avec le nombre d’appels et un échec, une liste d’autorisation, et l’onglet Appels d’outils listant un git push en échec, un envoi curl vers paste.example avec sa réponse et sa requête, un npm install, un ticket GitHub, une recherche dans la documentation et une lecture de fichier, chacun avec son résultat
Un agent, tous ses appels d’outils. Le trafic des git, curl et serveurs MCP lancés par Claude Code compte comme celui de Claude Code. Avec l’inspection HTTPS activée, l’onglet Appels d’outils montre ce que le modèle a demandé, ce qui est revenu et les requêtes émises par chaque outil : @@ -470,10 +470,10 @@

Un moniteur que vous n’avez pas à surveiller

-
La vue Historique avec les barres horaires des données reçues et envoyées et des anneaux pour les apps, les destinations et les protocoles
Historique. Zoom de la seconde à l’année, trois regroupements, des graphiques qui restent lisibles avec des centaines d’apps.
-
La vue Alertes listant des alertes d’exfiltration par un agent, d’e-mail, de FTP, d’activité pendant une absence et de pic de trafic
Alertes. Chacune nomme l’app, la destination et le chiffre qui l’a déclenchée.
@@ -589,7 +589,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index dcb3e9ae..76f75bbc 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -125,7 +125,7 @@

Les requêtes réseau que fait l’app

- + @@ -189,7 +189,7 @@

Contact

diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html index 8401ad79..397e2b50 100644 --- a/docs/fr/threat-model/index.html +++ b/docs/fr/threat-model/index.html @@ -224,7 +224,7 @@

Signaler une vulnérabilité

diff --git a/docs/index.html b/docs/index.html index d869c362..bc0f7314 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniversalGPL-3.0No telemetry

+

v0.17.4macOS 15+UniversalGPL-3.0No telemetry

@@ -127,7 +127,7 @@

See your apps' network activity.
-
Flowlight's AI activity map. Agent cards connect through attributed tools and configured gateways; the observed magpie to Flowlight route then branches to LLM services and domains, with ribbon width showing period traffic volume.

See the real AI route: agents, attributed tools, configured gateways, Flowlight's observed proxy stage, and exact destinations. Ribbon width shows total received plus sent traffic for the selected period. Shown with demo data.

@@ -135,7 +135,7 @@

See your apps' network activity.
-
Flowlight's AI Agents view. Claude Code, Cursor, Codex and a Python script are listed with their AI providers, traffic sent elsewhere and risk badges for SMTP, SSH, FTP, a large upload and a raw IP.

The AI Agents view: each agent's model provider, and everything else it talked to. Shown with demo data.

@@ -143,7 +143,7 @@

See your apps' network activity.
-
The AI Agents view offering to always monitor Windsurf, and confirming Flowlight is keeping Claude Code and Cursor routed through it on every launch, each with a one-click Stop Monitoring.

Always monitor an agent: Flowlight writes the proxy into the agent's own settings so it routes through on every launch, and says what it's keeping — stop any of it in a click. Shown with demo data.

@@ -187,11 +187,11 @@

History

Where did it all go? From 15 minutes through all history, gro

Alerts

What looked wrong? Explainable, per-app alerts with the destination and the threshold.

⌘5
-

Rules

What have I refused, and is it happening? Every rule, what each has blocked, and a feed of every decision.

⌘8 · Developer ID / ⌘7 · App Store
+

Policies

What controls exist, and are they effective? Every policy, its real state, and a unified activity trail.

⌘8 · Developer ID / ⌘7 · App Store

Ask

Questions in plain language, answered from the history — with every query it ran listed underneath.

⌘6
-

Sources

Is data arriving, how much is named, and which device channels are active? Collector, Devices and Coverage in one place.

⌘9 · Developer ID / ⌘8 · App Store
+

Sources

Is data arriving, which device channels are active, and what can Flowlight account for? One page: Collector, Devices, then Coverage.

⌘9 · Developer ID / ⌘8 · App Store

Inspect

Optional. What did the agent actually send, and which tool call made it? Decrypted requests, tool calls, results.

⌘7 · Developer ID
@@ -427,16 +427,16 @@

Correlate supported tool calls with network requests

Mac. Start an agent from an inspected Terminal and supported HTTP traffic from the agent and its tools is decrypted, with tool calls the model asked for and the results that went back.

-
Flowlight's Inspect view: a list of decrypted Claude Code requests with their tool calls, and the detail of curl's upload to paste.example, made by the Bash tool call that ran it, with the uploaded notes in the request body
Where did my notes go? curl's upload to paste.example, traced to the Bash command the model asked for, with the body it sent.
-
The Tools tab for Claude Code: Bash, Read and two MCP tools with descriptions and how often the model called each
Every tool an agent offers the model. Agents declare their tools on each turn, so Flowlight lists them all, with the ones the model actually used and the ones it never touched.
-
The MCP servers tab for Codex: a sentry server marked via provider, its URL, approval set to never, allowed tools and the tools it offers
Including the ones you can't see on the wire. When an agent asks the provider to connect to an MCP server, that traffic never reaches your Mac. Its request does, so the server, its URL, its approval setting and its tools still show up.
@@ -482,7 +482,7 @@

A monitor you don't have to monitor

-
Claude Code's detail in Flowlight: Tools and MCP servers with call counts and one failure, an allowlist, and the Tool calls tab listing a failed git push, a curl upload to paste.example with its response and request, npm install, a GitHub issue, a docs search and a file read, each with its result
One agent, every tool call. Traffic from the git, curl and MCP servers Claude Code started counts as Claude Code's. With HTTPS inspection on, the Tool calls tab shows what the model asked for, what came back, and the requests each tool made: @@ -492,10 +492,10 @@

A monitor you don't have to monitor

-
History view with hourly received and sent bars and donut charts of apps, destinations and protocols
History. Preset ranges, a draggable navigator, three groupings, and charts that stay readable with hundreds of apps.
-
Alerts view listing agent exfiltration, email, FTP, while-away and traffic spike alerts
Alerts. Each one names the app, the destination and the number that tripped it.
@@ -612,7 +612,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index cecbcdc5..a8cc84d0 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -152,7 +152,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index 4591d5d8..e8a50c14 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -76,7 +76,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.17.3, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.17.4, macOS 15 o successivo.

@@ -170,11 +170,11 @@

Le viste

- - + +
RequêteEnvoyée àCe qu’elle contientQuand
Recherche de mise à jourapi.github.comUne requête HTTPS standard pour la dernière version ; GitHub voit votre adresse IP et l’agent utilisateur Flowlight/<version>Au lancement et une fois par jour, depuis 0.1.2. Interrupteur d’arrêt dans Réglages › Général
Téléchargement de mise à jourgithub.comL’image disque et son fichier de somme de contrôleSeulement quand vous cliquez sur Télécharger & ouvrir
Mise à jour du pack de règles officielflowlight.xinbetween.comUne requête HTTPS standard vers le flux officiel fixe. Le serveur voit votre adresse IP, l’agent utilisateur Flowlight/<version> et les validateurs conditionnels du cache ; il ne reçoit ni trafic, ni réglages des packs, ni identifiant d’appareilAprès le démarrage, lorsqu’une vérification est due, puis au maximum une fois par jour tant que le service fonctionne normalement. Activée par défaut ; interrupteur de désactivation et option manuelle Rechercher maintenant dans Règles › Packs
Mise à jour du pack de règles officielflowlight.xinbetween.comUne requête HTTPS standard vers le flux officiel fixe. Le serveur voit votre adresse IP, l’agent utilisateur Flowlight/<version> et les validateurs conditionnels du cache ; il ne reçoit ni trafic, ni réglages des packs, ni identifiant d’appareilAprès le démarrage, lorsqu’une vérification est due, puis au maximum une fois par jour tant que le service fonctionne normalement. Activée par défaut ; interrupteur de désactivation et option manuelle Rechercher maintenant dans Politiques › Packs gérés
DNS inverseLe résolveur DNS que vous avez configuréUne recherche du nom enregistré d’une adresse IP, quand aucun nom d’hôte n’a été observéAu besoin, dans le mode de capture par échantillonnage, qui est celui par défaut jusqu’à ce que l’extension réseau tourne. Pas d’interrupteur d’arrêt : sans elle, l’échantillonneur ne peut pas nommer la plupart des connexions. L’extension réseau les nomme à partir du trafic lui-même et ne fait pas cette recherche
Recherche du propriétaire de réseauTeam Cymru, par DNSDes adresses IP publiques sans nom d’hôte connu. Les adresses privées et réservées ne quittent jamais votre MacActivé par défaut. Interrupteur d’arrêt dans Sources › Collecteur › Identifier les propriétaires de réseau
Demander avec un modèle hébergéLe fournisseur que vous choisissez : Anthropic, OpenAI, Google Gemini ou un point de terminaison compatibleVotre question et les résultats renvoyés par les requêtes Flowlight en lecture seule choisies par le modèle. La base de données de trafic elle-même n’est pas envoyéeSeulement quand vous choisissez un fournisseur hébergé et soumettez une question. L’écran Demander montre les requêtes transmises
Avvisi⌘5Tutto ciò che è sembrato insolito, con l'app, la destinazione e il numero che ha fatto scattare la regola.
Chiedi⌘6Domande sul tuo traffico in linguaggio naturale, a cui un modello locale o ospitato risponde a partire dalla cronologia registrata.
Ispeziona⌘7 · Developer IDOpzionale: le richieste decifrate degli agenti che instradi attraverso Flowlight e le chiamate agli strumenti che le hanno prodotte. La ricerca entra nei corpi di richieste e risposte.
Regole⌘8 · Developer ID
⌘7 · App Store
Ogni elenco consentiti e ogni rifiuto che hai impostato, cosa copre ciascuno e cosa ha fermato davvero.
Origini⌘9 · Developer ID
⌘8 · App Store
Stato del Collector, canali dei dispositivi e copertura dei nomi in tre schede.
Criteri⌘8 · Developer ID
⌘7 · App Store
Ogni criterio configurato, se è davvero effettivo e una cronologia unificata delle attività in sola lettura.
Origini⌘9 · Developer ID
⌘8 · App Store
Stato e ripristino del Collector, canali facoltativi dei dispositivi e copertura in un'unica pagina operativa a scorrimento.
-

Le scorciatoie seguono l'ordine visibile della barra laterale. Le versioni Developer ID includono Ispeziona su ⌘7; quelle App Store lo omettono, quindi Regole e Origini passano a ⌘7 e ⌘8.

-
Le scorciatoie seguono l'ordine visibile della barra laterale. Le versioni Developer ID includono Ispeziona su ⌘7; quelle App Store lo omettono, quindi Criteri e Origini passano a ⌘7 e ⌘8.

+
La schermata In diretta: un elenco di applicazioni con le loro velocità attuali di invio e ricezione e la destinazione più trafficata.
In diretta, la schermata con cui Flowlight si apre.

In Cronologia, passa il puntatore su una barra per aprire una scheda scorrevole che segue il cursore e mostra le app che hanno contribuito; le barre non ingrandiscono al clic. Fai doppio clic su una riga per limitare la cronologia a essa.

@@ -404,16 +404,20 @@

Risposte simulate

contattata: una simulazione cambia la risposta, non la possibilità di connettersi, quindi «l'API restituisce 500» funziona mentre «l'API è del tutto irraggiungibile» no.

-

Regole

-

Un elenco consentiti è l'idea che un singolo agente ha di dove può andare. Una regola è la versione generale: indica un +

Criteri

+

Criteri riunisce in un solo inventario tutti i controlli che Flowlight può applicare: normali regole di consenso o blocco, elenchi consentiti + degli agenti, pacchetti gestiti e, dove è disponibile l'ispezione HTTPS, protezioni, simulazioni, riscritture delle richieste e trasformazioni delle + risposte. Ogni riga distingue lo stato configurato dall'effetto reale: applicazione attiva, sola segnalazione, monitoraggio, attesa, pianificato, + in pausa, esaurito, in corso di applicazione, revisione necessaria, non disponibile, incompatibile o non riuscito. Selezionando una riga si apre l'editor + contestuale responsabile di quel criterio; Criteri non sostituisce né confonde i distinti modelli di consenso e persistenza.

+

Un elenco consentiti è l'idea che un singolo agente ha di dove può andare. Una normale regola è la versione generale: indica un soggetto — un'app, un agente, una destinazione, un URL o una coppia di due di questi — e dice blocca o consenti. Fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona per scriverne una su ciò che stai guardando; - tutte finiscono nello stesso editor della schermata Regole, perché una regola che entra in vigore fuori dalla vista - sarebbe una regola che non potresti più ritrovare.

+ ognuna resta reperibile in Criteri e si apre nel proprio editor autorevole.

Modelli. Ventidue, in cinque gruppi: impedire a un agente di raggiungere i registri di pacchetti o altri provider di modelli, chiudere le vie più comuni per cui i dati escono (siti di paste, raccoglitori di webhook, servizi anonimi per caricare file, tunnel, DNS over HTTPS), analisi di prodotto e segnalazione degli arresti anomali, politiche sull'orario di lavoro, e il classico «blocca questo per un'ora mentre capisco - cos'è». Sfogliali dalla barra strumenti di Regole, oppure fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona e + cos'è». Sfogliali dalla barra strumenti di Criteri, oppure fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona e applicane uno a ciò che stai guardando.

Ogni modello dice cosa farà prima di farlo. Tutto ciò che scrive più di una regola apre una scheda con l'elenco di ogni host e orario; tutto ciò che è permanente apre l'editor già compilato; e solo le regole che si annullano da sole — un'ora, @@ -434,8 +438,9 @@

Regole

l'indirizzo e la porta, nulla dei percorsi, e solo in modalità estensione. L'ispezione HTTPS rifiuta una singola richiesta, quindi può corrispondere a un URL e rispondere con uno stato che l'agente può leggere, invece di un socket morto senza spiegazioni. Il campionatore nettop non può rifiutare nulla. Una regola che la configurazione attuale non può applicare lo dice nell'elenco, invece di starsene lì con l'aria di essere in vigore.

-

Il secondo pannello è il flusso delle violazioni: ogni connessione decisa da una regola, con l'app, dove era diretta, quale - regola ha deciso e se si è trattato di un rifiuto o di un'eccezione che l'ha lasciata passare.

+

La seconda vista è Attività, in sola lettura: riunisce in un'unica cronologia le decisioni delle regole comuni, le violazioni degli elenchi consentiti, + gli eventi dei pacchetti gestiti e gli effetti distinti dei criteri di ispezione. Usa i record originali, ciascuno autorevole nel proprio ambito, invece di + riscrivere la cronologia in un unico modello generico di evento; le versioni Store omettono i tipi di criterio che dipendono dall'ispezione e le relative attività conservate.

Cosa avrebbe fatto. Una regola di blocco è facile da scrivere e spaventosa da attivare, perché la prima cosa che normalmente scopri di una regola così è che cosa ha rotto — e lo scopri dopo, da qualcosa che smette di funzionare altrove. @@ -456,7 +461,7 @@

Regole

testata su due ore di storico.

Pacchetti gestiti per pubblicità e tracker

-

Regole › Pacchetti contiene tre elenchi ufficiali di nomi host: Pubblicità, Tracciamento tra siti e Privacy e telemetria. Sono visibili fin dall’inizio e tutti disattivati per impostazione predefinita. Il blocco richiede l’Estensione di rete; il campionatore nettop può solo osservare.

+

Criteri › Pacchetti gestiti contiene tre elenchi ufficiali di nomi host: Pubblicità, Tracciamento tra siti e Privacy e telemetria. Sono visibili fin dall’inizio e tutti disattivati per impostazione predefinita. Il blocco richiede l’Estensione di rete; il campionatore nettop può solo osservare.

Solo aggiornamenti ufficiali e firmati. Flowlight controlla un’unica fonte HTTPS fissa per trovare cataloghi più recenti firmati dall’editore. I controlli automatici sono attivi per impostazione predefinita e possono essere disattivati qui. Una firma autentica il contenuto, ma non lo attiva: un nuovo pacchetto resta disattivato e, se cambiano le destinazioni, l’applicazione di un pacchetto attivo viene sospesa finché non esamini l’esatta nuova versione e premi Salva. Durante un’interruzione, i contenuti accettati possono restare disponibili come ultima versione valida nota, visibilmente indicata come obsoleta. Si tratta di blocchi di rete a livello di nome host: non possono eliminare spazi vuoti nel DOM o nel layout, sovrapposizioni del browser, popup generati da script o finestre di app native.

Quando l’ispezione HTTPS instrada un’app attraverso il proxy di Flowlight, questo decide le normali regole di destinazione e i pacchetti gestiti attivi usando l’identità originale dell’app prima di aprire qualsiasi connessione al server di origine. Per i tunnel CONNECT con pinning o mai decifrati, ciò funziona in base al nome host; gli elenchi consentiti degli agenti restano limitati al percorso diretto attraverso l’Estensione di rete. Se il proprietario o il nome host è sconosciuto, la connessione viene consentita.

@@ -505,7 +510,10 @@

Protezioni per gli agenti: bloccare uno strumento, un server

Questa non è una sandbox. Uno strumento rifiutato non viene più offerto e non riceve più risposta. Un agente che ha ancora una shell può fare a mano ciò che avrebbe fatto lo strumento. Flowlight lo registra; non pretende di impedirlo.

-

Origini: Collector e motori

+

Origini: Collector, dispositivi e copertura

+

Origini è un'unica pagina operativa a scorrimento: prima il Collector, poi Dispositivi e Copertura come sezioni chiaramente + contrassegnate. Gli avvisi di stato, le risposte di Chiedi, le finestre ripristinate e i collegamenti usati per gli screenshot scorrono fino alla sezione pertinente + e la mettono brevemente in evidenza senza nascondere le altre.

Flowlight registra il traffico in uno di due modi, da scegliere in Origini › Collector. Differiscono per ciò che possono perdere, e vale la pena capirlo prima di affidarsi all'uno o all'altro.

@@ -703,7 +711,7 @@

Domande e risposte

Con la finestra chiusa Flowlight fa quasi nulla: mantiene le velocità nella barra dei menu e scrive una riga al secondo. L'estensione di rete costa meno del campionatore di riserva, che avvia un breve processo nettop ogni secondo.
Può bloccare le connessioni?
-
Sì, con l'estensione di rete. L'elenco consentiti di un agente può rifiutare oltre che avvisare, una regola può bloccare un'app, +
Sì, con l'estensione di rete. L'elenco consentiti di un agente può rifiutare oltre che avvisare, una regola può bloccare un'app, una destinazione o un URL per il tempo che dici tu, e una protezione può togliere uno strumento a un agente prima che venga offerto al suo modello. Nulla blocca finché non glielo chiedi, e ogni rifiuto porta con sé la sua via d'uscita.
Mi serve l'estensione di rete?
@@ -758,7 +766,7 @@

Limitazioni

diff --git a/docs/it/index.html b/docs/it/index.html index 791acc2f..7ca937d5 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

Vedi la rete delle tue app.
Ca

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.17.4macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -127,7 +127,7 @@

Vedi la rete delle tue app.
Ca
-
La mappa dell’attività IA di Flowlight. Le schede degli agenti passano attraverso strumenti attribuiti e gateway configurati; il percorso osservato da magpie a Flowlight si dirama poi verso servizi LLM e domini. La larghezza dei nastri mostra il traffico del periodo.

Vedi il percorso IA reale: agenti, strumenti attribuiti, gateway configurati, lo stadio proxy osservato da Flowlight e le destinazioni esatte. La larghezza mostra il totale ricevuto e inviato nel periodo selezionato. Con dati dimostrativi.

@@ -135,7 +135,7 @@

Vedi la rete delle tue app.
Ca
-
La vista Agenti IA di Flowlight. Claude Code, Cursor, Codex e uno script Python sono elencati con i loro provider IA, il traffico inviato altrove e i contrassegni di rischio per SMTP, SSH, FTP, un caricamento di grandi dimensioni e un IP grezzo.

La vista Agenti IA: il provider del modello di ogni agente e tutto il resto con cui ha parlato. Mostrata con dati dimostrativi.

@@ -179,11 +179,11 @@

Cronologia

Dove è finito tutto? Dal secondo all'anno, per app, desti

Avvisi

Cosa non tornava? Avvisi per app, spiegati, con la destinazione e la soglia.

⌘5
-

Regole

Cosa ho rifiutato, e sta funzionando? Ogni regola, che cosa ha bloccato e il flusso di tutte le decisioni.

⌘8 · Developer ID / ⌘7 · App Store
+

Criteri

Quali controlli esistono, e sono effettivi? Ogni criterio, il suo stato reale e una cronologia unificata delle attività.

⌘8 · Developer ID / ⌘7 · App Store

Chiedi

Domande in linguaggio naturale, con risposte tratte dalla cronologia e sotto l'elenco di tutte le query eseguite.

⌘6
-

Origini

I dati stanno arrivando, quanti hanno un nome e quali canali dei dispositivi sono attivi? Collector, Dispositivi e Copertura in un unico posto.

⌘9 · Developer ID / ⌘8 · App Store
+

Origini

I dati stanno arrivando, quali canali dei dispositivi sono attivi e che cosa può contabilizzare Flowlight? Un'unica pagina, dal Collector alla Copertura.

⌘9 · Developer ID / ⌘8 · App Store

Ispeziona

Opzionale. Cosa ha inviato davvero l'agente, e quale chiamata a uno strumento l'ha prodotta? Richieste decifrate, chiamate agli strumenti, risultati.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

Collega le chiamate agli strumenti supportate alle richieste di rete

sul tuo Mac. Avvia un agente da un Terminale ispezionato e il traffico HTTP supportato dell'agente e dei suoi strumenti viene decifrato, con le chiamate agli strumenti richieste dal modello e i risultati tornati indietro.

-
La vista Ispeziona di Flowlight: un elenco di richieste decifrate di Claude Code con le loro chiamate agli strumenti e il dettaglio del caricamento di curl verso paste.example, prodotto dalla chiamata allo strumento Bash che lo ha eseguito, con le note caricate nel corpo della richiesta
Dove sono finite le mie note? Il caricamento di curl verso paste.example, risalendo al comando Bash richiesto dal modello, con il corpo che ha inviato.
-
Il pannello Strumenti per Claude Code: Bash, Read e due strumenti MCP con le descrizioni e quante volte il modello ha chiamato ciascuno
Ogni strumento che un agente offre al modello. Gli agenti dichiarano i loro strumenti a ogni turno, così Flowlight li elenca tutti, indicando quelli che il modello ha davvero usato e quelli che non ha mai toccato.
-
Il pannello Server MCP per Codex: un server sentry contrassegnato come «tramite provider», il suo URL, approvazione impostata su «mai», gli strumenti consentiti e quelli che offre
Compresi quelli che non vedi passare in rete. Quando un agente chiede al provider di collegarsi a un server MCP, quel traffico non arriva mai al tuo Mac. La sua richiesta sì, così il server, il suo URL, la sua impostazione di approvazione e i suoi strumenti compaiono comunque.
@@ -460,7 +460,7 @@

Un monitoraggio che non devi monitorare

-
Il dettaglio di Claude Code in Flowlight: strumenti e server MCP con il numero di chiamate e un errore, un elenco consentiti e il pannello Chiamate agli strumenti con un git push fallito, un caricamento curl verso paste.example con la sua risposta e la sua richiesta, npm install, un issue su GitHub, una ricerca nella documentazione e la lettura di un file, ciascuno con il proprio risultato
Un agente, tutte le chiamate agli strumenti. Il traffico di git, curl e dei server MCP avviati da Claude Code conta come traffico di Claude Code. Con l'ispezione HTTPS attiva, il pannello Chiamate agli strumenti mostra che cosa ha chiesto il modello, che cosa è tornato indietro e le richieste fatte da ogni strumento: @@ -470,10 +470,10 @@

Un monitoraggio che non devi monitorare

-
La vista Cronologia con le barre orarie dei dati ricevuti e inviati e i grafici ad anello di app, destinazioni e protocolli
Cronologia. Zoom dal secondo all'anno, tre raggruppamenti, grafici che restano leggibili con centinaia di app.
-
La vista Avvisi con avvisi di esfiltrazione da parte di un agente, e-mail, FTP, attività mentre sei via e picco di traffico
Avvisi. Ognuno indica l'app, la destinazione e il numero che lo ha fatto scattare.
@@ -589,7 +589,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index 10fa7286..58c17eb2 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -125,7 +125,7 @@

Richieste di rete che fa l'app

- + @@ -189,7 +189,7 @@

Contatti

diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html index 75068625..938f3211 100644 --- a/docs/it/threat-model/index.html +++ b/docs/it/threat-model/index.html @@ -223,7 +223,7 @@

Segnalare una vulnerabilità

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index cd963463..39312132 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -153,7 +153,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 719a211e..06249c84 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -76,7 +76,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.17.3、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.17.4、macOS 15 以降。

@@ -168,11 +168,11 @@

各ビュー

- - + +
RichiestaInviata aCosa contieneQuando
Controllo aggiornamentiapi.github.comUna normale richiesta HTTPS per l'ultima versione; GitHub vede il tuo indirizzo IP e lo user agent Flowlight/<version>All'avvio e una volta al giorno, dalla 0.1.2. Interruttore per disattivarla in Impostazioni › Generali
Download dell'aggiornamentogithub.comL'immagine disco e il suo file di checksumSolo quando fai clic su Scarica e apri
Aggiornamento ufficiale del pacchetto di regoleflowlight.xinbetween.comUna richiesta HTTPS standard al feed ufficiale fisso. Il server vede il tuo indirizzo IP, lo user agent Flowlight/<version> e i validatori condizionali della cache; non riceve traffico, impostazioni dei pacchetti o identificatori del dispositivoDopo l’avvio, quando previsto, e successivamente non più di una volta al giorno durante il normale funzionamento. Attivo per impostazione predefinita; interruttore per disattivarlo e opzione manuale Controlla ora in Regole › Pacchetti
Aggiornamento ufficiale del pacchetto di regoleflowlight.xinbetween.comUna richiesta HTTPS standard al feed ufficiale fisso. Il server vede il tuo indirizzo IP, lo user agent Flowlight/<version> e i validatori condizionali della cache; non riceve traffico, impostazioni dei pacchetti o identificatori del dispositivoDopo l’avvio, quando previsto, e successivamente non più di una volta al giorno durante il normale funzionamento. Attivo per impostazione predefinita; interruttore per disattivarlo e opzione manuale Controlla ora in Criteri › Pacchetti gestiti
DNS inversoIl resolver DNS che hai configuratoUna ricerca del nome registrato di un indirizzo IP, quando non è stato rilevato alcun nome hostQuando serve, nella modalità di acquisizione a campionamento, che è quella predefinita finché l'estensione di rete non è in funzione. Nessun interruttore per disattivarla: senza di essa il campionatore non può dare un nome alla maggior parte delle connessioni. L'estensione di rete le nomina dal traffico stesso e non fa questa ricerca
Ricerca del proprietario della reteTeam Cymru, via DNSIndirizzi IP pubblici senza nome host noto. Gli indirizzi privati e riservati non lasciano mai il tuo MacAttiva per impostazione predefinita. Interruttore per disattivarla in Acquisizione › Identifica i proprietari della rete
Chiedi con un modello ospitatoIl provider che selezioni: Anthropic, OpenAI, Google Gemini o un endpoint compatibileLa tua domanda e i risultati restituiti dalle query di sola lettura di Flowlight selezionate dal modello. Il database del traffico non viene caricatoSolo quando selezioni un provider ospitato e invii una domanda. La schermata Chiedi mostra le richieste trasmesse
アラート⌘5いつもと違って見えたものすべて。アプリ、接続先、そしてルールのしきい値を超えた数値とともに。
質問⌘6自分のトラフィックについての、ふつうの言葉での質問。記録された履歴から、ローカルまたはホスト型のモデルが答えます。
インスペクト⌘7 · Developer ID任意。Flowlight を経由させたエージェントの復号済みリクエストと、その裏にあるツール呼び出し。検索はリクエストとレスポンスの本文にも届きます。
ルール⌘8 · Developer ID
⌘7 · App Store
設定したすべての許可リストと拒否。それぞれが何を対象にし、実際に何を止めたか。
ソース⌘9 · Developer ID
⌘8 · App Store
コレクターの状態、デバイスのチャンネル、名前付けのカバー範囲を 3 つのタブで表示。
ポリシー⌘8 · Developer ID
⌘7 · App Store
設定されたすべてのポリシー、その実際の適用状況、そして統合された読み取り専用のアクティビティ履歴。
ソース⌘9 · Developer ID
⌘8 · App Store
コレクターの状態と復旧、任意で有効にするデバイスチャンネル、カバー範囲を、1 つのスクロール可能な運用ページに表示。
-

ショートカットは表示中のサイドバー順です。Developer ID 版は ⌘7 にインスペクトがあります。App Store 版では省かれるため、ルールとソースは ⌘7 と ⌘8 になります。

-
ショートカットは表示中のサイドバー順です。Developer ID 版は ⌘7 にインスペクトがあります。App Store 版では省かれるため、ポリシーとソースは ⌘7 と ⌘8 になります。

+
ライブビュー。アプリの一覧に、現在の送信と受信の速度、そして最も忙しい接続先が並んでいます。
ライブ。Flowlight が最初に開く画面です。

履歴では、棒にポインタを重ねると、原因のアプリを示すスクロール可能なカードがポインタに追従します。棒をクリックしても拡大しません。行をダブルクリックすると、その項目だけに履歴を絞り込めます。

@@ -211,7 +211,7 @@

ツールと MCP サーバ

書かれていてしかも実際に接続もあったということは、どちらか一方だけよりも多くを語るからです。設定されているだけの ドメインには一度も届いておらず、観測されただけのドメインは、前もって読めるどこにも書かれていませんでした。

ドメインごとに拒否の仕方が二つあります。このエージェントに対してはエージェントとそれが起動するツールを覆い、どこでもは - この Mac のすべてのアプリを覆います。どちらもふつうのルールを書きます。ですから違反の履歴がそれを報告し、ルールのエディタが + この Mac のすべてのアプリを覆います。どちらもふつうのルールを書きます。ですからアクティビティがそれを報告し、ルールのエディタが それを説明します。そしてふつうのルールなので、有効にする前に、エディタのシミュレーションがそのルールなら何を拒否していたかを 教えてくれます。

ボタンの付かないドメインが一種類あります。モデルプロバイダが自分で接続するコネクタです(リクエストの本文で宣言され、 @@ -383,14 +383,19 @@

モック応答

接続先への接続そのものは行われるということです。モックが変えるのは答えであって、接続できるかどうかではありません。つまり「API が 500 を返す」は再現できますが、 「API にまったく到達できない」は再現できません。

-

ルール

-

許可リストは、1 つのエージェントについて「どこへ行ってよいか」を定めたものです。ルールはその一般版で、 +

ポリシー

+

ポリシーは、Flowlight が適用できるすべての制御をまとめた一覧です。通常の許可・ブロックルール、エージェントの + 許可リスト、管理対象パック、さらに HTTPS インスペクションを利用できる場合は、ガードレール、モック、リクエストの書き換え、レスポンスの + 変換も含まれます。各行では、設定された状態と実際の効果を区別し、適用中、報告のみ、監視中、待機中、スケジュール待ち、一時停止、使用済み、 + 適用処理中、要確認、利用不可、互換性なし、失敗のいずれかを示します。行を選ぶと、そのポリシーを管理する本来のエディタが開きます。 + ポリシー画面が、それぞれ異なる同意や保存の仕組みを置き換えたり、曖昧にしたりすることはありません。

+

許可リストは、1 つのエージェントについて「どこへ行ってよいか」を定めたものです。通常のルールはその一般版で、 対象(アプリ、エージェント、接続先、URL、またはそのうち 2 つの組み合わせ)を指定し、ブロックか 許可を決めます。ライブ、履歴、AI エージェント、インスペクトのどの行でも右クリックすれば、いま見ているものについてルールを書けます。 - そのどれもがルール画面の同じエディタに集まります。見えないところで効き始めるルールは、二度と見つけられないルールになってしまうからです。

+ どこから作成したルールもポリシー画面で見つけられ、選ぶとそのルールを管理する本来のエディタが開きます。

テンプレート。22 種類あり、5 つのグループに分かれています。エージェントがパッケージレジストリやほかのモデルプロバイダへ届かないようにするもの、 データが出ていきやすい経路をふさぐもの(ペーストサイト、Webhook の受け口、匿名のファイル置き場、トンネル、DNS over HTTPS)、プロダクト分析とクラッシュレポート、 - 勤務時間のポリシー、そして「作業のあいだ 1 時間だけこれをブロックして、何なのか調べる」という単発のものです。ルールのツールバーから一覧を見るか、 + 勤務時間のポリシー、そして「作業のあいだ 1 時間だけこれをブロックして、何なのか調べる」という単発のものです。ポリシーのツールバーから一覧を見るか、 ライブ、履歴、AI エージェント、インスペクトのどこかで行を右クリックして、いま見ているものに当てはめてください。

どのテンプレートも、実行する前に何をするかを述べます。2 つ以上のルールを書くものは、すべてのホストとスケジュールを並べたカードを開きます。恒久的なものは あらかじめ入力されたエディタを開きます。そして自分で元に戻るルールだけ(1 時間、またはこの回の Flowlight の実行中だけ)が、先に示されることなく @@ -408,8 +413,9 @@

ルール

しかも機能拡張モードのときだけです。HTTPS インスペクションは 1 つのリクエストを拒否するので、URL に一致させられますし、理由も告げずに死んだソケットの代わりに、 エージェントが読めるステータスで応答できます。nettop サンプラーは何も拒否できません。現在の構成では実行できないルールは、効いているように見せかけて並ぶのではなく、 一覧にそう表示されます。

-

2 つめのタブは違反の履歴です。ルールが判断したすべての接続について、アプリ、向かおうとしていた先、どのルールが判断したか、 - そしてそれが拒否だったのか、通過を許した例外だったのかが並びます。

+

2 つめのビューは読み取り専用のアクティビティです。通常のルールによる判断、許可リスト違反、管理対象パックのイベント、 + インスペクションポリシー固有の効果を、1 つの時系列にまとめます。すでにある正式な記録をそのまま参照し、履歴を汎用イベントへ + 書き換えることはありません。App Store 版では、インスペクションに依存するポリシーの種類と、それに保存されたアクティビティは表示されません。

このルールが何をしていたか。ブロックのルールは、書くのは簡単でも、有効にするのは怖いものです。そのルールについて最初に分かるのは、 ふつう何を壊したかであり、しかもそれが分かるのは後になってから、どこか別の場所で何かが失敗したときだからです。ルールのエディタは、そこに先に答えます。 @@ -427,7 +433,7 @@

ルール

履歴でテストしたルールは、2 時間分の履歴でテストしたということです。

管理対象の広告・トラッカーパック

-

ルール › パックには、広告、サイト横断トラッキング、プライバシーとテレメトリという3つの公式ホスト名リストがあります。これらは最初から表示され、すべてデフォルトでオフになっています。ブロックにはネットワーク機能拡張が必要です。nettop サンプラーは監視のみ可能です。

+

ポリシー › 管理対象パックには、広告、サイト横断トラッキング、プライバシーとテレメトリという3つの公式ホスト名リストがあります。これらは最初から表示され、すべてデフォルトでオフになっています。ブロックにはネットワーク機能拡張が必要です。nettop サンプラーは監視のみ可能です。

公式の署名付きアップデートのみ。 Flowlight は、公開元が署名した新しいカタログがないか、1つの固定 HTTPS ソースを確認します。自動確認はデフォルトでオンになっており、ここでオフにできます。署名はコンテンツを認証しますが、有効化はしません。新しいパックはオフのままで、宛先が変更された場合は、正確な新バージョンを確認して保存を押すまで、有効なパックの適用が停止されます。障害中も、承認済みコンテンツが、古いことを明示した既知の正常な最終版として保持される場合があります。これらはホスト名単位のネットワークブロックです。DOM やレイアウトの空白、ブラウザのオーバーレイ、スクリプトによるポップアップ、ネイティブアプリのウインドウは除去できません。

HTTPS インスペクションがアプリを Flowlight のプロキシ経由にすると、接続先の通常ルールと有効な管理対象パックは、接続先サーバへの接続が開かれる前に、元のアプリの識別情報を使って判定されます。証明書がピン留めされている、または復号されない CONNECT トンネルでも、ホスト名によって判定できます。エージェントの許可リストは引き続き、ネットワーク機能拡張を直接通る経路だけに適用されます。所有者またはホスト名が不明な場合は、接続を許可します。

@@ -470,7 +476,10 @@

エージェントのガードレール:ツール、サー

これはサンドボックスではありません。拒否したツールは提示されなくなり、呼び出しにも応答しなくなります。しかしシェルを使えるエージェントは、 そのツールがしていたことを手作業で行えます。Flowlight はそれを記録しますが、防げるふりはしません。

-

ソース:コレクターとエンジン

+

ソース:コレクター、デバイス、カバー範囲

+

ソースは、コレクター、デバイス、カバー範囲の順に、見出しで区切って並べた 1 つのスクロール可能な運用ページです。 + 状況の通知、質問への回答、復元されたウインドウ、スクリーンショットからのリンクは、ほかのセクションを隠さず、該当するセクションまで + スクロールして短時間強調表示します。

Flowlight がトラフィックを記録する方法は 2 つあり、ソース › コレクターで選びます。何を取りこぼしうるかが違うので、どちらかに頼る前に 知っておく価値があります。

@@ -653,7 +662,7 @@

質問と回答

ウインドウを閉じているあいだ、Flowlight はほとんど何もしません。メニューバーの速度を保ち、1 秒に 1 行書き込むだけです。ネットワーク機能拡張のほうが、 1 秒ごとに短命な nettop プロセスを起動する代替のサンプラーよりも負荷が低いです。
接続をブロックできますか?
-
はい、ネットワーク機能拡張があれば。エージェントの許可リストは警告だけでなく拒否もでき、ルールはアプリ、接続先、URL を +
はい、ネットワーク機能拡張があれば。エージェントの許可リストは警告だけでなく拒否もでき、ルールはアプリ、接続先、URL を 好きな期間ブロックでき、ガードレールはモデルに提示される前にエージェントからツールを取り上げられます。 あなたが指示するまで何もブロックされず、どの拒否にも抜け道が添えてあります。
ネットワーク機能拡張は必要ですか?
@@ -708,7 +717,7 @@

制限事項

diff --git a/docs/ja/index.html b/docs/ja/index.html index 1e4a2930..2bc236d3 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.17.4macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -127,7 +127,7 @@

アプリの通信が見える。
-
Flowlight の AI アクティビティマップ。エージェントカードは関連付けられたツールと設定済みゲートウェイを経由し、観測された magpie から Flowlight への経路が LLM サービスとドメインへ分岐します。帯の幅は期間内の通信量を示します。

実際の AI 経路を確認できます。エージェント、関連付けられたツール、設定済みゲートウェイ、Flowlight が観測したプロキシ段階、正確な接続先を表示。帯の幅は選択期間の受信量と送信量の合計です。デモデータで表示しています。

@@ -135,7 +135,7 @@

アプリの通信が見える。
-
Flowlight の AI エージェントビュー。Claude Code、Cursor、Codex、Python スクリプトが、利用している AI プロバイダ、ほかへ送ったトラフィック、SMTP・SSH・FTP・大量送信・生 IP のリスクバッジとともに並んでいます。

AI エージェントビュー。各エージェントのモデルプロバイダと、それ以外に通信した相手のすべて。デモデータで表示しています。

@@ -179,11 +179,11 @@

履歴

結局どこへ行った?秒から年まで、アプリ・

アラート

何がおかしかった?理由がわかるアプリ単位のアラートに、接続先としきい値を添えて。

⌘5
-

ルール

何を拒否し、それは効いている?すべてのルール、それぞれがブロックしたもの、判定の履歴。

⌘8 · Developer ID / ⌘7 · App Store
+

ポリシー

どんな制御があり、実際に効いている?すべてのポリシー、その実際の状態、統合されたアクティビティ履歴。

⌘8 · Developer ID / ⌘7 · App Store

質問

ふつうの言葉で聞けば、履歴から答えます。実行したクエリもすべて下に並びます。

⌘6
-

ソース

データは届いているか、どれだけ名前が付いているか、どのデバイス経路が有効か。コレクター、デバイス、カバー範囲を 1 か所に。

⌘9 · Developer ID / ⌘8 · App Store
+

ソース

データは届いているか、どのデバイス経路が有効か、Flowlight はどこまで把握できるか。コレクターからカバー範囲までを 1 ページに。

⌘9 · Developer ID / ⌘8 · App Store

インスペクト

任意。エージェントは実際に何を送り、どのツール呼び出しが原因だった?復号したリクエスト、ツール呼び出し、結果。

⌘7 · Developer ID
@@ -404,16 +404,16 @@

対応するツール呼び出しとネットワークリクエストを結 インスペクト対象のターミナルからエージェントを起動すれば、エージェントとそのツールの対応 HTTP 通信が復号され、モデルが要求した ツール呼び出しと、返っていった結果まで見えます。

-
Flowlight のインスペクトビュー。復号された Claude Code のリクエストとそのツール呼び出しの一覧、そして paste.example への curl の送信の詳細。実行元となった Bash のツール呼び出しと、リクエスト本文に入った送信済みのメモが表示されています
あのメモはどこへ行った?paste.example への curl の送信を、モデルが要求した Bash コマンドまでたどり、 送られた本文まで示します。
-
Claude Code のツールタブ。Bash、Read と 2 つの MCP ツールが、説明とモデルが呼び出した回数とともに並んでいます
エージェントがモデルに渡すツールのすべて。エージェントはターンごとにツールを宣言するので、Flowlight はそれを全部並べ、 モデルが実際に使ったものと、一度も触らなかったものを示します。
-
Codex の MCP サーバタブ。プロバイダ経由と記された sentry サーバ、その URL、承認は「しない」に設定、許可されたツールと提供するツールが表示されています
回線上では見えないものまで。エージェントがプロバイダ側に MCP サーバへの接続を任せると、その通信は Mac を通りません。 でも依頼そのものは通るので、サーバ、その URL、承認の設定、提供するツールはやはり表示されます。
@@ -459,7 +459,7 @@

監視しなくていい監視ツール

-
Flowlight での Claude Code の詳細。呼び出し回数と 1 件の失敗が付いたツールと MCP サーバ、許可リスト、そしてツール呼び出しタブに、失敗した git push、応答とリクエストを伴う paste.example への curl 送信、npm install、GitHub の issue、ドキュメント検索、ファイル読み取りが、それぞれの結果とともに並んでいます
1 つのエージェント、すべてのツール呼び出し。Claude Code が起動した git や curl、MCP サーバの通信は、Claude Code のものとして数えます。 HTTPS インスペクションをオンにすると、ツール呼び出しタブに、モデルが何を要求し、何が返り、各ツールがどんなリクエストを出したかが並びます。 @@ -469,10 +469,10 @@

監視しなくていい監視ツール

-
履歴ビュー。1 時間ごとの受信と送信の棒グラフ、アプリ・接続先・プロトコルのドーナツグラフが表示されています
履歴。秒から年までのズーム、3 つのグループ化、アプリが数百あっても読めるグラフ。
-
アラートビュー。エージェントの情報流出、メール、FTP、離席中、トラフィック急増のアラートが並んでいます
アラート。どれもアプリ、接続先、きっかけになった数値を示します。
@@ -588,7 +588,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index 11dcb21d..4d02e58b 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -123,7 +123,7 @@

アプリが行うネットワークリクエスト

- + @@ -187,7 +187,7 @@

連絡先

diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html index c7b64500..9d3f78f7 100644 --- a/docs/ja/threat-model/index.html +++ b/docs/ja/threat-model/index.html @@ -222,7 +222,7 @@

脆弱性を報告する

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index 7d4b4821..9c5a18fa 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -153,7 +153,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index 06fc0120..76a0def1 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -76,7 +76,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.17.3, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.17.4, macOS 15 이상.

@@ -169,11 +169,11 @@

여러 뷰

- - + +
リクエスト送信先含まれるものタイミング
アップデートの確認api.github.com最新リリースを尋ねる通常の HTTPS リクエスト。GitHub が見るのは、IP アドレスと Flowlight/<version> というユーザーエージェントです起動時と 1 日 1 回(0.1.2 以降)。オフのスイッチは設定 › 一般
アップデートのダウンロードgithub.comディスクイメージと、そのチェックサムファイル「ダウンロードして開く」をクリックしたときだけ
公式ルールパックのアップデートflowlight.xinbetween.com固定された公式フィードへの標準 HTTPS リクエストです。サーバには、IP アドレス、Flowlight/<version>ユーザーエージェント、条件付きキャッシュバリデータが表示されます。トラフィック、パック設定、デバイス識別子は送信されません起動後、確認期限に達している場合に実行し、その後は正常稼働中に1日最大1回実行します。デフォルトでオンです。ルール › パックでオフにするか、今すぐ確認を手動で実行できます
公式ルールパックのアップデートflowlight.xinbetween.com固定された公式フィードへの標準 HTTPS リクエストです。サーバには、IP アドレス、Flowlight/<version>ユーザーエージェント、条件付きキャッシュバリデータが表示されます。トラフィック、パック設定、デバイス識別子は送信されません起動後、確認期限に達している場合に実行し、その後は正常稼働中に1日最大1回実行します。デフォルトでオンです。ポリシー › 管理対象パックでオフにするか、「今すぐ確認」を手動で実行できます
逆引き DNSあなたが設定した DNS リゾルバホスト名が見つからなかった場合の、IP アドレスの登録名の照会必要に応じて、サンプラーのキャプチャモードで。ネットワーク機能拡張が動き出すまではこれがデフォルトです。オフのスイッチはありません。これがないとサンプラーはほとんどの接続に名前を付けられないからです。ネットワーク機能拡張はトラフィック自身から名前を付けるので、これを行いません
ネットワーク所有者の照会Team Cymru(DNS 経由)ホスト名がわからないパブリック IP アドレス。プライベートアドレスと予約アドレスは、この Mac から決して出ませんデフォルトでオン。オフのスイッチはキャプチャ › ネットワーク所有者を特定
ホスト型モデルでの質問あなたが選んだプロバイダ:Anthropic、OpenAI、Google Gemini、または互換のエンドポイントあなたの質問と、モデルが選んだ読み取り専用の Flowlight の問い合わせが返した結果。トラフィックのデータベース自体はアップロードされませんホスト型のプロバイダを選び、質問を送信したときだけ。質問の画面には、送信されたリクエストが表示されます
알림⌘5이상해 보인 모든 것. 앱과 목적지, 그리고 규칙의 기준을 넘긴 수치와 함께.
질문⌘6내 트래픽에 대한 평범한 말로 된 질문. 기록된 이력에서 로컬 모델이나 호스팅 모델이 답합니다.
검사⌘7 · Developer ID선택 사항. Flowlight를 거치도록 지정한 에이전트의 복호화된 요청과 그 뒤에 있는 도구 호출. 검색은 요청·응답 본문까지 들어갑니다.
규칙⌘8 · Developer ID
⌘7 · App Store
설정해 둔 모든 허용 목록과 거부, 각각이 무엇을 포괄하는지, 그리고 실제로 무엇을 막았는지.
소스⌘9 · Developer ID
⌘8 · App Store
컬렉터 상태, 기기 채널, 이름 지정 포착 범위를 세 개의 탭에서 표시합니다.
정책⌘8 · Developer ID
⌘7 · App Store
설정된 모든 정책과 실제 적용 상태, 그리고 하나로 모은 읽기 전용 활동 기록.
소스⌘9 · Developer ID
⌘8 · App Store
컬렉터 상태와 복구, 선택형 기기 채널, 포착 범위를 하나의 스크롤 가능한 운영 페이지에서 봅니다.
-

단축키는 보이는 사이드바 순서를 따릅니다. Developer ID 빌드에는 ⌘7의 검사가 있고, App Store 빌드에는 없으므로 규칙과 소스가 ⌘7과 ⌘8로 이동합니다.

-
단축키는 보이는 사이드바 순서를 따릅니다. Developer ID 빌드에는 ⌘7의 검사가 있고, App Store 빌드에는 없으므로 정책과 소스가 ⌘7과 ⌘8로 이동합니다.

+
실시간 뷰: 앱 목록에 각각의 현재 보내기·받기 속도와 가장 바쁜 목적지가 함께 나열되어 있습니다.
실시간, Flowlight가 처음 여는 화면.

기록에서 막대에 포인터를 올리면 포인터를 따라다니는 스크롤 가능한 카드에 기여한 앱이 표시됩니다. 막대를 클릭해 확대하지는 않습니다. 행을 두 번 클릭하면 기록을 그 항목으로 좁힐 수 있습니다.

@@ -213,7 +213,7 @@

도구와 MCP 서버

것은 둘 중 하나보다 더 많은 것을 말해 줍니다. 설정만 되어 있는 도메인은 한 번도 닿은 적이 없고, 관찰만 된 도메인은 미리 읽어 볼 수 있는 어디에도 적혀 있지 않았습니다.

도메인마다 거부하는 방법이 두 가지입니다. 이 에이전트에 대해는 에이전트와 그것이 시작하는 도구를 덮고, - 어디서나는 이 Mac의 모든 앱을 덮습니다. 둘 다 평범한 규칙을 씁니다. 그래서 위반 기록이 그것을 + 어디서나는 이 Mac의 모든 앱을 덮습니다. 둘 다 평범한 규칙을 씁니다. 그래서 활동이 그것을 보고하고 규칙 편집기가 그것을 설명하며, 평범한 규칙이기 때문에 편집기의 시뮬레이션이 규칙을 켜기 전에 그것이 무엇을 거부했을지 알려 줍니다.

버튼이 붙지 않는 도메인이 한 종류 있습니다. 모델 제공업체가 스스로 닿는 커넥터입니다(요청 본문에 선언되고 제공업체 쪽에서 @@ -402,15 +402,19 @@

모의 응답

그리고 목적지에는 여전히 연결합니다. 모의 응답은 답을 바꾸는 것이고 연결 자체가 되는지를 바꾸는 것이 아니므로, “API가 500을 돌려준다”는 만들 수 있지만 “API에 아예 닿지 않는다”는 만들 수 없습니다.

-

규칙

-

허용 목록은 에이전트 하나가 어디까지 갈 수 있는지에 대한 이야기입니다. 규칙은 그것의 일반형입니다. +

정책

+

정책은 Flowlight가 적용할 수 있는 모든 제어를 한데 모은 목록입니다. 일반 허용·차단 규칙, 에이전트 허용 목록, + 관리형 팩, 그리고 HTTPS 검사를 사용할 수 있을 때는 가드레일, 모의 응답, 요청 다시 쓰기, 응답 변환까지 포함합니다. 각 행은 설정된 상태와 + 실제 효력을 구분해 적용 중, 보고만 함, 감시 중, 대기 중, 예약됨, 일시 중지, 사용 완료, 적용 중, 검토 필요, 사용 불가, 호환되지 않음, + 실패를 표시합니다. 행을 선택하면 그 정책을 관리하는 원래 편집기가 열립니다. 정책 화면이 서로 다른 동의 방식과 저장 방식을 대신하거나 + 뒤섞지는 않습니다.

+

허용 목록은 에이전트 하나가 어디까지 갈 수 있는지에 대한 이야기입니다. 일반 규칙은 그것의 일반형입니다. 대상, 즉 앱이나 에이전트, 목적지, URL, 또는 그 둘의 조합을 지목하고 차단인지 허용인지 정합니다. 실시간이나 기록, AI 에이전트, 검사에서 아무 행이나 오른쪽 클릭하면 지금 보고 있는 것에 - 대한 규칙을 쓸 수 있습니다. 어느 경로로 만들어도 모두 규칙 화면의 같은 편집기로 모입니다. 보이지 않는 곳에서 - 효력을 갖는 규칙은 다시는 찾을 수 없는 규칙이 되기 때문입니다.

+ 대한 규칙을 쓸 수 있습니다. 어느 경로에서 만들었든 정책 화면에서 찾을 수 있고, 선택하면 그 규칙을 관리하는 원래 편집기가 열립니다.

템플릿. 스물두 개가 다섯 묶음으로 있습니다. 에이전트가 패키지 레지스트리나 다른 모델 제공업체에 닿지 못하게 막는 것, 데이터가 나가는 흔한 경로를 닫는 것(붙여넣기 사이트, 웹훅 수집기, 익명 파일 업로드, 터널, DNS over HTTPS), 제품 분석과 - 크래시 보고, 업무 시간 정책, 그리고 “이게 뭔지 알아내는 동안 한 시간만 막아 두기” 같은 일회성 규칙입니다. 규칙 툴바에서 + 크래시 보고, 업무 시간 정책, 그리고 “이게 뭔지 알아내는 동안 한 시간만 막아 두기” 같은 일회성 규칙입니다. 정책 툴바에서 둘러보거나, 실시간·기록·AI 에이전트·검사의 아무 행에서 오른쪽 클릭해 지금 보고 있는 것에 적용하세요.

각 템플릿은 무엇을 할지 먼저 알려줍니다. 규칙을 두 개 이상 쓰는 템플릿은 모든 호스트와 일정을 나열한 카드를 열고, 영구적인 것은 미리 채워진 편집기를 열며, 스스로 사라지는 규칙, 즉 한 시간짜리나 이번 Flowlight 실행 동안만 유효한 @@ -432,8 +436,9 @@

규칙

경로는 모르고, 확장 모드에서만 동작합니다. HTTPS 검사는 요청 하나를 거부하므로 URL까지 일치시킬 수 있고, 이유도 없이 죽은 소켓 대신 에이전트가 읽을 수 있는 상태 코드로 답할 수 있습니다. nettop 샘플러는 아무것도 거부하지 못합니다. 현재 설정으로 실행할 수 없는 규칙은 적용된 척 앉아 있지 않고 목록에서 그렇다고 밝힙니다.

-

두 번째 탭은 위반 기록입니다. 규칙이 판정한 모든 연결이, 앱과 향하던 곳, 어떤 규칙이 판정했는지, 그리고 - 그것이 거부였는지 통과시킨 예외였는지와 함께 나옵니다.

+

두 번째 뷰는 읽기 전용 활동입니다. 일반 규칙 판정, 허용 목록 위반, 관리형 팩 이벤트, 검사 정책 고유의 효과를 + 하나의 시간순 기록으로 모읍니다. 기존의 원본 기록을 그대로 연결해 보여 줄 뿐, 기록을 하나의 일반 이벤트 형식으로 다시 쓰지 않습니다. + App Store 빌드에서는 검사에 의존하는 정책 유형과 그 정책에 보관된 활동을 표시하지 않습니다.

이 규칙이 했을 일. 차단 규칙은 쓰기는 쉽고 켜기는 무섭습니다. 어떤 규칙에 대해 보통 가장 먼저 알게 되는 것은 그것이 무엇을 망가뜨렸는지이고, 그것도 나중에 다른 어딘가에서 무언가가 실패할 때 알게 되기 때문입니다. 규칙 편집기는 그 질문에 @@ -451,7 +456,7 @@

규칙

있었던 가장 오래된 트래픽도 함께 밝힙니다. 두 시간짜리 이력으로 대조한 규칙은 두 시간짜리 이력으로 대조한 것입니다.

관리형 광고 및 추적기 팩

-

규칙 › 팩에는 광고, 사이트 간 추적, 개인정보 보호 및 원격 측정이라는 세 가지 공식 호스트 이름 목록이 있습니다. 처음부터 표시되며 모두 기본적으로 꺼져 있습니다. 차단하려면 네트워크 확장 프로그램이 필요하며, nettop 샘플러는 관찰만 할 수 있습니다.

+

정책 › 관리형 팩에는 광고, 사이트 간 추적, 개인정보 보호 및 원격 측정이라는 세 가지 공식 호스트 이름 목록이 있습니다. 처음부터 표시되며 모두 기본적으로 꺼져 있습니다. 차단하려면 네트워크 확장 프로그램이 필요하며, nettop 샘플러는 관찰만 할 수 있습니다.

공식 서명 업데이트만 허용됩니다. Flowlight는 게시자가 서명한 최신 카탈로그가 있는지 하나의 고정 HTTPS 소스를 확인합니다. 자동 확인은 기본적으로 켜져 있으며 여기에서 끌 수 있습니다. 서명은 콘텐츠를 인증하지만 활성화하지는 않습니다. 새 팩은 꺼진 상태로 유지되며, 대상이 변경되면 활성화된 팩은 정확한 새 버전을 검토하고 저장을 누를 때까지 적용되지 않습니다. 서비스 중단 중에도 승인된 콘텐츠가 눈에 띄게 오래된 것으로 표시된 마지막 정상 버전으로 유지될 수 있습니다. 이는 호스트 이름 수준의 네트워크 차단이므로 DOM이나 레이아웃의 빈 공간, 브라우저 오버레이, 스크립트로 생성된 팝업 또는 네이티브 앱 창은 제거할 수 없습니다.

HTTPS 검사가 앱을 Flowlight의 프록시로 보내면, 일반 대상 규칙과 활성화된 관리형 팩은 원본 서버 연결이 열리기 전에 원래 앱의 신원을 사용해 결정됩니다. 인증서가 고정되었거나 복호화되지 않는 CONNECT 터널도 호스트 이름으로 판정할 수 있습니다. 에이전트 허용 목록은 계속 네트워크 확장을 직접 지나는 경로에만 적용됩니다. 소유자나 호스트 이름을 알 수 없으면 연결을 허용합니다.

@@ -500,7 +505,9 @@

에이전트 가드레일: 도구나 서버, 리소스를

이것은 샌드박스가 아닙니다. 거부된 도구는 더 이상 제공되지도, 응답되지도 않습니다. 하지만 셸이 남아 있는 에이전트는 그 도구가 하던 일을 직접 할 수 있습니다. Flowlight는 그것을 기록합니다. 막아 주는 척하지는 않습니다.

-

소스: 컬렉터와 엔진

+

소스: 컬렉터, 기기 및 포착 범위

+

소스는 컬렉터, 기기, 포착 범위를 차례로 뚜렷한 제목 아래 배치한 하나의 스크롤 가능한 운영 페이지입니다. + 상태 알림, 질문 답변, 복원된 창, 스크린샷 링크는 다른 섹션을 숨기지 않고 관련 섹션으로 스크롤한 뒤 잠시 강조합니다.

Flowlight는 소스 › 컬렉터에서 고르는 두 가지 방식 중 하나로 트래픽을 기록합니다. 두 방식은 무엇을 놓칠 수 있는지가 다르므로, 어느 쪽에 의지하기 전에 알아 둘 만합니다.

@@ -693,7 +700,7 @@

질문과 답

창을 닫아 두면 Flowlight는 거의 아무 일도 하지 않습니다. 메뉴 막대의 속도를 유지하고 1초에 한 행을 씁니다. 네트워크 확장은 1초마다 짧게 사는 nettop 프로세스를 실행하는 대체 샘플러보다 비용이 적습니다.
연결을 차단할 수 있나요?
-
네, 네트워크 확장이 있으면 됩니다. 에이전트의 허용 목록은 경고뿐 아니라 거부도 할 수 있고, 규칙은 앱이나 +
네, 네트워크 확장이 있으면 됩니다. 에이전트의 허용 목록은 경고뿐 아니라 거부도 할 수 있고, 규칙은 앱이나 목적지, URL을 원하는 기간만큼 막을 수 있으며, 가드레일은 모델에 제공되기 전에 에이전트에서 도구를 걷어낼 수 있습니다. 요청하기 전까지 차단되는 것은 없고, 모든 거부에는 빠져나갈 길이 함께 있습니다.
네트워크 확장이 꼭 필요한가요?
@@ -748,7 +755,7 @@

한계

diff --git a/docs/ko/index.html b/docs/ko/index.html index c83c6364..0332dc2d 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.17.4macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -127,7 +127,7 @@

앱이 무엇을 하는지 봅니다.
-
Flowlight의 AI 활동 지도. 에이전트 카드는 귀속된 도구와 구성된 게이트웨이를 거치며, 관측된 magpie에서 Flowlight로의 경로가 LLM 서비스와 도메인으로 분기됩니다. 리본 너비는 기간 내 트래픽 양을 나타냅니다.

실제 AI 경로를 확인하세요. 에이전트, 귀속된 도구, 구성된 게이트웨이, Flowlight가 관측한 프록시 단계와 정확한 목적지를 표시합니다. 리본 너비는 선택한 기간의 수신량과 송신량 합계입니다. 데모 데이터로 표시했습니다.

@@ -135,7 +135,7 @@

앱이 무엇을 하는지 봅니다.
-
Flowlight의 AI 에이전트 뷰. Claude Code, Cursor, Codex, Python 스크립트가 각자의 AI 제공업체, 다른 곳으로 보낸 트래픽, 그리고 SMTP, SSH, FTP, 대용량 업로드, 원시 IP에 대한 위험 배지와 함께 나열되어 있습니다.

AI 에이전트 뷰: 각 에이전트의 모델 제공업체, 그리고 그 밖에 통신한 모든 곳. 데모 데이터로 표시했습니다.

@@ -179,11 +179,11 @@

기록

다 어디로 갔나? 초에서 연까지, 앱·목적지·IP

알림

무엇이 이상했나? 목적지와 임계값까지 밝히는, 설명되는 앱별 알림.

⌘5
-

규칙

무엇을 거부했고, 실제로 막히고 있나? 모든 규칙과 각 규칙이 막은 것, 그리고 모든 판정의 기록.

⌘8 · Developer ID / ⌘7 · App Store
+

정책

어떤 제어가 있고 실제로 효력이 있나? 모든 정책과 실제 상태, 그리고 하나로 모은 활동 기록.

⌘8 · Developer ID / ⌘7 · App Store

질문

평소 쓰는 말로 묻고 기록에서 답을 받습니다. 실행된 질의는 아래에 전부 표시됩니다.

⌘6
-

소스

데이터가 들어오는지, 이름이 붙은 비율과 활성 기기 채널은 어떤지. 컬렉터, 기기, 포착 범위를 한곳에서 봅니다.

⌘9 · Developer ID / ⌘8 · App Store
+

소스

데이터가 들어오는지, 어떤 기기 채널이 활성화되었는지, Flowlight가 어디까지 파악할 수 있는지. 컬렉터부터 포착 범위까지 한 페이지에서 봅니다.

⌘9 · Developer ID / ⌘8 · App Store

검사

선택 사항. 에이전트가 실제로 무엇을 보냈고, 어떤 도구 호출이 그렇게 했나? 복호화된 요청, 도구 호출, 결과.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

도구 호출과 네트워크 요청을 잇습니다

검사 대상인 터미널에서 에이전트를 실행하면 에이전트와 그 도구의 지원되는 HTTP 트래픽이 복호화되고, 모델이 요청한 도구 호출과 되돌아간 결과가 함께 보입니다.

-
Flowlight의 검사 뷰: 복호화된 Claude Code 요청과 각 요청의 도구 호출 목록, 그리고 curl이 paste.example로 올린 업로드의 상세 내용. 그 업로드를 실행한 Bash 도구 호출과 요청 본문에 담긴 메모가 함께 보입니다
내 메모는 어디로 갔나? curl이 paste.example로 올린 업로드를, 모델이 요청한 Bash 명령까지 거슬러 올라가 보낸 본문과 함께.
-
Claude Code의 도구 탭: Bash, Read와 MCP 도구 두 개가 설명, 그리고 모델이 각각을 몇 번 호출했는지와 함께 나열되어 있습니다
에이전트가 모델에게 내주는 모든 도구. 에이전트는 매 턴마다 도구를 선언하므로 Flowlight는 그것을 전부 나열하고, 모델이 실제로 쓴 것과 한 번도 건드리지 않은 것을 구분해 보여줍니다.
-
Codex의 MCP 서버 탭: 제공업체 경유로 표시된 sentry 서버, 그 URL, 승인은 사용 안 함, 허용된 도구, 그리고 서버가 제공하는 도구들
회선에서는 보이지 않는 것까지. 에이전트가 제공업체에게 MCP 서버로 연결해 달라고 하면 그 트래픽은 내 Mac을 거치지 않습니다. 하지만 요청은 거치므로 서버와 URL, 승인 설정, 제공하는 도구는 그대로 드러납니다.
@@ -460,7 +460,7 @@

감시하지 않아도 되는 감시 도구

-
Flowlight에 표시된 Claude Code 상세: 호출 횟수와 실패 하나가 붙은 도구와 MCP 서버, 허용 목록, 그리고 실패한 git push, 응답과 요청이 함께 있는 paste.example로의 curl 업로드, npm install, GitHub 이슈, 문서 검색, 파일 읽기가 각각 결과와 함께 나열된 도구 호출 탭
에이전트 하나, 도구 호출 전부. Claude Code가 띄운 git과 curl, MCP 서버의 트래픽은 Claude Code의 것으로 집계됩니다. HTTPS 검사를 켜면 도구 호출 탭에서 모델이 무엇을 요청했고 무엇이 돌아왔으며 각 도구가 어떤 요청을 @@ -470,10 +470,10 @@

감시하지 않아도 되는 감시 도구

-
시간별 수신·송신 막대와 앱, 목적지, 프로토콜 도넛 차트가 있는 기록 뷰
기록. 초에서 연까지의 확대·축소, 세 가지 묶음, 앱이 수백 개여도 읽히는 차트.
-
에이전트 유출, 이메일, FTP, 자리 비움, 트래픽 급증 알림이 나열된 알림 뷰
알림. 하나하나가 앱과 목적지, 그리고 기준을 넘긴 수치를 밝힙니다.
@@ -589,7 +589,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 6ea64c96..ab3fb278 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -126,7 +126,7 @@

앱이 내는 네트워크 요청

- + @@ -190,7 +190,7 @@

문의

diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html index e6efc75c..706dbf6f 100644 --- a/docs/ko/threat-model/index.html +++ b/docs/ko/threat-model/index.html @@ -221,7 +221,7 @@

취약점 신고하기

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 18efdd43..fd6b6f91 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,7 +62,7 @@ Documentation Using Flowlight - Everything from the first launch to tuning the agent rules. Flowlight 0.17.3, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.17.4, macOS 15 or later. On this page @@ -79,7 +79,7 @@ Documentation AskHTTPS inspectionMock responses Control - RulesAllowlists + PoliciesAllowlists Refusing connectionsGuardrails Sources @@ -163,13 +163,13 @@ open build/Build/Products/Release/Flowlight.app Inspect⌘7 · Developer IDOptional: decrypted requests from the agents you route through Flowlight, and the tool calls behind them. Search reaches into request and response bodies. - Rules⌘8 · Developer ID -⌘7 · App StoreEvery allowlist and refusal you have set, what each one covers, and what it has actually stopped. + Policies⌘8 · Developer ID +⌘7 · App StoreEvery configured policy, whether it is actually effective, and a unified read-only activity trail. Sources⌘9 · Developer ID -⌘8 · App StoreCollector health and recovery, hostname coverage, and opt-in camera, Bluetooth, USB and storage activity in three tabs. +⌘8 · App StoreCollector health and recovery, opt-in device channels, and coverage as one scrollable operational page. - The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Rules and Sources move to ⌘7 and ⌘8. + The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Policies and Sources move to ⌘7 and ⌘8. Live, the screen Flowlight opens on. In History, choose 15 min, 1h, 6h, 1d, 1w, 1 mo, 6 mo, 1y, or All. Hover-only bars open a cursor-following, scrollable card with the bucket totals and contributing apps; they never change the selected range. The navigator below the chart starts with Nearby context and can switch to All history: drag the brush to pan the selected window, or drag either edge to resize it. Double-click any breakdown row to narrow History to it. @@ -224,7 +224,7 @@ open build/Build/Products/Release/Flowlight.app never been reached, and one only ever observed was never written down anywhere you could have read it. Each domain offers two refusals: for this agent, which covers the agent and the tools it starts, or - everywhere, which covers every app on the Mac. Both write an ordinary rule, so the violations feed reports + everywhere, which covers every app on the Mac. Both write an ordinary rule, so Activity reports them and the rule editor explains them — and because they are ordinary rules, the simulation in the editor will tell you what one would have refused before you switch it on. @@ -464,18 +464,23 @@ open build/Build/Products/Release/Flowlight.app connected to: a mock changes the answer, not whether the connection can be made, so "the API returns 500" works while "the API is entirely unreachable" does not. - Rules + Policies - An allowlist is one agent's idea of where it may go. A rule is the general version: it names a + Policies is one inventory for every control Flowlight can apply: ordinary allow/block rules, agent + allowlists, managed packs, and—where HTTPS inspection is available—guardrails, mocks, request rewrites, and response + transforms. Each row distinguishes configured state from actual effect: enforcing, reporting, watching, waiting, + scheduled, paused, spent, applying, review required, unavailable, incompatible, or failed. Selecting a row opens the + contextual editor that owns that policy; Policies does not replace or blur those separate consent and persistence models. + + An allowlist is one agent's idea of where it may go. An ordinary rule is the general version: it names a subject — an app, an agent, a destination, a URL, or a pairing of two of those — and says block or allow. Right-click any row in Live, History, AI Agents or Inspect to write one about what you're looking at; - every one of them lands in the same editor on the Rules screen, because a rule that took effect out of sight - would be a rule you could never find again. + every one remains findable in Policies and opens in its authoritative editor. Templates. Twenty-two of them, in five groups — stopping an agent reaching package registries or other model providers, closing the common ways data leaves (paste sites, webhook catchers, anonymous file drops, tunnels, DNS over HTTPS), product analytics and crash reporting, working-hours policies, and one-off "block this for an hour while I work out - what it is". Browse them from the Rules toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and + what it is". Browse them from the Policies toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and apply one to what you're looking at. Each template says what it will do before it does it. Anything that writes more than one rule opens a card listing every @@ -502,8 +507,9 @@ open build/Build/Products/Release/Flowlight.app URL and answer with a status the agent can read instead of a socket that died without saying why. The nettop sampler cannot refuse anything at all. A rule the current setup can't carry out says so in the list rather than sitting there looking enforced. - The second tab is the violations feed: every connection a rule decided, with the app, where it was headed, which - rule decided it and whether that was a refusal or an exception letting it through. + The second view is read-only Activity: ordinary rule decisions, allowlist violations, managed-pack events, + and distinct inspection-policy effects in one chronology. It adapts the existing authoritative records rather than rewriting + history into one generic event model; Store builds omit inspection-dependent policy kinds and their retained activity. What it would have done. A blocking rule is easy to write and frightening to switch on, because the first thing you normally learn about one is what it broke — and you learn it later, from something failing somewhere else. The rule @@ -526,7 +532,7 @@ open build/Build/Products/Release/Flowlight.app Managed ad and tracker packs - Rules › Packs contains three official lists built into Flowlight: Advertising covers known + Policies › Managed packs contains three official lists built into Flowlight: Advertising covers known advertising delivery and auction hosts; Cross-site tracking covers common analytics, session replay and cross-site measurement hosts; and Privacy and telemetry covers crash reporting, install attribution, and audited Apple advertising and analytics hosts. The packs are visible from the start and all are off by default. @@ -631,9 +637,11 @@ open build/Build/Products/Release/Flowlight.app This is not a sandbox. A refused tool stops being offered and stops being answered. An agent that still has a shell can do by hand what the tool would have done. Flowlight will record that; it does not pretend to prevent it. - Sources: collector, coverage and devices + Sources: collector, devices and coverage - Sources keeps operational setup in one place. Its health summary shows whether traffic is arriving, how much is named, and which optional device channels are active; any unfinished collector step links directly to Sources › Collector. + Sources is one scrollable operational page: Collector first, then Devices and Coverage as clearly headed + sections. Status notices, Ask answers, restored windows, and screenshot links scroll to and briefly focus the relevant section + without hiding the others. Flowlight records traffic one of two ways, chosen under Sources › Collector. They differ in what they can miss, which is worth understanding before you rely on either. @@ -920,7 +928,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.17.3macOS 15+UniversalGPL-3.0No telemetry + v0.17.4macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1003,8 +1011,8 @@ Where did it all go? From 15 minutes through all history, grouped by app, destin What looked wrong? Explainable, per-app alerts with the destination and the threshold. ⌘5 - Rules -What have I refused, and is it happening? Every rule, what each has blocked, and a feed of every decision. + Policies +What controls exist, and are they effective? Every policy, its real state, and a unified activity trail. ⌘8 · Developer ID / ⌘7 · App Store Ask @@ -1012,7 +1020,7 @@ Questions in plain language, answered from the history — with every query it r ⌘6 Sources -Is data arriving, how much is named, and which device channels are active? Collector, Devices and Coverage in one place. +Is data arriving, which device channels are active, and what can Flowlight account for? One page: Collector, Devices, then Coverage. ⌘9 · Developer ID / ⌘8 · App Store Inspect @@ -1614,7 +1622,7 @@ Privacy Update downloadgithub.comThe disk image and its checksum fileOnly when you click Download & Open - Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/ user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now in Rules › Packs + Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/ user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now under Policies › Managed packs Reverse DNSYour configured DNS resolverA lookup of an IP address's registered name, when no hostname was seenAs needed, in the sampler capture mode, which is the default until the Network Extension is running. No off switch: without it the sampler cannot name most connections. The Network Extension names them from the traffic itself and does not do this @@ -1664,9 +1672,22 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. - 0.17.3 + 0.17.4 November 4, 2026Latest + See every control in Policies. Rules is now a unified Policies inventory for ordinary rules, agent allowlists, managed packs, and Developer-only inspection policies, with truthful effective states rather than treating every enabled item as active. + + Keep each policy's real editor and evidence. Selecting an item opens the contextual editor that owns it; Managed packs retain compact provenance and consent, while Activity adapts the existing authoritative records without conflating policy types. + + Read Sources as one operational page. Collector, Devices and Coverage now flow in that order without a segmented control, while status links, Ask answers, restored windows and screenshot arguments still focus the requested section. + + Dismiss chart details when the pointer leaves. History no longer leaves a zero-value tooltip floating at the upper-left after the cursor exits the All traffic plot; keyboard bucket inspection remains unchanged. + + Sign each release once. The immutable tag run builds, signs, notarizes, Gatekeeper-verifies, attests and retains one artifact; publication has no signing credentials and can only verify and publish those exact prepared bytes. + + 0.17.3 +November 4, 2026 + Keep every input in one operational home. Capture, Devices and Coverage are now Collector, Devices and Coverage tabs inside Sources, with a shared health summary and a step-by-step recovery card when traffic is not arriving. Tune alerts where their results appear. Detection controls now live beside the alert list under Alerts › Detection, while database retention, location and deletion live in Settings › Data & Privacy. diff --git a/docs/llms.txt b/docs/llms.txt index 96c8b063..cc51e04c 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Flowlight -> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.17.3. +> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.17.4. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/plugins/index.html b/docs/plugins/index.html index 7ef95b0c..18b4cd0c 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -212,7 +212,7 @@

Review checklist

diff --git a/docs/privacy/index.html b/docs/privacy/index.html index e0d1be2d..59ba96e4 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -131,7 +131,7 @@

Network requests the app makes

- + @@ -196,7 +196,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 014a54a7..c3ff23ba 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -152,7 +152,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 2cf81572..4efa4c80 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -76,7 +76,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.17.3, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.17.4, macOS 15 ou posterior.

@@ -170,11 +170,11 @@

As vistas

- - + +
요청보내는 곳담기는 내용시점
업데이트 확인api.github.com최신 릴리스를 묻는 일반적인 HTTPS 요청. GitHub는 IP 주소와 Flowlight/<version> 사용자 에이전트를 보게 됩니다0.1.2부터, 실행 시와 하루에 한 번. 끄는 스위치는 설정 › 일반
업데이트 다운로드github.com디스크 이미지와 체크섬 파일다운로드 후 열기를 클릭할 때만
공식 규칙 팩 업데이트flowlight.xinbetween.com고정된 공식 피드로 보내는 표준 HTTPS 요청입니다. 서버에는 사용자의 IP 주소, Flowlight/<version> 사용자 에이전트 및 조건부 캐시 유효성 검사기가 표시되며, 트래픽, 팩 설정 또는 기기 식별자는 전송되지 않습니다시작 후 확인 시점이 되면 실행되고, 이후 정상 작동 중에는 하루에 최대 한 번 실행됩니다. 기본적으로 켜져 있으며, 규칙 › 팩에서 끄거나 지금 확인을 수동으로 실행할 수 있습니다
공식 규칙 팩 업데이트flowlight.xinbetween.com고정된 공식 피드로 보내는 표준 HTTPS 요청입니다. 서버에는 사용자의 IP 주소, Flowlight/<version> 사용자 에이전트 및 조건부 캐시 유효성 검사기가 표시되며, 트래픽, 팩 설정 또는 기기 식별자는 전송되지 않습니다시작 후 확인 시점이 되면 실행되고, 이후 정상 작동 중에는 하루에 최대 한 번 실행됩니다. 기본적으로 켜져 있으며, 정책 › 관리형 팩에서 끄거나 지금 확인을 수동으로 실행할 수 있습니다
역방향 DNS설정된 DNS 리졸버호스트 이름이 관측되지 않았을 때, IP 주소에 등록된 이름 조회필요할 때, 샘플러 캡처 모드에서. 네트워크 확장이 돌기 시작할 때까지는 이것이 기본값입니다. 끄는 스위치는 없습니다. 이것이 없으면 샘플러는 대부분의 연결에 이름을 붙일 수 없습니다. 네트워크 확장은 트래픽 자체에서 이름을 붙이므로 이 조회를 하지 않습니다
네트워크 소유자 조회Team Cymru, DNS로알려진 호스트 이름이 없는 공용 IP 주소. 사설 주소와 예약 주소는 절대 Mac을 벗어나지 않습니다기본값 켬. 끄는 스위치는 캡처 › 네트워크 소유자 확인
호스팅 모델로 질문선택한 제공업체: Anthropic, OpenAI, Google Gemini 또는 호환 엔드포인트내 질문과, 모델이 고른 Flowlight 읽기 전용 질의가 돌려준 결과. 트래픽 데이터베이스 자체는 업로드되지 않습니다호스팅 제공업체를 고르고 질문을 보낼 때만. 질문 화면에 전송된 요청이 표시됩니다
RequestSent toWhat it containsWhen
Update checkapi.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agentAfter a clean quit and relaunch, and daily. Off switch in Settings › General
Update downloadgithub.comThe disk image and its checksum fileOnly when you click Download & Open
Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/<version> user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now in Rules › Packs
Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/<version> user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now under Policies › Managed packs
Reverse DNSYour configured DNS resolverA lookup of an IP address's registered name, when no hostname was seenAs needed, in the sampler capture mode, which is the default until the Network Extension is running. No off switch: without it the sampler cannot name most connections. The Network Extension names them from the traffic itself and does not do this
Network owner lookupTeam Cymru, over DNSPublic IP addresses with no known hostname. Private and reserved addresses never leave your MacOn by default. Off switch in Sources › Collector › Identify network owners
Ask with a hosted modelThe provider you select: Anthropic, OpenAI, Google Gemini or a compatible endpointYour question and the results returned by the read-only Flowlight queries selected by the model. The traffic database itself is not uploadedOnly when you select a hosted provider and submit a question. The Ask screen shows the transmitted requests
Alertas⌘5Tudo o que pareceu invulgar, com a app, o destino e o número que disparou a regra.
Perguntar⌘6Perguntas sobre o seu próprio tráfego em linguagem simples, respondidas a partir do histórico registado por um modelo local ou alojado.
Inspecionar⌘7 · Developer IDOpcional: pedidos desencriptados dos agentes que encaminha pelo Flowlight e as chamadas de ferramentas por trás deles. A pesquisa entra nos corpos dos pedidos e das respostas.
Regras⌘8 · Developer ID
⌘7 · App Store
Todas as listas de permissões e recusas que definiu, o que cada uma abrange e o que de facto travou.
Origens⌘9 · Developer ID
⌘8 · App Store
Estado do coletor, canais de dispositivos e cobertura de nomes em três separadores.
Políticas⌘8 · Developer ID
⌘7 · App Store
Todas as políticas configuradas, se estão realmente em vigor e um histórico unificado de atividade só de leitura.
Origens⌘9 · Developer ID
⌘8 · App Store
Estado e recuperação do Coletor, canais opcionais de dispositivos e cobertura numa única página operacional deslocável.
-

Os atalhos seguem a ordem visível da barra lateral. As versões Developer ID incluem Inspecionar em ⌘7; as versões App Store omitem-no, por isso Regras e Origens passam para ⌘7 e ⌘8.

-
Os atalhos seguem a ordem visível da barra lateral. As versões Developer ID incluem Inspecionar em ⌘7; as versões App Store omitem-no, por isso Políticas e Origens passam para ⌘7 e ⌘8.

+
A vista Em direto: uma lista de aplicações com as suas taxas de envio e receção atuais e o destino mais ativo.
Em direto, a vista com que o Flowlight abre.

Em Histórico, passe o rato sobre uma barra para abrir um cartão deslocável que segue o ponteiro e mostra as apps que contribuíram; as barras não ampliam ao clicar. Faça duplo clique numa linha para restringir o histórico a essa linha.

@@ -406,16 +406,20 @@

Respostas simuladas

continua a ser feita: uma simulação muda a resposta, não a possibilidade de estabelecer a ligação, por isso «a API devolve 500» funciona e «a API está completamente inacessível» não.

-

Regras

-

Uma lista de permissões é a ideia de um agente sobre onde pode ir. Uma regra é a versão geral: nomeia um +

Políticas

+

Políticas reúne num só inventário todos os controlos que o Flowlight pode aplicar: regras comuns de permissão ou bloqueio, listas de + permissões dos agentes, pacotes geridos e, quando a inspeção HTTPS está disponível, proteções, simulações, reescritas de pedidos e transformações de + respostas. Cada linha distingue o estado configurado do efeito real: em aplicação, apenas a comunicar, a vigiar, à espera, agendada, em pausa, + esgotada, a ser aplicada, a precisar de revisão, indisponível, incompatível ou com falha. Selecionar uma linha abre o editor contextual responsável + por essa política; Políticas não substitui nem confunde os diferentes modelos de consentimento e persistência.

+

Uma lista de permissões é a ideia de um agente sobre onde pode ir. Uma regra comum é a versão geral: nomeia um sujeito — uma app, um agente, um destino, um URL, ou um par formado por dois deles — e diz bloquear ou permitir. Clique com o botão direito em qualquer linha de Em direto, Histórico, Agentes de IA ou Inspecionar para escrever uma sobre o que está a ver; - todas elas vão dar ao mesmo editor no ecrã Regras, porque uma regra que entrasse em vigor fora da vista - seria uma regra que nunca mais conseguiria encontrar.

+ cada uma continua disponível em Políticas e abre no seu editor responsável.

Modelos. Vinte e dois, em cinco grupos — impedir um agente de chegar a registos de pacotes ou a outros fornecedores de modelos, fechar as vias habituais por onde os dados saem (sites de paste, apanhadores de webhooks, depósitos anónimos de ficheiros, túneis, DNS over HTTPS), analítica de produto e relatórios de falhas, políticas de horário de trabalho, e o pontual «bloquear isto por uma hora enquanto descubro - o que é». Consulte-os na barra de ferramentas de Regras, ou clique com o botão direito numa linha em Em direto, Histórico, Agentes de IA ou Inspecionar e + o que é». Consulte-os na barra de ferramentas de Políticas, ou clique com o botão direito numa linha em Em direto, Histórico, Agentes de IA ou Inspecionar e aplique um ao que está a ver.

Cada modelo diz o que vai fazer antes de o fazer. Tudo o que escreva mais do que uma regra abre um cartão com todos os anfitriões e horários; tudo o que seja permanente abre o editor pré-preenchido; e só as regras que se desfazem sozinhas — uma @@ -436,8 +440,9 @@

Regras

endereço e a porta, nada sobre caminhos, e só no modo de extensão. A inspeção HTTPS recusa um pedido, por isso consegue corresponder a um URL e responder com um estado que o agente pode ler, em vez de um socket que morreu sem dizer porquê. O amostrador nettop não consegue recusar nada. Uma regra que a configuração atual não consegue aplicar di-lo na lista, em vez de ficar ali com ar de estar em vigor.

-

O segundo separador é o fluxo de violações: todas as ligações que uma regra decidiu, com a app, para onde ia, que - regra a decidiu e se isso foi uma recusa ou uma exceção a deixá-la passar.

+

A segunda vista é a Atividade, só de leitura: reúne numa única cronologia as decisões das regras comuns, as violações das listas de permissões, + os eventos dos pacotes geridos e os efeitos distintos das políticas de inspeção. Baseia-se nos registos originais fidedignos em vez de reescrever o + histórico num único modelo genérico de eventos; as versões Store omitem os tipos de política dependentes da inspeção e a respetiva atividade conservada.

O que teria feito. Uma regra de bloqueio é fácil de escrever e assustadora de ligar, porque a primeira coisa que normalmente se aprende sobre uma delas é o que estragou — e aprende-se mais tarde, a partir de algo que falha noutro sítio. @@ -458,7 +463,7 @@

Regras

testada contra duas horas de histórico.

Pacotes geridos de publicidade e rastreadores

-

Regras › Pacotes contém três listas oficiais de nomes de anfitrião: Publicidade, Rastreio entre sites e Privacidade e telemetria. Estão visíveis desde o início e todas desativadas por predefinição. O bloqueio requer a Extensão de rede; o amostrador nettop apenas pode observar.

+

Políticas › Pacotes geridos contém três listas oficiais de nomes de anfitrião: Publicidade, Rastreio entre sites e Privacidade e telemetria. Estão visíveis desde o início e todas desativadas por predefinição. O bloqueio requer a Extensão de rede; o amostrador nettop apenas pode observar.

Apenas atualizações oficiais e assinadas. O Flowlight consulta uma única fonte HTTPS fixa para procurar catálogos mais recentes assinados pelo editor. As verificações automáticas estão ativas por predefinição e podem ser desativadas aqui. Uma assinatura autentica o conteúdo, mas não o ativa: um novo pacote permanece desativado e, se os destinos forem alterados, um pacote ativo deixa de ser aplicado até que reveja a nova versão exata e prima Guardar. Durante uma interrupção, o conteúdo aceite pode permanecer disponível como a última versão válida conhecida, visivelmente assinalada como desatualizada. Estes são bloqueios de rede ao nível do nome de anfitrião: não podem remover espaços vazios no DOM ou no esquema, sobreposições do navegador, janelas pop-up geradas por scripts ou janelas de aplicações nativas.

Quando a inspeção HTTPS encaminha uma app pelo proxy do Flowlight, este decide as regras de destino comuns e os pacotes geridos ativos com base na identidade original da app antes de abrir qualquer ligação ao servidor de origem. Nos túneis CONNECT com certificado fixado ou que nunca são decifrados, isto funciona pelo nome de anfitrião; as listas de permissões dos agentes continuam limitadas ao caminho direto pela Extensão de rede. Se o proprietário ou o nome de anfitrião for desconhecido, a ligação é permitida.

@@ -507,7 +512,10 @@

Proteções dos agentes: bloquear uma ferramenta, um servido

Isto não é uma sandbox. Uma ferramenta recusada deixa de ser oferecida e deixa de ser respondida. Um agente que ainda tenha uma shell pode fazer à mão o que a ferramenta teria feito. O Flowlight vai registá-lo; não finge impedi-lo.

-

Origens: coletor e motores

+

Origens: coletor, dispositivos e cobertura

+

Origens é uma única página operacional deslocável: primeiro o Coletor, depois Dispositivos e Cobertura como secções claramente + identificadas. Os avisos de estado, as respostas de Perguntar, as janelas restauradas e as ligações usadas nas capturas de ecrã deslocam-se até à secção relevante + e realçam-na por instantes sem ocultar as restantes.

O Flowlight regista o tráfego de uma de duas formas, escolhida em Origens › Coletor. Diferem no que podem deixar escapar, e vale a pena entender isso antes de confiar numa delas.

@@ -699,7 +707,7 @@

Perguntas e respostas

Com a janela fechada, o Flowlight quase não faz nada: mantém as taxas na barra de menus e escreve uma linha por segundo. A extensão de rede custa menos do que o amostrador de reserva, que inicia um processo nettop de vida curta a cada segundo.
Consegue bloquear ligações?
-
Sim, com a extensão de rede. A lista de permissões de um agente pode recusar e não só avisar, uma regra pode bloquear uma app, +
Sim, com a extensão de rede. A lista de permissões de um agente pode recusar e não só avisar, uma regra pode bloquear uma app, um destino ou um URL pelo tempo que quiser, e uma proteção pode retirar uma ferramenta a um agente antes de ser oferecida ao seu modelo. Nada bloqueia até o pedir, e cada recusa traz consigo a sua saída.
Preciso da extensão de rede?
@@ -754,7 +762,7 @@

Limitações

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index b220806e..01ed344d 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.17.4macOS 15+UniversalGPL-3.0Sem telemetria

@@ -127,7 +127,7 @@

Veja a atividade de rede das suas apps.
-
O mapa de atividade de IA do Flowlight. Os cartões de agentes passam por ferramentas atribuídas e gateways configurados; a rota observada do magpie para o Flowlight ramifica-se depois para serviços LLM e domínios. A largura das faixas mostra o tráfego do período.

Veja a rota de IA real: agentes, ferramentas atribuídas, gateways configurados, a etapa de proxy observada pelo Flowlight e destinos exatos. A largura mostra o total recebido e enviado no período selecionado. Com dados de demonstração.

@@ -135,7 +135,7 @@

Veja a atividade de rede das suas apps.
-
A vista Agentes de IA do Flowlight. O Claude Code, o Cursor, o Codex e um script de Python aparecem com os respetivos fornecedores de IA, o tráfego enviado para outros sítios e indicadores de risco para SMTP, SSH, FTP, um envio grande e um IP direto.

A vista Agentes de IA: o fornecedor do modelo de cada agente e tudo o mais com que falou. Mostrada com dados de demonstração.

@@ -179,11 +179,11 @@

Histórico

Para onde foi tudo? Do segundo ao ano, agrupado por app, d

Alertas

O que pareceu errado? Alertas por app, explicáveis, com o destino e o limiar.

⌘5
-

Regras

O que recusei e está a acontecer? Todas as regras, o que cada uma bloqueou e um fluxo de todas as decisões.

⌘8 · Developer ID / ⌘7 · App Store
+

Políticas

Que controlos existem e estão em vigor? Todas as políticas, o seu estado real e um histórico unificado de atividade.

⌘8 · Developer ID / ⌘7 · App Store

Perguntar

Perguntas em linguagem comum, respondidas a partir do histórico — com todas as consultas que fez listadas abaixo.

⌘6
-

Origens

Os dados estão a chegar, quanto está identificado e que canais de dispositivos estão ativos? Coletor, Dispositivos e Cobertura num só lugar.

⌘9 · Developer ID / ⌘8 · App Store
+

Origens

Os dados estão a chegar, que canais de dispositivos estão ativos e o que consegue o Flowlight contabilizar? Uma única página, do Coletor à Cobertura.

⌘9 · Developer ID / ⌘8 · App Store

Inspecionar

Opcional. O que é que o agente enviou de facto, e que chamada de ferramenta o fez? Pedidos desencriptados, chamadas de ferramentas, resultados.

⌘7 · Developer ID
@@ -405,16 +405,16 @@

Relacione as chamadas de ferramentas suportadas com os pedidos de rede

seu Mac. Inicie um agente a partir de um Terminal inspecionado e o tráfego HTTP suportado do agente e das suas ferramentas é desencriptado, com as chamadas de ferramentas que o modelo pediu e os resultados que voltaram.

-
A vista Inspecionar do Flowlight: uma lista de pedidos desencriptados do Claude Code com as respetivas chamadas de ferramentas e o detalhe do envio do curl para paste.example, feito pela chamada da ferramenta Bash que o executou, com as notas enviadas no corpo do pedido
Para onde foram as minhas notas? O envio do curl para paste.example, seguido até ao comando Bash que o modelo pediu, com o corpo que enviou.
-
O separador Ferramentas do Claude Code: Bash, Read e duas ferramentas MCP com descrições e a frequência com que o modelo chamou cada uma
Todas as ferramentas que um agente oferece ao modelo. Os agentes declaram as suas ferramentas em cada turno, por isso o Flowlight lista-as todas, com as que o modelo usou de facto e as que nunca tocou.
-
O separador Servidores MCP do Codex: um servidor sentry marcado como via fornecedor, o seu URL, a aprovação definida como nunca, as ferramentas permitidas e as ferramentas que oferece
Incluindo as que não se veem na rede. Quando um agente pede ao fornecedor que se ligue a um servidor MCP, esse tráfego nunca passa pelo seu Mac. O pedido passa, por isso o servidor, o seu URL, a sua definição de aprovação e as suas ferramentas aparecem mesmo assim.
@@ -460,7 +460,7 @@

Um monitor que não tem de monitorizar

-
O detalhe do Claude Code no Flowlight: ferramentas e servidores MCP com contagens de chamadas e uma falha, uma lista de permissões e o separador Chamadas de ferramentas com um git push falhado, um envio do curl para paste.example com a resposta e o pedido, um npm install, um problema no GitHub, uma pesquisa na documentação e uma leitura de ficheiro, cada um com o seu resultado
Um agente, todas as chamadas de ferramentas. O tráfego do git, do curl e dos servidores MCP que o Claude Code iniciou conta como tráfego do Claude Code. Com a inspeção HTTPS ativada, o separador Chamadas de ferramentas mostra o que o modelo pediu, o que voltou e os pedidos que cada ferramenta fez: @@ -470,10 +470,10 @@

Um monitor que não tem de monitorizar

-
A vista Histórico com barras horárias de dados recebidos e enviados e gráficos circulares de apps, destinos e protocolos
Histórico. Zoom do segundo ao ano, três agrupamentos, gráficos que continuam legíveis com centenas de apps.
-
A vista Alertas a listar alertas de exfiltração por um agente, de e-mail, de FTP, de atividade durante a ausência e de picos de tráfego
Alertas. Cada um nomeia a app, o destino e o número que o disparou.
@@ -589,7 +589,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index 1ff8d6db..2ab8ec22 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -125,7 +125,7 @@

Pedidos de rede que a app faz

- + @@ -189,7 +189,7 @@

Contacto

diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html index fca537eb..64933c88 100644 --- a/docs/pt-PT/threat-model/index.html +++ b/docs/pt-PT/threat-model/index.html @@ -222,7 +222,7 @@

Comunicar uma vulnerabilidade

diff --git a/docs/releases/index.html b/docs/releases/index.html index 56203ee2..b96cc987 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -61,7 +61,18 @@

What's new

-

0.17.3

Latest
+

0.17.4

Latest
+
    +
  • See every control in Policies. Rules is now a unified Policies inventory for ordinary rules, agent allowlists, managed packs, and Developer-only inspection policies, with truthful effective states rather than treating every enabled item as active.
  • +
  • Keep each policy's real editor and evidence. Selecting an item opens the contextual editor that owns it; Managed packs retain compact provenance and consent, while Activity adapts the existing authoritative records without conflating policy types.
  • +
  • Read Sources as one operational page. Collector, Devices and Coverage now flow in that order without a segmented control, while status links, Ask answers, restored windows and screenshot arguments still focus the requested section.
  • +
  • Dismiss chart details when the pointer leaves. History no longer leaves a zero-value tooltip floating at the upper-left after the cursor exits the All traffic plot; keyboard bucket inspection remains unchanged.
  • +
  • Sign each release once. The immutable tag run builds, signs, notarizes, Gatekeeper-verifies, attests and retains one artifact; publication has no signing credentials and can only verify and publish those exact prepared bytes.
  • +
+
+ +
+

0.17.3

  • Keep every input in one operational home. Capture, Devices and Coverage are now Collector, Devices and Coverage tabs inside Sources, with a shared health summary and a step-by-step recovery card when traffic is not arriving.
  • Tune alerts where their results appear. Detection controls now live beside the alert list under Alerts › Detection, while database retention, location and deletion live in Settings › Data & Privacy.
  • @@ -1592,7 +1603,7 @@

    What's new

diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index 1134aa04..c48c9711 100644 --- a/docs/threat-model/index.html +++ b/docs/threat-model/index.html @@ -231,7 +231,7 @@

Reporting a vulnerability

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index a8036237..ab6afb9f 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -152,7 +152,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index a5624183..f7abb70c 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -76,7 +76,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.17.3,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.17.4,macOS 15 或更高版本。

@@ -166,11 +166,11 @@

各个视图

- - + +
PedidoEnviado paraO que contémQuando
Procura de atualizaçõesapi.github.comUm pedido HTTPS normal pela versão mais recente; o GitHub vê o seu endereço IP e o agente de utilizador Flowlight/<version>Na abertura e uma vez por dia, a partir da 0.1.2. Interruptor para desativar em Definições › Geral
Transferência da atualizaçãogithub.comA imagem de disco e o seu ficheiro de soma de verificaçãoSó quando clica em Transferir e abrir
Atualização oficial do pacote de regrasflowlight.xinbetween.comUm pedido HTTPS padrão ao feed oficial fixo. O servidor vê o seu endereço IP, o agente do utilizador Flowlight/<version> e os validadores condicionais de cache; não recebe tráfego, definições dos pacotes nem qualquer identificador do dispositivoApós o arranque, quando for necessário, e depois, no máximo, uma vez por dia enquanto funcionar normalmente. Ativa por predefinição; opção para desativar e Procurar agora manualmente em Regras › Pacotes
Atualização oficial do pacote de regrasflowlight.xinbetween.comUm pedido HTTPS padrão ao feed oficial fixo. O servidor vê o seu endereço IP, o agente do utilizador Flowlight/<version> e os validadores condicionais de cache; não recebe tráfego, definições dos pacotes nem qualquer identificador do dispositivoApós o arranque, quando for necessário, e depois, no máximo, uma vez por dia enquanto funcionar normalmente. Ativa por predefinição; opção para desativar e Procurar agora manualmente em Políticas › Pacotes geridos
DNS inversoO resolvedor de DNS que tem configuradoUma consulta do nome registado de um endereço IP, quando não foi visto nenhum nome de anfitriãoConforme necessário, no modo de captura por amostrador, que é a predefinição até a extensão de rede estar a correr. Sem interruptor para desligar: sem isto, o amostrador não consegue dar nome à maioria das ligações. A extensão de rede dá-lhes nome a partir do próprio tráfego e não faz esta consulta
Consulta do proprietário da redeTeam Cymru, por DNSEndereços IP públicos sem nome de anfitrião conhecido. Os endereços privados e reservados nunca saem do seu MacAtivada por predefinição. Interruptor para desativar em Captura › Identificar os proprietários da rede
Perguntar com um modelo alojadoO fornecedor que escolher: Anthropic, OpenAI, Google Gemini ou um endpoint compatívelA sua pergunta e os resultados devolvidos pelas consultas de leitura do Flowlight escolhidas pelo modelo. A base de dados de tráfego em si não é enviadaSó quando escolhe um fornecedor alojado e submete uma pergunta. O ecrã Perguntar mostra os pedidos transmitidos
警报⌘5所有看起来不寻常的事,带上应用、目标和触发规则的那个数字。
提问⌘6用平常的话问你自己的流量,由本地模型或托管模型从记录下来的历史里作答。
检查⌘7 · Developer ID可选:你交给 Flowlight 转发的那些代理的解密请求,以及它们背后的工具调用。搜索能深入请求和响应正文。
规则⌘8 · Developer ID
⌘7 · App Store
你设过的每一条允许列表和拒绝,各自管到哪里,以及实际拦下了什么。
来源⌘9 · Developer ID
⌘8 · App Store
在三个标签页中显示收集器状态、设备通道和命名覆盖范围。
策略⌘8 · Developer ID
⌘7 · App Store
每项已配置的策略、是否真正生效,以及统一的只读活动记录。
来源⌘9 · Developer ID
⌘8 · App Store
收集器状态与恢复、可选设备通道和覆盖范围,集中在一张可滚动的运维页面中。
-

快捷键遵循可见的侧边栏顺序。Developer ID 版本在 ⌘7 包含检查;App Store 版本省略检查,因此规则和来源会移到 ⌘7 和 ⌘8。

-
快捷键遵循可见的侧边栏顺序。Developer ID 版本在 ⌘7 包含检查;App Store 版本省略检查,因此策略和来源会移到 ⌘7 和 ⌘8。

+
实时视图:一列应用,各自带着当前的发送与接收速率,以及最忙的目标。
实时,Flowlight 打开时进入的那个视图。

在历史里,把指针停在柱子上,就会打开一张跟随指针、可滚动的卡片,列出贡献流量的应用;点击柱子不会放大。双击任意一行可把历史收窄到那一项。

@@ -208,7 +208,7 @@

工具与 MCP 服务器

每个域名都标明是从这四种里的哪一种知道的。带着好几个标签的域名,是比只带一个标签更强的事实:配置文件里写了而且确实 连过,比其中任何一项单独说得更多;只被配置过的域名从未被访问过,而只被观察到的域名从来没有写在任何你本可以读到的地方。

每个域名给出两种拒绝:对这个代理,覆盖该代理以及它启动的工具;或者在所有地方,覆盖这台 Mac 上的 - 每一个应用。两者写下的都是普通规则,所以违规流水会报告它们,规则编辑器会解释它们——而且正因为它们是普通规则, + 每一个应用。两者写下的都是普通规则,所以活动会报告它们,规则编辑器会解释它们——而且正因为它们是普通规则, 编辑器里的模拟会在你把它打开之前告诉你,它本会拒绝掉什么。

有一类域名没有按钮。模型提供方自己去连的连接器——在请求正文里声明,从提供方那一侧连接——根本不会碰到这台 Mac,所以这里的 任何规则都够不到它。那一行会把这件事说出来,而不是递给你一个悄悄什么都不做的拒绝。如果同一个主机在本地也被配置过,或者 @@ -373,16 +373,16 @@

模拟响应

连接:模拟改变的是答案,而不是连接能否建立,所以“这个 API 返回 500”行得通,而“这个 API 完全不可达”不行。

-

规则

-

允许列表是某一个代理关于它可以去哪里的说法。规则是它的通用版本:它指明一个 +

策略

+

策略集中列出 Flowlight 能执行的所有管控:普通的允许或阻止规则、代理允许列表、托管包,以及在支持 HTTPS 检查时的护栏、模拟响应、请求改写和响应转换。每一行都会区分配置状态与实际效果:生效、仅报告、监视、等待、已排期、暂停、已用完、正在应用、需要检查、不可用、不兼容或失败。选择一行会打开负责该策略的对应编辑器;“策略”不会取代或混淆这些功能各自独立的授权和持久化方式。

+

允许列表是某一个代理关于它可以去哪里的说法。普通规则是它的通用版本:它指明一个 对象——应用、代理、目标、URL,或其中两者的组合——并决定阻止 还是允许。在实时、历史、AI 代理或检查里右键任意一行,就能针对你正看着的东西写一条; - 它们全都落在规则界面上的同一个编辑器里,因为一条在看不见的地方生效的规则, - 会是一条你再也找不到的规则。

+ 每条规则都能在“策略”中找到,并会在其权威编辑器中打开。

模板。共 22 个,分为五组——阻止代理访问软件包仓库或其他 模型提供方;封住数据外流的常见途径(粘贴站点、webhook 接收器、匿名文件投放、隧道、DNS over HTTPS);产品分析与崩溃上报;工作时段策略;以及一次性的“先把这个拦一小时,我弄清它 - 是什么”。可以从规则工具栏浏览它们,也可以在实时、历史、AI 代理或检查的任意一行上右键, + 是什么”。可以从策略工具栏浏览它们,也可以在实时、历史、AI 代理或检查的任意一行上右键, 把某个模板应用到你正看着的东西上。

每个模板在动手之前都会说明它要做什么。凡是会写入多条规则的,都会打开一张卡片,列出每个 主机和时段;凡是永久生效的,都会打开预填好的编辑器;而只有会自行撤销的规则——一 @@ -403,8 +403,7 @@

规则

地址和端口,对路径一无所知,而且只在扩展模式下有效。HTTPS 检查拒绝的是单个请求,所以它能匹配 URL,并用代理读得懂的状态码作答,而不是一个不声不响就断掉的套接字。nettop 采样器 完全无法拒绝任何东西。当前配置无法执行的规则会在列表里明说,而不是摆在那里装作已经生效。

-

第二个标签页是违规流水:每一条由规则判定的连接,带上应用、它要去哪里、是哪条 - 规则判定的,以及那究竟是一次拒绝,还是一次放它通过的例外。

+

第二个视图是只读的活动:普通规则判定、允许列表违规、托管包事件,以及各类检查策略的效果,都按时间排列在一处。它会适配现有的权威记录,而不是把历史改写成一种笼统的事件模型;App Store 版本会省略依赖检查的策略类型及其保留的活动。

它本来会做什么。阻止的规则写起来容易,打开却让人害怕,因为你通常最先了解到的是它 弄坏了什么——而且是后来才知道的,从别处某个东西失败上知道的。规则编辑器先回答这个问题:它拿记录 @@ -422,7 +421,7 @@

规则

的最早的流量:一条只对照了两小时历史的规则,就是只对照了两小时历史。

托管广告和跟踪器包

-

规则 › 包包含三个官方主机名列表:广告、跨站跟踪和隐私与遥测。它们从一开始就可见,并且默认均为关闭状态。阻止功能需要网络扩展;nettop 采样器只能观察。

+

策略 › 托管包包含三个官方主机名列表:广告、跨站跟踪和隐私与遥测。它们从一开始就可见,并且默认均为关闭状态。阻止功能需要网络扩展;nettop 采样器只能观察。

仅限官方签名更新。 Flowlight 会检查一个固定的 HTTPS 源,以查找由发布者签名的新版目录。自动检查默认开启,可在此处关闭。签名可验证内容,但不会启用内容:新包会保持关闭;如果目标发生变化,已启用的包将停止执行,直到你审查确切的新版本并按下保存。服务中断期间,已接受的内容可能会继续作为最后已知正常版本保留,并明确显示为已过期。这些是主机名级别的网络阻止,无法移除 DOM 或布局中的空白、浏览器叠加层、脚本生成的弹出窗口或原生应用窗口。

当 HTTPS 检查将应用经由 Flowlight 的代理转发时,普通目标规则和已启用的托管包会在打开任何源站连接之前,使用原应用的身份作出判定。对于固定了证书或始终不解密的 CONNECT 隧道,这一判定按主机名进行;代理允许列表仍只适用于直接经过网络扩展的路径。如果所有者或主机名未知,则会允许连接。

@@ -471,7 +470,8 @@

代理护栏:阻止一个工具、一个服务器或一个

这不是沙盒。被拒绝的工具不再提供给模型,也不再被响应。而仍然拥有 shell 的代理,可以手动做到那个工具本来要做的事。Flowlight 会把它记录下来;它不会假装能阻止。

-

来源:收集器与引擎

+

来源:收集器、设备与覆盖范围

+

来源是一张可滚动的运维页面:先是收集器,接着是标题清晰的设备与覆盖范围分区。状态通知、提问答案、恢复后的窗口和截图链接都会滚动到相关分区并短暂突出显示,同时不隐藏其他内容。

Flowlight 用两种方式之一记录流量,在来源 › 收集器里选择。它们会漏掉的东西不同,在依赖任何 一种之前,值得先了解清楚。

@@ -657,7 +657,7 @@

问题与解答

窗口关闭时 Flowlight 几乎什么都不做:它保留菜单栏速率,每秒写入一行。网络扩展 比作为后备的采样器更省,后者每秒都要启动一个短命的 nettop 进程。
它能阻止连接吗?
-
能,配合网络扩展。代理的允许列表除了警告也可以拒绝,一条规则可以按你指定的时长阻止一个应用、 +
能,配合网络扩展。代理的允许列表除了警告也可以拒绝,一条规则可以按你指定的时长阻止一个应用、 一个目标或一个 URL,而一条护栏可以在模型看到某个工具之前,把它从代理手里 拿走。在你要求之前不会阻止任何东西,而且每次拒绝都带着它的出口。
我需要网络扩展吗?
@@ -712,7 +712,7 @@

限制

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index ec6515b9..ce38f5a3 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+通用架构GPL-3.0无遥测

+

v0.17.4macOS 15+通用架构GPL-3.0无遥测

@@ -127,7 +127,7 @@

看清应用的网络活动。
-
Flowlight 的 AI 活动图。代理卡片经过已归属工具和已配置网关;观测到的 magpie 到 Flowlight 路由随后分支至 LLM 服务和域名。带状线宽度表示所选时段的流量。

查看真实 AI 路由:代理、已归属工具、已配置网关、Flowlight 观测到的代理阶段和精确目标。带状线宽度表示所选时段接收与发送流量的总和。图中为演示数据。

@@ -135,7 +135,7 @@

看清应用的网络活动。
-
Flowlight 的 AI 代理视图。Claude Code、Cursor、Codex 和一个 Python 脚本逐行列出,显示各自的 AI 提供方、发往别处的流量,以及 SMTP、SSH、FTP、大量上传和裸 IP 的风险标记。

AI 代理视图:每个代理的模型提供方,以及它联系过的其他一切。图中为演示数据。

@@ -179,11 +179,11 @@

历史

流量究竟去了哪?从秒到年,按应用、目标或 I

警报

哪里不对?逐应用、讲得清道理的警报,带上目标和阈值。

⌘5
-

规则

我拒绝过什么,真的生效了吗?每条规则、各自阻止了什么,以及每次判定的流水。

⌘8 · Developer ID / ⌘7 · App Store
+

策略

有哪些管控,它们是否生效?每项策略、实际状态,以及统一的活动记录。

⌘8 · Developer ID / ⌘7 · App Store

提问

用日常语言提问,从历史数据里作答——下方列出它执行过的每一条查询。

⌘6
-

来源

数据是否正在到达、多少已有名称、哪些设备通道处于活动状态?把收集器、设备和覆盖范围放在一处。

⌘9 · Developer ID / ⌘8 · App Store
+

来源

数据是否正在到达、哪些设备通道处于活动状态、Flowlight 能解释多少?一张页面从收集器一直看到覆盖范围。

⌘9 · Developer ID / ⌘8 · App Store

检查

可选。代理究竟发出了什么,是哪次工具调用引起的?解密后的请求、工具调用和结果。

⌘7 · Developer ID
@@ -402,16 +402,16 @@

把受支持的工具调用与网络请求对上

默认关闭。打开后,Flowlight 会成为一个本地代理,使用在你 Mac 上生成的专属证书颁发机构。 从受检查的终端启动代理,代理及其工具的受支持 HTTP 流量就会被解密,并附上模型请求的工具调用和返回的结果。

-
Flowlight 的检查视图:一列已解密的 Claude Code 请求及其工具调用,以及 curl 向 paste.example 上传的详情——由运行它的那次 Bash 工具调用发起,请求体里正是上传的笔记
我的笔记去哪了?curl 向 paste.example 的上传,一路追到模型要求执行的那条 Bash 命令, 连它发出的请求体一起。
-
Claude Code 的工具标签页:Bash、Read 和两个 MCP 工具,附带说明以及模型各自调用了多少次
代理提供给模型的每一个工具。代理每一轮都会声明自己的工具,所以 Flowlight 把它们全部列出, 并区分模型真正用过的和从未碰过的。
-
Codex 的 MCP 服务器标签页:一个标为“经由提供方”的 sentry 服务器,以及它的 URL、设为“从不”的审批、允许的工具和它提供的工具
连线路上看不到的那些也算。当代理请求提供方去连接某个 MCP 服务器时,那段流量根本不会经过你的 Mac。 但它的请求会,所以这个服务器、它的 URL、它的审批设置和它的工具依然会出现。
@@ -457,7 +457,7 @@

一个不用你盯着的监控工具

-
Flowlight 里 Claude Code 的详情:带调用次数和一次失败的工具与 MCP 服务器、一个允许列表,以及工具调用标签页,里面列出一次失败的 git push、一次 curl 向 paste.example 的上传及其响应和请求、npm install、一个 GitHub issue、一次文档搜索和一次文件读取,每项都带结果
一个代理,每一次工具调用。Claude Code 启动的 git、curl 和 MCP 服务器产生的流量都算作 Claude Code 的。 打开 HTTPS 检查后,工具调用标签页会显示模型要求了什么、返回了什么,以及每个工具发出的请求: @@ -467,10 +467,10 @@

一个不用你盯着的监控工具

-
历史视图,含每小时接收与发送的柱状图,以及应用、目标和协议的环形图
历史。从秒到年的缩放、三种分组方式,几百个应用也读得清的图表。
-
警报视图,列出代理外泄、邮件、FTP、你离开时活动和流量突增等警报
警报。每一条都点明应用、目标和触发它的那个数字。
@@ -586,7 +586,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 6b827754..0e3d52a8 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -124,7 +124,7 @@

应用发出的网络请求

- + @@ -187,7 +187,7 @@

联系方式

diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html index f7567b77..28b457fd 100644 --- a/docs/zh-Hans/threat-model/index.html +++ b/docs/zh-Hans/threat-model/index.html @@ -210,7 +210,7 @@

报告漏洞

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index cb2ede04..9923385b 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -152,7 +152,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index f0aa9829..49855297 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -76,7 +76,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.17.3,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.17.4,macOS 15 或以上版本。

@@ -169,11 +169,11 @@

各個畫面

- - + +
请求发往包含什么什么时候
更新检查api.github.com一个查询最新版本的标准 HTTPS 请求;GitHub 会看到你的 IP 地址和 Flowlight/<version> 这个 User-Agent 标识启动时以及每天一次,自 0.1.2 起。关闭开关在设置 › 通用
更新下载github.com磁盘映像及其校验和文件只在你点击“下载并打开”时
官方规则包更新flowlight.xinbetween.com向固定官方源发送的标准 HTTPS 请求。服务器会看到你的 IP 地址、Flowlight/<version>用户代理和缓存条件验证器;不会收到任何流量、包设置或设备标识符启动后在检查到期时执行,此后在运行正常的情况下最多每天执行一次。默认开启;可在规则 › 包中关闭或手动选择立即检查
官方规则包更新flowlight.xinbetween.com向固定官方源发送的标准 HTTPS 请求。服务器会看到你的 IP 地址、Flowlight/<version>用户代理和缓存条件验证器;不会收到任何流量、包设置或设备标识符启动后在检查到期时执行,此后在运行正常的情况下最多每天执行一次。默认开启;可在策略 › 托管包中关闭或手动选择立即检查
反向 DNS你配置的 DNS 解析器在没有看到主机名时,查询某个 IP 地址登记的名称按需进行,在采样器捕获模式下,也就是网络扩展开始运行之前的默认模式。没有关闭开关:没有它,采样器就没法给大多数连接命名。网络扩展从流量本身给它们命名,因此不做这件事
网络所有者查询Team Cymru,通过 DNS没有已知主机名的公网 IP 地址。私有地址和保留地址绝不离开你的 Mac默认开启。关闭开关在捕获 › 识别网络所有者
用托管模型提问你选择的提供方:Anthropic、OpenAI、Google Gemini 或兼容的端点你的问题,以及模型选中的那些只读 Flowlight 查询所返回的结果。流量数据库本身不会被上传只在你选择了托管提供方并提交问题时。提问界面会显示发送出去的请求
警示⌘5所有看起來不尋常的事,附上 App、目標,以及觸發規則的那個數字。
提問⌘6用平常的話問你自己的流量,由本機模型或託管模型從記錄下來的歷史裡作答。
檢查⌘7 · Developer ID選用:你轉送經過 Flowlight 的代理,其解密後的請求,以及背後的工具呼叫。搜尋也會查進請求與回應的內文。
規則⌘8 · Developer ID
⌘7 · App Store
你設過的每一條允許清單和拒絕,各自管到哪裡,以及實際攔下了什麼。
來源⌘9 · Developer ID
⌘8 · App Store
在三個分頁中顯示收集器狀態、裝置頻道與命名涵蓋範圍。
政策⌘8 · Developer ID
⌘7 · App Store
每項已設定的政策、是否真的生效,以及整合後的唯讀活動記錄。
來源⌘9 · Developer ID
⌘8 · App Store
收集器狀態與復原、選用的裝置通道和涵蓋範圍,集中在一張可捲動的操作頁面中。
-

快速鍵遵循可見的側邊欄順序。Developer ID 版本在 ⌘7 包含「檢查」;App Store 版本省略它,因此「規則」與「來源」會移到 ⌘7 和 ⌘8。

-
快速鍵遵循可見的側邊欄順序。Developer ID 版本在 ⌘7 包含「檢查」;App Store 版本省略它,因此「政策」與「來源」會移到 ⌘7 和 ⌘8。

+
即時畫面:一列 App,各自帶著目前的傳送與接收速率,以及最忙的目標。
即時,Flowlight 開啟時的那個畫面。

在「歷史」中,把指標移到長條上,就會開啟一張跟隨指標、可捲動的卡片,列出貢獻流量的 App;點選長條不會放大。任一列雙按可把歷史縮小到那一列。

@@ -211,7 +211,7 @@

工具與 MCP 伺服器

一組,每個網域都標明是從這四種裡的哪一種知道的。帶著好幾個標籤的網域,是比只帶一個標籤更強的事實:設定檔裡寫了而且確實連過, 比其中任何一項單獨說得更多;只被設定過的網域從來沒有被連上,而只被觀察到的網域從來沒有寫在任何你本來可以讀到的地方。

每個網域提供兩種拒絕:「對這個代理」,涵蓋該代理以及它啟動的工具;或是「在所有地方」,涵蓋這台 Mac 上的 - 每一個 App。兩者寫下的都是普通規則,所以「違規紀錄」會報告它們,規則編輯器會解釋它們 — 而且正因為它們是普通規則, + 每一個 App。兩者寫下的都是普通規則,所以「活動」會報告它們,規則編輯器會解釋它們 — 而且正因為它們是普通規則, 編輯器裡的模擬會在你把它打開之前告訴你,它本來會拒絕掉什麼。

有一類網域沒有按鈕。模型供應商自己去連的連接器 — 在請求內文裡聲明,從供應商那一側連過去 — 根本不會碰到這台 Mac,所以這裡的 任何規則都碰不到它。那一列會把這件事說出來,而不是遞給你一個默默什麼都不做的拒絕。如果同一個主機在本機也有設定,或者 @@ -378,16 +378,16 @@

模擬回應

會被連線:模擬改變的是答案,而不是連線能不能建立,所以「API 回傳 500」做得到,「API 完全連不上」則做不到。

-

規則

-

允許清單講的是某一個代理可以去哪裡。規則是它的通用版本:它會指名一個 +

政策

+

政策集中列出 Flowlight 能套用的所有管控:一般的允許或封鎖規則、代理允許清單、受管理套件,以及在支援 HTTPS 檢查時的防護機制、模擬回應、請求改寫和回應轉換。每一列都會區分設定狀態與實際效果:生效、僅回報、監看、等待、已排程、暫停、已用完、正在套用、需要檢閱、無法使用、不相容或失敗。選取一列會開啟負責該政策的對應編輯器;「政策」不會取代或混淆這些功能各自獨立的授權與持久化方式。

+

允許清單講的是某一個代理可以去哪裡。一般規則是它的通用版本:它會指名一個 對象 — 一個 App、一個代理、一個目標、一個 URL,或其中兩者的組合 — 然後說封鎖 或允許。在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵,就能針對你正在看的東西寫一條; - 每一條都會進到「規則」畫面的同一個編輯器,因為在你看不到的地方生效的規則, - 會是一條你再也找不到的規則。

+ 每條規則都能在「政策」中找到,並會在其權威編輯器中開啟。

範本。共二十二個,分成五組 — 阻止代理連到套件登錄或其他 模型供應商、關上資料外流的常見途徑(貼文網站、webhook 接收器、匿名檔案上傳、通道、DNS over HTTPS)、產品分析與當掉回報、工作時段政策,以及一次性的「先封鎖一小時,讓我弄清楚 - 這是什麼」。你可以從「規則」工具列瀏覽它們,或在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵, + 這是什麼」。你可以從「政策」工具列瀏覽它們,或在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵, 把範本套用到你正在看的東西上。

每個範本都會在動手之前先說明它要做什麼。會寫入多條規則的,會開一張卡片列出每一台 主機和時程;任何永久性的,會開啟已預先填好的編輯器;而只有會自己失效的規則 — @@ -408,8 +408,7 @@

規則

位址和連接埠,但完全不知道路徑,而且只在延伸功能模式下有效。HTTPS 檢查拒絕的是單一請求,所以它能比對 URL,並回覆一個代理讀得懂的狀態,而不是一個莫名死掉的 socket。nettop 取樣器完全無法 拒絕任何東西。目前的設定執行不了的規則,會在列表中說明這一點,而不是擺在那裡看起來像已生效。

-

第二個標籤頁是違規紀錄:每一條由規則決定的連線,包含 App、它要去哪裡、是哪一條 - 規則決定的,以及那是一次拒絕,還是一個放行的例外。

+

第二個畫面是唯讀的活動:一般規則判定、允許清單違規、受管理套件事件,以及各類檢查政策的效果,都依時間排列在一起。它會轉接現有的權威記錄,而不是把歷史改寫成一種籠統的事件模型;App Store 版本會省略依賴檢查的政策類型及其保留的活動。

它本來會做什麼。封鎖的規則寫起來容易,打開卻令人害怕,因為你通常最先知道的是它 弄壞了什麼 — 而且是後來才知道,從別處某個東西失敗上知道的。規則編輯器先回答這個問題:它拿記錄 @@ -427,7 +426,7 @@

規則

它能看到的最早的流量:一條只對照了兩小時歷史的規則,就是只對照了兩小時歷史。

受管理的廣告與追蹤器套件

-

規則 › 套件包含三個官方主機名稱清單:廣告、跨網站追蹤和隱私權與遙測。它們從一開始便可見,且預設全部關閉。封鎖功能需要網路延伸功能;nettop 取樣器只能觀察。

+

政策 › 受管理套件包含三個官方主機名稱清單:廣告、跨網站追蹤和隱私權與遙測。它們從一開始便可見,且預設全部關閉。封鎖功能需要網路延伸功能;nettop 取樣器只能觀察。

僅限官方簽署的更新。 Flowlight 會檢查一個固定的 HTTPS 來源,以尋找由發佈者簽署的新版目錄。自動檢查預設開啟,可在此處關閉。簽章可驗證內容,但不會啟用內容:新套件會保持關閉;如果目的地有所變更,已啟用的套件將停止執行,直到你檢閱確切的新版本並按下儲存。服務中斷期間,已接受的內容可能會繼續保留為已知正常的最後版本,並明確顯示為已過時。這些是主機名稱層級的網路封鎖,無法移除 DOM 或版面配置中的空白、瀏覽器覆疊層、由指令碼產生的彈出式視窗或原生應用程式視窗。

當 HTTPS 檢查將 App 轉送經過 Flowlight 的代理伺服器時,一般目的地規則和已啟用的受管理套件會在開啟任何來源伺服器連線之前,使用原 App 的身分作出判定。對於使用憑證綁定或始終不解密的 CONNECT 通道,這項判定會按主機名稱進行;代理的允許清單仍只適用於直接經過網路延伸功能的路徑。如果擁有者或主機名稱未知,則會允許連線。

@@ -476,7 +475,8 @@

代理防護機制:封鎖一個工具、一個伺服器或

這不是沙箱。被拒絕的工具不會再被提供,也不會再得到回應。但仍然有 shell 的代理,可以自己動手做那個工具原本要做的事。Flowlight 會把那件事記錄下來;它不會假裝自己能阻止。

-

來源:收集器與引擎

+

來源:收集器、裝置與涵蓋範圍

+

來源是一張可捲動的操作頁面:先是收集器,接著是標題清楚的裝置與涵蓋範圍區段。狀態通知、提問答案、還原後的視窗和螢幕截圖連結都會捲動到相關區段並短暫標示,同時不隱藏其他內容。

Flowlight 用兩種方式之一記錄流量,在「來源 › 收集器」中選擇。兩者會漏掉的東西不同,在你倚賴 任何一種之前,值得先了解。

@@ -662,7 +662,7 @@

常見問答

視窗關著時,Flowlight 幾乎什麼都不做:只維持選單列上的速率,並每秒寫入一列資料。網路延伸功能 比備用的取樣器更省,後者每秒都會啟動一個短命的 nettop 程序。
它能封鎖連線嗎?
-
可以,搭配網路延伸功能。代理的允許清單除了警示也能拒絕,規則可以封鎖一個 App、 +
可以,搭配網路延伸功能。代理的允許清單除了警示也能拒絕,規則可以封鎖一個 App、 一個目標或一個 URL,時間長短由你決定,而防護機制可以在工具送到模型面前之前, 先把它從代理手上拿走。在你要求之前不會封鎖任何東西,而每一次拒絕都帶著它的退路。
我一定要用網路延伸功能嗎?
@@ -717,7 +717,7 @@

限制

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index 3390adf6..f1e515f0 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -48,7 +48,7 @@ - + @@ -97,7 +97,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.17.3macOS 15+通用架構GPL-3.0無遙測

+

v0.17.4macOS 15+通用架構GPL-3.0無遙測

@@ -127,7 +127,7 @@

看見每個 App 的網路活動。
-
Flowlight 的 AI 活動圖。代理卡片經過已歸屬工具和已設定閘道;觀測到的 magpie 至 Flowlight 路由隨後分支至 LLM 服務和網域。帶狀線寬度表示所選時段的流量。

查看真實 AI 路由:代理、已歸屬工具、已設定閘道、Flowlight 觀測到的代理階段和精確目的地。帶狀線寬度表示所選時段接收與傳送流量的總和。圖中為示範資料。

@@ -135,7 +135,7 @@

看見每個 App 的網路活動。
-
Flowlight 的「AI 代理」畫面。Claude Code、Cursor、Codex 和一個 Python 指令碼列在其中,顯示各自的 AI 供應商、傳往其他地方的流量,以及 SMTP、SSH、FTP、大量上傳和裸 IP 的風險標記。

「AI 代理」畫面:每個代理的模型供應商,以及它連線過的其他所有目標。圖中為示範資料。

@@ -179,11 +179,11 @@

歷史

流量都到哪去了?從秒到年,依 App、目標或 IP

警示

哪裡看起來不對?每個 App 都有說得清楚的警示,附上目標和閾值。

⌘5
-

規則

我拒絕了什麼,真的有在擋嗎?每一條規則、各自封鎖過什麼,以及每一次決定的紀錄。

⌘8 · Developer ID / ⌘7 · App Store
+

政策

有哪些管控,又是否真的生效?每項政策、實際狀態,以及整合後的活動記錄。

⌘8 · Developer ID / ⌘7 · App Store

提問

用日常語言提問,從歷史記錄裡得到答案 — 下方還列出它跑過的每一條查詢。

⌘6
-

來源

資料是否正在到達、有多少已有名稱、哪些裝置通道正在使用?把收集器、裝置和涵蓋範圍放在同一處。

⌘9 · Developer ID / ⌘8 · App Store
+

來源

資料是否正在到達、哪些裝置通道正在使用、Flowlight 能交代多少?一張頁面從收集器一路看到涵蓋範圍。

⌘9 · Developer ID / ⌘8 · App Store

檢查

選用功能。代理實際送出了什麼,又是哪一次工具呼叫造成的?解密後的請求、工具呼叫、結果。

⌘7 · Developer ID
@@ -403,16 +403,16 @@

把支援的工具呼叫和網路請求對起來

單位。從受檢查的「終端機」啟動代理,它和它的工具所產生的支援 HTTP 流量就會被解密,連同模型要求的 工具呼叫和回傳的結果。

-
Flowlight 的「檢查」畫面:一份解密後的 Claude Code 請求列表及其工具呼叫,以及 curl 上傳到 paste.example 的詳細內容 — 那次上傳由執行它的 Bash 工具呼叫發出,請求內文中還有上傳的筆記
我的筆記去哪了? curl 上傳到 paste.example,追溯到模型要求執行的 Bash 指令,連同它 送出的內文。
-
Claude Code 的「工具」標籤頁:Bash、Read 和兩個 MCP 工具,附上說明以及模型各呼叫了幾次
代理提供給模型的每一個工具。 代理每一輪都會宣告自己的工具,所以 Flowlight 會全部列出來,包含模型 真正用過的,以及完全沒碰過的。
-
Codex 的「MCP 伺服器」標籤頁:一個標示為「透過供應商」的 sentry 伺服器,包含它的 URL、設為永不核准的核准設定、允許的工具,以及它提供的工具
連你在線路上看不到的也算。 當代理請供應商去連接某個 MCP 伺服器,那段流量 根本不會經過你的 Mac。但代理的請求會,所以那個伺服器、它的 URL、它的核准設定和它的工具仍然會出現。
@@ -458,7 +458,7 @@

一個不用你盯著的監控工具

-
Flowlight 中 Claude Code 的詳細資料:工具與 MCP 伺服器及其呼叫次數和一次失敗、一份允許清單,以及「工具呼叫」標籤頁 — 其中列出一次失敗的 git push、一次 curl 上傳到 paste.example 及其回應與請求、npm install、一個 GitHub issue、一次說明文件搜尋和一次檔案讀取,每一項都附上結果
一個代理,每一次工具呼叫。 Claude Code 啟動的 git、curl 和 MCP 伺服器所產生的流量,都算成 Claude Code 的。 開啟 HTTPS 檢查後,工具呼叫標籤頁會顯示模型要求了什麼、回來了什麼,以及各個工具發出的請求: @@ -468,10 +468,10 @@

一個不用你盯著的監控工具

-
「歷史」畫面:每小時接收與傳送的長條圖,以及 App、目標和協定的環圈圖
歷史。 從秒到年的縮放、三種分組方式,即使有上百個 App,圖表依然看得清楚。
-
「警示」畫面:列出代理資料外洩、郵件、FTP、離開期間活動和流量尖峰等警示
警示。 每一則都指出 App、目標,以及觸發它的那個數字。
@@ -587,7 +587,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index 31220b1a..6d08f6f8 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -124,7 +124,7 @@

這個 App 發出的網路請求

- + @@ -187,7 +187,7 @@

聯絡

diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html index ad0221bb..458b3208 100644 --- a/docs/zh-Hant/threat-model/index.html +++ b/docs/zh-Hant/threat-model/index.html @@ -211,7 +211,7 @@

回報漏洞

diff --git a/packaging/app-store/screenshots/README.md b/packaging/app-store/screenshots/README.md index 35453cb8..97b553f0 100644 --- a/packaging/app-store/screenshots/README.md +++ b/packaging/app-store/screenshots/README.md @@ -17,4 +17,4 @@ The capture gate requires: - every retained image to be exactly 2880×1800 pixels; - English and light appearance for a consistent US storefront set. -Upload the numbered files in `macos/` in filename order. The Store set uses Rules in place of Inspect because the Store product deliberately omits HTTPS inspection. Replace the complete existing App Store Connect screenshot set together; do not mix images from different builds or menu vocabularies. +Upload the numbered files in `macos/` in filename order. The Store set uses Policies in place of Inspect because the Store product deliberately omits HTTPS inspection. Replace the complete existing App Store Connect screenshot set together; do not mix images from different builds or menu vocabularies. diff --git a/packaging/app-store/screenshots/macos/01-connections.png b/packaging/app-store/screenshots/macos/01-connections.png index 0423ff5a..084f46a7 100644 Binary files a/packaging/app-store/screenshots/macos/01-connections.png and b/packaging/app-store/screenshots/macos/01-connections.png differ diff --git a/packaging/app-store/screenshots/macos/02-ai-agents.png b/packaging/app-store/screenshots/macos/02-ai-agents.png index ffb86e40..426d510a 100644 Binary files a/packaging/app-store/screenshots/macos/02-ai-agents.png and b/packaging/app-store/screenshots/macos/02-ai-agents.png differ diff --git a/packaging/app-store/screenshots/macos/03-history.png b/packaging/app-store/screenshots/macos/03-history.png index d0b052eb..720dde82 100644 Binary files a/packaging/app-store/screenshots/macos/03-history.png and b/packaging/app-store/screenshots/macos/03-history.png differ diff --git a/packaging/app-store/screenshots/macos/04-rules.png b/packaging/app-store/screenshots/macos/04-rules.png index 287cb904..abd857cd 100644 Binary files a/packaging/app-store/screenshots/macos/04-rules.png and b/packaging/app-store/screenshots/macos/04-rules.png differ diff --git a/packaging/app-store/screenshots/macos/05-live.png b/packaging/app-store/screenshots/macos/05-live.png index c7f1f9c6..cce3a16a 100644 Binary files a/packaging/app-store/screenshots/macos/05-live.png and b/packaging/app-store/screenshots/macos/05-live.png differ diff --git a/packaging/app-store/screenshots/macos/06-alerts.png b/packaging/app-store/screenshots/macos/06-alerts.png index f3340732..24ab5e7a 100644 Binary files a/packaging/app-store/screenshots/macos/06-alerts.png and b/packaging/app-store/screenshots/macos/06-alerts.png differ diff --git a/packaging/app-store/screenshots/macos/07-agent-detail.png b/packaging/app-store/screenshots/macos/07-agent-detail.png index d2cf5093..0cd53ecd 100644 Binary files a/packaging/app-store/screenshots/macos/07-agent-detail.png and b/packaging/app-store/screenshots/macos/07-agent-detail.png differ diff --git a/packaging/app-store/screenshots/macos/08-agent-tools.png b/packaging/app-store/screenshots/macos/08-agent-tools.png index 3f29fdad..2d756ad1 100644 Binary files a/packaging/app-store/screenshots/macos/08-agent-tools.png and b/packaging/app-store/screenshots/macos/08-agent-tools.png differ diff --git a/packaging/app-store/screenshots/macos/09-mcp-servers.png b/packaging/app-store/screenshots/macos/09-mcp-servers.png index 7be9cd69..e7725681 100644 Binary files a/packaging/app-store/screenshots/macos/09-mcp-servers.png and b/packaging/app-store/screenshots/macos/09-mcp-servers.png differ diff --git a/packaging/app-store/screenshots/macos/manifest.json b/packaging/app-store/screenshots/macos/manifest.json index 43cfdf63..5c253008 100644 --- a/packaging/app-store/screenshots/macos/manifest.json +++ b/packaging/app-store/screenshots/macos/manifest.json @@ -3,56 +3,56 @@ "screenshots": { "01-connections.png": { "height": 1800, - "sha256": "a395b95c0672a0a6c65348132f2c644b7f6452cb359848c9080f59f9bdaba3f1", - "version": "0.17.3", + "sha256": "53c91bfbcc17d1af55a118a200dd735228d7a01d0e32bdf7f8209cd836997799", + "version": "0.17.4", "width": 2880 }, "02-ai-agents.png": { "height": 1800, - "sha256": "df042863dadbd8230774cdd6d6fa806e14be5e3fe2476dfa6f1fe2ea16af60e9", - "version": "0.17.3", + "sha256": "0057280406d7a80a6e28bb333f260feb8cdec4870b7c7de446e59d8847c9c377", + "version": "0.17.4", "width": 2880 }, "03-history.png": { "height": 1800, - "sha256": "1d8786f1e00766c5ec8cdbeabb51b72d25dcf76363170cbd7e88f8d91c0d4250", - "version": "0.17.3", + "sha256": "aecf4e2d9d803767d555f3631d70fc1c6f3f8526e34ec2d71a1cc82a6556d320", + "version": "0.17.4", "width": 2880 }, "04-rules.png": { "height": 1800, - "sha256": "c6702955fb72c6f5953f3fb102c0f184199f7418cc4984a90f1791ccfd880f14", - "version": "0.17.3", + "sha256": "a477dfd351921321f41bc5be88c9465820b14fb1b177a896530e21193c2fa833", + "version": "0.17.4", "width": 2880 }, "05-live.png": { "height": 1800, - "sha256": "5f556d28d8fe5cefd0c51419c18b0d34a93bd6eb771ab433d810dd62f46cee55", - "version": "0.17.3", + "sha256": "e7d351a00a431c34661b795d61b22cdf2dbb0ad02da19b94d64b2637dc8d2ffa", + "version": "0.17.4", "width": 2880 }, "06-alerts.png": { "height": 1800, - "sha256": "bf4fe86ebc5e996837537ff9baed8a43cd4f332807f18c3493424b563f1f5372", - "version": "0.17.3", + "sha256": "54d4aa50adf2feb55547086f16f02744969b448258f6a9dc57461e4c973c092b", + "version": "0.17.4", "width": 2880 }, "07-agent-detail.png": { "height": 1800, - "sha256": "7090f56a0716a4de06831c7f43bc1bde0c7e2d13d521155ad671c7bc8b95cece", - "version": "0.17.3", + "sha256": "b3c1343cbeb505639831cd049b497cdfeaeead5acc625450dbb7212bac86cf45", + "version": "0.17.4", "width": 2880 }, "08-agent-tools.png": { "height": 1800, - "sha256": "4447d3c1ff5e12f04fe6d7a9c6a2d0572f160253645e32937e4aa50b1fa9e918", - "version": "0.17.3", + "sha256": "21847d1b756a5b10479e1c9cd62204a3e57068f3063e4e291366aee79627d7ff", + "version": "0.17.4", "width": 2880 }, "09-mcp-servers.png": { "height": 1800, - "sha256": "8822aa24b48a793701caf4ad1378edb2233dfc0ca545a6553fd91bd2f353def3", - "version": "0.17.3", + "sha256": "6b58562f0bf91f461d6e561c406dfd9607812aaa2469da58515ea892ffcef3d3", + "version": "0.17.4", "width": 2880 } } diff --git a/packaging/release-notes/0.17.4.md b/packaging/release-notes/0.17.4.md new file mode 100644 index 00000000..3f31632b --- /dev/null +++ b/packaging/release-notes/0.17.4.md @@ -0,0 +1,19 @@ +### What's new in 0.17.4 + +- **See every control in Policies.** Rules is now a unified Policies inventory for ordinary rules, agent allowlists, managed packs, and Developer-only inspection policies, with truthful states such as enforcing, reporting, watching, waiting, scheduled, paused, applying, review required, or unavailable. +- **Keep each policy's real editor and evidence.** Selecting an item opens the contextual editor that owns it; Managed packs retain signer, publisher, catalog, version, and license provenance without weakening review or consent, while Activity combines existing records without pretending they share one persistence model. +- **Read Sources as one operational page.** Collector, Devices, and Coverage now flow in that order without a segmented control. Existing status links, Ask answers, restored windows, and screenshot arguments scroll to and briefly focus the requested section. +- **Dismiss chart details when the pointer leaves.** History no longer leaves a zero-value tooltip floating at the upper-left after the cursor exits the All traffic plot; keyboard bucket inspection remains unchanged. +- **Sign each release once.** The immutable tag run builds, signs, notarizes, Gatekeeper-verifies, attests, and retains one artifact; publication has no signing credentials and can only verify and publish those exact prepared bytes. + +### Install + +Signed with Developer ID and notarized by Apple. + +- `brew install --cask xinbetween/tap/flowlight` +- `Flowlight.dmg` — drag to Applications. +- `Flowlight-0.17.4.pkg` — installs to /Applications; offers to quit a running copy first. + +macOS 15 or later, Apple silicon and Intel. + +Full changelog: https://github.com/xinbetween/flowlight/releases diff --git a/packaging/release-notes/README.md b/packaging/release-notes/README.md index 95fc0380..623bf7b3 100644 --- a/packaging/release-notes/README.md +++ b/packaging/release-notes/README.md @@ -2,6 +2,7 @@ One file per version, named `.md` — `0.2.2.md` for the tag `v0.2.2`. The release workflow uses it as the body of the GitHub release and appends the checksums, which it only knows once the artifacts are built. Without a -file for the version, the release falls back to GitHub's generated notes. +file for the version, preparation extracts the matching entry from the generated releases page; if neither source +exists, it fails rather than publishing generic notes from a different checkout. The same text usually belongs in `site/pages/releases.html`, which is what the website shows. diff --git a/project.yml b/project.yml index 53928371..bca5ad92 100644 --- a/project.yml +++ b/project.yml @@ -15,8 +15,8 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.17.3" - CURRENT_PROJECT_VERSION: "77" + MARKETING_VERSION: "0.17.4" + CURRENT_PROJECT_VERSION: "78" configs: AppStore: DEVELOPMENT_TEAM: 38RJUJHKZS diff --git a/scripts/check_release_checks.py b/scripts/check_release_checks.py index 9eca91a0..3cd2814c 100644 --- a/scripts/check_release_checks.py +++ b/scripts/check_release_checks.py @@ -4,6 +4,7 @@ import argparse import json import pathlib +import re import sys @@ -16,6 +17,14 @@ ("github-actions", "CodeQL / gate"), ("github-advanced-security", "CodeQL"), ) +EXPECTED_ACTIONS_WORKFLOWS = { + ("github-actions", "changes"): ".github/workflows/ci.yml", + ("github-actions", "site"): ".github/workflows/ci.yml", + ("github-actions", "unit (oldest Xcode)"): ".github/workflows/ci.yml", + ("github-actions", "unit (newest Xcode)"): ".github/workflows/ci.yml", + ("github-actions", "Review / gate"): ".github/workflows/ci.yml", + ("github-actions", "CodeQL / gate"): ".github/workflows/codeql.yml", +} def load(path): @@ -32,6 +41,16 @@ def check_runs(document): return runs +def workflow_runs(document): + pages = document if isinstance(document, list) else [document] + runs = [] + for page in pages: + if not isinstance(page, dict) or not isinstance(page.get("workflow_runs"), list): + raise ValueError("workflow-run response has an unexpected shape") + runs.extend(page["workflow_runs"]) + return runs + + def pull_requests(document): pages = document if isinstance(document, list) else [document] pulls = [] @@ -45,47 +64,147 @@ def pull_requests(document): return pulls -def verify(repo, sha, checks_document, pulls_document): +def issue_comments(document): + pages = document if isinstance(document, list) else [document] + comments = [] + for page in pages: + if not isinstance(page, list): + raise ValueError("issue-comment response has an unexpected shape") + comments.extend(page) + return comments + + +def select_checks(repo, sha, checks_document, runs_document, reviewed_pull_numbers=None): + actions_runs = {int(run.get("id", 0)): run for run in workflow_runs(runs_document)} + latest = {} + provenance = {} + repo_prefix = f"https://github.com/{repo}/" + run_url = re.compile(rf"^https://github\.com/{re.escape(repo)}/actions/runs/([0-9]+)(?:/.*)?$") + for check in check_runs(checks_document): + key = (check.get("app", {}).get("slug"), check.get("name")) + if key not in REQUIRED_CHECKS: + continue + details_url = check.get("details_url", "") + if not details_url.startswith(repo_prefix): + continue + workflow = None + if key in EXPECTED_ACTIONS_WORKFLOWS: + match = run_url.fullmatch(details_url) + if not match: + continue + workflow = actions_runs.get(int(match.group(1))) + if not workflow: + continue + if workflow.get("path") != EXPECTED_ACTIONS_WORKFLOWS[key]: + continue + if workflow.get("head_sha") != sha or workflow.get("event") != "pull_request": + continue + if workflow.get("repository", {}).get("full_name") != repo: + continue + if reviewed_pull_numbers is not None: + linked_pulls = workflow.get("pull_requests") + linked_pull = next(( + pull for pull in linked_pulls + if isinstance(pull, dict) + and pull.get("number") in reviewed_pull_numbers + and pull.get("head", {}).get("sha") == sha + and pull.get("base", {}).get("ref") == "main" + ), None) if isinstance(linked_pulls, list) else None + if linked_pull is None: + continue + workflow = dict(workflow) + workflow["_reviewed_pull_number"] = int(linked_pull["number"]) + if int(check.get("id", 0)) > int(latest.get(key, {}).get("id", 0)): + latest[key] = check + provenance[key] = workflow + return latest, provenance + + +def verify(repo, sha, checks_document, pulls_document, runs_document, comments_document, trusted_reviewers): errors = [] - eligible_pulls = [] + associated_pulls = [] + review_marker = f"Reviewed-Commit: {sha}" + trusted_reviewers = {value.lower() for value in re.split(r"[\s,]+", trusted_reviewers) if value} + if not trusted_reviewers: + errors.append("no trusted release reviewers are configured") for pull in pull_requests(pulls_document): head_sha = pull.get("head", {}).get("sha") base_ref = pull.get("base", {}).get("ref") state = pull.get("state") merged = bool(pull.get("merged_at")) if head_sha == sha and base_ref == "main" and (state == "open" or merged): - eligible_pulls.append(pull) - if not eligible_pulls: - errors.append(f"commit {sha} has no open or merged pull request targeting main at that exact head") + associated_pulls.append(pull) + if not associated_pulls: + errors.append("exact source commit has no open or merged pull request targeting main at that exact head") - latest = {} - repo_prefix = f"https://github.com/{repo}/" - for run in check_runs(checks_document): - app = run.get("app", {}).get("slug") - name = run.get("name") - if (app, name) not in REQUIRED_CHECKS: + associated_numbers = {int(pull["number"]) for pull in associated_pulls} + receipts = [] + trusted_associations = {"OWNER", "MEMBER", "COLLABORATOR"} + issue_url = re.compile(rf"^https://api\.github\.com/repos/{re.escape(repo)}/issues/([0-9]+)$") + for comment in issue_comments(comments_document): + if not isinstance(comment, dict): continue - details_url = run.get("details_url", "") - if not details_url.startswith(repo_prefix): + match = issue_url.fullmatch(comment.get("issue_url", "")) + if not match or int(match.group(1)) not in associated_numbers: + continue + pull_number = int(match.group(1)) + body = comment.get("body") + body_lines = {line.strip() for line in body.splitlines()} if isinstance(body, str) else set() + user = comment.get("user") + reviewer = user.get("login") if isinstance(user, dict) else None + if review_marker not in body_lines or not isinstance(reviewer, str) or reviewer.lower() not in trusted_reviewers: continue - key = (app, name) - if int(run.get("id", 0)) > int(latest.get(key, {}).get("id", 0)): - latest[key] = run + association = comment.get("author_association") + comment_id = comment.get("id") + html_url = comment.get("html_url") + if association not in trusted_associations: + continue + if not isinstance(comment_id, int) or isinstance(comment_id, bool) or comment_id <= 0: + continue + expected_url = f"https://github.com/{repo}/pull/{pull_number}#issuecomment-{comment_id}" + if html_url != expected_url: + continue + receipts.append({ + "pull_request": pull_number, + "comment_id": comment_id, + "reviewer": reviewer, + "author_association": association, + "reviewed_commit": sha, + "url": html_url, + }) + receipts.sort(key=lambda receipt: receipt["comment_id"]) + if len({receipt["comment_id"] for receipt in receipts}) != len(receipts): + errors.append("authenticated review comments contain duplicate comment IDs") + if associated_pulls and not receipts: + errors.append( + "no authenticated exact-head review comment from a configured trusted reviewer" + ) + reviewed_pull_numbers = {receipt["pull_request"] for receipt in receipts} + latest, provenance = select_checks( + repo, sha, checks_document, runs_document, reviewed_pull_numbers + ) for key in REQUIRED_CHECKS: - run = latest.get(key) + check = latest.get(key) label = f"{key[1]} ({key[0]})" - if not run: - errors.append(f"missing required check: {label}") - elif run.get("status") != "completed" or run.get("conclusion") != "success": + if not check: + errors.append(f"missing required check from its trusted workflow: {label}") + continue + if check.get("status") != "completed" or check.get("conclusion") != "success": errors.append( f"required check is not successful: {label} " - f"status={run.get('status')} conclusion={run.get('conclusion')}" + f"status={check.get('status')} conclusion={check.get('conclusion')}" + ) + workflow = provenance.get(key) + if workflow and (workflow.get("status") != "completed" or workflow.get("conclusion") != "success"): + errors.append( + f"trusted workflow run is not successful: {label} " + f"status={workflow.get('status')} conclusion={workflow.get('conclusion')}" ) if errors: - return errors, [] - return [], [int(pull["number"]) for pull in eligible_pulls] + return errors, [], [] + return [], sorted(associated_numbers), receipts def main(): @@ -94,10 +213,25 @@ def main(): parser.add_argument("--sha", required=True) parser.add_argument("--checks", type=pathlib.Path, required=True) parser.add_argument("--pulls", type=pathlib.Path, required=True) + parser.add_argument("--runs", type=pathlib.Path, required=True) + parser.add_argument("--comments", type=pathlib.Path, required=True) + parser.add_argument("--reviewers", required=True, + help="comma- or whitespace-separated trusted GitHub reviewer logins") + parser.add_argument("--evidence-output", type=pathlib.Path) args = parser.parse_args() try: - errors, pull_numbers = verify(args.repo, args.sha, load(args.checks), load(args.pulls)) + checks_document = load(args.checks) + runs_document = load(args.runs) + errors, pull_numbers, review_receipts = verify( + args.repo, + args.sha, + checks_document, + load(args.pulls), + runs_document, + load(args.comments), + args.reviewers, + ) except (OSError, ValueError, json.JSONDecodeError) as error: print(f"release check evidence could not be read: {error}", file=sys.stderr) return 1 @@ -107,8 +241,40 @@ def main(): print(f"release check rejected: {error}", file=sys.stderr) return 1 + if args.evidence_output: + latest, provenance = select_checks( + args.repo, + args.sha, + checks_document, + runs_document, + {receipt["pull_request"] for receipt in review_receipts}, + ) + evidence = { + "repository": args.repo, + "source_sha": args.sha, + "pull_requests": pull_numbers, + "review_receipts": review_receipts, + "checks": [ + { + "app": app, + "name": name, + "id": int(latest[(app, name)]["id"]), + "details_url": latest[(app, name)]["details_url"], + "workflow": provenance[(app, name)]["path"] if provenance.get((app, name)) else None, + "workflow_run_id": int(provenance[(app, name)]["id"]) if provenance.get((app, name)) else None, + "pull_request": provenance[(app, name)]["_reviewed_pull_number"] if provenance.get((app, name)) else None, + } + for app, name in REQUIRED_CHECKS + ], + } + args.evidence_output.write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n") + joined = ", ".join(f"#{number}" for number in pull_numbers) - print(f"Exact commit {args.sha} has reviewed release evidence from {joined} and all required checks passed.") + reviewers = ", ".join(sorted({receipt["reviewer"] for receipt in review_receipts})) + print( + f"Exact commit {args.sha} has authenticated review evidence from {reviewers} " + f"on {joined} and all required checks passed." + ) return 0 diff --git a/scripts/check_site.py b/scripts/check_site.py index 2c2ac502..48e095fe 100644 --- a/scripts/check_site.py +++ b/scripts/check_site.py @@ -283,7 +283,7 @@ def entries(text): f"button opens it — a new screen the documentation never got") # 12. A shortcut the documentation gets wrong. Shortcuts follow each product's visible sidebar. Developer ID - # includes Inspect; the Store product does not, so Rules and Sources move up by one there. + # includes Inspect; the Store product does not, so Policies and Sources move up by one there. order = sidebar_order(nav) requirements = shortcut_requirements(order) titles = dict(HELP_ANCHOR.findall(nav[nav.find("var englishTitle"):nav.find("var title")])) @@ -300,24 +300,24 @@ def entries(text): f"but the sidebar puts it at \u2318{key}") docs_pages = [OUT / "docs/index.html"] + [OUT / lang / "docs/index.html" for lang in LANGS] - shortcut_anchors = { - "inspect": ("inspection",), - "rules": ("rules",), - "sources": ("capture", "sources"), + shortcut_rows = { + "inspect": ("inspect", ("inspection",)), + "policies": ("rules", ("policies",)), + "sources": ("sources", ("capture", "sources")), } for docs_page in docs_pages: page_text = docs_page.read_text() - for screen, anchors in shortcut_anchors.items(): + for label_name, (screen, anchors) in shortcut_rows.items(): anchor_pattern = "|".join(re.escape(anchor) for anchor in anchors) cell_match = re.search( rf'', page_text, re.S) label = str(docs_page.relative_to(ROOT)) if cell_match is None: - found.append(f"{label}: the views table has no {screen} shortcut row") + found.append(f"{label}: the views table has no {label_name} shortcut row") continue for missing in missing_shortcuts(cell_match.group(1), requirements[screen]): - found.append(f"{label}: the {screen} row never lists {missing}") + found.append(f"{label}: the {label_name} row never lists {missing}") # 14. The homepage repeats the same shortcut badges and a compact Sources promise. Keep Collector, Devices, # and Coverage together, and keep both product-specific shortcut maps truthful. diff --git a/scripts/ci-secrets.sh b/scripts/ci-secrets.sh index 8f05949c..105e3f92 100755 --- a/scripts/ci-secrets.sh +++ b/scripts/ci-secrets.sh @@ -1,14 +1,16 @@ #!/bin/zsh -# Puts everything .github/workflows/release.yml needs into GitHub Actions secrets. +# Puts the signing inputs .github/workflows/release.yml's preparation job needs into GitHub Actions secrets. # # scripts/ci-secrets.sh # # It reads the two .p12 certificate exports, the two provisioning profiles and the App Store Connect key you point # it at, and uploads each with `gh secret set`. File contents are piped straight to gh and never printed. # -# They go into the `release` environment, not the repository, so only the release job can read them and only after -# a human approves the run. Even so: anyone who can approve a release can sign software as you, and anyone with -# admin access can change who that is. Revoke the identities in Apple's developer portal if that stops being true. +# They go into the `release` preparation environment, not the repository, so only the tag-push preparation job can +# read them. RELEASE_ACTORS, the authenticated RELEASE_REVIEWERS comment receipt, and the environment's v* tag policy +# gate that job; the environment currently has no human reviewer. Anyone who can change those controls can sign software +# as you. Revoke the identities in Apple's developer +# portal if that stops being acceptable. Publication uses a separate `release-publish` environment with no signing input. set -euo pipefail cd "$(dirname "$0")/.." @@ -21,8 +23,8 @@ ask() { printf '%s' "$1" >&2; read -r REPLY; print -r -- "$REPLY"; } echo "Setting Actions secrets on $REPO, in the $ENVIRONMENT environment." gh api "repos/$REPO/environments/$ENVIRONMENT" >/dev/null 2>&1 || { - echo "No $ENVIRONMENT environment on $REPO. Create it first (Settings › Environments), with a required reviewer" >&2 - echo "and a deployment branch policy limited to the tag pattern v*." >&2 + echo "No $ENVIRONMENT environment on $REPO. Create it first (Settings › Environments)," >&2 + echo "with a deployment branch policy limited to the tag pattern v*." >&2 exit 1 } echo @@ -178,9 +180,9 @@ set_secret DMG_SIGN_IDENTITY "$APP_ID" set_secret INSTALLER_IDENTITY "$INSTALLER_ID" echo -echo "TAP_TOKEN is optional: a fine-grained token with Contents: write on xinbetween/homebrew-tap, which lets the" -echo "release job update the Homebrew cask. Add it to the same environment; without it that step is skipped and" -echo "you run scripts/update-cask.sh yourself." +echo "TAP_TOKEN is optional: a fine-grained token with Contents: write on xinbetween/homebrew-tap. Put it only in" +echo "the separate release-publish environment; never add signing or notarization secrets there. Without it, GitHub" +echo "publication still succeeds and you update the cask manually from the verified DMG." echo -echo "Try it against a tag that already exists: gh workflow run Release --repo $REPO --ref v0.2.2" -echo "It will wait for your approval before it touches any of this." +echo "A v* tag push prepares the signed artifact. After that run succeeds, publish with:" +echo " gh workflow run Release --repo $REPO --ref -f prepare_run_id=" diff --git a/scripts/release_artifact.py b/scripts/release_artifact.py new file mode 100755 index 00000000..9690babc --- /dev/null +++ b/scripts/release_artifact.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +"""Create and verify Flowlight's build-once release handoff.""" + +import argparse +import datetime as dt +import hashlib +import json +import os +import pathlib +import re +import shutil +import stat +import sys +import zipfile + +SCHEMA = "flowlight-release-v1" +WORKFLOW_PATH = ".github/workflows/release.yml" +SHA256 = re.compile(r"[0-9a-f]{64}\Z") +SOURCE_SHA = re.compile(r"[0-9a-f]{40}\Z") +VERSION = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+\Z") +REQUIRED_REVIEW_CHECKS = { + ("github-actions", "changes"), + ("github-actions", "site"), + ("github-actions", "unit (oldest Xcode)"), + ("github-actions", "unit (newest Xcode)"), + ("github-actions", "Review / gate"), + ("github-actions", "CodeQL / gate"), + ("github-advanced-security", "CodeQL"), +} +REVIEW_WORKFLOWS = { + ("github-actions", "changes"): ".github/workflows/ci.yml", + ("github-actions", "site"): ".github/workflows/ci.yml", + ("github-actions", "unit (oldest Xcode)"): ".github/workflows/ci.yml", + ("github-actions", "unit (newest Xcode)"): ".github/workflows/ci.yml", + ("github-actions", "Review / gate"): ".github/workflows/ci.yml", + ("github-actions", "CodeQL / gate"): ".github/workflows/codeql.yml", +} + + +class ReleaseArtifactError(ValueError): + pass + + +def reject_duplicate_keys(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ReleaseArtifactError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def load_json(path): + try: + return json.loads(pathlib.Path(path).read_text(), object_pairs_hook=reject_duplicate_keys) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise ReleaseArtifactError(f"cannot read JSON from {path}: {error}") from error + + +def sha256(path): + digest = hashlib.sha256() + with pathlib.Path(path).open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def file_record(path): + path = pathlib.Path(path) + return {"name": path.name, "size": path.stat().st_size, "sha256": sha256(path)} + + +def require_keys(value, expected, label): + if not isinstance(value, dict): + raise ReleaseArtifactError(f"{label} must be an object") + actual = set(value) + expected = set(expected) + if actual != expected: + raise ReleaseArtifactError( + f"{label} fields differ: missing={sorted(expected - actual)} extra={sorted(actual - expected)}" + ) + + +def require_plain_name(name, label): + if not isinstance(name, str) or not name or pathlib.PurePosixPath(name).name != name: + raise ReleaseArtifactError(f"{label} is not a plain filename: {name!r}") + if name in {".", ".."} or "/" in name or "\\" in name: + raise ReleaseArtifactError(f"{label} is not a plain filename: {name!r}") + + +def parse_timestamp(value, label): + if not isinstance(value, str): + raise ReleaseArtifactError(f"{label} must be an ISO-8601 timestamp") + try: + return dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as error: + raise ReleaseArtifactError(f"{label} is not an ISO-8601 timestamp") from error + + +def verify_run(document, artifacts_document, *, repository, workflow_id, run_id, tag, source_sha, now=None): + if not str(run_id).isdigit(): + raise ReleaseArtifactError("prepare run ID must contain decimal digits only") + run_id = int(run_id) + if not SOURCE_SHA.fullmatch(source_sha): + raise ReleaseArtifactError("expected source SHA must be 40 lowercase hexadecimal characters") + if not isinstance(document, dict): + raise ReleaseArtifactError("run response must be an object") + checks = { + "run ID": (document.get("id"), run_id), + "repository": (document.get("repository", {}).get("full_name"), repository), + "workflow path": (document.get("path"), WORKFLOW_PATH), + "workflow ID": (document.get("workflow_id"), int(workflow_id)), + "event": (document.get("event"), "push"), + "tag": (document.get("head_branch"), tag), + "source SHA": (document.get("head_sha"), source_sha), + "status": (document.get("status"), "completed"), + "conclusion": (document.get("conclusion"), "success"), + } + for label, (actual, expected) in checks.items(): + if actual != expected: + raise ReleaseArtifactError(f"preparation {label} is {actual!r}, expected {expected!r}") + + if not isinstance(artifacts_document, dict) or not isinstance(artifacts_document.get("artifacts"), list): + raise ReleaseArtifactError("artifact response has an unexpected shape") + artifacts = artifacts_document["artifacts"] + if artifacts_document.get("total_count") != len(artifacts): + raise ReleaseArtifactError("artifact response count does not match its artifact list") + if len(artifacts) != 1: + raise ReleaseArtifactError(f"preparation run must have exactly one artifact, found {len(artifacts)}") + artifact = artifacts[0] + expected_name = f"flowlight-release-{tag}-{run_id}" + if artifact.get("name") != expected_name: + raise ReleaseArtifactError(f"artifact is {artifact.get('name')!r}, expected {expected_name!r}") + if artifact.get("expired") is not False: + raise ReleaseArtifactError("prepared artifact is expired") + now = now or dt.datetime.now(dt.timezone.utc) + if parse_timestamp(artifact.get("expires_at"), "artifact expiry") <= now: + raise ReleaseArtifactError("prepared artifact has no remaining retention") + if artifact.get("workflow_run", {}).get("id") != run_id: + raise ReleaseArtifactError("artifact is not attached to the selected preparation run") + digest = artifact.get("digest") + if not isinstance(digest, str) or not digest.startswith("sha256:") or not SHA256.fullmatch(digest[7:]): + raise ReleaseArtifactError("artifact archive digest is missing or malformed") + if not isinstance(artifact.get("id"), int) or artifact["id"] <= 0: + raise ReleaseArtifactError("artifact ID is missing or malformed") + if not isinstance(artifact.get("size_in_bytes"), int) or artifact["size_in_bytes"] <= 0: + raise ReleaseArtifactError("artifact size is missing or empty") + run_attempt = document.get("run_attempt") + if not isinstance(run_attempt, int) or run_attempt <= 0: + raise ReleaseArtifactError("preparation run attempt is missing or malformed") + return { + "artifact_id": artifact["id"], + "artifact_name": expected_name, + "artifact_digest": digest, + "run_attempt": run_attempt, + } + + +def create_manifest(args): + bundle = pathlib.Path(args.bundle) + version = args.version + if not VERSION.fullmatch(version): + raise ReleaseArtifactError("version must be three numeric components") + expected = [bundle / "Flowlight.dmg", bundle / f"Flowlight-{version}.pkg", bundle / "SHA256SUMS.txt"] + notes = bundle / "RELEASE-NOTES.md" + for path in [*expected, notes]: + if not path.is_file() or path.is_symlink(): + raise ReleaseArtifactError(f"missing regular prepared file: {path.name}") + evidence = load_json(args.evidence) + manifest = { + "schema": SCHEMA, + "repository": args.repository, + "tag": args.tag, + "source_sha": args.source_sha, + "source_tree": args.source_tree, + "version": version, + "build": str(args.build), + "preparation": { + "workflow": WORKFLOW_PATH, + "workflow_id": int(args.workflow_id), + "run_id": int(args.run_id), + "run_attempt": int(args.run_attempt), + }, + "review_evidence": evidence, + "assets": [file_record(path) for path in expected], + "release_notes": file_record(notes), + } + output = bundle / "RELEASE-MANIFEST.json" + output.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return output + + +def verify_record(record, expected_name, path): + require_keys(record, {"name", "size", "sha256"}, f"record for {expected_name}") + require_plain_name(record["name"], "manifest filename") + if record["name"] != expected_name: + raise ReleaseArtifactError(f"manifest names {record['name']!r}, expected {expected_name!r}") + if not isinstance(record["size"], int) or record["size"] < 0: + raise ReleaseArtifactError(f"manifest size for {expected_name} is malformed") + if not isinstance(record["sha256"], str) or not SHA256.fullmatch(record["sha256"]): + raise ReleaseArtifactError(f"manifest SHA-256 for {expected_name} is malformed") + if path.stat().st_size != record["size"]: + raise ReleaseArtifactError(f"size mismatch for {expected_name}") + actual = sha256(path) + if actual != record["sha256"]: + raise ReleaseArtifactError(f"SHA-256 mismatch for {expected_name}") + + +def parse_checksums(path, expected_names): + try: + lines = path.read_text().splitlines() + except (OSError, UnicodeError) as error: + raise ReleaseArtifactError(f"cannot read checksum file: {error}") from error + if len(lines) != len(expected_names): + raise ReleaseArtifactError("checksum file must contain exactly one line per binary asset") + checksums = {} + for line in lines: + match = re.fullmatch(r"([0-9a-f]{64}) [ *]([^/\\]+)", line) + if not match: + raise ReleaseArtifactError(f"malformed checksum line: {line!r}") + digest, name = match.groups() + require_plain_name(name, "checksum filename") + if name in checksums: + raise ReleaseArtifactError(f"duplicate checksum entry: {name}") + checksums[name] = digest + if set(checksums) != set(expected_names): + raise ReleaseArtifactError("checksum filenames do not match the binary assets") + return checksums + + +def verify_review_evidence(evidence, repository, source_sha): + require_keys( + evidence, + {"repository", "source_sha", "pull_requests", "review_receipts", "checks"}, + "review evidence", + ) + if evidence["repository"] != repository or evidence["source_sha"] != source_sha: + raise ReleaseArtifactError("manifest review evidence does not bind the release repository and source SHA") + pulls = evidence["pull_requests"] + if not isinstance(pulls, list) or not pulls or any(not isinstance(number, int) or number <= 0 for number in pulls): + raise ReleaseArtifactError("review evidence must name at least one valid pull request") + if len(set(pulls)) != len(pulls): + raise ReleaseArtifactError("review evidence contains duplicate pull requests") + receipts = evidence["review_receipts"] + if not isinstance(receipts, list) or not receipts: + raise ReleaseArtifactError("review evidence must retain an authenticated review receipt") + receipt_ids = set() + trusted_associations = {"OWNER", "MEMBER", "COLLABORATOR"} + for receipt in receipts: + require_keys( + receipt, + {"pull_request", "comment_id", "reviewer", "author_association", "reviewed_commit", "url"}, + "review receipt", + ) + pull_number = receipt["pull_request"] + comment_id = receipt["comment_id"] + if pull_number not in pulls: + raise ReleaseArtifactError("review receipt does not belong to an exact-head pull request") + if not isinstance(comment_id, int) or isinstance(comment_id, bool) or comment_id <= 0: + raise ReleaseArtifactError("review receipt comment ID is malformed") + if comment_id in receipt_ids: + raise ReleaseArtifactError("review evidence contains duplicate review receipt IDs") + receipt_ids.add(comment_id) + reviewer = receipt["reviewer"] + if not isinstance(reviewer, str) or not reviewer.strip(): + raise ReleaseArtifactError("review receipt reviewer is malformed") + if receipt["author_association"] not in trusted_associations: + raise ReleaseArtifactError("review receipt author association is not trusted") + if receipt["reviewed_commit"] != source_sha: + raise ReleaseArtifactError("review receipt does not bind the exact release commit") + expected_url = f"https://github.com/{repository}/pull/{pull_number}#issuecomment-{comment_id}" + if receipt["url"] != expected_url: + raise ReleaseArtifactError("review receipt URL does not bind its pull request and comment ID") + checks = evidence["checks"] + if not isinstance(checks, list) or len(checks) != len(REQUIRED_REVIEW_CHECKS): + raise ReleaseArtifactError("review evidence does not contain every required check exactly once") + found = set() + for check in checks: + require_keys( + check, + {"app", "name", "id", "details_url", "workflow", "workflow_run_id", "pull_request"}, + "review check", + ) + key = (check["app"], check["name"]) + if key in found or key not in REQUIRED_REVIEW_CHECKS: + raise ReleaseArtifactError(f"unexpected or duplicate review check: {key}") + if not isinstance(check["id"], int) or check["id"] <= 0: + raise ReleaseArtifactError(f"review check has an invalid ID: {key}") + if not isinstance(check["details_url"], str) or not check["details_url"].startswith(f"https://github.com/{repository}/"): + raise ReleaseArtifactError(f"review check points outside the release repository: {key}") + expected_workflow = REVIEW_WORKFLOWS.get(key) + if expected_workflow: + if check["workflow"] != expected_workflow: + raise ReleaseArtifactError(f"review check came from the wrong workflow: {key}") + if not isinstance(check["workflow_run_id"], int) or check["workflow_run_id"] <= 0: + raise ReleaseArtifactError(f"review check has no trusted workflow run ID: {key}") + if check["pull_request"] not in pulls: + raise ReleaseArtifactError(f"review check is not bound to an exact-head reviewed pull request: {key}") + run_url = re.fullmatch( + rf"https://github\.com/{re.escape(repository)}/actions/runs/([0-9]+)(?:/.*)?", + check["details_url"], + ) + if not run_url or int(run_url.group(1)) != check["workflow_run_id"]: + raise ReleaseArtifactError(f"review check URL does not bind its workflow run ID: {key}") + elif check["workflow"] is not None or check["workflow_run_id"] is not None or check["pull_request"] is not None: + raise ReleaseArtifactError(f"non-Actions review check unexpectedly names workflow or pull-request provenance: {key}") + found.add(key) + if found != REQUIRED_REVIEW_CHECKS: + raise ReleaseArtifactError("review evidence check set is incomplete") + + +def extract_archive(archive, destination, *, version, max_expanded_size=2_000_000_000): + archive = pathlib.Path(archive) + destination = pathlib.Path(destination) + if not VERSION.fullmatch(version): + raise ReleaseArtifactError("version must be three numeric components") + if destination.exists(): + if not destination.is_dir() or any(destination.iterdir()): + raise ReleaseArtifactError("archive destination must be an empty directory") + else: + destination.mkdir() + expected = { + "Flowlight.dmg", + f"Flowlight-{version}.pkg", + "SHA256SUMS.txt", + "RELEASE-NOTES.md", + "RELEASE-MANIFEST.json", + } + try: + with zipfile.ZipFile(archive) as bundle: + infos = bundle.infolist() + names = [entry.filename for entry in infos] + if len(names) != len(set(names)): + raise ReleaseArtifactError("prepared archive contains duplicate paths") + if set(names) != expected or len(infos) != len(expected): + raise ReleaseArtifactError(f"prepared archive paths differ: {sorted(names)}") + expanded_size = sum(entry.file_size for entry in infos) + if expanded_size > max_expanded_size: + raise ReleaseArtifactError("prepared archive expands beyond the release size bound") + for entry in infos: + require_plain_name(entry.filename, "prepared archive path") + mode = entry.external_attr >> 16 + if mode and not stat.S_ISREG(mode): + raise ReleaseArtifactError( + f"prepared archive entry is not a regular file: {entry.filename}" + ) + if entry.flag_bits & 1: + raise ReleaseArtifactError(f"prepared archive entry is encrypted: {entry.filename}") + for entry in infos: + target = destination / entry.filename + with bundle.open(entry) as source, target.open("xb") as output: + shutil.copyfileobj(source, output) + except (zipfile.BadZipFile, zipfile.LargeZipFile, RuntimeError) as error: + raise ReleaseArtifactError(f"prepared archive cannot be extracted safely: {error}") from error + return destination + + +def verify_bundle(bundle, *, repository, tag, source_sha, version, build, workflow_id, run_id, run_attempt, public=False): + bundle = pathlib.Path(bundle) + if not bundle.is_dir(): + raise ReleaseArtifactError(f"bundle is not a directory: {bundle}") + names = set() + for entry in bundle.iterdir(): + mode = entry.lstat().st_mode + if not stat.S_ISREG(mode): + raise ReleaseArtifactError(f"bundle entry is not a regular file: {entry.name}") + require_plain_name(entry.name, "bundle entry") + names.add(entry.name) + pkg_name = f"Flowlight-{version}.pkg" + prepared_names = {"Flowlight.dmg", pkg_name, "SHA256SUMS.txt", "RELEASE-NOTES.md", "RELEASE-MANIFEST.json"} + public_names = prepared_names - {"RELEASE-NOTES.md"} + expected_names = public_names if public else prepared_names + if names != expected_names: + raise ReleaseArtifactError( + f"bundle files differ: missing={sorted(expected_names - names)} extra={sorted(names - expected_names)}" + ) + + manifest = load_json(bundle / "RELEASE-MANIFEST.json") + require_keys( + manifest, + {"schema", "repository", "tag", "source_sha", "source_tree", "version", "build", "preparation", "review_evidence", "assets", "release_notes"}, + "manifest", + ) + expected_scalars = { + "schema": SCHEMA, + "repository": repository, + "tag": tag, + "source_sha": source_sha, + "version": version, + "build": str(build), + } + for field, expected in expected_scalars.items(): + if manifest[field] != expected: + raise ReleaseArtifactError(f"manifest {field} is {manifest[field]!r}, expected {expected!r}") + if not SOURCE_SHA.fullmatch(manifest["source_sha"]): + raise ReleaseArtifactError("manifest source SHA is malformed") + if not SOURCE_SHA.fullmatch(manifest["source_tree"]): + raise ReleaseArtifactError("manifest source tree is malformed") + if not VERSION.fullmatch(manifest["version"]): + raise ReleaseArtifactError("manifest version is malformed") + preparation = manifest["preparation"] + require_keys(preparation, {"workflow", "workflow_id", "run_id", "run_attempt"}, "preparation") + if preparation != { + "workflow": WORKFLOW_PATH, + "workflow_id": int(workflow_id), + "run_id": int(run_id), + "run_attempt": int(run_attempt), + }: + raise ReleaseArtifactError("manifest preparation identity does not match the selected run") + if preparation["run_attempt"] <= 0: + raise ReleaseArtifactError("manifest run attempt is malformed") + verify_review_evidence(manifest["review_evidence"], repository, source_sha) + + assets = manifest["assets"] + if not isinstance(assets, list) or len(assets) != 3: + raise ReleaseArtifactError("manifest must describe exactly three non-manifest public assets") + records = {} + for record in assets: + if not isinstance(record, dict) or not isinstance(record.get("name"), str): + raise ReleaseArtifactError("manifest asset record is malformed") + if record["name"] in records: + raise ReleaseArtifactError(f"duplicate manifest asset: {record['name']}") + records[record["name"]] = record + expected_asset_names = {"Flowlight.dmg", pkg_name, "SHA256SUMS.txt"} + if set(records) != expected_asset_names: + raise ReleaseArtifactError("manifest asset names do not match the release assets") + for name in expected_asset_names: + verify_record(records[name], name, bundle / name) + + notes_record = manifest["release_notes"] + require_keys(notes_record, {"name", "size", "sha256"}, "release notes record") + if notes_record.get("name") != "RELEASE-NOTES.md": + raise ReleaseArtifactError("release notes record has an unexpected name") + if not isinstance(notes_record.get("size"), int) or notes_record["size"] <= 0: + raise ReleaseArtifactError("release notes must not be empty") + if not isinstance(notes_record.get("sha256"), str) or not SHA256.fullmatch(notes_record["sha256"]): + raise ReleaseArtifactError("release notes digest is malformed") + if not public: + verify_record(notes_record, "RELEASE-NOTES.md", bundle / "RELEASE-NOTES.md") + + checksums = parse_checksums(bundle / "SHA256SUMS.txt", ["Flowlight.dmg", pkg_name]) + for name, digest in checksums.items(): + actual = sha256(bundle / name) + if digest != actual or digest != records[name]["sha256"]: + raise ReleaseArtifactError(f"checksum disagreement for {name}") + return manifest + + +def parser(): + root = argparse.ArgumentParser() + commands = root.add_subparsers(dest="command", required=True) + + run = commands.add_parser("verify-run") + run.add_argument("--run-json", required=True) + run.add_argument("--artifacts-json", required=True) + run.add_argument("--repository", required=True) + run.add_argument("--workflow-id", required=True, type=int) + run.add_argument("--run-id", required=True) + run.add_argument("--tag", required=True) + run.add_argument("--source-sha", required=True) + + extract = commands.add_parser("extract-archive") + extract.add_argument("--archive", required=True) + extract.add_argument("--destination", required=True) + extract.add_argument("--version", required=True) + + create = commands.add_parser("create-manifest") + create.add_argument("--bundle", required=True) + create.add_argument("--repository", required=True) + create.add_argument("--tag", required=True) + create.add_argument("--source-sha", required=True) + create.add_argument("--source-tree", required=True) + create.add_argument("--version", required=True) + create.add_argument("--build", required=True) + create.add_argument("--workflow-id", required=True, type=int) + create.add_argument("--run-id", required=True, type=int) + create.add_argument("--run-attempt", required=True, type=int) + create.add_argument("--evidence", required=True) + + verify = commands.add_parser("verify-bundle") + verify.add_argument("--bundle", required=True) + verify.add_argument("--repository", required=True) + verify.add_argument("--tag", required=True) + verify.add_argument("--source-sha", required=True) + verify.add_argument("--version", required=True) + verify.add_argument("--build", required=True) + verify.add_argument("--workflow-id", required=True, type=int) + verify.add_argument("--run-id", required=True, type=int) + verify.add_argument("--run-attempt", required=True, type=int) + verify.add_argument("--public", action="store_true") + return root + + +def main(): + args = parser().parse_args() + try: + if args.command == "verify-run": + result = verify_run( + load_json(args.run_json), + load_json(args.artifacts_json), + repository=args.repository, + workflow_id=args.workflow_id, + run_id=args.run_id, + tag=args.tag, + source_sha=args.source_sha, + ) + print(json.dumps(result, sort_keys=True)) + elif args.command == "extract-archive": + print(extract_archive(args.archive, args.destination, version=args.version)) + elif args.command == "create-manifest": + print(create_manifest(args)) + else: + verify_bundle( + args.bundle, + repository=args.repository, + tag=args.tag, + source_sha=args.source_sha, + version=args.version, + build=args.build, + workflow_id=args.workflow_id, + run_id=args.run_id, + run_attempt=args.run_attempt, + public=args.public, + ) + print(f"Verified release bundle in {args.bundle}") + except (OSError, ReleaseArtifactError) as error: + print(f"release artifact rejected: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/screenshots.sh b/scripts/screenshots.sh index a88f4432..840e1e8c 100755 --- a/scripts/screenshots.sh +++ b/scripts/screenshots.sh @@ -46,11 +46,15 @@ SHOTS=( "inspect:inspect::" "visualize:visualize::1600x994" ) -# Rules is Store-capable but not one of the website's ten published assets. Add it only when an explicit capture set -# asks for it, keeping the default website inventory and its all-images manifest stable. +# Policies is Store-capable but not one of the website's ten published assets. The internal `rules` screen value and +# output filename remain stable for screenshot automation and old arguments. if [ -n "${FL_ONLY:-}" ] && [[ ",${FL_ONLY}," == *",rules,"* ]]; then SHOTS+=("rules:rules::") fi +# Sources is likewise available for release acceptance without expanding the website's committed image inventory. +if [ -n "${FL_ONLY:-}" ] && [[ ",${FL_ONLY}," == *",sources,"* ]]; then + SHOTS+=("sources:sources::") +fi exact_app_pids() { local expected="$1" pid command @@ -80,7 +84,8 @@ expected_window_title() { agents) print -r -- "AI Agents" ;; reports) print -r -- "History" ;; alerts) print -r -- "Alerts" ;; - rules) print -r -- "Rules" ;; + rules) print -r -- "Policies" ;; + sources) print -r -- "Sources" ;; inspect) print -r -- "Inspect" ;; visualize) print -r -- "Connections" ;; *) echo " no expected window title for screen $1" >&2; return 1 ;; @@ -92,7 +97,7 @@ title_matches_screen() { expected=$(expected_window_title "$screen") || return 1 case "$screen" in alerts) [[ "$actual" == "$expected" || "$actual" == "$expected – "* ]] ;; - rules) [[ "$actual" == "$expected" || "$actual" == "$expected – "*" in force" ]] ;; + rules) [[ "$actual" == "$expected" || "$actual" == "$expected – "* ]] ;; inspect) [[ "$actual" == "$expected" || "$actual" == "$expected – Off" ]] ;; *) [[ "$actual" == "$expected" ]] ;; esac diff --git a/scripts/test-update-cask.sh b/scripts/test-update-cask.sh new file mode 100755 index 00000000..c93f1211 --- /dev/null +++ b/scripts/test-update-cask.sh @@ -0,0 +1,45 @@ +#!/bin/zsh +set -euo pipefail + +root=${0:A:h:h} +temporary=$(mktemp -d "${TMPDIR:-/tmp}/flowlight-cask-test.XXXXXX") +trap 'rm -rf "$temporary"' EXIT + +cask="$temporary/flowlight.rb" +dmg="$temporary/Flowlight.dmg" +tap="$temporary/tap" +cat > "$cask" <<'CASK' +cask "flowlight" do + version "0.0.0" + sha256 "old" +end +CASK +print -n -- 'trusted prepared dmg fixture' > "$dmg" +mkdir -p "$tap/Casks" +git -C "$tap" init -q +git -C "$tap" config user.name fixture +git -C "$tap" config user.email fixture@example.com +print -- fixture > "$tap/README" +git -C "$tap" add README +git -C "$tap" commit -qm fixture + +VERSION=9.8.7 REQUIRE_LOCAL_DMG=1 CASK_PATH="$cask" DMG_PATH="$dmg" TAP="$tap" \ + "$root/scripts/update-cask.sh" +expected=$(shasum -a 256 "$dmg" | cut -d' ' -f1) +grep -qx ' version "9.8.7"' "$tap/Casks/flowlight.rb" +grep -qx " sha256 \"$expected\"" "$tap/Casks/flowlight.rb" +first_commit=$(git -C "$tap" rev-parse HEAD) + +VERSION=9.8.7 REQUIRE_LOCAL_DMG=1 CASK_PATH="$cask" DMG_PATH="$dmg" TAP="$tap" \ + "$root/scripts/update-cask.sh" +[[ "$(git -C "$tap" rev-parse HEAD)" == "$first_commit" ]] + +set +e +VERSION=9.8.7 REQUIRE_LOCAL_DMG=1 CASK_PATH="$cask" DMG_PATH="$temporary/missing.dmg" TAP="$tap" \ + "$root/scripts/update-cask.sh" > "$temporary/missing.out" 2>&1 +missing_status=$? +set -e +[[ "$missing_status" != 0 ]] +grep -q 'REQUIRE_LOCAL_DMG is set' "$temporary/missing.out" + +print -- 'Homebrew cask fixtures passed: portable rewrite, exact digest, idempotent rerun, and missing-local failure.' diff --git a/scripts/test_release_artifact.py b/scripts/test_release_artifact.py new file mode 100755 index 00000000..d3757935 --- /dev/null +++ b/scripts/test_release_artifact.py @@ -0,0 +1,430 @@ +#!/usr/bin/env python3 + +import copy +import datetime as dt +import hashlib +import importlib.util +import json +import pathlib +import stat +import sys +import tempfile +import types +import unittest +import warnings +import zipfile + +SCRIPT = pathlib.Path(__file__).with_name("release_artifact.py") +SPEC = importlib.util.spec_from_file_location("release_artifact", SCRIPT) +assert SPEC and SPEC.loader +release_artifact = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = release_artifact +SPEC.loader.exec_module(release_artifact) +ReleaseArtifactError = release_artifact.ReleaseArtifactError +verify_bundle = release_artifact.verify_bundle +create_manifest = release_artifact.create_manifest +extract_archive = release_artifact.extract_archive +verify_run = release_artifact.verify_run +REQUIRED_REVIEW_CHECKS = release_artifact.REQUIRED_REVIEW_CHECKS + +REPOSITORY = "xinbetween/flowlight" +TAG = "v0.17.4" +SHA = "a" * 40 +TREE = "b" * 40 +VERSION = "0.17.4" +BUILD = "78" +WORKFLOW_ID = 123 +RUN_ID = 456 + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def run_fixture(): + run = { + "id": RUN_ID, + "repository": {"full_name": REPOSITORY}, + "path": ".github/workflows/release.yml", + "workflow_id": WORKFLOW_ID, + "event": "push", + "head_branch": TAG, + "head_sha": SHA, + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + } + artifact = { + "id": 789, + "name": f"flowlight-release-{TAG}-{RUN_ID}", + "expired": False, + "expires_at": "2030-01-01T00:00:00Z", + "workflow_run": {"id": RUN_ID}, + "digest": f"sha256:{'c' * 64}", + "size_in_bytes": 1234, + } + return run, {"total_count": 1, "artifacts": [artifact]} + + +def write_bundle(root): + root = pathlib.Path(root) + files = { + "Flowlight.dmg": b"signed dmg fixture", + f"Flowlight-{VERSION}.pkg": b"signed pkg fixture", + } + for name, data in files.items(): + (root / name).write_bytes(data) + sums = "".join(f"{digest(data)} {name}\n" for name, data in files.items()).encode() + (root / "SHA256SUMS.txt").write_bytes(sums) + notes = b"Flowlight 0.17.4 release notes\n\n### Checksums\n" + (root / "RELEASE-NOTES.md").write_bytes(notes) + asset_names = ["Flowlight.dmg", f"Flowlight-{VERSION}.pkg", "SHA256SUMS.txt"] + manifest = { + "schema": "flowlight-release-v1", + "repository": REPOSITORY, + "tag": TAG, + "source_sha": SHA, + "source_tree": TREE, + "version": VERSION, + "build": BUILD, + "preparation": { + "workflow": ".github/workflows/release.yml", + "workflow_id": WORKFLOW_ID, + "run_id": RUN_ID, + "run_attempt": 1, + }, + "review_evidence": { + "repository": REPOSITORY, + "source_sha": SHA, + "pull_requests": [82], + "review_receipts": [{ + "pull_request": 82, + "comment_id": 9_001, + "reviewer": "release-reviewer", + "author_association": "OWNER", + "reviewed_commit": SHA, + "url": f"https://github.com/{REPOSITORY}/pull/82#issuecomment-9001", + }], + "checks": [ + { + "app": app, + "name": name, + "id": index, + "details_url": f"https://github.com/{REPOSITORY}/actions/runs/{index}", + "workflow": release_artifact.REVIEW_WORKFLOWS.get((app, name)), + "workflow_run_id": index if (app, name) in release_artifact.REVIEW_WORKFLOWS else None, + "pull_request": 82 if (app, name) in release_artifact.REVIEW_WORKFLOWS else None, + } + for index, (app, name) in enumerate(sorted(REQUIRED_REVIEW_CHECKS), start=1) + ], + }, + "assets": [ + {"name": name, "size": (root / name).stat().st_size, "sha256": digest((root / name).read_bytes())} + for name in asset_names + ], + "release_notes": {"name": "RELEASE-NOTES.md", "size": len(notes), "sha256": digest(notes)}, + } + (root / "RELEASE-MANIFEST.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + + +def verify(root, public=False): + return verify_bundle( + root, + repository=REPOSITORY, + tag=TAG, + source_sha=SHA, + version=VERSION, + build=BUILD, + workflow_id=WORKFLOW_ID, + run_id=RUN_ID, + run_attempt=1, + public=public, + ) + + +class PreparationRunTests(unittest.TestCase): + def test_accepts_exact_successful_tag_push(self): + run, artifacts = run_fixture() + result = verify_run( + run, artifacts, repository=REPOSITORY, workflow_id=WORKFLOW_ID, + run_id=str(RUN_ID), tag=TAG, source_sha=SHA, + now=dt.datetime(2029, 1, 1, tzinfo=dt.timezone.utc), + ) + self.assertEqual(result["artifact_id"], 789) + + def test_rejects_wrong_run_metadata_and_artifacts(self): + mutations = { + "nonnumeric run ID": lambda run, artifacts: (run, artifacts, "not-a-run"), + "another repository": lambda run, artifacts: (self._set(run, "repository", {"full_name": "other/repo"}), artifacts, str(RUN_ID)), + "another workflow": lambda run, artifacts: (self._set(run, "path", ".github/workflows/ci.yml"), artifacts, str(RUN_ID)), + "another workflow ID": lambda run, artifacts: (self._set(run, "workflow_id", 999), artifacts, str(RUN_ID)), + "dispatch run": lambda run, artifacts: (self._set(run, "event", "workflow_dispatch"), artifacts, str(RUN_ID)), + "another tag": lambda run, artifacts: (self._set(run, "head_branch", "v9.9.9"), artifacts, str(RUN_ID)), + "another SHA": lambda run, artifacts: (self._set(run, "head_sha", "d" * 40), artifacts, str(RUN_ID)), + "in progress": lambda run, artifacts: (self._set(run, "status", "in_progress"), artifacts, str(RUN_ID)), + "failed": lambda run, artifacts: (self._set(run, "conclusion", "failure"), artifacts, str(RUN_ID)), + "missing artifact": lambda run, artifacts: (run, {"total_count": 0, "artifacts": []}, str(RUN_ID)), + "duplicate artifacts": lambda run, artifacts: (run, {"total_count": 2, "artifacts": artifacts["artifacts"] * 2}, str(RUN_ID)), + "expired artifact": lambda run, artifacts: (run, self._artifact(artifacts, expired=True), str(RUN_ID)), + "wrong artifact name": lambda run, artifacts: (run, self._artifact(artifacts, name="other"), str(RUN_ID)), + "wrong attached run": lambda run, artifacts: (run, self._artifact(artifacts, workflow_run={"id": 999}), str(RUN_ID)), + "malformed digest": lambda run, artifacts: (run, self._artifact(artifacts, digest="sha256:BAD"), str(RUN_ID)), + } + for label, mutate in mutations.items(): + with self.subTest(label=label): + run, artifacts = run_fixture() + run, artifacts, run_id = mutate(run, artifacts) + with self.assertRaises(ReleaseArtifactError): + verify_run( + run, artifacts, repository=REPOSITORY, workflow_id=WORKFLOW_ID, + run_id=run_id, tag=TAG, source_sha=SHA, + now=dt.datetime(2029, 1, 1, tzinfo=dt.timezone.utc), + ) + + @staticmethod + def _set(document, key, value): + document[key] = value + return document + + @staticmethod + def _artifact(document, **changes): + document["artifacts"][0].update(changes) + return document + + +class PreparedArchiveTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = pathlib.Path(self.temporary.name) + self.bundle = self.root / "bundle" + self.bundle.mkdir() + write_bundle(self.bundle) + self.archive = self.root / "prepared.zip" + self.destination = self.root / "extracted" + + def tearDown(self): + self.temporary.cleanup() + + def _entries(self): + return {path.name: path.read_bytes() for path in self.bundle.iterdir()} + + def _write_custom(self, entries, *, duplicate=None, special_mode=None): + special_mode = special_mode or {} + with zipfile.ZipFile(self.archive, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for name, data in entries.items(): + info = zipfile.ZipInfo(name) + info.create_system = 3 + info.external_attr = special_mode.get(name, stat.S_IFREG | 0o644) << 16 + archive.writestr(info, data) + if duplicate: + info = zipfile.ZipInfo(duplicate) + info.create_system = 3 + info.external_attr = (stat.S_IFREG | 0o644) << 16 + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + archive.writestr(info, entries[duplicate]) + + def test_extracts_exact_archive_and_round_trips_bundle(self): + with zipfile.ZipFile(self.archive, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path in self.bundle.iterdir(): + archive.write(path, path.name) + extract_archive(self.archive, self.destination, version=VERSION) + verify(self.destination) + + def test_rejects_unsafe_or_substituted_archive_entries(self): + def extra(entries): + entries["extra"] = b"extra" + return entries, {} + + def traversal(entries): + entries["../Flowlight.dmg"] = entries.pop("Flowlight.dmg") + return entries, {} + + def wrong_package(entries): + entries["Flowlight-9.9.9.pkg"] = entries.pop(f"Flowlight-{VERSION}.pkg") + return entries, {} + + def symlink(entries): + return entries, {"Flowlight.dmg": stat.S_IFLNK | 0o777} + + def directory(entries): + return entries, {"Flowlight.dmg": stat.S_IFDIR | 0o755} + + cases = { + "extra path": extra, + "traversal path": traversal, + "wrong package": wrong_package, + "symlink entry": symlink, + "directory entry": directory, + } + for label, mutate in cases.items(): + with self.subTest(label=label): + entries, modes = mutate(self._entries()) + self._write_custom(entries, special_mode=modes) + with self.assertRaises(ReleaseArtifactError): + extract_archive(self.archive, self.destination, version=VERSION) + if self.destination.exists(): + for path in self.destination.iterdir(): + path.unlink() + + def test_rejects_duplicate_corrupt_and_oversized_archives(self): + self._write_custom(self._entries(), duplicate="Flowlight.dmg") + with self.assertRaises(ReleaseArtifactError): + extract_archive(self.archive, self.destination, version=VERSION) + + self.destination.rmdir() + self.archive.write_bytes(b"not a zip") + with self.assertRaises(ReleaseArtifactError): + extract_archive(self.archive, self.destination, version=VERSION) + + self.archive.unlink() + self._write_custom(self._entries()) + with self.assertRaises(ReleaseArtifactError): + extract_archive(self.archive, self.destination, version=VERSION, max_expanded_size=1) + + +class PreparedBundleTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = pathlib.Path(self.temporary.name) + write_bundle(self.root) + + def tearDown(self): + self.temporary.cleanup() + + def test_create_manifest_round_trip(self): + manifest = json.loads((self.root / "RELEASE-MANIFEST.json").read_text()) + evidence = self.root / "review-evidence.json" + evidence.write_text(json.dumps(manifest["review_evidence"])) + (self.root / "RELEASE-MANIFEST.json").unlink() + create_manifest(types.SimpleNamespace( + bundle=str(self.root), repository=REPOSITORY, tag=TAG, source_sha=SHA, + source_tree=TREE, version=VERSION, build=BUILD, workflow_id=WORKFLOW_ID, + run_id=RUN_ID, run_attempt=1, evidence=str(evidence), + )) + evidence.unlink() + verify(self.root) + + def test_accepts_exact_prepared_and_public_bundles(self): + verify(self.root) + (self.root / "RELEASE-NOTES.md").unlink() + verify(self.root, public=True) + + def test_rejects_changed_or_malformed_bundles(self): + cases = { + "changed DMG": lambda: (self.root / "Flowlight.dmg").write_bytes(b"tampered"), + "changed PKG": lambda: (self.root / f"Flowlight-{VERSION}.pkg").write_bytes(b"tampered"), + "changed notes": lambda: (self.root / "RELEASE-NOTES.md").write_bytes(b"tampered"), + "changed checksums": lambda: (self.root / "SHA256SUMS.txt").write_text("0" * 64 + " Flowlight.dmg\n"), + "missing file": lambda: (self.root / "Flowlight.dmg").unlink(), + "extra file": lambda: (self.root / "extra").write_text("extra"), + "nested directory": lambda: (self.root / "nested").mkdir(), + "symlink": lambda: (self.root / "extra").symlink_to(self.root / "Flowlight.dmg"), + "duplicate checksum": self._duplicate_checksum, + "uppercase checksum": self._uppercase_checksum, + "manifest wrong repository": lambda: self._manifest_field("repository", "other/repo"), + "manifest wrong tag": lambda: self._manifest_field("tag", "v9.9.9"), + "manifest wrong SHA": lambda: self._manifest_field("source_sha", "d" * 40), + "manifest wrong version": lambda: self._manifest_field("version", "9.9.9"), + "manifest wrong build": lambda: self._manifest_field("build", "999"), + "manifest wrong workflow": lambda: self._preparation_field("workflow", ".github/workflows/ci.yml"), + "manifest wrong run": lambda: self._preparation_field("run_id", 999), + "manifest wrong attempt": lambda: self._preparation_field("run_attempt", 2), + "manifest incomplete evidence": self._incomplete_evidence, + "manifest evidence wrong repository": lambda: self._evidence_field("repository", "other/repo"), + "manifest evidence wrong SHA": lambda: self._evidence_field("source_sha", "d" * 40), + "manifest evidence missing PR": lambda: self._evidence_field("pull_requests", []), + "manifest evidence missing receipt": lambda: self._evidence_field("review_receipts", []), + "manifest receipt wrong PR": lambda: self._review_receipt_field(0, "pull_request", 999), + "manifest receipt invalid ID": lambda: self._review_receipt_field(0, "comment_id", 0), + "manifest receipt missing reviewer": lambda: self._review_receipt_field(0, "reviewer", ""), + "manifest receipt invalid association": lambda: self._review_receipt_field(0, "author_association", "NONE"), + "manifest receipt wrong SHA": lambda: self._review_receipt_field(0, "reviewed_commit", "d" * 40), + "manifest receipt wrong URL": lambda: self._review_receipt_field( + 0, "url", f"https://github.com/{REPOSITORY}/pull/82#issuecomment-9002" + ), + "manifest duplicate receipt": self._duplicate_review_receipt, + "manifest evidence wrong workflow": lambda: self._review_check_field(0, "workflow", ".github/workflows/release.yml"), + "manifest evidence wrong workflow run": lambda: self._review_check_field(0, "workflow_run_id", 999), + "manifest evidence wrong check PR": lambda: self._review_check_field(0, "pull_request", 999), + "manifest evidence wrong check URL": lambda: self._review_check_field(0, "details_url", f"https://github.com/{REPOSITORY}/actions/runs/999"), + "manifest size mismatch": self._size_mismatch, + "manifest checksum disagreement": self._manifest_checksum_disagreement, + } + for label, mutation in cases.items(): + with self.subTest(label=label): + with tempfile.TemporaryDirectory() as temporary: + root = pathlib.Path(temporary) + write_bundle(root) + old_root, self.root = self.root, root + try: + mutation() + with self.assertRaises(ReleaseArtifactError): + verify(self.root) + finally: + self.root = old_root + + def _load_manifest(self): + return json.loads((self.root / "RELEASE-MANIFEST.json").read_text()) + + def _save_manifest(self, value): + (self.root / "RELEASE-MANIFEST.json").write_text(json.dumps(value, indent=2, sort_keys=True) + "\n") + + def _manifest_field(self, key, value): + manifest = self._load_manifest() + manifest[key] = value + self._save_manifest(manifest) + + def _preparation_field(self, key, value): + manifest = self._load_manifest() + manifest["preparation"][key] = value + self._save_manifest(manifest) + + def _incomplete_evidence(self): + manifest = self._load_manifest() + manifest["review_evidence"]["checks"].pop() + self._save_manifest(manifest) + + def _evidence_field(self, key, value): + manifest = self._load_manifest() + manifest["review_evidence"][key] = value + self._save_manifest(manifest) + + def _review_check_field(self, index, key, value): + manifest = self._load_manifest() + manifest["review_evidence"]["checks"][index][key] = value + self._save_manifest(manifest) + + def _review_receipt_field(self, index, key, value): + manifest = self._load_manifest() + manifest["review_evidence"]["review_receipts"][index][key] = value + self._save_manifest(manifest) + + def _duplicate_review_receipt(self): + manifest = self._load_manifest() + manifest["review_evidence"]["review_receipts"].append( + copy.deepcopy(manifest["review_evidence"]["review_receipts"][0]) + ) + self._save_manifest(manifest) + + def _size_mismatch(self): + manifest = self._load_manifest() + manifest["assets"][0]["size"] += 1 + self._save_manifest(manifest) + + def _manifest_checksum_disagreement(self): + manifest = self._load_manifest() + manifest["assets"][0]["sha256"] = "e" * 64 + self._save_manifest(manifest) + + def _duplicate_checksum(self): + line = (self.root / "SHA256SUMS.txt").read_text().splitlines()[0] + (self.root / "SHA256SUMS.txt").write_text(f"{line}\n{line}\n") + + def _uppercase_checksum(self): + path = self.root / "SHA256SUMS.txt" + path.write_text(path.read_text().upper()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_release_checks.py b/scripts/test_release_checks.py index dccb111c..109e23f7 100644 --- a/scripts/test_release_checks.py +++ b/scripts/test_release_checks.py @@ -1,28 +1,61 @@ #!/usr/bin/env python3 import copy +import importlib.util +import pathlib +import sys import unittest -from check_release_checks import REQUIRED_CHECKS, verify - +SCRIPT = pathlib.Path(__file__).with_name("check_release_checks.py") +SPEC = importlib.util.spec_from_file_location("check_release_checks", SCRIPT) +assert SPEC and SPEC.loader +check_release_checks = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = check_release_checks +SPEC.loader.exec_module(check_release_checks) +EXPECTED_ACTIONS_WORKFLOWS = check_release_checks.EXPECTED_ACTIONS_WORKFLOWS +REQUIRED_CHECKS = check_release_checks.REQUIRED_CHECKS +verify = check_release_checks.verify REPO = "xinbetween/flowlight" SHA = "a" * 40 +TRUSTED_REVIEWER = "release-reviewer" -def successful_checks(): - runs = [] +def successful_evidence(): + checks = [] + workflow_runs = [] for index, (app, name) in enumerate(REQUIRED_CHECKS, start=1): - path = "actions/runs/1/job/2" if app == "github-actions" else "runs/3" - runs.append({ + if app == "github-actions": + run_id = 100 + index + details = f"https://github.com/{REPO}/actions/runs/{run_id}/job/{1_000 + index}" + workflow_runs.append({ + "id": run_id, + "path": EXPECTED_ACTIONS_WORKFLOWS[(app, name)], + "head_sha": SHA, + "event": "pull_request", + "status": "completed", + "conclusion": "success", + "repository": {"full_name": REPO}, + "pull_requests": [{ + "number": 76, + "head": {"sha": SHA}, + "base": {"ref": "main"}, + }], + }) + else: + details = f"https://github.com/{REPO}/runs/3" + checks.append({ "id": index, "name": name, "status": "completed", "conclusion": "success", - "details_url": f"https://github.com/{REPO}/{path}", + "details_url": details, "app": {"slug": app}, }) - return [{"total_count": len(runs), "check_runs": runs}] + return ( + [{"total_count": len(checks), "check_runs": checks}], + [{"total_count": len(workflow_runs), "workflow_runs": workflow_runs}], + ) def reviewed_pull(**changes): @@ -32,26 +65,108 @@ def reviewed_pull(**changes): "merged_at": None, "base": {"ref": "main"}, "head": {"sha": SHA}, + "body": f"A pull request author can write this and it is not evidence:\nReviewed-Commit: {SHA}\n", } pull.update(changes) return [[pull]] +def review_comment(**changes): + comment = { + "id": 9_001, + "issue_url": f"https://api.github.com/repos/{REPO}/issues/76", + "html_url": f"https://github.com/{REPO}/pull/76#issuecomment-9001", + "author_association": "OWNER", + "user": {"login": TRUSTED_REVIEWER}, + "body": f"Independent review complete.\n\nReviewed-Commit: {SHA}\n", + } + comment.update(changes) + return [[comment]] + + +def run_verify(checks=None, pulls=None, runs=None, comments=None, reviewers=TRUSTED_REVIEWER): + default_checks, default_runs = successful_evidence() + return verify( + REPO, + SHA, + checks or default_checks, + pulls or reviewed_pull(), + runs or default_runs, + review_comment() if comments is None else comments, + reviewers, + ) + + class ReleaseCheckTests(unittest.TestCase): def test_accepts_exact_reviewed_commit_with_all_checks(self): - errors, pulls = verify(REPO, SHA, successful_checks(), reviewed_pull()) + errors, pulls, receipts = run_verify() self.assertEqual(errors, []) self.assertEqual(pulls, [76]) + self.assertEqual(receipts, [{ + "pull_request": 76, + "comment_id": 9_001, + "reviewer": TRUSTED_REVIEWER, + "author_association": "OWNER", + "reviewed_commit": SHA, + "url": f"https://github.com/{REPO}/pull/76#issuecomment-9001", + }]) def test_accepts_merged_pull(self): pulls = reviewed_pull(state="closed", merged_at="2026-11-04T00:00:00Z") - errors, _ = verify(REPO, SHA, successful_checks(), pulls) + errors, _, _ = run_verify(pulls=pulls) self.assertEqual(errors, []) + def test_pr_author_body_marker_is_not_review_evidence(self): + errors, _, _ = run_verify(comments=[]) + self.assertTrue(any("no authenticated exact-head review comment" in error for error in errors)) + self.assertFalse(any(TRUSTED_REVIEWER in error for error in errors), + "trusted reviewer configuration must not be written to logs") + self.assertFalse(any(SHA in error for error in errors), + "the exact review marker must not be written to logs") + + def test_rejects_spoofed_or_malformed_review_comments(self): + cases = { + "untrusted commenter": review_comment(user={"login": "pull-author"}), + "wrong SHA": review_comment(body=f"Reviewed-Commit: {'b' * 40}"), + "wrong pull": review_comment( + issue_url=f"https://api.github.com/repos/{REPO}/issues/77", + html_url=f"https://github.com/{REPO}/pull/77#issuecomment-9001", + ), + "wrong repository": review_comment( + issue_url="https://api.github.com/repos/other/repo/issues/76", + html_url="https://github.com/other/repo/pull/76#issuecomment-9001", + ), + "invalid association": review_comment(author_association="NONE"), + "marker not on its own line": review_comment(body=f"Approved Reviewed-Commit: {SHA}"), + "comment URL wrong pull": review_comment( + html_url=f"https://github.com/{REPO}/pull/77#issuecomment-9001" + ), + "comment URL wrong ID": review_comment( + html_url=f"https://github.com/{REPO}/pull/76#issuecomment-9002" + ), + "missing comment ID": review_comment(id=None), + "missing reviewer": review_comment(user=None), + } + for label, comments in cases.items(): + with self.subTest(label=label): + errors, _, receipts = run_verify(comments=comments) + self.assertEqual(receipts, []) + self.assertTrue(any("no authenticated exact-head review comment" in error for error in errors)) + + def test_requires_a_configured_trusted_reviewer(self): + errors, _, receipts = run_verify(reviewers="") + self.assertEqual(receipts, []) + self.assertTrue(any("no trusted release reviewers" in error for error in errors)) + + def test_rejects_duplicate_review_receipts(self): + duplicate = review_comment()[0][0] + errors, _, _ = run_verify(comments=[[duplicate, copy.deepcopy(duplicate)]]) + self.assertTrue(any("duplicate comment IDs" in error for error in errors)) + def test_rejects_missing_check(self): - checks = successful_checks() + checks, runs = successful_evidence() checks[0]["check_runs"].pop() - errors, _ = verify(REPO, SHA, checks, reviewed_pull()) + errors, _, _ = run_verify(checks=checks, runs=runs) self.assertTrue(any("missing required check" in error for error in errors)) def test_requires_stable_codeql_gate_instead_of_optional_analyzers(self): @@ -59,49 +174,78 @@ def test_requires_stable_codeql_gate_instead_of_optional_analyzers(self): for language in ("swift", "python", "actions"): self.assertNotIn(("github-actions", f"analyze ({language})"), REQUIRED_CHECKS) - checks = successful_checks() + checks, runs_document = successful_evidence() runs = checks[0]["check_runs"] runs[:] = [run for run in runs if run["name"] != "CodeQL / gate"] for language in ("swift", "python", "actions"): runs.append({ - "id": 100 + len(runs), + "id": 200 + len(runs), "name": f"analyze ({language})", "status": "completed", "conclusion": "success", "details_url": f"https://github.com/{REPO}/actions/runs/1/job/{language}", "app": {"slug": "github-actions"}, }) - errors, _ = verify(REPO, SHA, checks, reviewed_pull()) - self.assertTrue(any("missing required check: CodeQL / gate" in error for error in errors)) + errors, _, _ = run_verify(checks=checks, runs=runs_document) + self.assertTrue(any("CodeQL / gate" in error for error in errors)) def test_newer_failure_cannot_hide_behind_older_success(self): - checks = successful_checks() + checks, runs = successful_evidence() failed = copy.deepcopy(checks[0]["check_runs"][0]) failed.update(id=999, status="completed", conclusion="failure") checks[0]["check_runs"].append(failed) - errors, _ = verify(REPO, SHA, checks, reviewed_pull()) + errors, _, _ = run_verify(checks=checks, runs=runs) self.assertTrue(any("not successful" in error for error in errors)) + def test_rejects_same_named_check_from_wrong_workflow(self): + checks, runs = successful_evidence() + first_url = checks[0]["check_runs"][0]["details_url"] + run_id = int(first_url.split("/actions/runs/")[1].split("/")[0]) + target = next(run for run in runs[0]["workflow_runs"] if run["id"] == run_id) + target["path"] = ".github/workflows/spoof.yml" + errors, _, _ = run_verify(checks=checks, runs=runs) + self.assertTrue(any("missing required check from its trusted workflow" in error for error in errors)) + + def test_rejects_workflow_run_for_another_event_or_sha(self): + for field, value in (("event", "workflow_dispatch"), ("head_sha", "b" * 40)): + with self.subTest(field=field): + checks, runs = successful_evidence() + runs[0]["workflow_runs"][0][field] = value + errors, _, _ = run_verify(checks=checks, runs=runs) + self.assertTrue(any("missing required check from its trusted workflow" in error for error in errors)) + + def test_rejects_checks_from_another_pr_at_the_same_commit(self): + cases = { + "another pull request": {"number": 77, "head": {"sha": SHA}, "base": {"ref": "main"}}, + "another base": {"number": 76, "head": {"sha": SHA}, "base": {"ref": "develop"}}, + "another head": {"number": 76, "head": {"sha": "b" * 40}, "base": {"ref": "main"}}, + } + for label, linked_pull in cases.items(): + with self.subTest(label=label): + checks, runs = successful_evidence() + for run in runs[0]["workflow_runs"]: + run["pull_requests"] = [linked_pull] + errors, _, _ = run_verify(checks=checks, runs=runs) + self.assertTrue(any("missing required check from its trusted workflow" in error for error in errors)) + def test_rejects_check_from_untrusted_app(self): - checks = successful_checks() + checks, runs = successful_evidence() checks[0]["check_runs"][0]["app"]["slug"] = "untrusted" - errors, _ = verify(REPO, SHA, checks, reviewed_pull()) + errors, _, _ = run_verify(checks=checks, runs=runs) self.assertTrue(any("missing required check" in error for error in errors)) def test_rejects_check_linked_to_another_repository(self): - checks = successful_checks() + checks, runs = successful_evidence() checks[0]["check_runs"][0]["details_url"] = "https://github.com/other/repo/actions/runs/1" - errors, _ = verify(REPO, SHA, checks, reviewed_pull()) + errors, _, _ = run_verify(checks=checks, runs=runs) self.assertTrue(any("missing required check" in error for error in errors)) def test_rejects_wrong_pull_head(self): - pulls = reviewed_pull(head={"sha": "b" * 40}) - errors, _ = verify(REPO, SHA, successful_checks(), pulls) + errors, _, _ = run_verify(pulls=reviewed_pull(head={"sha": "b" * 40})) self.assertTrue(any("no open or merged pull request" in error for error in errors)) def test_rejects_closed_unmerged_pull(self): - pulls = reviewed_pull(state="closed", merged_at=None) - errors, _ = verify(REPO, SHA, successful_checks(), pulls) + errors, _, _ = run_verify(pulls=reviewed_pull(state="closed", merged_at=None)) self.assertTrue(any("no open or merged pull request" in error for error in errors)) diff --git a/scripts/test_release_workflow.py b/scripts/test_release_workflow.py new file mode 100755 index 00000000..464f18d9 --- /dev/null +++ b/scripts/test_release_workflow.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Static fail-closed contract for the prepare/publish privilege split.""" + +import pathlib +import re +import unittest + +WORKFLOW = pathlib.Path(__file__).resolve().parents[1] / ".github/workflows/release.yml" +FORBIDDEN_PUBLICATION = ( + "APP_CERTIFICATE_P12", "INSTALLER_CERTIFICATE_P12", "CERTIFICATE_PASSWORD", + "APP_PROVISIONING_PROFILE", "EXT_PROVISIONING_PROFILE", "APP_PROFILE", "EXT_PROFILE", + "NOTARY_", "TEAM_ID", "DMG_SIGN_IDENTITY", "INSTALLER_IDENTITY", + "security ", "xcodebuild", "xcrun ", "build-signed.sh", "build-dmg.sh", "build-pkg.sh", + "notarize.sh", "codesign", "productbuild", "productsign", "notarytool", +) + + +def job_block(text, name, next_name=None): + start = text.index(f" {name}:\n") + end = text.index(f" {next_name}:\n", start) if next_name else len(text) + return text[start:end] + + +def permissions(block): + lines = block.splitlines() + start = lines.index(" permissions:") + 1 + result = {} + for line in lines[start:]: + match = re.fullmatch(r" ([a-z-]+): (read|write|none)", line) + if not match: + break + result[match.group(1)] = match.group(2) + return result + + +def run_scripts(block): + lines = block.splitlines() + scripts = [] + index = 0 + while index < len(lines): + if lines[index] == " run: |": + index += 1 + body = [] + while index < len(lines) and (not lines[index] or lines[index].startswith(" ")): + body.append(lines[index][10:] if lines[index].startswith(" ") else "") + index += 1 + scripts.append("\n".join(body)) + else: + index += 1 + return scripts + + +def forbidden_publication_values(block): + return [value for value in FORBIDDEN_PUBLICATION if value in block] + + +class ReleaseWorkflowContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.workflow = WORKFLOW.read_text() + cls.prepare = job_block(cls.workflow, "prepare", "publish") + cls.publish = job_block(cls.workflow, "publish") + + def test_manual_publication_requires_preparation_run(self): + self.assertRegex(self.workflow, r"(?m)^ prepare_run_id:\n") + self.assertIn("github.event_name == 'push'", self.prepare) + self.assertIn("github.event_name == 'workflow_dispatch'", self.publish) + + def test_preparation_cannot_publish(self): + self.assertEqual(permissions(self.prepare), { + "contents": "read", "checks": "read", "issues": "read", "pull-requests": "read", + "actions": "read", "id-token": "write", "attestations": "write", + }) + self.assertNotIn("gh release", self.prepare) + self.assertIn("actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02", self.prepare) + self.assertIn("if-no-files-found: error", self.prepare) + self.assertIn("retention-days: 90", self.prepare) + + def test_preparation_requires_authenticated_review_comments(self): + self.assertIn("RELEASE_REVIEWERS", self.prepare) + self.assertIn("issues/comments?per_page=100", self.prepare) + self.assertIn('--comments "$COMMENTS"', self.prepare) + self.assertIn('--reviewers "$RELEASE_REVIEWERS"', self.prepare) + self.assertNotIn("pull request whose body", self.prepare) + + def test_publication_has_only_distribution_permissions(self): + self.assertEqual(permissions(self.publish), { + "actions": "read", "attestations": "read", "contents": "write", + }) + self.assertNotRegex(self.publish, r"(?m)^ environment: release$") + self.assertIn("persist-credentials: false", self.publish) + + def test_shell_scripts_never_interpolate_actions_expressions(self): + declarations = re.findall(r"(?m)^\s+run:", self.workflow) + self.assertEqual(len(run_scripts(self.workflow)), len(declarations), "every shell step must use a literal run: | block") + for script in run_scripts(self.workflow): + self.assertNotIn("${{", script) + self.assertIn("github.triggering_actor", self.prepare) + self.assertIn("github.triggering_actor", self.publish) + self.assertNotIn("TAP_TOKEN", self.publish[:self.publish.index("- name: Update Homebrew")]) + self.assertLess(self.publish.index("- name: Update Homebrew"), self.publish.index("TAP_TOKEN:")) + self.assertNotIn("x-access-token", self.publish) + self.assertIn('export GH_TOKEN="$TAP_TOKEN"', self.publish) + + def test_expression_detector_rejects_deliberate_input_injection_fixture(self): + unsafe = " run: |\n run_id=\"${{ inputs.prepare_run_id }}\"\n" + scripts = run_scripts(unsafe) + self.assertEqual(len(scripts), 1) + self.assertIn("${{", scripts[0]) + + def test_publication_cannot_build_sign_or_notarize(self): + self.assertEqual(forbidden_publication_values(self.publish), []) + + def test_forbidden_command_detector_rejects_deliberate_tamper(self): + self.assertEqual(forbidden_publication_values(self.publish + "\nxcodebuild archive\n"), ["xcodebuild"]) + + def test_archive_provenance_and_draft_bytes_are_ordered_before_publication(self): + digest = self.publish.index('ACTUAL="sha256:$(sha256sum') + extract = self.publish.index("release_artifact.py extract-archive") + bundle = self.publish.index("verify-bundle") + attest = self.publish.index("gh attestation verify") + create = self.publish.index("gh release create") + download = self.publish.index("gh release download") + compare = self.publish.index('cmp "$PREPARED/Flowlight.dmg"') + publish = self.publish.index('gh release edit "$TAG" --draft=false') + self.assertLess(digest, extract) + self.assertLess(extract, bundle) + self.assertLess(bundle, attest) + self.assertLess(attest, create) + self.assertLess(create, download) + self.assertLess(download, compare) + self.assertLess(compare, publish) + for value in ( + 'cmp "$PREPARED/Flowlight-$VERSION.pkg"', + 'cmp "$PREPARED/SHA256SUMS.txt"', + 'cmp "$PREPARED/RELEASE-MANIFEST.json"', + "release body differs from the prepared release notes", + ): + self.assertIn(value, self.publish) + + def test_publication_is_resumable_without_mutating_public_assets(self): + self.assertIn("Resuming the existing draft", self.publish) + self.assertIn("public release already exists", self.publish) + self.assertIn("it will be verified without mutation", self.publish) + self.assertIn("gh release upload", self.publish) + self.assertIn("--clobber", self.publish) + + def test_homebrew_requires_the_verified_local_dmg(self): + self.assertIn("REQUIRE_LOCAL_DMG=1", self.publish) + self.assertNotRegex(self.publish, r"update-cask\.sh[^\n]*https?://") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/update-cask.sh b/scripts/update-cask.sh index 32c9c175..9982935f 100755 --- a/scripts/update-cask.sh +++ b/scripts/update-cask.sh @@ -1,9 +1,10 @@ -#!/bin/zsh +#!/usr/bin/env bash # Updates packaging/homebrew/flowlight.rb to the current version and the DMG's checksum, then copies it into the # Homebrew tap so `brew install --cask xinbetween/tap/flowlight` gets the new release. # # scripts/update-cask.sh # version from project.yml, checksum from build/Flowlight.dmg # VERSION=0.2.1 scripts/update-cask.sh # no local build: checksum from the published release +# REQUIRE_LOCAL_DMG=1 scripts/update-cask.sh # CI: refuse a fallback download # TAP=/path/to/homebrew-tap scripts/update-cask.sh # # Without TAP it uses the tap Homebrew already has checked out, and only copies the file — pushing is yours to do. @@ -11,20 +12,49 @@ set -euo pipefail cd "$(dirname "$0")/.." VERSION="${VERSION:-$(sed -n 's/.*MARKETING_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml)}" -CASK=packaging/homebrew/flowlight.rb +CASK="${CASK_PATH:-packaging/homebrew/flowlight.rb}" +LOCAL_DMG="${DMG_PATH:-build/Flowlight.dmg}" +sha256_file() { + python3 -I - "$1" <<'PY' +import hashlib +import pathlib +import sys -if [[ -f build/Flowlight.dmg ]]; then - SHA=$(shasum -a 256 build/Flowlight.dmg | cut -d' ' -f1) - echo "Checksum from build/Flowlight.dmg" +value = hashlib.sha256() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + value.update(chunk) +print(value.hexdigest()) +PY +} + +if [[ -f "$LOCAL_DMG" ]]; then + SHA=$(sha256_file "$LOCAL_DMG") + echo "Checksum from $LOCAL_DMG" +elif [[ -n "${REQUIRE_LOCAL_DMG:-}" ]]; then + echo "REQUIRE_LOCAL_DMG is set, but $LOCAL_DMG is missing." >&2 + exit 1 else TMP=$(mktemp -d) curl -fsSL -o "$TMP/Flowlight.dmg" "https://github.com/xinbetween/flowlight/releases/download/v$VERSION/Flowlight.dmg" - SHA=$(shasum -a 256 "$TMP/Flowlight.dmg" | cut -d' ' -f1) + SHA=$(sha256_file "$TMP/Flowlight.dmg") rm -rf "$TMP" echo "Checksum from the published v$VERSION DMG" fi -/usr/bin/sed -i '' -e "s/^ version \".*\"/ version \"$VERSION\"/" -e "s/^ sha256 \".*\"/ sha256 \"$SHA\"/" "$CASK" +python3 -I - "$CASK" "$VERSION" "$SHA" <<'PY' +import pathlib +import re +import sys + +path = pathlib.Path(sys.argv[1]) +text = path.read_text() +text, versions = re.subn(r'^ version ".*"$', f' version "{sys.argv[2]}"', text, count=1, flags=re.MULTILINE) +text, checksums = re.subn(r'^ sha256 ".*"$', f' sha256 "{sys.argv[3]}"', text, count=1, flags=re.MULTILINE) +if versions != 1 or checksums != 1: + raise SystemExit("cask must contain exactly one version and one sha256 declaration") +path.write_text(text) +PY echo "$CASK → version $VERSION, sha256 $SHA" # Homebrew refuses to lint a cask outside a tap, so both checks run on the tap's copy. @@ -45,5 +75,10 @@ else fi if [[ -d "$TAP/.git" ]]; then git -C "$TAP" add Casks/flowlight.rb - git -C "$TAP" commit -m "flowlight $VERSION" >/dev/null && echo "Committed in $TAP — push it to publish." + if git -C "$TAP" diff --cached --quiet; then + echo "The tap already has Flowlight $VERSION; no commit needed." + else + git -C "$TAP" commit -m "flowlight $VERSION" >/dev/null + echo "Committed in $TAP — push it to publish." + fi fi diff --git a/site/pages/de/docs.html b/site/pages/de/docs.html index 0a2d6380..08efeb5d 100644 --- a/site/pages/de/docs.html +++ b/site/pages/de/docs.html @@ -30,7 +30,7 @@

Flowlight benutzen

@@ -101,10 +101,10 @@

Die Ansichten

- - + +
請求送到哪裡內容是什麼什麼時候
更新檢查api.github.com一個查詢最新版本的標準 HTTPS 請求;GitHub 會看到你的 IP 位址和 Flowlight/<version> 這個 user agent啟動時以及每天一次,自 0.1.2 起。關閉開關在「設定」› 「一般」
更新下載github.com磁碟映像檔及其總和檢查碼檔案只有在你點「下載並開啟」時
官方規則套件更新flowlight.xinbetween.com向固定官方來源傳送的標準 HTTPS 要求。伺服器會看到你的 IP 位址、Flowlight/<version>使用者代理程式和條件式快取驗證器;不會收到任何流量、套件設定或裝置識別碼啟動後在檢查到期時執行,此後在運作正常的情況下每天最多執行一次。預設開啟;可在規則 › 套件中關閉或手動選擇立即檢查
官方規則套件更新flowlight.xinbetween.com向固定官方來源傳送的標準 HTTPS 要求。伺服器會看到你的 IP 位址、Flowlight/<version>使用者代理程式和條件式快取驗證器;不會收到任何流量、套件設定或裝置識別碼啟動後在檢查到期時執行,此後在運作正常的情況下每天最多執行一次。預設開啟;可在「政策 › 受管理套件」中關閉或手動選擇立即檢查
反向 DNS你設定的 DNS 解析器在沒有看到主機名稱時,查詢某個 IP 位址登記的名稱需要時進行,在取樣器擷取模式下,也就是網路延伸功能開始運作之前的預設模式。沒有關閉開關:少了它,取樣器就沒辦法為大多數連線命名。網路延伸功能會從流量本身為它們命名,因此不做這件事
網路擁有者查詢Team Cymru,透過 DNS沒有已知主機名稱的公開 IP 位址。私有和保留位址絕不會離開你的 Mac預設開啟。關閉開關在「擷取」› 「辨識網路擁有者」
用託管模型提問你選擇的供應商:Anthropic、OpenAI、Google Gemini 或相容的端點你的問題,以及模型所選用的 Flowlight 唯讀查詢回傳的結果。流量資料庫本身不會被上傳只有在你選擇託管供應商並送出問題時。「提問」畫面會顯示送出的請求
.*?(.*?)Warnungen⌘5Alles, was ungewöhnlich aussah, mit App, Ziel und dem Wert, der die Regel ausgelöst hat.
Fragen⌘6Fragen zu deinem eigenen Datenverkehr in normaler Sprache, beantwortet aus der aufgezeichneten Historie von einem lokalen oder einem gehosteten Modell.
Inspizieren⌘7 · Developer IDOptional: entschlüsselte Anfragen der Agenten, die du über Flowlight leitest, und die Tool-Aufrufe dahinter. Die Suche reicht in Anfrage- und Antwort-Bodys hinein.
Regeln⌘8 · Developer ID
⌘7 · App Store
Jede Erlaubnisliste und jede Verweigerung, die du gesetzt hast, was jede davon abdeckt und was sie tatsächlich gestoppt hat.
Quellen⌘9 · Developer ID
⌘8 · App Store
Collector-Zustand, Gerätekanäle und Benennungsabdeckung in drei Tabs.
Richtlinien⌘8 · Developer ID
⌘7 · App Store
Jede konfigurierte Richtlinie, ob sie tatsächlich wirksam ist, und ein einheitlicher schreibgeschützter Aktivitätsverlauf.
Quellen⌘9 · Developer ID
⌘8 · App Store
Zustand und Wiederherstellung des Collectors, optionale Gerätekanäle und Abdeckung auf einer einzigen scrollbaren Betriebsseite.
-

Die Kurzbefehle folgen der sichtbaren Reihenfolge der Seitenleiste. Developer-ID-Versionen enthalten Inspizieren auf ⌘7; App-Store-Versionen lassen es weg, daher rücken Regeln und Quellen auf ⌘7 und ⌘8.

+

Die Kurzbefehle folgen der sichtbaren Reihenfolge der Seitenleiste. Developer-ID-Versionen enthalten Inspizieren auf ⌘7; App-Store-Versionen lassen es weg, daher rücken Richtlinien und Quellen auf ⌘7 und ⌘8.

Der Bildschirm Live: eine Liste von Anwendungen mit ihren aktuellen Sende- und Empfangsraten und ihrem meistgenutzten Ziel.
Live, der Bildschirm, mit dem Flowlight startet.
@@ -147,7 +147,7 @@

Tools & MCP-Server

eine nur konfigurierte Domain nie erreicht wurde und eine nur beobachtete nirgends aufgeschrieben stand, wo du sie hättest lesen können.

Jede Domain bietet zwei Verweigerungen an: für diesen Agenten, was den Agenten und die von ihm gestarteten Tools abdeckt, oder überall, was jede App auf dem Mac abdeckt. Beide schreiben eine ganz gewöhnliche Regel, sodass der - Verstoß-Verlauf sie meldet und der Regeleditor sie erklärt – und weil es gewöhnliche Regeln sind, sagt dir die Simulation im Editor, was + schreibgeschützte Aktivität sie meldet und der Regeleditor sie erklärt – und weil es gewöhnliche Regeln sind, sagt dir die Simulation im Editor, was eine davon verweigert hätte, bevor du sie einschaltest.

Eine Art von Domain hat keine Knöpfe. Ein Connector, den der Modellanbieter von sich aus erreicht – im Body der Anfrage deklariert und von der Seite des Anbieters aus kontaktiert –, berührt diesen Mac überhaupt nicht, also kann keine Regel hier ihn erreichen. Die Zeile sagt @@ -338,16 +338,16 @@

Mock-Antworten

verbunden: Ein Mock ändert die Antwort, nicht die Frage, ob die Verbindung zustande kommt. „Die API liefert 500“ funktioniert also, „die API ist überhaupt nicht erreichbar“ nicht.

-

Regeln

-

Eine Erlaubnisliste ist die Vorstellung eines Agenten davon, wohin er gehen darf. Eine Regel ist die allgemeine Fassung: Sie nennt ein +

Richtlinien

+

Unter Richtlinien sind alle Kontrollen, die Flowlight anwenden kann, in einem gemeinsamen Verzeichnis zusammengefasst: gewöhnliche Erlaubnis- und Sperrregeln, Agenten-Erlaubnislisten, verwaltete Pakete und – wo die HTTPS-Inspektion verfügbar ist – Schutzregeln, Mock-Antworten, Anfrageumschreibungen und Antworttransformationen. Jede Zeile unterscheidet den konfigurierten Zustand von der tatsächlichen Wirkung: wirksam, nur meldend, beobachtend, wartend, geplant, pausiert, verbraucht, in Anwendung, Prüfung erforderlich, nicht verfügbar, inkompatibel oder fehlgeschlagen. Wenn du eine Zeile auswählst, öffnet sich der zugehörige Editor dieser Richtlinie; die gemeinsame Ansicht ersetzt oder vermischt nicht deren unterschiedliche Einwilligungs- und Speichermodelle.

+

Eine Erlaubnisliste ist die Vorstellung eines Agenten davon, wohin er gehen darf. Eine gewöhnliche Regel ist die allgemeine Fassung: Sie nennt ein Subjekt – eine App, einen Agenten, ein Ziel, eine URL oder ein Paar aus zwei davon – und sagt blockieren oder erlauben. Rechtsklicke eine Zeile in Live, Verlauf, KI-Agenten oder Inspizieren, um eine Regel über das zu schreiben, was du gerade ansiehst; - jede davon landet im selben Editor auf dem Bildschirm Regeln, denn eine Regel, die im Verborgenen greift, - wäre eine Regel, die du nie wiederfindest.

+ jede bleibt unter Richtlinien auffindbar und öffnet sich in ihrem maßgeblichen Editor.

Vorlagen. Zweiundzwanzig, in fünf Gruppen – einen Agenten von Paketregistern oder anderen Modellanbietern fernhalten, die üblichen Wege schließen, auf denen Daten abfließen (Paste-Dienste, Webhook-Fänger, anonyme Datei-Ablagen, Tunnel, DNS over HTTPS), Produktanalyse und Crash-Berichte, Regeln für Arbeitszeiten und das einmalige „blockiere das eine Stunde lang, bis ich - herausgefunden habe, was es ist“. Durchsuche sie über die Symbolleiste von Regeln, oder rechtsklicke irgendwo in Live, Verlauf, KI-Agenten oder Inspizieren eine Zeile und + herausgefunden habe, was es ist“. Durchsuche sie über die Symbolleiste von Richtlinien, oder rechtsklicke irgendwo in Live, Verlauf, KI-Agenten oder Inspizieren eine Zeile und wende eine auf das an, was du ansiehst.

Jede Vorlage sagt, was sie tun wird, bevor sie es tut. Alles, was mehr als eine Regel schreibt, öffnet eine Karte mit jedem Host und jedem Zeitplan; alles Dauerhafte öffnet den vorausgefüllten Editor; und nur Regeln, die sich selbst aufheben – nach einer @@ -368,8 +368,7 @@

Regeln

Adresse und Port, nichts über Pfade, und das nur im Erweiterungsmodus. Die HTTPS-Inspektion verweigert eine einzelne Anfrage, kann also eine URL treffen und mit einem Status antworten, den der Agent lesen kann, statt mit einem Socket, der ohne Erklärung starb. Der nettop-Sampler kann überhaupt nichts verweigern. Eine Regel, die die aktuelle Einrichtung nicht umsetzen kann, sagt das in der Liste, statt dort durchgesetzt auszusehen.

-

Der zweite Tab ist der Verstoß-Verlauf: jede Verbindung, über die eine Regel entschieden hat, mit der App, dem Ziel, welche - Regel entschied und ob das eine Verweigerung war oder eine Ausnahme, die sie durchließ.

+

Die zweite Ansicht ist die schreibgeschützte Aktivität: gewöhnliche Regelentscheidungen, Verstöße gegen Erlaubnislisten, Ereignisse verwalteter Pakete und eigenständige Wirkungen von Inspektionsrichtlinien in einer gemeinsamen Chronologie. Sie bereitet die bestehenden maßgeblichen Einträge auf, statt den Verlauf in ein einheitliches allgemeines Ereignismodell umzuschreiben; App-Store-Versionen lassen von der Inspektion abhängige Richtlinienarten und deren aufbewahrte Aktivität weg.

Was sie bewirkt hätte. Eine Regel, die blockiert, ist leicht zu schreiben und beängstigend einzuschalten, denn das Erste, was man normalerweise über eine solche Regel erfährt, ist, was sie kaputt gemacht hat – und man erfährt es später, @@ -391,7 +390,7 @@

Regeln

getestet wurde, ist gegen zwei Stunden Aufzeichnung getestet.

Verwaltete Werbe- und Tracker-Pakete

-

Regeln › Pakete enthält drei offizielle Hostnamenlisten: Werbung, Websiteübergreifendes Tracking und Datenschutz und Telemetrie. Sie sind von Anfang an sichtbar und standardmäßig alle deaktiviert. Zum Blockieren ist die Netzwerkerweiterung erforderlich; der nettop-Sampler kann nur beobachten.

+

Richtlinien › Verwaltete Pakete enthält drei offizielle Hostnamenlisten: Werbung, Websiteübergreifendes Tracking und Datenschutz und Telemetrie. Sie sind von Anfang an sichtbar und standardmäßig alle deaktiviert. Zum Blockieren ist die Netzwerkerweiterung erforderlich; der nettop-Sampler kann nur beobachten.

Ausschließlich offizielle, signierte Updates. Flowlight prüft eine einzige festgelegte HTTPS-Quelle auf neuere, vom Herausgeber signierte Kataloge. Automatische Prüfungen sind standardmäßig aktiviert und können hier deaktiviert werden. Eine Signatur authentifiziert den Inhalt, aktiviert ihn aber nicht: Ein neues Paket bleibt deaktiviert, und bei geänderten Zielen wird ein aktiviertes Paket nicht mehr durchgesetzt, bis du die genaue neue Version geprüft und auf Sichern geklickt hast. Akzeptierte Inhalte können während eines Ausfalls als sichtbar veralteter letzter bekannter funktionsfähiger Stand erhalten bleiben. Dies sind Netzwerksperren auf Hostnamenebene: Sie können keine Lücken im DOM oder Layout, Browser-Overlays, skriptgesteuerten Pop-ups oder Fenster nativer Apps entfernen.

Wenn die HTTPS-Inspektion eine App durch Flowlights Proxy leitet, entscheidet er anhand der ursprünglichen App-Identität über gewöhnliche Zielregeln und aktivierte verwaltete Pakete, bevor eine Verbindung zum Ursprungsserver geöffnet wird. Bei angepinnten oder nie entschlüsselten CONNECT-Tunneln funktioniert das anhand des Hostnamens; Agenten-Erlaubnislisten bleiben auf den direkten Weg über die Netzwerkerweiterung beschränkt. Ist der Eigentümer oder Hostname unbekannt, wird die Verbindung im Zweifel zugelassen.

@@ -440,7 +439,8 @@

Schutzregeln für Agenten: ein Tool, einen Server oder eine

Das ist keine Sandbox. Ein verweigertes Tool wird nicht mehr angeboten und nicht mehr beantwortet. Ein Agent, der noch eine Shell hat, kann von Hand tun, was das Tool getan hätte. Flowlight zeichnet das auf; es tut nicht so, als würde es das verhindern.

-

Quellen: Collector und Engines

+

Quellen: Collector, Geräte und Abdeckung

+

Quellen ist eine einzige scrollbare Betriebsseite: zuerst Collector, dann Geräte und Abdeckung als klar bezeichnete Abschnitte. Statushinweise, Antworten unter Fragen, wiederhergestellte Fenster und Links aus Screenshots lassen die Seite zum passenden Abschnitt scrollen und heben ihn kurz hervor, ohne die anderen auszublenden.

Flowlight zeichnet Datenverkehr auf eine von zwei Weisen auf, wählbar unter Quellen › Collector. Sie unterscheiden sich darin, was sie verpassen können, und das lohnt sich zu verstehen, bevor du dich auf eine verlässt.

@@ -637,7 +637,7 @@

Fragen & Antworten

Mit geschlossenem Fenster tut Flowlight fast nichts: Es hält die Raten in der Menüleiste und schreibt eine Zeile pro Sekunde. Die Netzwerkerweiterung kostet weniger als der Sampler als Rückfallebene, der jede Sekunde einen kurzlebigen nettop-Prozess startet.
Kann es Verbindungen blockieren?
-
Ja, mit der Netzwerkerweiterung. Die Erlaubnisliste eines Agenten kann verweigern und nicht nur warnen, eine Regel kann eine App, +
Ja, mit der Netzwerkerweiterung. Die Erlaubnisliste eines Agenten kann verweigern und nicht nur warnen, eine Regel kann eine App, ein Ziel oder eine URL so lange blockieren, wie du sagst, und eine Schutzregel kann einem Agenten ein Tool wegnehmen, bevor sein Modell es angeboten bekommt. Nichts blockiert, bis du es verlangst, und jede Verweigerung führt ihren Ausweg mit.
Brauche ich die Netzwerkerweiterung?
diff --git a/site/pages/de/index.html b/site/pages/de/index.html index ee2744f4..8393aae2 100644 --- a/site/pages/de/index.html +++ b/site/pages/de/index.html @@ -110,11 +110,11 @@

Verlauf

Wo ist alles hingegangen? Sekunde bis Jahr, gruppiert nach Ap

Warnungen

Was sah falsch aus? Nachvollziehbare Warnungen pro App, mit Ziel und Schwellenwert.

⌘5
-

Regeln

Was habe ich verweigert – und passiert es? Jede Regel, was sie blockiert hat, und ein Verlauf jeder Entscheidung.

⌘8 · Developer ID / ⌘7 · App Store
+

Richtlinien

Welche Kontrollen gibt es, und sind sie wirksam? Jede Richtlinie, ihr tatsächlicher Zustand und ein einheitlicher Aktivitätsverlauf.

⌘8 · Developer ID / ⌘7 · App Store

Fragen

Fragen in normaler Sprache, beantwortet aus dem Verlauf – mit jeder ausgeführten Abfrage darunter.

⌘6
-

Quellen

Kommen Daten an, wie viel ist benannt und welche Gerätekanäle sind aktiv? Collector, Geräte und Abdeckung an einem Ort.

⌘9 · Developer ID / ⌘8 · App Store
+

Quellen

Kommen Daten an, welche Gerätekanäle sind aktiv, und was kann Flowlight erfassen? Eine Seite vom Collector bis zur Abdeckung.

⌘9 · Developer ID / ⌘8 · App Store

Inspizieren

Optional. Was hat der Agent wirklich gesendet, und welcher Tool-Aufruf hat es ausgelöst? Entschlüsselte Anfragen, Tool-Aufrufe, Ergebnisse.

⌘7 · Developer ID
diff --git a/site/pages/de/privacy.html b/site/pages/de/privacy.html index 9d1b5e6d..f8594a18 100644 --- a/site/pages/de/privacy.html +++ b/site/pages/de/privacy.html @@ -56,7 +56,7 @@

Netzwerkanfragen, die die App stellt

- + diff --git a/site/pages/docs.html b/site/pages/docs.html index 76ef74f5..5cae73ef 100644 --- a/site/pages/docs.html +++ b/site/pages/docs.html @@ -31,7 +31,7 @@

Using Flowlight

@@ -102,10 +102,10 @@

The views

- - + +
AnfrageGesendet anWas sie enthältWann
Update-Prüfungapi.github.comEine normale HTTPS-Anfrage nach der neuesten Version; GitHub sieht deine IP-Adresse und den User-Agent Flowlight/<version>Beim Start und täglich, ab 0.1.2. Abschalten in Einstellungen › Allgemein
Update-Downloadgithub.comDas Image und seine PrüfsummendateiNur wenn du auf „Herunterladen & öffnen“ klickst
Offizielle Regelpaket-Aktualisierungflowlight.xinbetween.comEine standardmäßige HTTPS-Anfrage an den festgelegten offiziellen Feed. Der Server sieht deine IP-Adresse, den User-Agent Flowlight/<version> und bedingte Cache-Validatoren; er erhält weder Datenverkehr noch Paketeinstellungen oder eine GerätekennungNach dem Start, sobald eine Prüfung fällig ist, und danach bei störungsfreiem Betrieb höchstens einmal täglich. Standardmäßig aktiviert; Ausschalter und manuelle Option Jetzt suchen unter Regeln › Pakete
Offizielle Regelpaket-Aktualisierungflowlight.xinbetween.comEine standardmäßige HTTPS-Anfrage an den festgelegten offiziellen Feed. Der Server sieht deine IP-Adresse, den User-Agent Flowlight/<version> und bedingte Cache-Validatoren; er erhält weder Datenverkehr noch Paketeinstellungen oder eine GerätekennungNach dem Start, sobald eine Prüfung fällig ist, und danach bei störungsfreiem Betrieb höchstens einmal täglich. Standardmäßig aktiviert; Ausschalter und manuelle Option Jetzt suchen unter Richtlinien › Verwaltete Pakete
Reverse-DNSDer von dir eingerichtete DNS-ResolverEine Abfrage des registrierten Namens einer IP-Adresse, wenn kein Hostname gesehen wurdeNach Bedarf, im Erfassungsmodus des Samplers, der Standard ist, bis die Netzwerkerweiterung läuft. Kein Schalter zum Abschalten: ohne sie kann der Sampler die meisten Verbindungen nicht benennen. Die Netzwerkerweiterung benennt sie aus dem Datenverkehr selbst und macht diese Abfrage nicht
Abfrage des NetzinhabersTeam Cymru, über DNSÖffentliche IP-Adressen ohne bekannten Hostnamen. Private und reservierte Adressen verlassen deinen Mac nieStandardmäßig ein. Abschalten in Erfassung › Netzinhaber bestimmen
Fragen mit einem gehosteten ModellDer Anbieter, den du wählst: Anthropic, OpenAI, Google Gemini oder ein kompatibler EndpunktDeine Frage und die Ergebnisse der nur lesenden Flowlight-Abfragen, die das Modell ausgewählt hat. Die Verkehrsdatenbank selbst wird nicht hochgeladenNur wenn du einen gehosteten Anbieter wählst und eine Frage abschickst. Der Bildschirm Fragen zeigt die gesendeten Anfragen
Alerts⌘5Everything that looked unusual, with the app, the destination and the number that tripped the rule.
Ask⌘6Questions about your own traffic in plain language, answered from the recorded history by a local or a hosted model.
Inspect⌘7 · Developer IDOptional: decrypted requests from the agents you route through Flowlight, and the tool calls behind them. Search reaches into request and response bodies.
Rules⌘8 · Developer ID
⌘7 · App Store
Every allowlist and refusal you have set, what each one covers, and what it has actually stopped.
Sources⌘9 · Developer ID
⌘8 · App Store
Collector health and recovery, hostname coverage, and opt-in camera, Bluetooth, USB and storage activity in three tabs.
Policies⌘8 · Developer ID
⌘7 · App Store
Every configured policy, whether it is actually effective, and a unified read-only activity trail.
Sources⌘9 · Developer ID
⌘8 · App Store
Collector health and recovery, opt-in device channels, and coverage as one scrollable operational page.
-

The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Rules and Sources move to ⌘7 and ⌘8.

+

The shortcuts follow the visible sidebar order. Developer ID builds include Inspect at ⌘7; App Store builds omit Inspect, so Policies and Sources move to ⌘7 and ⌘8.

The Live screen: a list of applications with their current send and receive rates and busiest destination.
Live, the screen Flowlight opens on.
@@ -148,7 +148,7 @@

Tools & MCP servers

in a configuration file and actually contacted says more than either alone, while a domain only ever configured has never been reached, and one only ever observed was never written down anywhere you could have read it.

Each domain offers two refusals: for this agent, which covers the agent and the tools it starts, or - everywhere, which covers every app on the Mac. Both write an ordinary rule, so the violations feed reports + everywhere, which covers every app on the Mac. Both write an ordinary rule, so Activity reports them and the rule editor explains them — and because they are ordinary rules, the simulation in the editor will tell you what one would have refused before you switch it on.

One kind of domain has no buttons. A connector the model provider reaches on its own — declared in the request body and @@ -347,16 +347,20 @@

Mock responses

connected to: a mock changes the answer, not whether the connection can be made, so "the API returns 500" works while "the API is entirely unreachable" does not.

-

Rules

-

An allowlist is one agent's idea of where it may go. A rule is the general version: it names a +

Policies

+

Policies is one inventory for every control Flowlight can apply: ordinary allow/block rules, agent + allowlists, managed packs, and—where HTTPS inspection is available—guardrails, mocks, request rewrites, and response + transforms. Each row distinguishes configured state from actual effect: enforcing, reporting, watching, waiting, + scheduled, paused, spent, applying, review required, unavailable, incompatible, or failed. Selecting a row opens the + contextual editor that owns that policy; Policies does not replace or blur those separate consent and persistence models.

+

An allowlist is one agent's idea of where it may go. An ordinary rule is the general version: it names a subject — an app, an agent, a destination, a URL, or a pairing of two of those — and says block or allow. Right-click any row in Live, History, AI Agents or Inspect to write one about what you're looking at; - every one of them lands in the same editor on the Rules screen, because a rule that took effect out of sight - would be a rule you could never find again.

+ every one remains findable in Policies and opens in its authoritative editor.

Templates. Twenty-two of them, in five groups — stopping an agent reaching package registries or other model providers, closing the common ways data leaves (paste sites, webhook catchers, anonymous file drops, tunnels, DNS over HTTPS), product analytics and crash reporting, working-hours policies, and one-off "block this for an hour while I work out - what it is". Browse them from the Rules toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and + what it is". Browse them from the Policies toolbar, or right-click a row anywhere in Live, History, AI Agents or Inspect and apply one to what you're looking at.

Each template says what it will do before it does it. Anything that writes more than one rule opens a card listing every host and schedule; anything permanent opens the editor pre-filled; and only rules that undo themselves — an @@ -377,8 +381,9 @@

Rules

address and the port, nothing about paths, and only in extension mode. HTTPS inspection refuses one request, so it can match a URL and answer with a status the agent can read instead of a socket that died without saying why. The nettop sampler cannot refuse anything at all. A rule the current setup can't carry out says so in the list rather than sitting there looking enforced.

-

The second tab is the violations feed: every connection a rule decided, with the app, where it was headed, which - rule decided it and whether that was a refusal or an exception letting it through.

+

The second view is read-only Activity: ordinary rule decisions, allowlist violations, managed-pack events, + and distinct inspection-policy effects in one chronology. It adapts the existing authoritative records rather than rewriting + history into one generic event model; Store builds omit inspection-dependent policy kinds and their retained activity.

What it would have done. A blocking rule is easy to write and frightening to switch on, because the first thing you normally learn about one is what it broke — and you learn it later, from something failing somewhere else. The rule @@ -398,7 +403,7 @@

Rules

history has been tested against two hours of history.

Managed ad and tracker packs

-

Rules › Packs contains three official lists built into Flowlight: Advertising covers known +

Policies › Managed packs contains three official lists built into Flowlight: Advertising covers known advertising delivery and auction hosts; Cross-site tracking covers common analytics, session replay and cross-site measurement hosts; and Privacy and telemetry covers crash reporting, install attribution, and audited Apple advertising and analytics hosts. The packs are visible from the start and all are off by default.

@@ -483,8 +488,10 @@

Agent guardrails: block a tool, a server or a resource

This is not a sandbox. A refused tool stops being offered and stops being answered. An agent that still has a shell can do by hand what the tool would have done. Flowlight will record that; it does not pretend to prevent it.

-

Sources: collector, coverage and devices

-

Sources keeps operational setup in one place. Its health summary shows whether traffic is arriving, how much is named, and which optional device channels are active; any unfinished collector step links directly to Sources › Collector.

+

Sources: collector, devices and coverage

+

Sources is one scrollable operational page: Collector first, then Devices and Coverage as clearly headed + sections. Status notices, Ask answers, restored windows, and screenshot links scroll to and briefly focus the relevant section + without hiding the others.

Flowlight records traffic one of two ways, chosen under Sources › Collector. They differ in what they can miss, which is worth understanding before you rely on either.

@@ -687,7 +694,7 @@

Questions & answers

With the window closed Flowlight does almost nothing: it keeps the menu bar rates and writes one row a second. The Network Extension costs less than the fallback sampler, which starts a short-lived nettop process every second.
Can it block connections?
-
Yes, with the Network Extension. An agent's allowlist can refuse as well as warn, a rule can block an app, +
Yes, with the Network Extension. An agent's allowlist can refuse as well as warn, a rule can block an app, a destination or a URL for as long as you say, and a guardrail can take a tool away from an agent before its model is offered it. Nothing blocks until you ask it to, and every refusal carries its way out.
Do I need the Network Extension?
diff --git a/site/pages/es/docs.html b/site/pages/es/docs.html index 952f8f79..19b08858 100644 --- a/site/pages/es/docs.html +++ b/site/pages/es/docs.html @@ -30,7 +30,7 @@

Usar Flowlight

@@ -101,10 +101,10 @@

Las vistas

- - + +
Alertas⌘5Todo lo que ha resultado inusual, con la app, el destino y la cifra que activó la regla.
Preguntar⌘6Preguntas sobre tu propio tráfico en lenguaje corriente, respondidas a partir del historial registrado por un modelo local o alojado.
Inspeccionar⌘7 · Developer IDOpcional: las peticiones descifradas de los agentes que encamines por Flowlight, y las llamadas de herramientas que hay detrás. La búsqueda llega a los cuerpos de las peticiones y las respuestas.
Reglas⌘8 · Developer ID
⌘7 · App Store
Cada lista de permitidos y cada rechazo que hayas puesto, qué abarca cada uno y qué ha detenido realmente.
Fuentes⌘9 · Developer ID
⌘8 · App Store
Estado del colector, canales de dispositivos y cobertura de nombres en tres pestañas.
Políticas⌘8 · Developer ID
⌘7 · App Store
Cada política configurada, si está surtiendo efecto de verdad y un historial de actividad unificado de solo lectura.
Fuentes⌘9 · Developer ID
⌘8 · App Store
Estado y recuperación del colector, canales de dispositivos opcionales y cobertura en una sola página operativa desplazable.
-

Los atajos siguen el orden visible de la barra lateral. Las versiones Developer ID incluyen Inspeccionar en ⌘7; las de App Store lo omiten, así que Reglas y Fuentes pasan a ⌘7 y ⌘8.

+

Los atajos siguen el orden visible de la barra lateral. Las versiones Developer ID incluyen Inspeccionar en ⌘7; las de App Store lo omiten, así que Políticas y Fuentes pasan a ⌘7 y ⌘8.

La pantalla En vivo: una lista de aplicaciones con sus tasas actuales de envío y recepción y su destino más activo.
En vivo, la pantalla con la que se abre Flowlight.
@@ -146,7 +146,7 @@

Herramientas y servidores MCP

separado, mientras que un dominio solo configurado nunca se ha alcanzado, y uno solo observado no estaba escrito en ningún sitio donde pudieras haberlo leído.

Cada dominio ofrece dos rechazos: para este agente, que cubre al agente y a las herramientas que arranca, o en - todas partes, que cubre todas las apps del Mac. Ambos escriben una regla corriente, así que el flujo de incumplimientos los informa + todas partes, que cubre todas las apps del Mac. Ambos escriben una regla corriente, así que Actividad los registra y el editor de reglas los explica — y, por ser reglas corrientes, la simulación del editor te dirá qué habría rechazado uno de ellos antes de que lo actives.

Hay un tipo de dominio que no trae botones. Un conector al que el proveedor del modelo llega por su cuenta —declarado en el cuerpo de la @@ -335,16 +335,16 @@

Respuestas simuladas

una simulación cambia la respuesta, no si la conexión puede hacerse, así que «la API devuelve 500» funciona y «la API es del todo inalcanzable» no.

-

Reglas

-

Una lista de permitidos es la idea que tiene un agente de adónde puede ir. Una regla es la versión general: nombra un +

Políticas

+

Políticas reúne en un solo inventario todos los controles que Flowlight puede aplicar: reglas normales de permiso o bloqueo, listas de permitidos de agentes, paquetes gestionados y, donde esté disponible la inspección HTTPS, salvaguardas, simulaciones, reescrituras de peticiones y transformaciones de respuestas. Cada fila distingue entre el estado configurado y el efecto real: en vigor, solo informando, vigilando, en espera, programada, en pausa, agotada, en proceso de aplicación, pendiente de revisión, no disponible, incompatible o fallida. Al seleccionar una fila se abre el editor contextual al que pertenece esa política; Políticas no sustituye ni confunde sus distintos modelos de consentimiento y persistencia.

+

Una lista de permitidos es la idea que tiene un agente de adónde puede ir. Una regla normal es la versión general: nombra un sujeto —una app, un agente, un destino, una URL o una pareja de dos de ellos— y dice bloquear o permitir. Haz clic derecho en cualquier fila de En vivo, Historial, Agentes de IA o Inspeccionar para escribir una sobre lo que estás mirando; - todas acaban en el mismo editor de la pantalla Reglas, porque una regla que surtiera efecto fuera de la vista - sería una regla que ya nunca podrías encontrar.

+ todas siguen localizables en Políticas y se abren en su editor de referencia.

Plantillas. Veintidós, en cinco grupos: impedir que un agente llegue a registros de paquetes o a otros proveedores de modelos, cerrar las vías habituales por las que salen los datos (sitios de pegado, receptores de webhooks, servicios anónimos de subida de archivos, túneles, DNS over HTTPS), analítica de producto e informes de errores, políticas de horario laboral, y el «bloquea esto una hora mientras averiguo - qué es» puntual. Explóralas desde la barra de herramientas de Reglas, o haz clic derecho en una fila de En vivo, Historial, Agentes de IA o Inspeccionar y + qué es» puntual. Explóralas desde la barra de herramientas de Políticas, o haz clic derecho en una fila de En vivo, Historial, Agentes de IA o Inspeccionar y aplica una a lo que estés mirando.

Cada plantilla dice qué va a hacer antes de hacerlo. Todo lo que escriba más de una regla abre una tarjeta con la lista de cada host y cada horario; todo lo permanente abre el editor ya rellenado; y solo las reglas que se deshacen solas —una @@ -365,8 +365,7 @@

Reglas

dirección y el puerto, nada de rutas, y solo en el modo de extensión. La inspección HTTPS rechaza una petición concreta, así que puede casar con una URL y responder con un estado que el agente pueda leer en lugar de un socket que murió sin explicar por qué. El muestreador nettop no puede rechazar nada en absoluto. Una regla que la configuración actual no puede aplicar lo dice en la lista en vez de quedarse ahí aparentando estar en vigor.

-

La segunda pestaña es el flujo de incumplimientos: cada conexión que decidió una regla, con la app, adónde iba, qué - regla lo decidió y si fue un rechazo o una excepción que la dejó pasar.

+

La segunda vista es Actividad, de solo lectura: reúne en una cronología las decisiones de reglas normales, los incumplimientos de listas de permitidos, los eventos de paquetes gestionados y los efectos diferenciados de las políticas de inspección. Adapta los registros de referencia existentes en vez de reescribir el historial en un modelo de eventos genérico; las versiones de la App Store omiten los tipos de políticas que dependen de la inspección y su actividad conservada.

Qué habría hecho. Una regla de bloqueo es fácil de escribir y da miedo activarla, porque lo primero que normalmente averiguas sobre una regla así es qué rompió —y lo averiguas más tarde, porque algo falla en otro sitio—. El editor de reglas @@ -385,7 +384,7 @@

Reglas

probado con dos horas de historial.

Paquetes gestionados de publicidad y rastreadores

-

Reglas › Paquetes contiene tres listas oficiales de nombres de host: Publicidad, Seguimiento entre sitios y Privacidad y telemetría. Son visibles desde el principio y todas están desactivadas de forma predeterminada. El bloqueo requiere la Extensión de red; el muestreador nettop solo puede observar.

+

Políticas › Paquetes gestionados contiene tres listas oficiales de nombres de host: Publicidad, Seguimiento entre sitios y Privacidad y telemetría. Son visibles desde el principio y todas están desactivadas de forma predeterminada. El bloqueo requiere la Extensión de red; el muestreador nettop solo puede observar.

Solo actualizaciones oficiales y firmadas. Flowlight consulta una única fuente HTTPS fija para buscar catálogos más recientes firmados por el editor. Las comprobaciones automáticas están activadas de forma predeterminada y pueden desactivarse aquí. Una firma autentica el contenido, pero no lo activa: un paquete nuevo permanece desactivado y, si cambian los destinos, un paquete activado deja de aplicarse hasta que revises la nueva versión exacta y pulses Guardar. Durante una interrupción, el contenido aceptado puede conservarse como la última versión válida conocida, marcada visiblemente como obsoleta. Estos son bloqueos de red a nivel de nombre de host: no pueden eliminar huecos del DOM o del diseño, superposiciones del navegador, ventanas emergentes generadas por scripts ni ventanas de aplicaciones nativas.

Cuando la inspección HTTPS encamina una app por el proxy de Flowlight, este decide las reglas de destino normales y los paquetes gestionados activados usando la identidad original de la app antes de abrir ninguna conexión con el servidor de origen. En los túneles CONNECT con certificado fijado o que nunca se descifran, esto funciona por nombre de host; las listas de permitidos de agentes siguen limitadas al camino directo por la Extensión de red. Si no se conoce el propietario o el nombre de host, la conexión se permite.

@@ -434,7 +433,8 @@

Salvaguardas de agente: bloquear una herramienta, un servido

Esto no es un sandbox. Una herramienta rechazada deja de ofrecerse y deja de responderse. Un agente que siga teniendo un shell puede hacer a mano lo que habría hecho la herramienta. Flowlight lo registrará; no pretende impedirlo.

-

Fuentes: colector y motores

+

Fuentes: colector, dispositivos y cobertura

+

Fuentes es una sola página operativa desplazable: primero Colector y después Dispositivos y Cobertura como secciones claramente encabezadas. Los avisos de estado, las respuestas de Preguntar, las ventanas restauradas y los enlaces de capturas de pantalla hacen que la página se desplace hasta la sección correspondiente y la resaltan brevemente sin ocultar las demás.

Flowlight registra el tráfico de una de dos maneras, que se eligen en Fuentes › Colector. Se diferencian en lo que pueden perderse, y conviene entenderlo antes de confiar en cualquiera de las dos.

@@ -630,7 +630,7 @@

Preguntas y respuestas

Con la ventana cerrada, Flowlight casi no hace nada: mantiene las tasas de la barra de menús y escribe una fila por segundo. La extensión de red cuesta menos que el muestreador de reserva, que arranca un proceso nettop efímero cada segundo.
¿Puede bloquear conexiones?
-
Sí, con la extensión de red. La lista de permitidos de un agente puede rechazar además de avisar, una regla puede bloquear una app, +
Sí, con la extensión de red. La lista de permitidos de un agente puede rechazar además de avisar, una regla puede bloquear una app, un destino o una URL el tiempo que digas, y una salvaguarda puede quitarle una herramienta a un agente antes de que se le ofrezca a su modelo. Nada se bloquea hasta que tú lo pides, y cada rechazo lleva consigo su salida.
¿Necesito la extensión de red?
diff --git a/site/pages/es/index.html b/site/pages/es/index.html index ea7a73b7..2e78edea 100644 --- a/site/pages/es/index.html +++ b/site/pages/es/index.html @@ -110,11 +110,11 @@

Historial

¿Adónde ha ido todo? Del segundo al año, agrupado por ap

Alertas

¿Qué ha pintado mal? Alertas por app, explicables, con el destino y el umbral.

⌘5
-

Reglas

¿Qué he rechazado y se está cumpliendo? Cada regla, lo que ha bloqueado y un flujo con todas las decisiones.

⌘8 · Developer ID / ⌘7 · App Store
+

Políticas

¿Qué controles hay y están surtiendo efecto? Cada política, su estado real y un historial de actividad unificado.

⌘8 · Developer ID / ⌘7 · App Store

Preguntar

Preguntas en lenguaje corriente, respondidas con el historial, y debajo cada consulta que ejecutó.

⌘6
-

Fuentes

¿Están llegando datos, cuánto tiene nombre y qué canales de dispositivos están activos? Colector, Dispositivos y Cobertura en un solo lugar.

⌘9 · Developer ID / ⌘8 · App Store
+

Fuentes

¿Están llegando datos, qué canales de dispositivos están activos y qué puede contabilizar Flowlight? Una página desde Colector hasta Cobertura.

⌘9 · Developer ID / ⌘8 · App Store

Inspeccionar

Opcional. ¿Qué envió el agente en realidad y qué llamada de herramienta lo provocó? Peticiones descifradas, llamadas de herramientas, resultados.

⌘7 · Developer ID
diff --git a/site/pages/es/privacy.html b/site/pages/es/privacy.html index 3d491fc0..ee8783a3 100644 --- a/site/pages/es/privacy.html +++ b/site/pages/es/privacy.html @@ -56,7 +56,7 @@

Peticiones de red que hace la app

- + diff --git a/site/pages/fr/docs.html b/site/pages/fr/docs.html index bfbc5763..f4953ce1 100644 --- a/site/pages/fr/docs.html +++ b/site/pages/fr/docs.html @@ -30,7 +30,7 @@

Utiliser Flowlight

@@ -101,10 +101,10 @@

Les vues

- - + +
PeticiónEnviada aQué contieneCuándo
Comprobación de actualizacionesapi.github.comUna petición HTTPS normal de la última versión; GitHub ve tu dirección IP y el agente de usuario Flowlight/<version>Al arrancar y a diario, desde la 0.1.2. Interruptor para desactivarla en Ajustes › General
Descarga de la actualizacióngithub.comLa imagen de disco y su archivo de suma de verificaciónSolo cuando pulsas Descargar y abrir
Actualización oficial del paquete de reglasflowlight.xinbetween.comUna solicitud HTTPS estándar al feed oficial fijo. El servidor ve tu dirección IP, el agente de usuario Flowlight/<version> y los validadores condicionales de caché; no recibe tráfico, ajustes de paquetes ni ningún identificador del dispositivoDespués del inicio, cuando corresponda, y luego como máximo una vez al día mientras todo funcione correctamente. Activada de forma predeterminada; interruptor para desactivarla y opción manual Buscar ahora en Reglas › Paquetes
Actualización oficial del paquete de reglasflowlight.xinbetween.comUna solicitud HTTPS estándar al feed oficial fijo. El servidor ve tu dirección IP, el agente de usuario Flowlight/<version> y los validadores condicionales de caché; no recibe tráfico, ajustes de paquetes ni ningún identificador del dispositivoDespués del inicio, cuando corresponda, y luego como máximo una vez al día mientras todo funcione correctamente. Activada de forma predeterminada; interruptor para desactivarla y opción manual Buscar ahora en Políticas › Paquetes gestionados
DNS inversoEl servidor DNS que tengas configuradoUna consulta del nombre registrado de una dirección IP, cuando no se ha visto ningún nombre de hostSegún haga falta, en el modo de captura del muestreador, que es el predeterminado hasta que la extensión de red esté en marcha. Sin interruptor para desactivarla: sin ella el muestreador no puede nombrar la mayoría de las conexiones. La extensión de red las nombra a partir del propio tráfico y no hace esta consulta
Consulta del propietario de la redTeam Cymru, por DNSDirecciones IP públicas sin nombre de host conocido. Las direcciones privadas y reservadas no salen nunca de tu MacActivada por defecto. Interruptor para desactivarla en Captura › Identificar a los propietarios de la red
Preguntar con un modelo alojadoEl proveedor que elijas: Anthropic, OpenAI, Google Gemini o un endpoint compatibleTu pregunta y los resultados que devuelvan las consultas de solo lectura de Flowlight que elija el modelo. La base de datos de tráfico en sí no se subeSolo cuando eliges un proveedor alojado y envías una pregunta. La pantalla Preguntar muestra las peticiones transmitidas
Alertes⌘5Tout ce qui a paru inhabituel, avec l’app, la destination et le chiffre qui a déclenché la règle.
Demander⌘6Des questions sur votre propre trafic en langage courant, auxquelles un modèle local ou hébergé répond à partir de l’historique enregistré.
Inspecter⌘7 · Developer IDFacultatif : les requêtes déchiffrées des agents que vous faites passer par Flowlight, et les appels d’outils qui les ont causées. La recherche va jusque dans les corps de requête et de réponse.
Règles⌘8 · Developer ID
⌘7 · App Store
Chaque liste d’autorisation et chaque refus que vous avez posés, ce que chacun couvre, et ce qu’il a réellement arrêté.
Sources⌘9 · Developer ID
⌘8 · App Store
La santé et la récupération du collecteur, la couverture des noms d’hôte, ainsi que l’activité facultative des caméras, du Bluetooth, de l’USB et du stockage dans trois onglets.
Politiques⌘8 · Developer ID
⌘7 · App Store
Chaque politique configurée, son effet réel et un historique d’activité unifié en lecture seule.
Sources⌘9 · Developer ID
⌘8 · App Store
La santé et la récupération du collecteur, les canaux d’appareils facultatifs et la couverture sur une seule page opérationnelle déroulante.
-

Les raccourcis suivent l’ordre visible de la barre latérale. Les versions Developer ID incluent Inspecter en ⌘7 ; les versions App Store l’omettent, donc Règles et Sources passent à ⌘7 et ⌘8.

+

Les raccourcis suivent l’ordre visible de la barre latérale. Les versions Developer ID incluent Inspecter en ⌘7 ; les versions App Store l’omettent, donc Politiques et Sources passent à ⌘7 et ⌘8.

L’écran En direct : une liste d’applications avec leurs débits d’envoi et de réception actuels et leur destination la plus active.
En direct, l’écran sur lequel Flowlight s’ouvre.
@@ -146,7 +146,7 @@

Outils & serveurs MCP

dit plus que l’un ou l’autre seul, tandis qu’un domaine seulement configuré n’a jamais été joint, et qu’un domaine seulement observé n’était écrit nulle part où vous auriez pu le lire.

Chaque domaine propose deux refus : pour cet agent, ce qui couvre l’agent et les outils qu’il lance, ou - partout, ce qui couvre toutes les apps du Mac. Les deux écrivent une règle ordinaire, si bien que le flux des violations + partout, ce qui couvre toutes les apps du Mac. Les deux écrivent une règle ordinaire, si bien qu’Activité les signale et que l’éditeur de règles les explique — et, parce que ce sont des règles ordinaires, la simulation de l’éditeur vous dira ce que l’une d’elles aurait refusé avant que vous ne l’activiez.

Un type de domaine n’a pas de boutons. Un connecteur que le fournisseur de modèle joint de lui-même — déclaré dans le corps de la requête @@ -337,16 +337,16 @@

Réponses simulées

contactée : une simulation change la réponse, pas la possibilité d’établir la connexion, donc « l’API renvoie 500 » fonctionne alors que « l’API est complètement injoignable » non.

-

Règles

-

Une liste d’autorisation, c’est l’idée qu’un agent se fait des endroits où il peut aller. Une règle en est la version générale : elle nomme un +

Politiques

+

Politiques réunit tous les contrôles que Flowlight peut appliquer dans un même inventaire : règles ordinaires d’autorisation ou de blocage, listes d’autorisation des agents, packs gérés et, lorsque l’inspection HTTPS est disponible, garde-fous, simulations, réécritures de requêtes et transformations de réponses. Chaque ligne distingue l’état configuré de l’effet réel : appliquée, signalement uniquement, surveillée, en attente, programmée, suspendue, utilisée, en cours d’application, à vérifier, indisponible, incompatible ou en échec. La sélection d’une ligne ouvre l’éditeur contextuel auquel cette politique appartient ; Politiques ne remplace ni ne confond leurs différents modèles de consentement et de conservation.

+

Une liste d’autorisation, c’est l’idée qu’un agent se fait des endroits où il peut aller. Une règle ordinaire en est la version générale : elle nomme un sujet — une app, un agent, une destination, une URL, ou un couple formé de deux d’entre eux — et dit bloquer ou autoriser. Faites un clic droit sur n’importe quelle ligne dans En direct, Historique, Agents IA ou Inspecter pour en écrire une sur ce que vous regardez ; - toutes atterrissent dans le même éditeur, sur l’écran Règles, parce qu’une règle qui prendrait effet hors de votre vue - serait une règle que vous ne pourriez plus jamais retrouver.

+ chacune reste accessible dans Politiques et s’ouvre dans son éditeur de référence.

Des modèles. Vingt-deux, en cinq groupes — empêcher un agent d’atteindre des registres de paquets ou d’autres fournisseurs de modèles, fermer les voies habituelles par lesquelles les données partent (sites de partage de texte, collecteurs de webhooks, dépôts de fichiers anonymes, tunnels, DNS over HTTPS), l’analytique produit et les rapports de plantage, les politiques d’horaires de travail, et le « bloque-moi ça une heure, le temps que je comprenne - ce que c’est ». Parcourez-les depuis la barre d’outils de Règles, ou faites un clic droit sur une ligne n’importe où dans En direct, Historique, Agents IA ou Inspecter et + ce que c’est ». Parcourez-les depuis la barre d’outils de Politiques, ou faites un clic droit sur une ligne n’importe où dans En direct, Historique, Agents IA ou Inspecter et appliquez-en un à ce que vous regardez.

Chaque modèle annonce ce qu’il va faire avant de le faire. Tout ce qui écrit plus d’une règle ouvre une fiche listant chaque hôte et chaque horaire ; tout ce qui est permanent ouvre l’éditeur prérempli ; et seules les règles qui se défont d’elles-mêmes — une @@ -367,8 +367,7 @@

Règles

l’adresse et le port, rien des chemins, et uniquement en mode extension. L’inspection HTTPS refuse une requête : elle peut donc viser une URL et répondre par un statut que l’agent sait lire, au lieu d’une socket morte sans explication. L’échantillonneur nettop ne peut rien refuser du tout. Une règle que la configuration actuelle ne peut pas appliquer le dit dans la liste, plutôt que de rester là en faisant semblant.

-

Le second onglet est le flux des violations : chaque connexion qu’une règle a tranchée, avec l’app, sa destination, quelle - règle a tranché et s’il s’agissait d’un refus ou d’une exception qui l’a laissée passer.

+

La seconde vue est Activité, en lecture seule : elle réunit dans une même chronologie les décisions des règles ordinaires, les violations de listes d’autorisation, les événements des packs gérés et les effets distincts des politiques d’inspection. Elle adapte les enregistrements de référence existants au lieu de réécrire l’historique dans un modèle d’événement générique ; les versions App Store omettent les types de politiques dépendant de l’inspection et leur activité conservée.

Ce qu’elle aurait fait. Une règle de blocage est facile à écrire et effrayante à activer, parce que la première chose que l’on apprend d’ordinaire à son sujet, c’est ce qu’elle a cassé — et on l’apprend plus tard, parce que quelque chose @@ -389,7 +388,7 @@

Règles

d’historique a été testée sur deux heures d’historique.

Packs gérés de publicité et de traqueurs

-

Règles › Packs contient trois listes officielles de noms d’hôte : Publicité, Suivi intersites et Confidentialité et télémétrie. Elles sont visibles dès le départ et toutes désactivées par défaut. Le blocage nécessite l’Extension réseau ; l’échantillonneur nettop ne peut qu’observer.

+

Politiques › Packs gérés contient trois listes officielles de noms d’hôte : Publicité, Suivi intersites et Confidentialité et télémétrie. Elles sont visibles dès le départ et toutes désactivées par défaut. Le blocage nécessite l’Extension réseau ; l’échantillonneur nettop ne peut qu’observer.

Uniquement des mises à jour officielles et signées. Flowlight consulte une source HTTPS fixe unique pour rechercher des catalogues plus récents signés par l’éditeur. Les vérifications automatiques sont activées par défaut et peuvent être désactivées ici. Une signature authentifie le contenu, mais ne l’active pas : un nouveau pack reste désactivé et, si les destinations changent, l’application d’un pack activé est suspendue jusqu’à ce que vous examiniez la nouvelle version exacte et appuyiez sur Enregistrer. Pendant une panne, le contenu accepté peut rester disponible comme dernière version valide connue, visiblement signalée comme obsolète. Il s’agit de blocages réseau au niveau des noms d’hôte : ils ne peuvent pas supprimer les vides dans le DOM ou la mise en page, les superpositions du navigateur, les fenêtres contextuelles générées par des scripts ni les fenêtres d’applications natives.

Quand l’inspection HTTPS achemine une app par le proxy de Flowlight, celui-ci décide des règles de destination ordinaires et des packs gérés activés d’après l’identité d’origine de l’app, avant d’ouvrir toute connexion au serveur d’origine. Pour les tunnels CONNECT épinglés ou jamais déchiffrés, cela fonctionne par nom d’hôte ; les listes d’autorisation des agents restent limitées au chemin direct par l’Extension réseau. Si le propriétaire ou le nom d’hôte est inconnu, la connexion est autorisée.

@@ -438,8 +437,8 @@

Garde-fous des agents : bloquer un outil, un serveur ou

Ce n’est pas un bac à sable. Un outil refusé cesse d’être proposé et cesse d’obtenir une réponse. Un agent qui a encore un shell peut faire à la main ce que l’outil aurait fait. Flowlight l’enregistrera ; il ne prétend pas l’empêcher.

-

Sources : collecteur, couverture et appareils

-

Sources réunit la configuration opérationnelle. Son résumé de santé indique si le trafic arrive, quelle part est nommée et quels canaux d’appareils facultatifs sont actifs ; toute étape inachevée mène directement à Sources › Collecteur.

+

Sources : collecteur, appareils et couverture

+

Sources est une seule page opérationnelle déroulante : d’abord Collecteur, puis Appareils et Couverture dans des sections clairement titrées. Les avis d’état, les réponses de Demander, les fenêtres restaurées et les liens des captures d’écran font défiler jusqu’à la section concernée et la mettent brièvement en évidence sans masquer les autres.

Flowlight enregistre le trafic de deux façons, au choix dans Sources › Collecteur. Elles diffèrent par ce qu’elles peuvent manquer, ce qu’il vaut la peine de comprendre avant de compter sur l’une ou l’autre.

@@ -637,7 +636,7 @@

Questions & réponses

Fenêtre fermée, Flowlight ne fait presque rien : il tient à jour les débits de la barre des menus et écrit une ligne par seconde. L’extension réseau coûte moins que l’échantillonneur de secours, qui lance un processus nettop éphémère chaque seconde.
Peut-il bloquer des connexions ?
-
Oui, avec l’extension réseau. La liste d’autorisation d’un agent peut refuser autant qu’avertir, une règle peut bloquer une app, +
Oui, avec l’extension réseau. La liste d’autorisation d’un agent peut refuser autant qu’avertir, une règle peut bloquer une app, une destination ou une URL aussi longtemps que vous le dites, et un garde-fou peut retirer un outil à un agent avant que son modèle ne le voie. Rien ne bloque avant que vous le demandiez, et chaque refus porte sa sortie de secours.
Ai-je besoin de l’extension réseau ?
diff --git a/site/pages/fr/index.html b/site/pages/fr/index.html index 18ae3aab..dc2b9f1a 100644 --- a/site/pages/fr/index.html +++ b/site/pages/fr/index.html @@ -110,11 +110,11 @@

Historique

Où tout cela est-il parti ? De la seconde à l’ann

Alertes

Qu’est-ce qui clochait ? Des alertes explicables, par app, avec la destination et le seuil.

⌘5
-

Règles

Qu’ai-je refusé, et est-ce bien appliqué ? Chaque règle, ce qu’elle a bloqué, et le flux de toutes les décisions.

⌘8 · Developer ID / ⌘7 · App Store
+

Politiques

Quels contrôles existent, et sont-ils effectifs ? Chaque politique, son état réel et un historique d’activité unifié.

⌘8 · Developer ID / ⌘7 · App Store

Demander

Des questions en langage courant, auxquelles l’historique répond — avec la liste des requêtes exécutées juste en dessous.

⌘6
-

Sources

Les données arrivent-elles, quelle part est nommée et quels canaux d’appareils sont actifs ? Collecteur, Appareils et Couverture au même endroit.

⌘9 · Developer ID / ⌘8 · App Store
+

Sources

Les données arrivent-elles, quels canaux d’appareils sont actifs et de quoi Flowlight peut-il rendre compte ? Une seule page, du Collecteur à la Couverture.

⌘9 · Developer ID / ⌘8 · App Store

Inspecter

Facultatif. Qu’a réellement envoyé l’agent, et quel appel d’outil en est la cause ? Requêtes déchiffrées, appels d’outils, résultats.

⌘7 · Developer ID
diff --git a/site/pages/fr/privacy.html b/site/pages/fr/privacy.html index 34ae0a0b..6b2cbf4e 100644 --- a/site/pages/fr/privacy.html +++ b/site/pages/fr/privacy.html @@ -56,7 +56,7 @@

Les requêtes réseau que fait l’app

- + diff --git a/site/pages/index.html b/site/pages/index.html index 6251dd2e..7b412f6c 100644 --- a/site/pages/index.html +++ b/site/pages/index.html @@ -119,11 +119,11 @@

History

Where did it all go? From 15 minutes through all history, gro

Alerts

What looked wrong? Explainable, per-app alerts with the destination and the threshold.

⌘5
-

Rules

What have I refused, and is it happening? Every rule, what each has blocked, and a feed of every decision.

⌘8 · Developer ID / ⌘7 · App Store
+

Policies

What controls exist, and are they effective? Every policy, its real state, and a unified activity trail.

⌘8 · Developer ID / ⌘7 · App Store

Ask

Questions in plain language, answered from the history — with every query it ran listed underneath.

⌘6
-

Sources

Is data arriving, how much is named, and which device channels are active? Collector, Devices and Coverage in one place.

⌘9 · Developer ID / ⌘8 · App Store
+

Sources

Is data arriving, which device channels are active, and what can Flowlight account for? One page: Collector, Devices, then Coverage.

⌘9 · Developer ID / ⌘8 · App Store

Inspect

Optional. What did the agent actually send, and which tool call made it? Decrypted requests, tool calls, results.

⌘7 · Developer ID
diff --git a/site/pages/it/docs.html b/site/pages/it/docs.html index ede0992a..d476052f 100644 --- a/site/pages/it/docs.html +++ b/site/pages/it/docs.html @@ -30,7 +30,7 @@

Usare Flowlight

@@ -101,10 +101,10 @@

Le viste

- - + +
RequêteEnvoyée àCe qu’elle contientQuand
Recherche de mise à jourapi.github.comUne requête HTTPS standard pour la dernière version ; GitHub voit votre adresse IP et l’agent utilisateur Flowlight/<version>Au lancement et une fois par jour, depuis 0.1.2. Interrupteur d’arrêt dans Réglages › Général
Téléchargement de mise à jourgithub.comL’image disque et son fichier de somme de contrôleSeulement quand vous cliquez sur Télécharger & ouvrir
Mise à jour du pack de règles officielflowlight.xinbetween.comUne requête HTTPS standard vers le flux officiel fixe. Le serveur voit votre adresse IP, l’agent utilisateur Flowlight/<version> et les validateurs conditionnels du cache ; il ne reçoit ni trafic, ni réglages des packs, ni identifiant d’appareilAprès le démarrage, lorsqu’une vérification est due, puis au maximum une fois par jour tant que le service fonctionne normalement. Activée par défaut ; interrupteur de désactivation et option manuelle Rechercher maintenant dans Règles › Packs
Mise à jour du pack de règles officielflowlight.xinbetween.comUne requête HTTPS standard vers le flux officiel fixe. Le serveur voit votre adresse IP, l’agent utilisateur Flowlight/<version> et les validateurs conditionnels du cache ; il ne reçoit ni trafic, ni réglages des packs, ni identifiant d’appareilAprès le démarrage, lorsqu’une vérification est due, puis au maximum une fois par jour tant que le service fonctionne normalement. Activée par défaut ; interrupteur de désactivation et option manuelle Rechercher maintenant dans Politiques › Packs gérés
DNS inverseLe résolveur DNS que vous avez configuréUne recherche du nom enregistré d’une adresse IP, quand aucun nom d’hôte n’a été observéAu besoin, dans le mode de capture par échantillonnage, qui est celui par défaut jusqu’à ce que l’extension réseau tourne. Pas d’interrupteur d’arrêt : sans elle, l’échantillonneur ne peut pas nommer la plupart des connexions. L’extension réseau les nomme à partir du trafic lui-même et ne fait pas cette recherche
Recherche du propriétaire de réseauTeam Cymru, par DNSDes adresses IP publiques sans nom d’hôte connu. Les adresses privées et réservées ne quittent jamais votre MacActivé par défaut. Interrupteur d’arrêt dans Sources › Collecteur › Identifier les propriétaires de réseau
Demander avec un modèle hébergéLe fournisseur que vous choisissez : Anthropic, OpenAI, Google Gemini ou un point de terminaison compatibleVotre question et les résultats renvoyés par les requêtes Flowlight en lecture seule choisies par le modèle. La base de données de trafic elle-même n’est pas envoyéeSeulement quand vous choisissez un fournisseur hébergé et soumettez une question. L’écran Demander montre les requêtes transmises
Avvisi⌘5Tutto ciò che è sembrato insolito, con l'app, la destinazione e il numero che ha fatto scattare la regola.
Chiedi⌘6Domande sul tuo traffico in linguaggio naturale, a cui un modello locale o ospitato risponde a partire dalla cronologia registrata.
Ispeziona⌘7 · Developer IDOpzionale: le richieste decifrate degli agenti che instradi attraverso Flowlight e le chiamate agli strumenti che le hanno prodotte. La ricerca entra nei corpi di richieste e risposte.
Regole⌘8 · Developer ID
⌘7 · App Store
Ogni elenco consentiti e ogni rifiuto che hai impostato, cosa copre ciascuno e cosa ha fermato davvero.
Origini⌘9 · Developer ID
⌘8 · App Store
Stato del Collector, canali dei dispositivi e copertura dei nomi in tre schede.
Criteri⌘8 · Developer ID
⌘7 · App Store
Ogni criterio configurato, se è davvero effettivo e una cronologia unificata delle attività in sola lettura.
Origini⌘9 · Developer ID
⌘8 · App Store
Stato e ripristino del Collector, canali facoltativi dei dispositivi e copertura in un'unica pagina operativa a scorrimento.
-

Le scorciatoie seguono l'ordine visibile della barra laterale. Le versioni Developer ID includono Ispeziona su ⌘7; quelle App Store lo omettono, quindi Regole e Origini passano a ⌘7 e ⌘8.

+

Le scorciatoie seguono l'ordine visibile della barra laterale. Le versioni Developer ID includono Ispeziona su ⌘7; quelle App Store lo omettono, quindi Criteri e Origini passano a ⌘7 e ⌘8.

La schermata In diretta: un elenco di applicazioni con le loro velocità attuali di invio e ricezione e la destinazione più trafficata.
In diretta, la schermata con cui Flowlight si apre.
@@ -335,16 +335,20 @@

Risposte simulate

contattata: una simulazione cambia la risposta, non la possibilità di connettersi, quindi «l'API restituisce 500» funziona mentre «l'API è del tutto irraggiungibile» no.

-

Regole

-

Un elenco consentiti è l'idea che un singolo agente ha di dove può andare. Una regola è la versione generale: indica un +

Criteri

+

Criteri riunisce in un solo inventario tutti i controlli che Flowlight può applicare: normali regole di consenso o blocco, elenchi consentiti + degli agenti, pacchetti gestiti e, dove è disponibile l'ispezione HTTPS, protezioni, simulazioni, riscritture delle richieste e trasformazioni delle + risposte. Ogni riga distingue lo stato configurato dall'effetto reale: applicazione attiva, sola segnalazione, monitoraggio, attesa, pianificato, + in pausa, esaurito, in corso di applicazione, revisione necessaria, non disponibile, incompatibile o non riuscito. Selezionando una riga si apre l'editor + contestuale responsabile di quel criterio; Criteri non sostituisce né confonde i distinti modelli di consenso e persistenza.

+

Un elenco consentiti è l'idea che un singolo agente ha di dove può andare. Una normale regola è la versione generale: indica un soggetto — un'app, un agente, una destinazione, un URL o una coppia di due di questi — e dice blocca o consenti. Fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona per scriverne una su ciò che stai guardando; - tutte finiscono nello stesso editor della schermata Regole, perché una regola che entra in vigore fuori dalla vista - sarebbe una regola che non potresti più ritrovare.

+ ognuna resta reperibile in Criteri e si apre nel proprio editor autorevole.

Modelli. Ventidue, in cinque gruppi: impedire a un agente di raggiungere i registri di pacchetti o altri provider di modelli, chiudere le vie più comuni per cui i dati escono (siti di paste, raccoglitori di webhook, servizi anonimi per caricare file, tunnel, DNS over HTTPS), analisi di prodotto e segnalazione degli arresti anomali, politiche sull'orario di lavoro, e il classico «blocca questo per un'ora mentre capisco - cos'è». Sfogliali dalla barra strumenti di Regole, oppure fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona e + cos'è». Sfogliali dalla barra strumenti di Criteri, oppure fai clic con il tasto destro su una riga in In diretta, Cronologia, Agenti IA o Ispeziona e applicane uno a ciò che stai guardando.

Ogni modello dice cosa farà prima di farlo. Tutto ciò che scrive più di una regola apre una scheda con l'elenco di ogni host e orario; tutto ciò che è permanente apre l'editor già compilato; e solo le regole che si annullano da sole — un'ora, @@ -365,8 +369,9 @@

Regole

l'indirizzo e la porta, nulla dei percorsi, e solo in modalità estensione. L'ispezione HTTPS rifiuta una singola richiesta, quindi può corrispondere a un URL e rispondere con uno stato che l'agente può leggere, invece di un socket morto senza spiegazioni. Il campionatore nettop non può rifiutare nulla. Una regola che la configurazione attuale non può applicare lo dice nell'elenco, invece di starsene lì con l'aria di essere in vigore.

-

Il secondo pannello è il flusso delle violazioni: ogni connessione decisa da una regola, con l'app, dove era diretta, quale - regola ha deciso e se si è trattato di un rifiuto o di un'eccezione che l'ha lasciata passare.

+

La seconda vista è Attività, in sola lettura: riunisce in un'unica cronologia le decisioni delle regole comuni, le violazioni degli elenchi consentiti, + gli eventi dei pacchetti gestiti e gli effetti distinti dei criteri di ispezione. Usa i record originali, ciascuno autorevole nel proprio ambito, invece di + riscrivere la cronologia in un unico modello generico di evento; le versioni Store omettono i tipi di criterio che dipendono dall'ispezione e le relative attività conservate.

Cosa avrebbe fatto. Una regola di blocco è facile da scrivere e spaventosa da attivare, perché la prima cosa che normalmente scopri di una regola così è che cosa ha rotto — e lo scopri dopo, da qualcosa che smette di funzionare altrove. @@ -387,7 +392,7 @@

Regole

testata su due ore di storico.

Pacchetti gestiti per pubblicità e tracker

-

Regole › Pacchetti contiene tre elenchi ufficiali di nomi host: Pubblicità, Tracciamento tra siti e Privacy e telemetria. Sono visibili fin dall’inizio e tutti disattivati per impostazione predefinita. Il blocco richiede l’Estensione di rete; il campionatore nettop può solo osservare.

+

Criteri › Pacchetti gestiti contiene tre elenchi ufficiali di nomi host: Pubblicità, Tracciamento tra siti e Privacy e telemetria. Sono visibili fin dall’inizio e tutti disattivati per impostazione predefinita. Il blocco richiede l’Estensione di rete; il campionatore nettop può solo osservare.

Solo aggiornamenti ufficiali e firmati. Flowlight controlla un’unica fonte HTTPS fissa per trovare cataloghi più recenti firmati dall’editore. I controlli automatici sono attivi per impostazione predefinita e possono essere disattivati qui. Una firma autentica il contenuto, ma non lo attiva: un nuovo pacchetto resta disattivato e, se cambiano le destinazioni, l’applicazione di un pacchetto attivo viene sospesa finché non esamini l’esatta nuova versione e premi Salva. Durante un’interruzione, i contenuti accettati possono restare disponibili come ultima versione valida nota, visibilmente indicata come obsoleta. Si tratta di blocchi di rete a livello di nome host: non possono eliminare spazi vuoti nel DOM o nel layout, sovrapposizioni del browser, popup generati da script o finestre di app native.

Quando l’ispezione HTTPS instrada un’app attraverso il proxy di Flowlight, questo decide le normali regole di destinazione e i pacchetti gestiti attivi usando l’identità originale dell’app prima di aprire qualsiasi connessione al server di origine. Per i tunnel CONNECT con pinning o mai decifrati, ciò funziona in base al nome host; gli elenchi consentiti degli agenti restano limitati al percorso diretto attraverso l’Estensione di rete. Se il proprietario o il nome host è sconosciuto, la connessione viene consentita.

@@ -436,7 +441,10 @@

Protezioni per gli agenti: bloccare uno strumento, un server

Questa non è una sandbox. Uno strumento rifiutato non viene più offerto e non riceve più risposta. Un agente che ha ancora una shell può fare a mano ciò che avrebbe fatto lo strumento. Flowlight lo registra; non pretende di impedirlo.

-

Origini: Collector e motori

+

Origini: Collector, dispositivi e copertura

+

Origini è un'unica pagina operativa a scorrimento: prima il Collector, poi Dispositivi e Copertura come sezioni chiaramente + contrassegnate. Gli avvisi di stato, le risposte di Chiedi, le finestre ripristinate e i collegamenti usati per gli screenshot scorrono fino alla sezione pertinente + e la mettono brevemente in evidenza senza nascondere le altre.

Flowlight registra il traffico in uno di due modi, da scegliere in Origini › Collector. Differiscono per ciò che possono perdere, e vale la pena capirlo prima di affidarsi all'uno o all'altro.

@@ -634,7 +642,7 @@

Domande e risposte

Con la finestra chiusa Flowlight fa quasi nulla: mantiene le velocità nella barra dei menu e scrive una riga al secondo. L'estensione di rete costa meno del campionatore di riserva, che avvia un breve processo nettop ogni secondo.
Può bloccare le connessioni?
-
Sì, con l'estensione di rete. L'elenco consentiti di un agente può rifiutare oltre che avvisare, una regola può bloccare un'app, +
Sì, con l'estensione di rete. L'elenco consentiti di un agente può rifiutare oltre che avvisare, una regola può bloccare un'app, una destinazione o un URL per il tempo che dici tu, e una protezione può togliere uno strumento a un agente prima che venga offerto al suo modello. Nulla blocca finché non glielo chiedi, e ogni rifiuto porta con sé la sua via d'uscita.
Mi serve l'estensione di rete?
diff --git a/site/pages/it/index.html b/site/pages/it/index.html index 62f4c544..7bb0f3a6 100644 --- a/site/pages/it/index.html +++ b/site/pages/it/index.html @@ -110,11 +110,11 @@

Cronologia

Dove è finito tutto? Dal secondo all'anno, per app, desti

Avvisi

Cosa non tornava? Avvisi per app, spiegati, con la destinazione e la soglia.

⌘5
-

Regole

Cosa ho rifiutato, e sta funzionando? Ogni regola, che cosa ha bloccato e il flusso di tutte le decisioni.

⌘8 · Developer ID / ⌘7 · App Store
+

Criteri

Quali controlli esistono, e sono effettivi? Ogni criterio, il suo stato reale e una cronologia unificata delle attività.

⌘8 · Developer ID / ⌘7 · App Store

Chiedi

Domande in linguaggio naturale, con risposte tratte dalla cronologia e sotto l'elenco di tutte le query eseguite.

⌘6
-

Origini

I dati stanno arrivando, quanti hanno un nome e quali canali dei dispositivi sono attivi? Collector, Dispositivi e Copertura in un unico posto.

⌘9 · Developer ID / ⌘8 · App Store
+

Origini

I dati stanno arrivando, quali canali dei dispositivi sono attivi e che cosa può contabilizzare Flowlight? Un'unica pagina, dal Collector alla Copertura.

⌘9 · Developer ID / ⌘8 · App Store

Ispeziona

Opzionale. Cosa ha inviato davvero l'agente, e quale chiamata a uno strumento l'ha prodotta? Richieste decifrate, chiamate agli strumenti, risultati.

⌘7 · Developer ID
diff --git a/site/pages/it/privacy.html b/site/pages/it/privacy.html index 7dc7a416..67dde5bf 100644 --- a/site/pages/it/privacy.html +++ b/site/pages/it/privacy.html @@ -56,7 +56,7 @@

Richieste di rete che fa l'app

- + diff --git a/site/pages/ja/docs.html b/site/pages/ja/docs.html index 4d22a593..d20ddb6e 100644 --- a/site/pages/ja/docs.html +++ b/site/pages/ja/docs.html @@ -30,7 +30,7 @@

Flowlight の使い方

@@ -99,10 +99,10 @@

各ビュー

- - + +
RichiestaInviata aCosa contieneQuando
Controllo aggiornamentiapi.github.comUna normale richiesta HTTPS per l'ultima versione; GitHub vede il tuo indirizzo IP e lo user agent Flowlight/<version>All'avvio e una volta al giorno, dalla 0.1.2. Interruttore per disattivarla in Impostazioni › Generali
Download dell'aggiornamentogithub.comL'immagine disco e il suo file di checksumSolo quando fai clic su Scarica e apri
Aggiornamento ufficiale del pacchetto di regoleflowlight.xinbetween.comUna richiesta HTTPS standard al feed ufficiale fisso. Il server vede il tuo indirizzo IP, lo user agent Flowlight/<version> e i validatori condizionali della cache; non riceve traffico, impostazioni dei pacchetti o identificatori del dispositivoDopo l’avvio, quando previsto, e successivamente non più di una volta al giorno durante il normale funzionamento. Attivo per impostazione predefinita; interruttore per disattivarlo e opzione manuale Controlla ora in Regole › Pacchetti
Aggiornamento ufficiale del pacchetto di regoleflowlight.xinbetween.comUna richiesta HTTPS standard al feed ufficiale fisso. Il server vede il tuo indirizzo IP, lo user agent Flowlight/<version> e i validatori condizionali della cache; non riceve traffico, impostazioni dei pacchetti o identificatori del dispositivoDopo l’avvio, quando previsto, e successivamente non più di una volta al giorno durante il normale funzionamento. Attivo per impostazione predefinita; interruttore per disattivarlo e opzione manuale Controlla ora in Criteri › Pacchetti gestiti
DNS inversoIl resolver DNS che hai configuratoUna ricerca del nome registrato di un indirizzo IP, quando non è stato rilevato alcun nome hostQuando serve, nella modalità di acquisizione a campionamento, che è quella predefinita finché l'estensione di rete non è in funzione. Nessun interruttore per disattivarla: senza di essa il campionatore non può dare un nome alla maggior parte delle connessioni. L'estensione di rete le nomina dal traffico stesso e non fa questa ricerca
Ricerca del proprietario della reteTeam Cymru, via DNSIndirizzi IP pubblici senza nome host noto. Gli indirizzi privati e riservati non lasciano mai il tuo MacAttiva per impostazione predefinita. Interruttore per disattivarla in Acquisizione › Identifica i proprietari della rete
Chiedi con un modello ospitatoIl provider che selezioni: Anthropic, OpenAI, Google Gemini o un endpoint compatibileLa tua domanda e i risultati restituiti dalle query di sola lettura di Flowlight selezionate dal modello. Il database del traffico non viene caricatoSolo quando selezioni un provider ospitato e invii una domanda. La schermata Chiedi mostra le richieste trasmesse
アラート⌘5いつもと違って見えたものすべて。アプリ、接続先、そしてルールのしきい値を超えた数値とともに。
質問⌘6自分のトラフィックについての、ふつうの言葉での質問。記録された履歴から、ローカルまたはホスト型のモデルが答えます。
インスペクト⌘7 · Developer ID任意。Flowlight を経由させたエージェントの復号済みリクエストと、その裏にあるツール呼び出し。検索はリクエストとレスポンスの本文にも届きます。
ルール⌘8 · Developer ID
⌘7 · App Store
設定したすべての許可リストと拒否。それぞれが何を対象にし、実際に何を止めたか。
ソース⌘9 · Developer ID
⌘8 · App Store
コレクターの状態、デバイスのチャンネル、名前付けのカバー範囲を 3 つのタブで表示。
ポリシー⌘8 · Developer ID
⌘7 · App Store
設定されたすべてのポリシー、その実際の適用状況、そして統合された読み取り専用のアクティビティ履歴。
ソース⌘9 · Developer ID
⌘8 · App Store
コレクターの状態と復旧、任意で有効にするデバイスチャンネル、カバー範囲を、1 つのスクロール可能な運用ページに表示。
-

ショートカットは表示中のサイドバー順です。Developer ID 版は ⌘7 にインスペクトがあります。App Store 版では省かれるため、ルールとソースは ⌘7 と ⌘8 になります。

+

ショートカットは表示中のサイドバー順です。Developer ID 版は ⌘7 にインスペクトがあります。App Store 版では省かれるため、ポリシーとソースは ⌘7 と ⌘8 になります。

ライブビュー。アプリの一覧に、現在の送信と受信の速度、そして最も忙しい接続先が並んでいます。
ライブ。Flowlight が最初に開く画面です。
@@ -142,7 +142,7 @@

ツールと MCP サーバ

書かれていてしかも実際に接続もあったということは、どちらか一方だけよりも多くを語るからです。設定されているだけの ドメインには一度も届いておらず、観測されただけのドメインは、前もって読めるどこにも書かれていませんでした。

ドメインごとに拒否の仕方が二つあります。このエージェントに対してはエージェントとそれが起動するツールを覆い、どこでもは - この Mac のすべてのアプリを覆います。どちらもふつうのルールを書きます。ですから違反の履歴がそれを報告し、ルールのエディタが + この Mac のすべてのアプリを覆います。どちらもふつうのルールを書きます。ですからアクティビティがそれを報告し、ルールのエディタが それを説明します。そしてふつうのルールなので、有効にする前に、エディタのシミュレーションがそのルールなら何を拒否していたかを 教えてくれます。

ボタンの付かないドメインが一種類あります。モデルプロバイダが自分で接続するコネクタです(リクエストの本文で宣言され、 @@ -314,14 +314,19 @@

モック応答

接続先への接続そのものは行われるということです。モックが変えるのは答えであって、接続できるかどうかではありません。つまり「API が 500 を返す」は再現できますが、 「API にまったく到達できない」は再現できません。

-

ルール

-

許可リストは、1 つのエージェントについて「どこへ行ってよいか」を定めたものです。ルールはその一般版で、 +

ポリシー

+

ポリシーは、Flowlight が適用できるすべての制御をまとめた一覧です。通常の許可・ブロックルール、エージェントの + 許可リスト、管理対象パック、さらに HTTPS インスペクションを利用できる場合は、ガードレール、モック、リクエストの書き換え、レスポンスの + 変換も含まれます。各行では、設定された状態と実際の効果を区別し、適用中、報告のみ、監視中、待機中、スケジュール待ち、一時停止、使用済み、 + 適用処理中、要確認、利用不可、互換性なし、失敗のいずれかを示します。行を選ぶと、そのポリシーを管理する本来のエディタが開きます。 + ポリシー画面が、それぞれ異なる同意や保存の仕組みを置き換えたり、曖昧にしたりすることはありません。

+

許可リストは、1 つのエージェントについて「どこへ行ってよいか」を定めたものです。通常のルールはその一般版で、 対象(アプリ、エージェント、接続先、URL、またはそのうち 2 つの組み合わせ)を指定し、ブロックか 許可を決めます。ライブ、履歴、AI エージェント、インスペクトのどの行でも右クリックすれば、いま見ているものについてルールを書けます。 - そのどれもがルール画面の同じエディタに集まります。見えないところで効き始めるルールは、二度と見つけられないルールになってしまうからです。

+ どこから作成したルールもポリシー画面で見つけられ、選ぶとそのルールを管理する本来のエディタが開きます。

テンプレート。22 種類あり、5 つのグループに分かれています。エージェントがパッケージレジストリやほかのモデルプロバイダへ届かないようにするもの、 データが出ていきやすい経路をふさぐもの(ペーストサイト、Webhook の受け口、匿名のファイル置き場、トンネル、DNS over HTTPS)、プロダクト分析とクラッシュレポート、 - 勤務時間のポリシー、そして「作業のあいだ 1 時間だけこれをブロックして、何なのか調べる」という単発のものです。ルールのツールバーから一覧を見るか、 + 勤務時間のポリシー、そして「作業のあいだ 1 時間だけこれをブロックして、何なのか調べる」という単発のものです。ポリシーのツールバーから一覧を見るか、 ライブ、履歴、AI エージェント、インスペクトのどこかで行を右クリックして、いま見ているものに当てはめてください。

どのテンプレートも、実行する前に何をするかを述べます。2 つ以上のルールを書くものは、すべてのホストとスケジュールを並べたカードを開きます。恒久的なものは あらかじめ入力されたエディタを開きます。そして自分で元に戻るルールだけ(1 時間、またはこの回の Flowlight の実行中だけ)が、先に示されることなく @@ -339,8 +344,9 @@

ルール

しかも機能拡張モードのときだけです。HTTPS インスペクションは 1 つのリクエストを拒否するので、URL に一致させられますし、理由も告げずに死んだソケットの代わりに、 エージェントが読めるステータスで応答できます。nettop サンプラーは何も拒否できません。現在の構成では実行できないルールは、効いているように見せかけて並ぶのではなく、 一覧にそう表示されます。

-

2 つめのタブは違反の履歴です。ルールが判断したすべての接続について、アプリ、向かおうとしていた先、どのルールが判断したか、 - そしてそれが拒否だったのか、通過を許した例外だったのかが並びます。

+

2 つめのビューは読み取り専用のアクティビティです。通常のルールによる判断、許可リスト違反、管理対象パックのイベント、 + インスペクションポリシー固有の効果を、1 つの時系列にまとめます。すでにある正式な記録をそのまま参照し、履歴を汎用イベントへ + 書き換えることはありません。App Store 版では、インスペクションに依存するポリシーの種類と、それに保存されたアクティビティは表示されません。

このルールが何をしていたか。ブロックのルールは、書くのは簡単でも、有効にするのは怖いものです。そのルールについて最初に分かるのは、 ふつう何を壊したかであり、しかもそれが分かるのは後になってから、どこか別の場所で何かが失敗したときだからです。ルールのエディタは、そこに先に答えます。 @@ -358,7 +364,7 @@

ルール

履歴でテストしたルールは、2 時間分の履歴でテストしたということです。

管理対象の広告・トラッカーパック

-

ルール › パックには、広告、サイト横断トラッキング、プライバシーとテレメトリという3つの公式ホスト名リストがあります。これらは最初から表示され、すべてデフォルトでオフになっています。ブロックにはネットワーク機能拡張が必要です。nettop サンプラーは監視のみ可能です。

+

ポリシー › 管理対象パックには、広告、サイト横断トラッキング、プライバシーとテレメトリという3つの公式ホスト名リストがあります。これらは最初から表示され、すべてデフォルトでオフになっています。ブロックにはネットワーク機能拡張が必要です。nettop サンプラーは監視のみ可能です。

公式の署名付きアップデートのみ。 Flowlight は、公開元が署名した新しいカタログがないか、1つの固定 HTTPS ソースを確認します。自動確認はデフォルトでオンになっており、ここでオフにできます。署名はコンテンツを認証しますが、有効化はしません。新しいパックはオフのままで、宛先が変更された場合は、正確な新バージョンを確認して保存を押すまで、有効なパックの適用が停止されます。障害中も、承認済みコンテンツが、古いことを明示した既知の正常な最終版として保持される場合があります。これらはホスト名単位のネットワークブロックです。DOM やレイアウトの空白、ブラウザのオーバーレイ、スクリプトによるポップアップ、ネイティブアプリのウインドウは除去できません。

HTTPS インスペクションがアプリを Flowlight のプロキシ経由にすると、接続先の通常ルールと有効な管理対象パックは、接続先サーバへの接続が開かれる前に、元のアプリの識別情報を使って判定されます。証明書がピン留めされている、または復号されない CONNECT トンネルでも、ホスト名によって判定できます。エージェントの許可リストは引き続き、ネットワーク機能拡張を直接通る経路だけに適用されます。所有者またはホスト名が不明な場合は、接続を許可します。

@@ -401,7 +407,10 @@

エージェントのガードレール:ツール、サー

これはサンドボックスではありません。拒否したツールは提示されなくなり、呼び出しにも応答しなくなります。しかしシェルを使えるエージェントは、 そのツールがしていたことを手作業で行えます。Flowlight はそれを記録しますが、防げるふりはしません。

-

ソース:コレクターとエンジン

+

ソース:コレクター、デバイス、カバー範囲

+

ソースは、コレクター、デバイス、カバー範囲の順に、見出しで区切って並べた 1 つのスクロール可能な運用ページです。 + 状況の通知、質問への回答、復元されたウインドウ、スクリーンショットからのリンクは、ほかのセクションを隠さず、該当するセクションまで + スクロールして短時間強調表示します。

Flowlight がトラフィックを記録する方法は 2 つあり、ソース › コレクターで選びます。何を取りこぼしうるかが違うので、どちらかに頼る前に 知っておく価値があります。

@@ -584,7 +593,7 @@

質問と回答

ウインドウを閉じているあいだ、Flowlight はほとんど何もしません。メニューバーの速度を保ち、1 秒に 1 行書き込むだけです。ネットワーク機能拡張のほうが、 1 秒ごとに短命な nettop プロセスを起動する代替のサンプラーよりも負荷が低いです。
接続をブロックできますか?
-
はい、ネットワーク機能拡張があれば。エージェントの許可リストは警告だけでなく拒否もでき、ルールはアプリ、接続先、URL を +
はい、ネットワーク機能拡張があれば。エージェントの許可リストは警告だけでなく拒否もでき、ルールはアプリ、接続先、URL を 好きな期間ブロックでき、ガードレールはモデルに提示される前にエージェントからツールを取り上げられます。 あなたが指示するまで何もブロックされず、どの拒否にも抜け道が添えてあります。
ネットワーク機能拡張は必要ですか?
diff --git a/site/pages/ja/index.html b/site/pages/ja/index.html index b6fdc3c9..8204db10 100644 --- a/site/pages/ja/index.html +++ b/site/pages/ja/index.html @@ -110,11 +110,11 @@

履歴

結局どこへ行った?秒から年まで、アプリ・

アラート

何がおかしかった?理由がわかるアプリ単位のアラートに、接続先としきい値を添えて。

⌘5
-

ルール

何を拒否し、それは効いている?すべてのルール、それぞれがブロックしたもの、判定の履歴。

⌘8 · Developer ID / ⌘7 · App Store
+

ポリシー

どんな制御があり、実際に効いている?すべてのポリシー、その実際の状態、統合されたアクティビティ履歴。

⌘8 · Developer ID / ⌘7 · App Store

質問

ふつうの言葉で聞けば、履歴から答えます。実行したクエリもすべて下に並びます。

⌘6
-

ソース

データは届いているか、どれだけ名前が付いているか、どのデバイス経路が有効か。コレクター、デバイス、カバー範囲を 1 か所に。

⌘9 · Developer ID / ⌘8 · App Store
+

ソース

データは届いているか、どのデバイス経路が有効か、Flowlight はどこまで把握できるか。コレクターからカバー範囲までを 1 ページに。

⌘9 · Developer ID / ⌘8 · App Store

インスペクト

任意。エージェントは実際に何を送り、どのツール呼び出しが原因だった?復号したリクエスト、ツール呼び出し、結果。

⌘7 · Developer ID
diff --git a/site/pages/ja/privacy.html b/site/pages/ja/privacy.html index 0d86c38f..c3b87ba4 100644 --- a/site/pages/ja/privacy.html +++ b/site/pages/ja/privacy.html @@ -54,7 +54,7 @@

アプリが行うネットワークリクエスト

- + diff --git a/site/pages/ko/docs.html b/site/pages/ko/docs.html index 78e4f9bb..f42e5244 100644 --- a/site/pages/ko/docs.html +++ b/site/pages/ko/docs.html @@ -30,7 +30,7 @@

Flowlight 사용하기

@@ -100,10 +100,10 @@

여러 뷰

- - + +
リクエスト送信先含まれるものタイミング
アップデートの確認api.github.com最新リリースを尋ねる通常の HTTPS リクエスト。GitHub が見るのは、IP アドレスと Flowlight/<version> というユーザーエージェントです起動時と 1 日 1 回(0.1.2 以降)。オフのスイッチは設定 › 一般
アップデートのダウンロードgithub.comディスクイメージと、そのチェックサムファイル「ダウンロードして開く」をクリックしたときだけ
公式ルールパックのアップデートflowlight.xinbetween.com固定された公式フィードへの標準 HTTPS リクエストです。サーバには、IP アドレス、Flowlight/<version>ユーザーエージェント、条件付きキャッシュバリデータが表示されます。トラフィック、パック設定、デバイス識別子は送信されません起動後、確認期限に達している場合に実行し、その後は正常稼働中に1日最大1回実行します。デフォルトでオンです。ルール › パックでオフにするか、今すぐ確認を手動で実行できます
公式ルールパックのアップデートflowlight.xinbetween.com固定された公式フィードへの標準 HTTPS リクエストです。サーバには、IP アドレス、Flowlight/<version>ユーザーエージェント、条件付きキャッシュバリデータが表示されます。トラフィック、パック設定、デバイス識別子は送信されません起動後、確認期限に達している場合に実行し、その後は正常稼働中に1日最大1回実行します。デフォルトでオンです。ポリシー › 管理対象パックでオフにするか、「今すぐ確認」を手動で実行できます
逆引き DNSあなたが設定した DNS リゾルバホスト名が見つからなかった場合の、IP アドレスの登録名の照会必要に応じて、サンプラーのキャプチャモードで。ネットワーク機能拡張が動き出すまではこれがデフォルトです。オフのスイッチはありません。これがないとサンプラーはほとんどの接続に名前を付けられないからです。ネットワーク機能拡張はトラフィック自身から名前を付けるので、これを行いません
ネットワーク所有者の照会Team Cymru(DNS 経由)ホスト名がわからないパブリック IP アドレス。プライベートアドレスと予約アドレスは、この Mac から決して出ませんデフォルトでオン。オフのスイッチはキャプチャ › ネットワーク所有者を特定
ホスト型モデルでの質問あなたが選んだプロバイダ:Anthropic、OpenAI、Google Gemini、または互換のエンドポイントあなたの質問と、モデルが選んだ読み取り専用の Flowlight の問い合わせが返した結果。トラフィックのデータベース自体はアップロードされませんホスト型のプロバイダを選び、質問を送信したときだけ。質問の画面には、送信されたリクエストが表示されます
알림⌘5이상해 보인 모든 것. 앱과 목적지, 그리고 규칙의 기준을 넘긴 수치와 함께.
질문⌘6내 트래픽에 대한 평범한 말로 된 질문. 기록된 이력에서 로컬 모델이나 호스팅 모델이 답합니다.
검사⌘7 · Developer ID선택 사항. Flowlight를 거치도록 지정한 에이전트의 복호화된 요청과 그 뒤에 있는 도구 호출. 검색은 요청·응답 본문까지 들어갑니다.
규칙⌘8 · Developer ID
⌘7 · App Store
설정해 둔 모든 허용 목록과 거부, 각각이 무엇을 포괄하는지, 그리고 실제로 무엇을 막았는지.
소스⌘9 · Developer ID
⌘8 · App Store
컬렉터 상태, 기기 채널, 이름 지정 포착 범위를 세 개의 탭에서 표시합니다.
정책⌘8 · Developer ID
⌘7 · App Store
설정된 모든 정책과 실제 적용 상태, 그리고 하나로 모은 읽기 전용 활동 기록.
소스⌘9 · Developer ID
⌘8 · App Store
컬렉터 상태와 복구, 선택형 기기 채널, 포착 범위를 하나의 스크롤 가능한 운영 페이지에서 봅니다.
-

단축키는 보이는 사이드바 순서를 따릅니다. Developer ID 빌드에는 ⌘7의 검사가 있고, App Store 빌드에는 없으므로 규칙과 소스가 ⌘7과 ⌘8로 이동합니다.

+

단축키는 보이는 사이드바 순서를 따릅니다. Developer ID 빌드에는 ⌘7의 검사가 있고, App Store 빌드에는 없으므로 정책과 소스가 ⌘7과 ⌘8로 이동합니다.

실시간 뷰: 앱 목록에 각각의 현재 보내기·받기 속도와 가장 바쁜 목적지가 함께 나열되어 있습니다.
실시간, Flowlight가 처음 여는 화면.
@@ -144,7 +144,7 @@

도구와 MCP 서버

것은 둘 중 하나보다 더 많은 것을 말해 줍니다. 설정만 되어 있는 도메인은 한 번도 닿은 적이 없고, 관찰만 된 도메인은 미리 읽어 볼 수 있는 어디에도 적혀 있지 않았습니다.

도메인마다 거부하는 방법이 두 가지입니다. 이 에이전트에 대해는 에이전트와 그것이 시작하는 도구를 덮고, - 어디서나는 이 Mac의 모든 앱을 덮습니다. 둘 다 평범한 규칙을 씁니다. 그래서 위반 기록이 그것을 + 어디서나는 이 Mac의 모든 앱을 덮습니다. 둘 다 평범한 규칙을 씁니다. 그래서 활동이 그것을 보고하고 규칙 편집기가 그것을 설명하며, 평범한 규칙이기 때문에 편집기의 시뮬레이션이 규칙을 켜기 전에 그것이 무엇을 거부했을지 알려 줍니다.

버튼이 붙지 않는 도메인이 한 종류 있습니다. 모델 제공업체가 스스로 닿는 커넥터입니다(요청 본문에 선언되고 제공업체 쪽에서 @@ -333,15 +333,19 @@

모의 응답

그리고 목적지에는 여전히 연결합니다. 모의 응답은 답을 바꾸는 것이고 연결 자체가 되는지를 바꾸는 것이 아니므로, “API가 500을 돌려준다”는 만들 수 있지만 “API에 아예 닿지 않는다”는 만들 수 없습니다.

-

규칙

-

허용 목록은 에이전트 하나가 어디까지 갈 수 있는지에 대한 이야기입니다. 규칙은 그것의 일반형입니다. +

정책

+

정책은 Flowlight가 적용할 수 있는 모든 제어를 한데 모은 목록입니다. 일반 허용·차단 규칙, 에이전트 허용 목록, + 관리형 팩, 그리고 HTTPS 검사를 사용할 수 있을 때는 가드레일, 모의 응답, 요청 다시 쓰기, 응답 변환까지 포함합니다. 각 행은 설정된 상태와 + 실제 효력을 구분해 적용 중, 보고만 함, 감시 중, 대기 중, 예약됨, 일시 중지, 사용 완료, 적용 중, 검토 필요, 사용 불가, 호환되지 않음, + 실패를 표시합니다. 행을 선택하면 그 정책을 관리하는 원래 편집기가 열립니다. 정책 화면이 서로 다른 동의 방식과 저장 방식을 대신하거나 + 뒤섞지는 않습니다.

+

허용 목록은 에이전트 하나가 어디까지 갈 수 있는지에 대한 이야기입니다. 일반 규칙은 그것의 일반형입니다. 대상, 즉 앱이나 에이전트, 목적지, URL, 또는 그 둘의 조합을 지목하고 차단인지 허용인지 정합니다. 실시간이나 기록, AI 에이전트, 검사에서 아무 행이나 오른쪽 클릭하면 지금 보고 있는 것에 - 대한 규칙을 쓸 수 있습니다. 어느 경로로 만들어도 모두 규칙 화면의 같은 편집기로 모입니다. 보이지 않는 곳에서 - 효력을 갖는 규칙은 다시는 찾을 수 없는 규칙이 되기 때문입니다.

+ 대한 규칙을 쓸 수 있습니다. 어느 경로에서 만들었든 정책 화면에서 찾을 수 있고, 선택하면 그 규칙을 관리하는 원래 편집기가 열립니다.

템플릿. 스물두 개가 다섯 묶음으로 있습니다. 에이전트가 패키지 레지스트리나 다른 모델 제공업체에 닿지 못하게 막는 것, 데이터가 나가는 흔한 경로를 닫는 것(붙여넣기 사이트, 웹훅 수집기, 익명 파일 업로드, 터널, DNS over HTTPS), 제품 분석과 - 크래시 보고, 업무 시간 정책, 그리고 “이게 뭔지 알아내는 동안 한 시간만 막아 두기” 같은 일회성 규칙입니다. 규칙 툴바에서 + 크래시 보고, 업무 시간 정책, 그리고 “이게 뭔지 알아내는 동안 한 시간만 막아 두기” 같은 일회성 규칙입니다. 정책 툴바에서 둘러보거나, 실시간·기록·AI 에이전트·검사의 아무 행에서 오른쪽 클릭해 지금 보고 있는 것에 적용하세요.

각 템플릿은 무엇을 할지 먼저 알려줍니다. 규칙을 두 개 이상 쓰는 템플릿은 모든 호스트와 일정을 나열한 카드를 열고, 영구적인 것은 미리 채워진 편집기를 열며, 스스로 사라지는 규칙, 즉 한 시간짜리나 이번 Flowlight 실행 동안만 유효한 @@ -363,8 +367,9 @@

규칙

경로는 모르고, 확장 모드에서만 동작합니다. HTTPS 검사는 요청 하나를 거부하므로 URL까지 일치시킬 수 있고, 이유도 없이 죽은 소켓 대신 에이전트가 읽을 수 있는 상태 코드로 답할 수 있습니다. nettop 샘플러는 아무것도 거부하지 못합니다. 현재 설정으로 실행할 수 없는 규칙은 적용된 척 앉아 있지 않고 목록에서 그렇다고 밝힙니다.

-

두 번째 탭은 위반 기록입니다. 규칙이 판정한 모든 연결이, 앱과 향하던 곳, 어떤 규칙이 판정했는지, 그리고 - 그것이 거부였는지 통과시킨 예외였는지와 함께 나옵니다.

+

두 번째 뷰는 읽기 전용 활동입니다. 일반 규칙 판정, 허용 목록 위반, 관리형 팩 이벤트, 검사 정책 고유의 효과를 + 하나의 시간순 기록으로 모읍니다. 기존의 원본 기록을 그대로 연결해 보여 줄 뿐, 기록을 하나의 일반 이벤트 형식으로 다시 쓰지 않습니다. + App Store 빌드에서는 검사에 의존하는 정책 유형과 그 정책에 보관된 활동을 표시하지 않습니다.

이 규칙이 했을 일. 차단 규칙은 쓰기는 쉽고 켜기는 무섭습니다. 어떤 규칙에 대해 보통 가장 먼저 알게 되는 것은 그것이 무엇을 망가뜨렸는지이고, 그것도 나중에 다른 어딘가에서 무언가가 실패할 때 알게 되기 때문입니다. 규칙 편집기는 그 질문에 @@ -382,7 +387,7 @@

규칙

있었던 가장 오래된 트래픽도 함께 밝힙니다. 두 시간짜리 이력으로 대조한 규칙은 두 시간짜리 이력으로 대조한 것입니다.

관리형 광고 및 추적기 팩

-

규칙 › 팩에는 광고, 사이트 간 추적, 개인정보 보호 및 원격 측정이라는 세 가지 공식 호스트 이름 목록이 있습니다. 처음부터 표시되며 모두 기본적으로 꺼져 있습니다. 차단하려면 네트워크 확장 프로그램이 필요하며, nettop 샘플러는 관찰만 할 수 있습니다.

+

정책 › 관리형 팩에는 광고, 사이트 간 추적, 개인정보 보호 및 원격 측정이라는 세 가지 공식 호스트 이름 목록이 있습니다. 처음부터 표시되며 모두 기본적으로 꺼져 있습니다. 차단하려면 네트워크 확장 프로그램이 필요하며, nettop 샘플러는 관찰만 할 수 있습니다.

공식 서명 업데이트만 허용됩니다. Flowlight는 게시자가 서명한 최신 카탈로그가 있는지 하나의 고정 HTTPS 소스를 확인합니다. 자동 확인은 기본적으로 켜져 있으며 여기에서 끌 수 있습니다. 서명은 콘텐츠를 인증하지만 활성화하지는 않습니다. 새 팩은 꺼진 상태로 유지되며, 대상이 변경되면 활성화된 팩은 정확한 새 버전을 검토하고 저장을 누를 때까지 적용되지 않습니다. 서비스 중단 중에도 승인된 콘텐츠가 눈에 띄게 오래된 것으로 표시된 마지막 정상 버전으로 유지될 수 있습니다. 이는 호스트 이름 수준의 네트워크 차단이므로 DOM이나 레이아웃의 빈 공간, 브라우저 오버레이, 스크립트로 생성된 팝업 또는 네이티브 앱 창은 제거할 수 없습니다.

HTTPS 검사가 앱을 Flowlight의 프록시로 보내면, 일반 대상 규칙과 활성화된 관리형 팩은 원본 서버 연결이 열리기 전에 원래 앱의 신원을 사용해 결정됩니다. 인증서가 고정되었거나 복호화되지 않는 CONNECT 터널도 호스트 이름으로 판정할 수 있습니다. 에이전트 허용 목록은 계속 네트워크 확장을 직접 지나는 경로에만 적용됩니다. 소유자나 호스트 이름을 알 수 없으면 연결을 허용합니다.

@@ -431,7 +436,9 @@

에이전트 가드레일: 도구나 서버, 리소스를

이것은 샌드박스가 아닙니다. 거부된 도구는 더 이상 제공되지도, 응답되지도 않습니다. 하지만 셸이 남아 있는 에이전트는 그 도구가 하던 일을 직접 할 수 있습니다. Flowlight는 그것을 기록합니다. 막아 주는 척하지는 않습니다.

-

소스: 컬렉터와 엔진

+

소스: 컬렉터, 기기 및 포착 범위

+

소스는 컬렉터, 기기, 포착 범위를 차례로 뚜렷한 제목 아래 배치한 하나의 스크롤 가능한 운영 페이지입니다. + 상태 알림, 질문 답변, 복원된 창, 스크린샷 링크는 다른 섹션을 숨기지 않고 관련 섹션으로 스크롤한 뒤 잠시 강조합니다.

Flowlight는 소스 › 컬렉터에서 고르는 두 가지 방식 중 하나로 트래픽을 기록합니다. 두 방식은 무엇을 놓칠 수 있는지가 다르므로, 어느 쪽에 의지하기 전에 알아 둘 만합니다.

@@ -624,7 +631,7 @@

질문과 답

창을 닫아 두면 Flowlight는 거의 아무 일도 하지 않습니다. 메뉴 막대의 속도를 유지하고 1초에 한 행을 씁니다. 네트워크 확장은 1초마다 짧게 사는 nettop 프로세스를 실행하는 대체 샘플러보다 비용이 적습니다.
연결을 차단할 수 있나요?
-
네, 네트워크 확장이 있으면 됩니다. 에이전트의 허용 목록은 경고뿐 아니라 거부도 할 수 있고, 규칙은 앱이나 +
네, 네트워크 확장이 있으면 됩니다. 에이전트의 허용 목록은 경고뿐 아니라 거부도 할 수 있고, 규칙은 앱이나 목적지, URL을 원하는 기간만큼 막을 수 있으며, 가드레일은 모델에 제공되기 전에 에이전트에서 도구를 걷어낼 수 있습니다. 요청하기 전까지 차단되는 것은 없고, 모든 거부에는 빠져나갈 길이 함께 있습니다.
네트워크 확장이 꼭 필요한가요?
diff --git a/site/pages/ko/index.html b/site/pages/ko/index.html index 671af742..228d897e 100644 --- a/site/pages/ko/index.html +++ b/site/pages/ko/index.html @@ -110,11 +110,11 @@

기록

다 어디로 갔나? 초에서 연까지, 앱·목적지·IP

알림

무엇이 이상했나? 목적지와 임계값까지 밝히는, 설명되는 앱별 알림.

⌘5
-

규칙

무엇을 거부했고, 실제로 막히고 있나? 모든 규칙과 각 규칙이 막은 것, 그리고 모든 판정의 기록.

⌘8 · Developer ID / ⌘7 · App Store
+

정책

어떤 제어가 있고 실제로 효력이 있나? 모든 정책과 실제 상태, 그리고 하나로 모은 활동 기록.

⌘8 · Developer ID / ⌘7 · App Store

질문

평소 쓰는 말로 묻고 기록에서 답을 받습니다. 실행된 질의는 아래에 전부 표시됩니다.

⌘6
-

소스

데이터가 들어오는지, 이름이 붙은 비율과 활성 기기 채널은 어떤지. 컬렉터, 기기, 포착 범위를 한곳에서 봅니다.

⌘9 · Developer ID / ⌘8 · App Store
+

소스

데이터가 들어오는지, 어떤 기기 채널이 활성화되었는지, Flowlight가 어디까지 파악할 수 있는지. 컬렉터부터 포착 범위까지 한 페이지에서 봅니다.

⌘9 · Developer ID / ⌘8 · App Store

검사

선택 사항. 에이전트가 실제로 무엇을 보냈고, 어떤 도구 호출이 그렇게 했나? 복호화된 요청, 도구 호출, 결과.

⌘7 · Developer ID
diff --git a/site/pages/ko/privacy.html b/site/pages/ko/privacy.html index 379a8988..2a92755d 100644 --- a/site/pages/ko/privacy.html +++ b/site/pages/ko/privacy.html @@ -57,7 +57,7 @@

앱이 내는 네트워크 요청

- + diff --git a/site/pages/privacy.html b/site/pages/privacy.html index e123ca8d..9f8dc9ac 100644 --- a/site/pages/privacy.html +++ b/site/pages/privacy.html @@ -63,7 +63,7 @@

Network requests the app makes

- + diff --git a/site/pages/pt-PT/docs.html b/site/pages/pt-PT/docs.html index ef6724b9..c0844cbe 100644 --- a/site/pages/pt-PT/docs.html +++ b/site/pages/pt-PT/docs.html @@ -30,7 +30,7 @@

Usar o Flowlight

@@ -101,10 +101,10 @@

As vistas

- - + +
요청보내는 곳담기는 내용시점
업데이트 확인api.github.com최신 릴리스를 묻는 일반적인 HTTPS 요청. GitHub는 IP 주소와 Flowlight/<version> 사용자 에이전트를 보게 됩니다0.1.2부터, 실행 시와 하루에 한 번. 끄는 스위치는 설정 › 일반
업데이트 다운로드github.com디스크 이미지와 체크섬 파일다운로드 후 열기를 클릭할 때만
공식 규칙 팩 업데이트flowlight.xinbetween.com고정된 공식 피드로 보내는 표준 HTTPS 요청입니다. 서버에는 사용자의 IP 주소, Flowlight/<version> 사용자 에이전트 및 조건부 캐시 유효성 검사기가 표시되며, 트래픽, 팩 설정 또는 기기 식별자는 전송되지 않습니다시작 후 확인 시점이 되면 실행되고, 이후 정상 작동 중에는 하루에 최대 한 번 실행됩니다. 기본적으로 켜져 있으며, 규칙 › 팩에서 끄거나 지금 확인을 수동으로 실행할 수 있습니다
공식 규칙 팩 업데이트flowlight.xinbetween.com고정된 공식 피드로 보내는 표준 HTTPS 요청입니다. 서버에는 사용자의 IP 주소, Flowlight/<version> 사용자 에이전트 및 조건부 캐시 유효성 검사기가 표시되며, 트래픽, 팩 설정 또는 기기 식별자는 전송되지 않습니다시작 후 확인 시점이 되면 실행되고, 이후 정상 작동 중에는 하루에 최대 한 번 실행됩니다. 기본적으로 켜져 있으며, 정책 › 관리형 팩에서 끄거나 지금 확인을 수동으로 실행할 수 있습니다
역방향 DNS설정된 DNS 리졸버호스트 이름이 관측되지 않았을 때, IP 주소에 등록된 이름 조회필요할 때, 샘플러 캡처 모드에서. 네트워크 확장이 돌기 시작할 때까지는 이것이 기본값입니다. 끄는 스위치는 없습니다. 이것이 없으면 샘플러는 대부분의 연결에 이름을 붙일 수 없습니다. 네트워크 확장은 트래픽 자체에서 이름을 붙이므로 이 조회를 하지 않습니다
네트워크 소유자 조회Team Cymru, DNS로알려진 호스트 이름이 없는 공용 IP 주소. 사설 주소와 예약 주소는 절대 Mac을 벗어나지 않습니다기본값 켬. 끄는 스위치는 캡처 › 네트워크 소유자 확인
호스팅 모델로 질문선택한 제공업체: Anthropic, OpenAI, Google Gemini 또는 호환 엔드포인트내 질문과, 모델이 고른 Flowlight 읽기 전용 질의가 돌려준 결과. 트래픽 데이터베이스 자체는 업로드되지 않습니다호스팅 제공업체를 고르고 질문을 보낼 때만. 질문 화면에 전송된 요청이 표시됩니다
RequestSent toWhat it containsWhen
Update checkapi.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agentAfter a clean quit and relaunch, and daily. Off switch in Settings › General
Update downloadgithub.comThe disk image and its checksum fileOnly when you click Download & Open
Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/<version> user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now in Rules › Packs
Official rule-pack updateflowlight.xinbetween.comA standard HTTPS request to the fixed official feed for a publisher-signed catalog. The server sees your IP address, the Flowlight/<version> user agent, and conditional cache validators; it receives no traffic, pack settings, or device identifierAfter startup when due and then at most daily while healthy. On by default; off switch and manual Check Now under Policies › Managed packs
Reverse DNSYour configured DNS resolverA lookup of an IP address's registered name, when no hostname was seenAs needed, in the sampler capture mode, which is the default until the Network Extension is running. No off switch: without it the sampler cannot name most connections. The Network Extension names them from the traffic itself and does not do this
Network owner lookupTeam Cymru, over DNSPublic IP addresses with no known hostname. Private and reserved addresses never leave your MacOn by default. Off switch in Sources › Collector › Identify network owners
Ask with a hosted modelThe provider you select: Anthropic, OpenAI, Google Gemini or a compatible endpointYour question and the results returned by the read-only Flowlight queries selected by the model. The traffic database itself is not uploadedOnly when you select a hosted provider and submit a question. The Ask screen shows the transmitted requests
Alertas⌘5Tudo o que pareceu invulgar, com a app, o destino e o número que disparou a regra.
Perguntar⌘6Perguntas sobre o seu próprio tráfego em linguagem simples, respondidas a partir do histórico registado por um modelo local ou alojado.
Inspecionar⌘7 · Developer IDOpcional: pedidos desencriptados dos agentes que encaminha pelo Flowlight e as chamadas de ferramentas por trás deles. A pesquisa entra nos corpos dos pedidos e das respostas.
Regras⌘8 · Developer ID
⌘7 · App Store
Todas as listas de permissões e recusas que definiu, o que cada uma abrange e o que de facto travou.
Origens⌘9 · Developer ID
⌘8 · App Store
Estado do coletor, canais de dispositivos e cobertura de nomes em três separadores.
Políticas⌘8 · Developer ID
⌘7 · App Store
Todas as políticas configuradas, se estão realmente em vigor e um histórico unificado de atividade só de leitura.
Origens⌘9 · Developer ID
⌘8 · App Store
Estado e recuperação do Coletor, canais opcionais de dispositivos e cobertura numa única página operacional deslocável.
-

Os atalhos seguem a ordem visível da barra lateral. As versões Developer ID incluem Inspecionar em ⌘7; as versões App Store omitem-no, por isso Regras e Origens passam para ⌘7 e ⌘8.

+

Os atalhos seguem a ordem visível da barra lateral. As versões Developer ID incluem Inspecionar em ⌘7; as versões App Store omitem-no, por isso Políticas e Origens passam para ⌘7 e ⌘8.

A vista Em direto: uma lista de aplicações com as suas taxas de envio e receção atuais e o destino mais ativo.
Em direto, a vista com que o Flowlight abre.
@@ -337,16 +337,20 @@

Respostas simuladas

continua a ser feita: uma simulação muda a resposta, não a possibilidade de estabelecer a ligação, por isso «a API devolve 500» funciona e «a API está completamente inacessível» não.

-

Regras

-

Uma lista de permissões é a ideia de um agente sobre onde pode ir. Uma regra é a versão geral: nomeia um +

Políticas

+

Políticas reúne num só inventário todos os controlos que o Flowlight pode aplicar: regras comuns de permissão ou bloqueio, listas de + permissões dos agentes, pacotes geridos e, quando a inspeção HTTPS está disponível, proteções, simulações, reescritas de pedidos e transformações de + respostas. Cada linha distingue o estado configurado do efeito real: em aplicação, apenas a comunicar, a vigiar, à espera, agendada, em pausa, + esgotada, a ser aplicada, a precisar de revisão, indisponível, incompatível ou com falha. Selecionar uma linha abre o editor contextual responsável + por essa política; Políticas não substitui nem confunde os diferentes modelos de consentimento e persistência.

+

Uma lista de permissões é a ideia de um agente sobre onde pode ir. Uma regra comum é a versão geral: nomeia um sujeito — uma app, um agente, um destino, um URL, ou um par formado por dois deles — e diz bloquear ou permitir. Clique com o botão direito em qualquer linha de Em direto, Histórico, Agentes de IA ou Inspecionar para escrever uma sobre o que está a ver; - todas elas vão dar ao mesmo editor no ecrã Regras, porque uma regra que entrasse em vigor fora da vista - seria uma regra que nunca mais conseguiria encontrar.

+ cada uma continua disponível em Políticas e abre no seu editor responsável.

Modelos. Vinte e dois, em cinco grupos — impedir um agente de chegar a registos de pacotes ou a outros fornecedores de modelos, fechar as vias habituais por onde os dados saem (sites de paste, apanhadores de webhooks, depósitos anónimos de ficheiros, túneis, DNS over HTTPS), analítica de produto e relatórios de falhas, políticas de horário de trabalho, e o pontual «bloquear isto por uma hora enquanto descubro - o que é». Consulte-os na barra de ferramentas de Regras, ou clique com o botão direito numa linha em Em direto, Histórico, Agentes de IA ou Inspecionar e + o que é». Consulte-os na barra de ferramentas de Políticas, ou clique com o botão direito numa linha em Em direto, Histórico, Agentes de IA ou Inspecionar e aplique um ao que está a ver.

Cada modelo diz o que vai fazer antes de o fazer. Tudo o que escreva mais do que uma regra abre um cartão com todos os anfitriões e horários; tudo o que seja permanente abre o editor pré-preenchido; e só as regras que se desfazem sozinhas — uma @@ -367,8 +371,9 @@

Regras

endereço e a porta, nada sobre caminhos, e só no modo de extensão. A inspeção HTTPS recusa um pedido, por isso consegue corresponder a um URL e responder com um estado que o agente pode ler, em vez de um socket que morreu sem dizer porquê. O amostrador nettop não consegue recusar nada. Uma regra que a configuração atual não consegue aplicar di-lo na lista, em vez de ficar ali com ar de estar em vigor.

-

O segundo separador é o fluxo de violações: todas as ligações que uma regra decidiu, com a app, para onde ia, que - regra a decidiu e se isso foi uma recusa ou uma exceção a deixá-la passar.

+

A segunda vista é a Atividade, só de leitura: reúne numa única cronologia as decisões das regras comuns, as violações das listas de permissões, + os eventos dos pacotes geridos e os efeitos distintos das políticas de inspeção. Baseia-se nos registos originais fidedignos em vez de reescrever o + histórico num único modelo genérico de eventos; as versões Store omitem os tipos de política dependentes da inspeção e a respetiva atividade conservada.

O que teria feito. Uma regra de bloqueio é fácil de escrever e assustadora de ligar, porque a primeira coisa que normalmente se aprende sobre uma delas é o que estragou — e aprende-se mais tarde, a partir de algo que falha noutro sítio. @@ -389,7 +394,7 @@

Regras

testada contra duas horas de histórico.

Pacotes geridos de publicidade e rastreadores

-

Regras › Pacotes contém três listas oficiais de nomes de anfitrião: Publicidade, Rastreio entre sites e Privacidade e telemetria. Estão visíveis desde o início e todas desativadas por predefinição. O bloqueio requer a Extensão de rede; o amostrador nettop apenas pode observar.

+

Políticas › Pacotes geridos contém três listas oficiais de nomes de anfitrião: Publicidade, Rastreio entre sites e Privacidade e telemetria. Estão visíveis desde o início e todas desativadas por predefinição. O bloqueio requer a Extensão de rede; o amostrador nettop apenas pode observar.

Apenas atualizações oficiais e assinadas. O Flowlight consulta uma única fonte HTTPS fixa para procurar catálogos mais recentes assinados pelo editor. As verificações automáticas estão ativas por predefinição e podem ser desativadas aqui. Uma assinatura autentica o conteúdo, mas não o ativa: um novo pacote permanece desativado e, se os destinos forem alterados, um pacote ativo deixa de ser aplicado até que reveja a nova versão exata e prima Guardar. Durante uma interrupção, o conteúdo aceite pode permanecer disponível como a última versão válida conhecida, visivelmente assinalada como desatualizada. Estes são bloqueios de rede ao nível do nome de anfitrião: não podem remover espaços vazios no DOM ou no esquema, sobreposições do navegador, janelas pop-up geradas por scripts ou janelas de aplicações nativas.

Quando a inspeção HTTPS encaminha uma app pelo proxy do Flowlight, este decide as regras de destino comuns e os pacotes geridos ativos com base na identidade original da app antes de abrir qualquer ligação ao servidor de origem. Nos túneis CONNECT com certificado fixado ou que nunca são decifrados, isto funciona pelo nome de anfitrião; as listas de permissões dos agentes continuam limitadas ao caminho direto pela Extensão de rede. Se o proprietário ou o nome de anfitrião for desconhecido, a ligação é permitida.

@@ -438,7 +443,10 @@

Proteções dos agentes: bloquear uma ferramenta, um servido

Isto não é uma sandbox. Uma ferramenta recusada deixa de ser oferecida e deixa de ser respondida. Um agente que ainda tenha uma shell pode fazer à mão o que a ferramenta teria feito. O Flowlight vai registá-lo; não finge impedi-lo.

-

Origens: coletor e motores

+

Origens: coletor, dispositivos e cobertura

+

Origens é uma única página operacional deslocável: primeiro o Coletor, depois Dispositivos e Cobertura como secções claramente + identificadas. Os avisos de estado, as respostas de Perguntar, as janelas restauradas e as ligações usadas nas capturas de ecrã deslocam-se até à secção relevante + e realçam-na por instantes sem ocultar as restantes.

O Flowlight regista o tráfego de uma de duas formas, escolhida em Origens › Coletor. Diferem no que podem deixar escapar, e vale a pena entender isso antes de confiar numa delas.

@@ -630,7 +638,7 @@

Perguntas e respostas

Com a janela fechada, o Flowlight quase não faz nada: mantém as taxas na barra de menus e escreve uma linha por segundo. A extensão de rede custa menos do que o amostrador de reserva, que inicia um processo nettop de vida curta a cada segundo.
Consegue bloquear ligações?
-
Sim, com a extensão de rede. A lista de permissões de um agente pode recusar e não só avisar, uma regra pode bloquear uma app, +
Sim, com a extensão de rede. A lista de permissões de um agente pode recusar e não só avisar, uma regra pode bloquear uma app, um destino ou um URL pelo tempo que quiser, e uma proteção pode retirar uma ferramenta a um agente antes de ser oferecida ao seu modelo. Nada bloqueia até o pedir, e cada recusa traz consigo a sua saída.
Preciso da extensão de rede?
diff --git a/site/pages/pt-PT/index.html b/site/pages/pt-PT/index.html index 1cdc1401..44dd8551 100644 --- a/site/pages/pt-PT/index.html +++ b/site/pages/pt-PT/index.html @@ -110,11 +110,11 @@

Histórico

Para onde foi tudo? Do segundo ao ano, agrupado por app, d

Alertas

O que pareceu errado? Alertas por app, explicáveis, com o destino e o limiar.

⌘5
-

Regras

O que recusei e está a acontecer? Todas as regras, o que cada uma bloqueou e um fluxo de todas as decisões.

⌘8 · Developer ID / ⌘7 · App Store
+

Políticas

Que controlos existem e estão em vigor? Todas as políticas, o seu estado real e um histórico unificado de atividade.

⌘8 · Developer ID / ⌘7 · App Store

Perguntar

Perguntas em linguagem comum, respondidas a partir do histórico — com todas as consultas que fez listadas abaixo.

⌘6
-

Origens

Os dados estão a chegar, quanto está identificado e que canais de dispositivos estão ativos? Coletor, Dispositivos e Cobertura num só lugar.

⌘9 · Developer ID / ⌘8 · App Store
+

Origens

Os dados estão a chegar, que canais de dispositivos estão ativos e o que consegue o Flowlight contabilizar? Uma única página, do Coletor à Cobertura.

⌘9 · Developer ID / ⌘8 · App Store

Inspecionar

Opcional. O que é que o agente enviou de facto, e que chamada de ferramenta o fez? Pedidos desencriptados, chamadas de ferramentas, resultados.

⌘7 · Developer ID
diff --git a/site/pages/pt-PT/privacy.html b/site/pages/pt-PT/privacy.html index 5395ff78..e6434fe4 100644 --- a/site/pages/pt-PT/privacy.html +++ b/site/pages/pt-PT/privacy.html @@ -56,7 +56,7 @@

Pedidos de rede que a app faz

- + diff --git a/site/pages/releases.html b/site/pages/releases.html index 790e6aec..5b24f7f8 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,18 @@

What's new

-

0.17.3

Latest
+

0.17.4

Latest
+
    +
  • See every control in Policies. Rules is now a unified Policies inventory for ordinary rules, agent allowlists, managed packs, and Developer-only inspection policies, with truthful effective states rather than treating every enabled item as active.
  • +
  • Keep each policy's real editor and evidence. Selecting an item opens the contextual editor that owns it; Managed packs retain compact provenance and consent, while Activity adapts the existing authoritative records without conflating policy types.
  • +
  • Read Sources as one operational page. Collector, Devices and Coverage now flow in that order without a segmented control, while status links, Ask answers, restored windows and screenshot arguments still focus the requested section.
  • +
  • Dismiss chart details when the pointer leaves. History no longer leaves a zero-value tooltip floating at the upper-left after the cursor exits the All traffic plot; keyboard bucket inspection remains unchanged.
  • +
  • Sign each release once. The immutable tag run builds, signs, notarizes, Gatekeeper-verifies, attests and retains one artifact; publication has no signing credentials and can only verify and publish those exact prepared bytes.
  • +
+
+ +
+

0.17.3

  • Keep every input in one operational home. Capture, Devices and Coverage are now Collector, Devices and Coverage tabs inside Sources, with a shared health summary and a step-by-step recovery card when traffic is not arriving.
  • Tune alerts where their results appear. Detection controls now live beside the alert list under Alerts › Detection, while database retention, location and deletion live in Settings › Data & Privacy.
  • diff --git a/site/pages/zh-Hans/docs.html b/site/pages/zh-Hans/docs.html index 61c034dd..21f82c64 100644 --- a/site/pages/zh-Hans/docs.html +++ b/site/pages/zh-Hans/docs.html @@ -30,7 +30,7 @@

    使用 Flowlight

@@ -97,10 +97,10 @@

各个视图

- - + +
PedidoEnviado paraO que contémQuando
Procura de atualizaçõesapi.github.comUm pedido HTTPS normal pela versão mais recente; o GitHub vê o seu endereço IP e o agente de utilizador Flowlight/<version>Na abertura e uma vez por dia, a partir da 0.1.2. Interruptor para desativar em Definições › Geral
Transferência da atualizaçãogithub.comA imagem de disco e o seu ficheiro de soma de verificaçãoSó quando clica em Transferir e abrir
Atualização oficial do pacote de regrasflowlight.xinbetween.comUm pedido HTTPS padrão ao feed oficial fixo. O servidor vê o seu endereço IP, o agente do utilizador Flowlight/<version> e os validadores condicionais de cache; não recebe tráfego, definições dos pacotes nem qualquer identificador do dispositivoApós o arranque, quando for necessário, e depois, no máximo, uma vez por dia enquanto funcionar normalmente. Ativa por predefinição; opção para desativar e Procurar agora manualmente em Regras › Pacotes
Atualização oficial do pacote de regrasflowlight.xinbetween.comUm pedido HTTPS padrão ao feed oficial fixo. O servidor vê o seu endereço IP, o agente do utilizador Flowlight/<version> e os validadores condicionais de cache; não recebe tráfego, definições dos pacotes nem qualquer identificador do dispositivoApós o arranque, quando for necessário, e depois, no máximo, uma vez por dia enquanto funcionar normalmente. Ativa por predefinição; opção para desativar e Procurar agora manualmente em Políticas › Pacotes geridos
DNS inversoO resolvedor de DNS que tem configuradoUma consulta do nome registado de um endereço IP, quando não foi visto nenhum nome de anfitriãoConforme necessário, no modo de captura por amostrador, que é a predefinição até a extensão de rede estar a correr. Sem interruptor para desligar: sem isto, o amostrador não consegue dar nome à maioria das ligações. A extensão de rede dá-lhes nome a partir do próprio tráfego e não faz esta consulta
Consulta do proprietário da redeTeam Cymru, por DNSEndereços IP públicos sem nome de anfitrião conhecido. Os endereços privados e reservados nunca saem do seu MacAtivada por predefinição. Interruptor para desativar em Captura › Identificar os proprietários da rede
Perguntar com um modelo alojadoO fornecedor que escolher: Anthropic, OpenAI, Google Gemini ou um endpoint compatívelA sua pergunta e os resultados devolvidos pelas consultas de leitura do Flowlight escolhidas pelo modelo. A base de dados de tráfego em si não é enviadaSó quando escolhe um fornecedor alojado e submete uma pergunta. O ecrã Perguntar mostra os pedidos transmitidos
警报⌘5所有看起来不寻常的事,带上应用、目标和触发规则的那个数字。
提问⌘6用平常的话问你自己的流量,由本地模型或托管模型从记录下来的历史里作答。
检查⌘7 · Developer ID可选:你交给 Flowlight 转发的那些代理的解密请求,以及它们背后的工具调用。搜索能深入请求和响应正文。
规则⌘8 · Developer ID
⌘7 · App Store
你设过的每一条允许列表和拒绝,各自管到哪里,以及实际拦下了什么。
来源⌘9 · Developer ID
⌘8 · App Store
在三个标签页中显示收集器状态、设备通道和命名覆盖范围。
策略⌘8 · Developer ID
⌘7 · App Store
每项已配置的策略、是否真正生效,以及统一的只读活动记录。
来源⌘9 · Developer ID
⌘8 · App Store
收集器状态与恢复、可选设备通道和覆盖范围,集中在一张可滚动的运维页面中。
-

快捷键遵循可见的侧边栏顺序。Developer ID 版本在 ⌘7 包含检查;App Store 版本省略检查,因此规则和来源会移到 ⌘7 和 ⌘8。

+

快捷键遵循可见的侧边栏顺序。Developer ID 版本在 ⌘7 包含检查;App Store 版本省略检查,因此策略和来源会移到 ⌘7 和 ⌘8。

实时视图:一列应用,各自带着当前的发送与接收速率,以及最忙的目标。
实时,Flowlight 打开时进入的那个视图。
@@ -139,7 +139,7 @@

工具与 MCP 服务器

每个域名都标明是从这四种里的哪一种知道的。带着好几个标签的域名,是比只带一个标签更强的事实:配置文件里写了而且确实 连过,比其中任何一项单独说得更多;只被配置过的域名从未被访问过,而只被观察到的域名从来没有写在任何你本可以读到的地方。

每个域名给出两种拒绝:对这个代理,覆盖该代理以及它启动的工具;或者在所有地方,覆盖这台 Mac 上的 - 每一个应用。两者写下的都是普通规则,所以违规流水会报告它们,规则编辑器会解释它们——而且正因为它们是普通规则, + 每一个应用。两者写下的都是普通规则,所以活动会报告它们,规则编辑器会解释它们——而且正因为它们是普通规则, 编辑器里的模拟会在你把它打开之前告诉你,它本会拒绝掉什么。

有一类域名没有按钮。模型提供方自己去连的连接器——在请求正文里声明,从提供方那一侧连接——根本不会碰到这台 Mac,所以这里的 任何规则都够不到它。那一行会把这件事说出来,而不是递给你一个悄悄什么都不做的拒绝。如果同一个主机在本地也被配置过,或者 @@ -304,16 +304,16 @@

模拟响应

连接:模拟改变的是答案,而不是连接能否建立,所以“这个 API 返回 500”行得通,而“这个 API 完全不可达”不行。

-

规则

-

允许列表是某一个代理关于它可以去哪里的说法。规则是它的通用版本:它指明一个 +

策略

+

策略集中列出 Flowlight 能执行的所有管控:普通的允许或阻止规则、代理允许列表、托管包,以及在支持 HTTPS 检查时的护栏、模拟响应、请求改写和响应转换。每一行都会区分配置状态与实际效果:生效、仅报告、监视、等待、已排期、暂停、已用完、正在应用、需要检查、不可用、不兼容或失败。选择一行会打开负责该策略的对应编辑器;“策略”不会取代或混淆这些功能各自独立的授权和持久化方式。

+

允许列表是某一个代理关于它可以去哪里的说法。普通规则是它的通用版本:它指明一个 对象——应用、代理、目标、URL,或其中两者的组合——并决定阻止 还是允许。在实时、历史、AI 代理或检查里右键任意一行,就能针对你正看着的东西写一条; - 它们全都落在规则界面上的同一个编辑器里,因为一条在看不见的地方生效的规则, - 会是一条你再也找不到的规则。

+ 每条规则都能在“策略”中找到,并会在其权威编辑器中打开。

模板。共 22 个,分为五组——阻止代理访问软件包仓库或其他 模型提供方;封住数据外流的常见途径(粘贴站点、webhook 接收器、匿名文件投放、隧道、DNS over HTTPS);产品分析与崩溃上报;工作时段策略;以及一次性的“先把这个拦一小时,我弄清它 - 是什么”。可以从规则工具栏浏览它们,也可以在实时、历史、AI 代理或检查的任意一行上右键, + 是什么”。可以从策略工具栏浏览它们,也可以在实时、历史、AI 代理或检查的任意一行上右键, 把某个模板应用到你正看着的东西上。

每个模板在动手之前都会说明它要做什么。凡是会写入多条规则的,都会打开一张卡片,列出每个 主机和时段;凡是永久生效的,都会打开预填好的编辑器;而只有会自行撤销的规则——一 @@ -334,8 +334,7 @@

规则

地址和端口,对路径一无所知,而且只在扩展模式下有效。HTTPS 检查拒绝的是单个请求,所以它能匹配 URL,并用代理读得懂的状态码作答,而不是一个不声不响就断掉的套接字。nettop 采样器 完全无法拒绝任何东西。当前配置无法执行的规则会在列表里明说,而不是摆在那里装作已经生效。

-

第二个标签页是违规流水:每一条由规则判定的连接,带上应用、它要去哪里、是哪条 - 规则判定的,以及那究竟是一次拒绝,还是一次放它通过的例外。

+

第二个视图是只读的活动:普通规则判定、允许列表违规、托管包事件,以及各类检查策略的效果,都按时间排列在一处。它会适配现有的权威记录,而不是把历史改写成一种笼统的事件模型;App Store 版本会省略依赖检查的策略类型及其保留的活动。

它本来会做什么。阻止的规则写起来容易,打开却让人害怕,因为你通常最先了解到的是它 弄坏了什么——而且是后来才知道的,从别处某个东西失败上知道的。规则编辑器先回答这个问题:它拿记录 @@ -353,7 +352,7 @@

规则

的最早的流量:一条只对照了两小时历史的规则,就是只对照了两小时历史。

托管广告和跟踪器包

-

规则 › 包包含三个官方主机名列表:广告、跨站跟踪和隐私与遥测。它们从一开始就可见,并且默认均为关闭状态。阻止功能需要网络扩展;nettop 采样器只能观察。

+

策略 › 托管包包含三个官方主机名列表:广告、跨站跟踪和隐私与遥测。它们从一开始就可见,并且默认均为关闭状态。阻止功能需要网络扩展;nettop 采样器只能观察。

仅限官方签名更新。 Flowlight 会检查一个固定的 HTTPS 源,以查找由发布者签名的新版目录。自动检查默认开启,可在此处关闭。签名可验证内容,但不会启用内容:新包会保持关闭;如果目标发生变化,已启用的包将停止执行,直到你审查确切的新版本并按下保存。服务中断期间,已接受的内容可能会继续作为最后已知正常版本保留,并明确显示为已过期。这些是主机名级别的网络阻止,无法移除 DOM 或布局中的空白、浏览器叠加层、脚本生成的弹出窗口或原生应用窗口。

当 HTTPS 检查将应用经由 Flowlight 的代理转发时,普通目标规则和已启用的托管包会在打开任何源站连接之前,使用原应用的身份作出判定。对于固定了证书或始终不解密的 CONNECT 隧道,这一判定按主机名进行;代理允许列表仍只适用于直接经过网络扩展的路径。如果所有者或主机名未知,则会允许连接。

@@ -402,7 +401,8 @@

代理护栏:阻止一个工具、一个服务器或一个

这不是沙盒。被拒绝的工具不再提供给模型,也不再被响应。而仍然拥有 shell 的代理,可以手动做到那个工具本来要做的事。Flowlight 会把它记录下来;它不会假装能阻止。

-

来源:收集器与引擎

+

来源:收集器、设备与覆盖范围

+

来源是一张可滚动的运维页面:先是收集器,接着是标题清晰的设备与覆盖范围分区。状态通知、提问答案、恢复后的窗口和截图链接都会滚动到相关分区并短暂突出显示,同时不隐藏其他内容。

Flowlight 用两种方式之一记录流量,在来源 › 收集器里选择。它们会漏掉的东西不同,在依赖任何 一种之前,值得先了解清楚。

@@ -588,7 +588,7 @@

问题与解答

窗口关闭时 Flowlight 几乎什么都不做:它保留菜单栏速率,每秒写入一行。网络扩展 比作为后备的采样器更省,后者每秒都要启动一个短命的 nettop 进程。
它能阻止连接吗?
-
能,配合网络扩展。代理的允许列表除了警告也可以拒绝,一条规则可以按你指定的时长阻止一个应用、 +
能,配合网络扩展。代理的允许列表除了警告也可以拒绝,一条规则可以按你指定的时长阻止一个应用、 一个目标或一个 URL,而一条护栏可以在模型看到某个工具之前,把它从代理手里 拿走。在你要求之前不会阻止任何东西,而且每次拒绝都带着它的出口。
我需要网络扩展吗?
diff --git a/site/pages/zh-Hans/index.html b/site/pages/zh-Hans/index.html index 442e99f8..2bb220a0 100644 --- a/site/pages/zh-Hans/index.html +++ b/site/pages/zh-Hans/index.html @@ -110,11 +110,11 @@

历史

流量究竟去了哪?从秒到年,按应用、目标或 I

警报

哪里不对?逐应用、讲得清道理的警报,带上目标和阈值。

⌘5
-

规则

我拒绝过什么,真的生效了吗?每条规则、各自阻止了什么,以及每次判定的流水。

⌘8 · Developer ID / ⌘7 · App Store
+

策略

有哪些管控,它们是否生效?每项策略、实际状态,以及统一的活动记录。

⌘8 · Developer ID / ⌘7 · App Store

提问

用日常语言提问,从历史数据里作答——下方列出它执行过的每一条查询。

⌘6
-

来源

数据是否正在到达、多少已有名称、哪些设备通道处于活动状态?把收集器、设备和覆盖范围放在一处。

⌘9 · Developer ID / ⌘8 · App Store
+

来源

数据是否正在到达、哪些设备通道处于活动状态、Flowlight 能解释多少?一张页面从收集器一直看到覆盖范围。

⌘9 · Developer ID / ⌘8 · App Store

检查

可选。代理究竟发出了什么,是哪次工具调用引起的?解密后的请求、工具调用和结果。

⌘7 · Developer ID
diff --git a/site/pages/zh-Hans/privacy.html b/site/pages/zh-Hans/privacy.html index e7bc7b48..2d41dcb7 100644 --- a/site/pages/zh-Hans/privacy.html +++ b/site/pages/zh-Hans/privacy.html @@ -55,7 +55,7 @@

应用发出的网络请求

- + diff --git a/site/pages/zh-Hant/docs.html b/site/pages/zh-Hant/docs.html index afd7aefc..0bac5a38 100644 --- a/site/pages/zh-Hant/docs.html +++ b/site/pages/zh-Hant/docs.html @@ -30,7 +30,7 @@

使用 Flowlight

@@ -100,10 +100,10 @@

各個畫面

- - + +
请求发往包含什么什么时候
更新检查api.github.com一个查询最新版本的标准 HTTPS 请求;GitHub 会看到你的 IP 地址和 Flowlight/<version> 这个 User-Agent 标识启动时以及每天一次,自 0.1.2 起。关闭开关在设置 › 通用
更新下载github.com磁盘映像及其校验和文件只在你点击“下载并打开”时
官方规则包更新flowlight.xinbetween.com向固定官方源发送的标准 HTTPS 请求。服务器会看到你的 IP 地址、Flowlight/<version>用户代理和缓存条件验证器;不会收到任何流量、包设置或设备标识符启动后在检查到期时执行,此后在运行正常的情况下最多每天执行一次。默认开启;可在规则 › 包中关闭或手动选择立即检查
官方规则包更新flowlight.xinbetween.com向固定官方源发送的标准 HTTPS 请求。服务器会看到你的 IP 地址、Flowlight/<version>用户代理和缓存条件验证器;不会收到任何流量、包设置或设备标识符启动后在检查到期时执行,此后在运行正常的情况下最多每天执行一次。默认开启;可在策略 › 托管包中关闭或手动选择立即检查
反向 DNS你配置的 DNS 解析器在没有看到主机名时,查询某个 IP 地址登记的名称按需进行,在采样器捕获模式下,也就是网络扩展开始运行之前的默认模式。没有关闭开关:没有它,采样器就没法给大多数连接命名。网络扩展从流量本身给它们命名,因此不做这件事
网络所有者查询Team Cymru,通过 DNS没有已知主机名的公网 IP 地址。私有地址和保留地址绝不离开你的 Mac默认开启。关闭开关在捕获 › 识别网络所有者
用托管模型提问你选择的提供方:Anthropic、OpenAI、Google Gemini 或兼容的端点你的问题,以及模型选中的那些只读 Flowlight 查询所返回的结果。流量数据库本身不会被上传只在你选择了托管提供方并提交问题时。提问界面会显示发送出去的请求
警示⌘5所有看起來不尋常的事,附上 App、目標,以及觸發規則的那個數字。
提問⌘6用平常的話問你自己的流量,由本機模型或託管模型從記錄下來的歷史裡作答。
檢查⌘7 · Developer ID選用:你轉送經過 Flowlight 的代理,其解密後的請求,以及背後的工具呼叫。搜尋也會查進請求與回應的內文。
規則⌘8 · Developer ID
⌘7 · App Store
你設過的每一條允許清單和拒絕,各自管到哪裡,以及實際攔下了什麼。
來源⌘9 · Developer ID
⌘8 · App Store
在三個分頁中顯示收集器狀態、裝置頻道與命名涵蓋範圍。
政策⌘8 · Developer ID
⌘7 · App Store
每項已設定的政策、是否真的生效,以及整合後的唯讀活動記錄。
來源⌘9 · Developer ID
⌘8 · App Store
收集器狀態與復原、選用的裝置通道和涵蓋範圍,集中在一張可捲動的操作頁面中。
-

快速鍵遵循可見的側邊欄順序。Developer ID 版本在 ⌘7 包含「檢查」;App Store 版本省略它,因此「規則」與「來源」會移到 ⌘7 和 ⌘8。

+

快速鍵遵循可見的側邊欄順序。Developer ID 版本在 ⌘7 包含「檢查」;App Store 版本省略它,因此「政策」與「來源」會移到 ⌘7 和 ⌘8。

即時畫面:一列 App,各自帶著目前的傳送與接收速率,以及最忙的目標。
即時,Flowlight 開啟時的那個畫面。
@@ -142,7 +142,7 @@

工具與 MCP 伺服器

一組,每個網域都標明是從這四種裡的哪一種知道的。帶著好幾個標籤的網域,是比只帶一個標籤更強的事實:設定檔裡寫了而且確實連過, 比其中任何一項單獨說得更多;只被設定過的網域從來沒有被連上,而只被觀察到的網域從來沒有寫在任何你本來可以讀到的地方。

每個網域提供兩種拒絕:「對這個代理」,涵蓋該代理以及它啟動的工具;或是「在所有地方」,涵蓋這台 Mac 上的 - 每一個 App。兩者寫下的都是普通規則,所以「違規紀錄」會報告它們,規則編輯器會解釋它們 — 而且正因為它們是普通規則, + 每一個 App。兩者寫下的都是普通規則,所以「活動」會報告它們,規則編輯器會解釋它們 — 而且正因為它們是普通規則, 編輯器裡的模擬會在你把它打開之前告訴你,它本來會拒絕掉什麼。

有一類網域沒有按鈕。模型供應商自己去連的連接器 — 在請求內文裡聲明,從供應商那一側連過去 — 根本不會碰到這台 Mac,所以這裡的 任何規則都碰不到它。那一列會把這件事說出來,而不是遞給你一個默默什麼都不做的拒絕。如果同一個主機在本機也有設定,或者 @@ -309,16 +309,16 @@

模擬回應

會被連線:模擬改變的是答案,而不是連線能不能建立,所以「API 回傳 500」做得到,「API 完全連不上」則做不到。

-

規則

-

允許清單講的是某一個代理可以去哪裡。規則是它的通用版本:它會指名一個 +

政策

+

政策集中列出 Flowlight 能套用的所有管控:一般的允許或封鎖規則、代理允許清單、受管理套件,以及在支援 HTTPS 檢查時的防護機制、模擬回應、請求改寫和回應轉換。每一列都會區分設定狀態與實際效果:生效、僅回報、監看、等待、已排程、暫停、已用完、正在套用、需要檢閱、無法使用、不相容或失敗。選取一列會開啟負責該政策的對應編輯器;「政策」不會取代或混淆這些功能各自獨立的授權與持久化方式。

+

允許清單講的是某一個代理可以去哪裡。一般規則是它的通用版本:它會指名一個 對象 — 一個 App、一個代理、一個目標、一個 URL,或其中兩者的組合 — 然後說封鎖 或允許。在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵,就能針對你正在看的東西寫一條; - 每一條都會進到「規則」畫面的同一個編輯器,因為在你看不到的地方生效的規則, - 會是一條你再也找不到的規則。

+ 每條規則都能在「政策」中找到,並會在其權威編輯器中開啟。

範本。共二十二個,分成五組 — 阻止代理連到套件登錄或其他 模型供應商、關上資料外流的常見途徑(貼文網站、webhook 接收器、匿名檔案上傳、通道、DNS over HTTPS)、產品分析與當掉回報、工作時段政策,以及一次性的「先封鎖一小時,讓我弄清楚 - 這是什麼」。你可以從「規則」工具列瀏覽它們,或在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵, + 這是什麼」。你可以從「政策」工具列瀏覽它們,或在「即時」、「歷史」、「AI 代理」或「檢查」的任一列上按右鍵, 把範本套用到你正在看的東西上。

每個範本都會在動手之前先說明它要做什麼。會寫入多條規則的,會開一張卡片列出每一台 主機和時程;任何永久性的,會開啟已預先填好的編輯器;而只有會自己失效的規則 — @@ -339,8 +339,7 @@

規則

位址和連接埠,但完全不知道路徑,而且只在延伸功能模式下有效。HTTPS 檢查拒絕的是單一請求,所以它能比對 URL,並回覆一個代理讀得懂的狀態,而不是一個莫名死掉的 socket。nettop 取樣器完全無法 拒絕任何東西。目前的設定執行不了的規則,會在列表中說明這一點,而不是擺在那裡看起來像已生效。

-

第二個標籤頁是違規紀錄:每一條由規則決定的連線,包含 App、它要去哪裡、是哪一條 - 規則決定的,以及那是一次拒絕,還是一個放行的例外。

+

第二個畫面是唯讀的活動:一般規則判定、允許清單違規、受管理套件事件,以及各類檢查政策的效果,都依時間排列在一起。它會轉接現有的權威記錄,而不是把歷史改寫成一種籠統的事件模型;App Store 版本會省略依賴檢查的政策類型及其保留的活動。

它本來會做什麼。封鎖的規則寫起來容易,打開卻令人害怕,因為你通常最先知道的是它 弄壞了什麼 — 而且是後來才知道,從別處某個東西失敗上知道的。規則編輯器先回答這個問題:它拿記錄 @@ -358,7 +357,7 @@

規則

它能看到的最早的流量:一條只對照了兩小時歷史的規則,就是只對照了兩小時歷史。

受管理的廣告與追蹤器套件

-

規則 › 套件包含三個官方主機名稱清單:廣告、跨網站追蹤和隱私權與遙測。它們從一開始便可見,且預設全部關閉。封鎖功能需要網路延伸功能;nettop 取樣器只能觀察。

+

政策 › 受管理套件包含三個官方主機名稱清單:廣告、跨網站追蹤和隱私權與遙測。它們從一開始便可見,且預設全部關閉。封鎖功能需要網路延伸功能;nettop 取樣器只能觀察。

僅限官方簽署的更新。 Flowlight 會檢查一個固定的 HTTPS 來源,以尋找由發佈者簽署的新版目錄。自動檢查預設開啟,可在此處關閉。簽章可驗證內容,但不會啟用內容:新套件會保持關閉;如果目的地有所變更,已啟用的套件將停止執行,直到你檢閱確切的新版本並按下儲存。服務中斷期間,已接受的內容可能會繼續保留為已知正常的最後版本,並明確顯示為已過時。這些是主機名稱層級的網路封鎖,無法移除 DOM 或版面配置中的空白、瀏覽器覆疊層、由指令碼產生的彈出式視窗或原生應用程式視窗。

當 HTTPS 檢查將 App 轉送經過 Flowlight 的代理伺服器時,一般目的地規則和已啟用的受管理套件會在開啟任何來源伺服器連線之前,使用原 App 的身分作出判定。對於使用憑證綁定或始終不解密的 CONNECT 通道,這項判定會按主機名稱進行;代理的允許清單仍只適用於直接經過網路延伸功能的路徑。如果擁有者或主機名稱未知,則會允許連線。

@@ -407,7 +406,8 @@

代理防護機制:封鎖一個工具、一個伺服器或

這不是沙箱。被拒絕的工具不會再被提供,也不會再得到回應。但仍然有 shell 的代理,可以自己動手做那個工具原本要做的事。Flowlight 會把那件事記錄下來;它不會假裝自己能阻止。

-

來源:收集器與引擎

+

來源:收集器、裝置與涵蓋範圍

+

來源是一張可捲動的操作頁面:先是收集器,接著是標題清楚的裝置與涵蓋範圍區段。狀態通知、提問答案、還原後的視窗和螢幕截圖連結都會捲動到相關區段並短暫標示,同時不隱藏其他內容。

Flowlight 用兩種方式之一記錄流量,在「來源 › 收集器」中選擇。兩者會漏掉的東西不同,在你倚賴 任何一種之前,值得先了解。

@@ -593,7 +593,7 @@

常見問答

視窗關著時,Flowlight 幾乎什麼都不做:只維持選單列上的速率,並每秒寫入一列資料。網路延伸功能 比備用的取樣器更省,後者每秒都會啟動一個短命的 nettop 程序。
它能封鎖連線嗎?
-
可以,搭配網路延伸功能。代理的允許清單除了警示也能拒絕,規則可以封鎖一個 App、 +
可以,搭配網路延伸功能。代理的允許清單除了警示也能拒絕,規則可以封鎖一個 App、 一個目標或一個 URL,時間長短由你決定,而防護機制可以在工具送到模型面前之前, 先把它從代理手上拿走。在你要求之前不會封鎖任何東西,而每一次拒絕都帶著它的退路。
我一定要用網路延伸功能嗎?
diff --git a/site/pages/zh-Hant/index.html b/site/pages/zh-Hant/index.html index 63900827..f4eeb05d 100644 --- a/site/pages/zh-Hant/index.html +++ b/site/pages/zh-Hant/index.html @@ -110,11 +110,11 @@

歷史

流量都到哪去了?從秒到年,依 App、目標或 IP

警示

哪裡看起來不對?每個 App 都有說得清楚的警示,附上目標和閾值。

⌘5
-

規則

我拒絕了什麼,真的有在擋嗎?每一條規則、各自封鎖過什麼,以及每一次決定的紀錄。

⌘8 · Developer ID / ⌘7 · App Store
+

政策

有哪些管控,又是否真的生效?每項政策、實際狀態,以及整合後的活動記錄。

⌘8 · Developer ID / ⌘7 · App Store

提問

用日常語言提問,從歷史記錄裡得到答案 — 下方還列出它跑過的每一條查詢。

⌘6
-

來源

資料是否正在到達、有多少已有名稱、哪些裝置通道正在使用?把收集器、裝置和涵蓋範圍放在同一處。

⌘9 · Developer ID / ⌘8 · App Store
+

來源

資料是否正在到達、哪些裝置通道正在使用、Flowlight 能交代多少?一張頁面從收集器一路看到涵蓋範圍。

⌘9 · Developer ID / ⌘8 · App Store

檢查

選用功能。代理實際送出了什麼,又是哪一次工具呼叫造成的?解密後的請求、工具呼叫、結果。

⌘7 · Developer ID
diff --git a/site/pages/zh-Hant/privacy.html b/site/pages/zh-Hant/privacy.html index de8bf30e..16b4ab3a 100644 --- a/site/pages/zh-Hant/privacy.html +++ b/site/pages/zh-Hant/privacy.html @@ -55,7 +55,7 @@

這個 App 發出的網路請求

- +
請求送到哪裡內容是什麼什麼時候
更新檢查api.github.com一個查詢最新版本的標準 HTTPS 請求;GitHub 會看到你的 IP 位址和 Flowlight/<version> 這個 user agent啟動時以及每天一次,自 0.1.2 起。關閉開關在「設定」› 「一般」
更新下載github.com磁碟映像檔及其總和檢查碼檔案只有在你點「下載並開啟」時
官方規則套件更新flowlight.xinbetween.com向固定官方來源傳送的標準 HTTPS 要求。伺服器會看到你的 IP 位址、Flowlight/<version>使用者代理程式和條件式快取驗證器;不會收到任何流量、套件設定或裝置識別碼啟動後在檢查到期時執行,此後在運作正常的情況下每天最多執行一次。預設開啟;可在規則 › 套件中關閉或手動選擇立即檢查
官方規則套件更新flowlight.xinbetween.com向固定官方來源傳送的標準 HTTPS 要求。伺服器會看到你的 IP 位址、Flowlight/<version>使用者代理程式和條件式快取驗證器;不會收到任何流量、套件設定或裝置識別碼啟動後在檢查到期時執行,此後在運作正常的情況下每天最多執行一次。預設開啟;可在「政策 › 受管理套件」中關閉或手動選擇立即檢查
反向 DNS你設定的 DNS 解析器在沒有看到主機名稱時,查詢某個 IP 位址登記的名稱需要時進行,在取樣器擷取模式下,也就是網路延伸功能開始運作之前的預設模式。沒有關閉開關:少了它,取樣器就沒辦法為大多數連線命名。網路延伸功能會從流量本身為它們命名,因此不做這件事
網路擁有者查詢Team Cymru,透過 DNS沒有已知主機名稱的公開 IP 位址。私有和保留位址絕不會離開你的 Mac預設開啟。關閉開關在「擷取」› 「辨識網路擁有者」
用託管模型提問你選擇的供應商:Anthropic、OpenAI、Google Gemini 或相容的端點你的問題,以及模型所選用的 Flowlight 唯讀查詢回傳的結果。流量資料庫本身不會被上傳只有在你選擇託管供應商並送出問題時。「提問」畫面會顯示送出的請求