Compare commits

...
Author SHA1 Message Date
lxcxjxhx 215482eb22 docs(skills): stop linking reference files that are not in this repo 2026-11-08 02:52:52 +02:00
Ahmed Allam 0601550662 ci: fold CI into one workflow with job timeouts and path-gate the binary and workflow lint 2026-11-05 23:03:29 +02:00
theo-at-strixandAlex Schapiro 851743adcf feat(skills): cover email identity canonicalization drift (#1661)
* feat(skills): cover email identity canonicalization drift

* revert root agent routing change

---------

Co-authored-by: Alex Schapiro <[email protected]>
2026-11-05 12:41:45 -05:00
10 changed files with 169 additions and 204 deletions

No files matched your search

@@ -1,48 +1,39 @@
name: Package
name: Binary
on:
workflow_call:
pull_request:
paths:
- pyproject.toml
- uv.lock
- strix.spec
- scripts/**
- strix/interface/tui/**
- .github/actions/**
- .github/workflows/binary.yml
push:
branches:
- main
paths:
- pyproject.toml
- uv.lock
- strix.spec
- scripts/**
- strix/interface/tui/**
- .github/actions/**
- .github/workflows/binary.yml
workflow_dispatch:
permissions:
contents: read
jobs:
wheel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: strix/interface/tui/go.mod
cache-dependency-path: strix/interface/tui/go.sum
- uses: ./.github/actions/setup-python-env
- run: uv build --wheel
- name: Wheel ships the TUI sidecar and the viewer bundle
run: |
uv run --frozen python - <<'PY'
import glob, zipfile
(wheel,) = glob.glob("dist/*.whl")
names = zipfile.ZipFile(wheel).namelist()
assert "strix/bin/strix-tui" in names, names
assert "strix/interface/viewer/static/index.html" in names, names
assert not any(name.startswith("strix/interface/viewer/frontend/") for name in names)
assert not any(name.startswith("strix/interface/tui/cmd/") for name in names)
PY
- name: Install into a clean environment and run the CLI
env:
UV_EXCLUDE_NEWER: 5 days
run: |
uv venv --python 3.12 /tmp/strix-smoke
uv pip install --python /tmp/strix-smoke/bin/python dist/*.whl
/tmp/strix-smoke/bin/strix --version
/tmp/strix-smoke/bin/strix --help >/dev/null
- name: Vertex AI and Bedrock dependencies install with the wheel
run: /tmp/strix-smoke/bin/python -c "import google.auth.transport.requests, boto3"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
binary:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
+2
View File
@@ -32,6 +32,7 @@ jobs:
wheel-platform: win_amd64
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
@@ -110,6 +111,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
-61
View File
@@ -1,61 +0,0 @@
name: Python
on:
workflow_call:
permissions:
contents: read
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- run: uv lock --check
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
- run: uv run --frozen ruff check .
- run: uv run --frozen ruff format --check .
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
- run: uv run --frozen mypy strix/
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
- run: uv run --frozen bandit -r strix/ -c pyproject.toml
pytest:
name: pytest (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
with:
python-version: ${{ matrix.python-version }}
- run: uv run --frozen pytest -q
-26
View File
@@ -1,26 +0,0 @@
name: TUI
on:
workflow_call:
permissions:
contents: read
jobs:
go:
runs-on: ubuntu-latest
defaults:
run:
working-directory: strix/interface/tui
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: strix/interface/tui/go.mod
cache-dependency-path: strix/interface/tui/go.sum
- run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: CGO_ENABLED=0 go build -trimpath -o /dev/null ./cmd/strix-tui
- run: go test -race ./...
-27
View File
@@ -1,27 +0,0 @@
name: Viewer
on:
workflow_call:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: strix/interface/viewer/frontend
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: npm
cache-dependency-path: strix/interface/viewer/frontend/package-lock.json
- run: npm ci --no-audit --no-fund
- run: npm run build
- name: Committed viewer bundle matches the build
run: git -C "$GITHUB_WORKSPACE" diff --exit-code --stat -- strix/interface/viewer/static && test -z "$(git -C "$GITHUB_WORKSPACE" status --porcelain -- strix/interface/viewer/static)"
+97 -28
View File
@@ -15,38 +15,107 @@ concurrency:
cancel-in-progress: true
jobs:
python:
uses: ./.github/workflows/ci-python.yml
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
- run: uv lock --check
- run: uv run --frozen ruff check .
- run: uv run --frozen ruff format --check .
- run: uv run --frozen mypy strix/
- run: uv run --frozen bandit -r strix/ -c pyproject.toml
pytest:
name: pytest (${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ['3.12', '3.14']
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: ./.github/actions/setup-python-env
with:
python-version: ${{ matrix.python-version }}
- run: uv run --frozen pytest -q
tui:
uses: ./.github/workflows/ci-tui.yml
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: strix/interface/tui
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: strix/interface/tui/go.mod
cache-dependency-path: strix/interface/tui/go.sum
- run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: CGO_ENABLED=0 go build -trimpath -o /dev/null ./cmd/strix-tui
- run: go test -race ./...
viewer:
uses: ./.github/workflows/ci-viewer.yml
package:
uses: ./.github/workflows/ci-package.yml
workflows:
uses: ./.github/workflows/ci-workflows.yml
ci-passed:
name: ci-passed
if: always()
needs:
- python
- tui
- viewer
- package
- workflows
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: strix/interface/viewer/frontend
steps:
- name: Every job succeeded
env:
NEEDS: ${{ toJSON(needs) }}
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: npm
cache-dependency-path: strix/interface/viewer/frontend/package-lock.json
- run: npm ci --no-audit --no-fund
- run: npm run build
- name: Committed viewer bundle matches the build
run: git -C "$GITHUB_WORKSPACE" diff --exit-code --stat -- strix/interface/viewer/static && test -z "$(git -C "$GITHUB_WORKSPACE" status --porcelain -- strix/interface/viewer/static)"
wheel:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: strix/interface/tui/go.mod
cache-dependency-path: strix/interface/tui/go.sum
- uses: ./.github/actions/setup-python-env
- run: uv build --wheel
- name: Wheel ships the TUI sidecar and the viewer bundle
run: |
failing=$(jq -r 'to_entries[] | select(.value.result != "success") | "\(.key): \(.value.result)"' <<< "$NEEDS")
if [ -n "$failing" ]; then
echo "$failing"
exit 1
fi
uv run --frozen python - <<'PY'
import glob, zipfile
(wheel,) = glob.glob("dist/*.whl")
names = zipfile.ZipFile(wheel).namelist()
assert "strix/bin/strix-tui" in names, names
assert "strix/interface/viewer/static/index.html" in names, names
assert not any(name.startswith("strix/interface/viewer/frontend/") for name in names)
assert not any(name.startswith("strix/interface/tui/cmd/") for name in names)
PY
- name: Install into a clean environment and run the CLI
env:
UV_EXCLUDE_NEWER: 5 days
run: |
uv venv --python 3.12 /tmp/strix-smoke
uv pip install --python /tmp/strix-smoke/bin/python dist/*.whl
/tmp/strix-smoke/bin/strix --version
/tmp/strix-smoke/bin/strix --help >/dev/null
- name: Vertex AI and Bedrock dependencies install with the wheel
run: /tmp/strix-smoke/bin/python -c "import google.auth.transport.requests, boto3"
@@ -1,14 +1,27 @@
name: Workflows
on:
workflow_call:
pull_request:
paths:
- .github/**
push:
branches:
- main
paths:
- .github/**
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
actionlint:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
ACTIONLINT_VERSION: 1.7.12
ACTIONLINT_SCRIPT_REF: 914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12
@@ -27,6 +40,7 @@ jobs:
zizmor:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
-20
View File
@@ -45,23 +45,3 @@ repos:
hooks:
- id: bandit
args: [-c, pyproject.toml]
# Additional Python code quality checks
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py312-plus]
ci:
autofix_commit_msg: |
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: ""
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
autoupdate_schedule: weekly
# pre-commit.ci cannot run `language: system` hooks; mypy runs via `make check-all`.
skip: [mypy]
submodules: false
+4 -4
View File
@@ -207,7 +207,7 @@ agent-browser snapshot -i
```
Credentials in shell history are a leak. For anything sensitive, use the
auth vault (see [references/authentication.md](references/authentication.md)):
auth vault (see `references/authentication.md`):
```bash
agent-browser auth save my-app --url https://app.example.com/login \
@@ -367,7 +367,7 @@ agent-browser click @e3
agent-browser record stop
```
See [references/video-recording.md](references/video-recording.md) for
See `references/video-recording.md` for
codec options, GIF export, and more.
### Iframes
@@ -491,8 +491,8 @@ snapshot — fall back to `eval` in the iframe's origin or use the
**Authentication expires mid-workflow**
Use `--session-name <name>` or `state save`/`state load` so your session
survives browser restarts. See [references/session-management.md](references/session-management.md)
and [references/authentication.md](references/authentication.md).
survives browser restarts. See `references/session-management.md`
and `references/authentication.md`.
## Global flags worth knowing
@@ -1,6 +1,6 @@
---
name: semantic-confusion
description: Cross-component semantic confusion testing for parser differentials, normalization mismatches, overloaded fields, lifecycle state drift, internal redirects, protocol translation, and validator-to-sink inconsistencies
description: Cross-component semantic confusion testing for parser differentials, normalization mismatches, email and IDNA identity drift, overloaded fields, lifecycle state drift, internal redirects, protocol translation, and validator-to-sink inconsistencies
---
# Semantic Confusion
@@ -59,6 +59,27 @@ The highest-signal condition is `security_check(value_A)` followed by `sink(tran
- Test malformed Unicode and replacement behavior; a rejected code point may become an allowed delimiter or wildcard later.
- Test path, query, and fragment separately. Browsers and routers commonly transform each source differently.
### Email and Account Identity Drift
Treat an email address as both a principal selector and a delivery destination. Trace the complete boundary before testing:
```text
submitted or identity-provider address
-> application normalization / IDNA
-> database comparison and collation
-> selected local principal
-> token, session, invitation, or link binding
-> recipient or linked identity consumed by the final service
```
- Cover recovery, magic links, invitations, registration uniqueness, email changes, and OAuth/OIDC account linking. Test case/accent-insensitive collations, Unicode normalization, IDNA U-label/A-label conversion, trailing dots, and provider canonicalization only when the observed stack supports them.
- In black-box testing, report the observed equivalence and routing behavior. Do not name a specific collation, normalizer, or provider algorithm without source, configuration, or version evidence.
- Determine whether two non-identical addresses can select the same protected principal while SMTP or an identity provider routes them to different owners. The address that receives authority must identify the same principal selected by the security check.
- In recovery flows, compare the submitted address with the selected account's stored canonical address. A fuzzy lookup followed by delivery to the submitted value is high risk; an exact lookup plus delivery to the stored value is the primary negative control.
- Use only an explicitly authorized attacker-controlled mailbox or test domain. Derive a bounded variant set from the deployed collation and canonicalizers rather than spraying external domains or the Unicode range.
For a recovery, magic-link, or invitation finding, a generic success response is not proof. Demonstrate that the variant selects the protected principal, the issued authority reaches the controlled mailbox or identity, and redemption yields the protected principal's access. If delivery or redemption cannot be observed, keep the path `needs_follow_up`.
### Field and Type Overloading
- Identify shared fields reused for different concepts: path vs URL, content type vs handler, display name vs executable name, route vs filesystem location.
@@ -170,6 +191,8 @@ A valid confusion finding should include:
- Different error messages with identical final authorization and sink behavior
- A parser accepts odd syntax but downstream consumers preserve the same safe meaning
- A normalization difference visible only in logs, with no security decision between representations
- A Unicode or Punycode lookalike that neither selects another principal nor receives authority intended for that principal
- A permissive database comparison where recovery delivery still uses the selected account's stored canonical address
- WAF bypass where the application itself rejects the request identically
- Version-specific behavior claimed as universal without testing the relevant deployment
- A search-path candidate that is attacker-named but cannot be created, claimed, loaded, or executed