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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: ci

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
ci:
name: ci
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Select Swift 6 toolchain
run: |
set -euo pipefail
for xcode in \
/Applications/Xcode_16.2.app \
/Applications/Xcode_16.1.app \
/Applications/Xcode_16.0.app \
/Applications/Xcode.app
do
if [ -d "$xcode" ]; then
sudo xcode-select -s "$xcode"
break
fi
done
swift --version

- name: Build with warnings as errors
run: swift build -Xswiftc -warnings-as-errors

- name: Test
run: swift test

- name: Lint Swift formatting
run: xcrun swift-format lint --strict --recursive Sources Tests Package.swift
20 changes: 20 additions & 0 deletions LICENSING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Licensing

agentd is licensed under the Business Source License 1.1.

The repository-level license is in `LICENSE`, and Swift source files carry:

```text
SPDX-License-Identifier: BUSL-1.1
```

Plain-language summary:

- non-production copying, modification, redistribution, and derivative work use
is allowed under BUSL 1.1
- production use requires a commercial license from EvalOps unless an additional
grant is published
- each version changes to GPL Version 2.0 or any later version on the earlier of
its stated Change Date or the fourth anniversary of first public distribution

This summary is not a replacement for `LICENSE`; the license text controls.
107 changes: 107 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Privacy

agentd is a macOS menu-bar client that captures screen activity, performs OCR
locally, and submits scrubbed frame metadata to the EvalOps Chronicle pipeline.
This document describes the behavior in this repository as implemented today.

## What agentd Captures

For accepted frames, agentd may collect:

- active display dimensions
- capture timestamp
- app bundle ID and app name
- focused window title
- focused document path or document URL when macOS Accessibility exposes it
- OCR text recognized by Apple Vision
- OCR confidence
- a 64-bit perceptual hash used for deduplication
- a raw-BGRA byte estimate for the frame size

Raw screen pixels are used in memory for OCR, hashing, and filtering. Raw pixels
are not written to batch JSON and are not PNG-encoded for metadata.

## What Stays On Device

agentd performs these steps locally:

- ScreenCaptureKit capture
- Vision OCR
- perceptual hashing and deduplication
- allowlist, denylist, pause-window, path, and secret checks
- local fallback persistence

Local fallback batches are stored as `0o600` JSON files under:

```text
~/.evalops/agentd/batches
```

The batch directory is swept on local persistence and after successful remote
submissions. Defaults are:

- maximum local batch age: 7 days
- maximum local batch size budget: 512 MiB

Oldest files are removed first when the byte budget is exceeded.

## What Crosses The Wire

When `localOnly` is `false`, agentd sends a Connect/proto JSON
`SubmitBatchRequest` to the configured Chronicle endpoint. Remote submission is
refused unless:

- the endpoint is HTTPS, or
- the endpoint is loopback HTTP such as `localhost`, `127.x.x.x`, or `::1`, or
- the endpoint uses a supported local socket scheme

Remote submission also requires configured client authentication. Bearer tokens
are resolved from Keychain. They are not stored in `config.json`. mTLS identities
are resolved from Keychain by label and attached through URLSession client
certificate authentication.

## Drop And Scrub Behavior

agentd drops a frame before batching when:

- the active app bundle is denied
- an allowlist is present and the active app is not in it
- the document path matches a denied path prefix
- the window title matches a pause pattern
- the window title, document path, or full OCR text matches a secret pattern
- the perceptual hash is near a recent accepted frame

Secret matches are fail-closed: agentd drops the whole frame rather than
redacting and shipping partial content. Secret scanning runs against the full OCR
text before any configured OCR text truncation.

## Inspecting Or Wiping State

Configuration:

```text
~/.evalops/agentd/config.json
```

Local batches:

```text
~/.evalops/agentd/batches
```

To wipe local batches:

```sh
rm -f ~/.evalops/agentd/batches/*.json
```

To force local-only behavior, set `localOnly` to `true` in `config.json`.

## Residual Risks

OCR can miss secrets inside images, screenshots, unusual fonts, or partially
visible text. Window titles and document paths can still contain sensitive
metadata even when OCR is clean, which is why those fields are also checked by
the secret scrubber before batching. Local JSON batches contain OCR-derived
content and should be treated as sensitive until encrypted-at-rest support is
added.
49 changes: 25 additions & 24 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
// swift-tools-version: 6.0
// SPDX-License-Identifier: BUSL-1.1
import PackageDescription

let package = Package(
name: "agentd",
platforms: [.macOS(.v14)],
products: [
.executable(name: "agentd", targets: ["agentd"])
],
targets: [
.executableTarget(
name: "agentd",
path: "Sources/agentd",
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-sectcreate",
"-Xlinker", "__TEXT",
"-Xlinker", "__info_plist",
"-Xlinker", "support/Info.plist"
])
]
),
.testTarget(
name: "agentdTests",
dependencies: ["agentd"],
path: "Tests/agentdTests"
)
]
name: "agentd",
platforms: [.macOS(.v14)],
products: [
.executable(name: "agentd", targets: ["agentd"])
],
targets: [
.executableTarget(
name: "agentd",
path: "Sources/agentd",
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-sectcreate",
"-Xlinker", "__TEXT",
"-Xlinker", "__info_plist",
"-Xlinker", "support/Info.plist",
])
]
),
.testTarget(
name: "agentdTests",
dependencies: ["agentd"],
path: "Tests/agentdTests"
),
]
)
67 changes: 58 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,30 @@ This is the desktop component of the work tracked in

## What it does (v0)

- Captures the active display via `ScreenCaptureKit` at an adaptive 0.2–1 fps.
- Captures the active display via `ScreenCaptureKit` at an adaptive 0.2–1 fps;
input idle time drops cadence to `idleFps` and activity restores
`captureFps`.
- Reads `(bundleId, windowTitle, documentPath)` per frame via the Accessibility
API and `NSWorkspace`.
- Runs Apple Vision OCR on-device.
- Drops near-duplicate frames via 64-bit pHash (Hamming ≤ 5).
- Fail-closed `SecretScrubber` against AWS / GCP / SSH / JWT / GitHub /
Anthropic / OpenAI / Slack / Stripe markers — match → frame dropped, never
partial-redacted.
- Drops near-duplicate frames via a 64-bit pHash ring buffer (Hamming ≤ 5).
- Fail-closed `SecretScrubber` against AWS / GCP / SSH / JWT / GitHub classic
and fine-grained tokens / Google API keys / npm / SendGrid / DigitalOcean /
Azure storage keys / Mailgun / Twilio / Discord / Anthropic / OpenAI / Slack /
Stripe markers — match → frame dropped, never partial-redacted.
- Per-app allow/deny list and per-path deny list.
- Window-title pause patterns (Zoom, FaceTime, 1Password…).
- Secret scanning covers OCR text, window titles, and document paths before a
frame is batched.
- OCR text is scrubbed at full length, then capped to `maxOcrTextChars`
(default 4096) with `ocrTextTruncated` set when the cap applies.
- Batches every 30s or 24 frames, whichever first.
- Local-only mode persists batches under `~/.evalops/agentd/batches/` as
`0o600` JSON; HTTP mode `POST`s a Connect/proto JSON `SubmitBatchRequest`
to `chronicle.v1.ChronicleService.SubmitBatch` and falls back to local on
failure.
`0o600` JSON and sweeps old or over-budget batches; HTTP mode `POST`s a
Connect/proto JSON `SubmitBatchRequest` to
`chronicle.v1.ChronicleService.SubmitBatch` and falls back to local on
failure. Remote HTTP is allowed only for loopback development; non-loopback
remote endpoints must use HTTPS and configured client auth.
- Menu-bar UI: pause/resume (`⌃⌥⌘P`), flush now (`⌃⌥⌘F`), reveal batches dir,
quit.

Expand All @@ -39,6 +48,45 @@ First run will trigger the system Screen Recording and Accessibility prompts the
first time the gated APIs are called. Grant both in System Settings → Privacy &
Security and relaunch.

## Configuration

agentd reads and writes `~/.evalops/agentd/config.json`. Important defaults:

- `localOnly: true`
- `captureFps: 1.0`
- `idleFps: 0.2`
- `idleThresholdSeconds: 60`
- `batchIntervalSeconds: 30`
- `maxFramesPerBatch: 24`
- `maxOcrTextChars: 4096`
- `maxBatchAgeDays: 7`
- `maxBatchBytes: 536870912`
- `auth: { "mode": "none" }`

Remote mode requires `localOnly: false`, an HTTPS or loopback endpoint, and an
auth mode. Bearer auth references a Keychain item:

```json
{
"auth": {
"mode": "bearer",
"keychainService": "dev.evalops.agentd",
"keychainAccount": "chronicle"
}
}
```

mTLS auth references a Keychain identity label:

```json
{
"auth": {
"mode": "mtls",
"identityLabel": "agentd Chronicle client"
}
}
```

## What's next

- Consume generated `chronicle.v1` Swift types when the platform SDK publishes
Expand Down Expand Up @@ -72,4 +120,5 @@ Tests/agentdTests/ # SecretScrubber + path policy

## License

Business Source License 1.1. See `LICENSE` for the current terms.
Business Source License 1.1. See `LICENSE` and `LICENSING.md` for the current
terms.
57 changes: 57 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Security Policy

## Reporting

Please report suspected vulnerabilities through GitHub private vulnerability
reporting for this repository, or email security@evalops.dev with the subject
`agentd security report`.

Include:

- affected version or commit
- macOS version and hardware class, if relevant
- reproduction steps
- expected and observed behavior
- whether screen content, OCR text, local batches, credentials, or transport
authentication are involved

Do not post exploitable details in public GitHub issues before EvalOps has had
a chance to triage and coordinate a fix.

## Scope

In scope:

- screen capture, OCR, and window-context handling
- local batch persistence under `~/.evalops/agentd/batches`
- secret detection and fail-closed drop behavior
- endpoint transport security and client authentication
- macOS permissions and Keychain integration used by agentd

Out of scope:

- social engineering
- denial-of-service reports without a concrete security impact
- vulnerabilities in macOS, ScreenCaptureKit, Vision, or GitHub Actions unless
agentd uses them in a way that creates additional exposure

## Severity Expectations

EvalOps treats these as high severity by default:

- screen pixels, OCR text, window titles, or document paths sent to an
unintended remote endpoint
- plaintext remote transport outside loopback development
- missing or bypassed client authentication for remote submission
- secret-bearing OCR text or window metadata shipped instead of dropped
- unbounded local retention of OCR-derived batch data

Lower-severity issues include missing hardening, incomplete docs, or telemetry
gaps that do not expose captured content or credentials directly.

## Handling

EvalOps will acknowledge credible reports as quickly as possible, triage impact,
and coordinate fixes through private advisories when warranted. Public issues
may be opened after a fix is available or when the report is not security
sensitive.
Loading