Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7dc9487
Rename Privacy Guard to Egress Gate and add domain models
johnnygreco Aug 3, 2026
6188958
Add Egress Gate refactor plan
johnnygreco Aug 3, 2026
f86907f
Update Egress Gate to OpenShell v0.0.97
johnnygreco Aug 3, 2026
f63767d
Refactor Egress Gate around request-level gates
johnnygreco Aug 3, 2026
e811aa7
Harden Egress Gate runtime boundaries
johnnygreco Aug 3, 2026
9410ac0
Implement request-rules built-in
johnnygreco Aug 3, 2026
e2273ac
Bound request-rules glob matching
johnnygreco Aug 3, 2026
b20080a
feat(egress-gate): add offline evaluation and semantic example
johnnygreco Aug 4, 2026
7ecfae2
feat(egress-gate): complete refactor integration
johnnygreco Aug 4, 2026
53f76cf
refactor(egress-gate): defer semantic gate
johnnygreco Aug 4, 2026
71eb948
chore(egress-gate): remove Privacy Guard artifacts
johnnygreco Aug 4, 2026
efc21cd
refactor(egress-gate): remove deterministic request gate
johnnygreco Aug 4, 2026
cbde03c
docs(egress-gate): add runnable custom gate example
johnnygreco Aug 4, 2026
95905dd
refactor(egress-gate): remove redundant validation
johnnygreco Aug 4, 2026
ab4f149
fix(egress-gate): address panel review findings
johnnygreco Aug 4, 2026
9de1e0b
fix(egress-gate): close review edge cases
johnnygreco Aug 4, 2026
3f53e62
docs(egress-gate): clarify extensible middleware guidance
johnnygreco Aug 4, 2026
d1c6d0d
docs: update Egress Gate landing description
johnnygreco Aug 4, 2026
ccb9b86
docs(egress-gate): refine documentation visuals
johnnygreco Aug 4, 2026
6cda904
docs(egress-gate): highlight code examples
johnnygreco Aug 4, 2026
460a036
docs(egress-gate): refine diagram routing
johnnygreco Aug 4, 2026
253325b
docs(egress-gate): polish request lifecycle diagram
johnnygreco Aug 4, 2026
92b7eff
docs(egress-gate): teach offline policy testing
johnnygreco Aug 4, 2026
c6b5924
Polish documentation navigation
johnnygreco Aug 4, 2026
a4d017b
Generalize Egress Gate request scanning
johnnygreco Aug 4, 2026
3173656
Polish Egress Gate CLI and examples
johnnygreco Aug 4, 2026
399f9ce
Simplify Egress Gate example setup
johnnygreco Aug 4, 2026
b05f413
Make CLI tests terminal-independent
johnnygreco Aug 4, 2026
6a19baa
Normalize styled CLI test output
johnnygreco Aug 4, 2026
7b352cd
Remove request-content logging
johnnygreco Aug 4, 2026
f80a2f6
Normalize CLI help test output
johnnygreco Aug 4, 2026
fc82923
Polish Egress Gate presentation
johnnygreco Aug 5, 2026
6eec148
Balance documentation header controls
johnnygreco Aug 5, 2026
259ce87
Keep documentation header width consistent
johnnygreco Aug 5, 2026
1cf756c
Harden documentation layout consistency
johnnygreco Aug 5, 2026
aa74572
Clarify Egress Gate request mutations
johnnygreco Aug 5, 2026
03514ab
Merge remote-tracking branch 'origin/main' into johnny/egress-gate-re…
johnnygreco Aug 5, 2026
13f58d5
Address Egress Gate QA findings
johnnygreco Aug 5, 2026
0100f11
Flatten Egress Gate policy schema
johnnygreco Aug 5, 2026
68b8fe9
Simplify Egress Gate extensibility
johnnygreco Aug 5, 2026
6756086
Clarify automatic registry sealing
johnnygreco Aug 5, 2026
8918414
Improve Egress Gate examples
johnnygreco Aug 5, 2026
a841823
Improve Egress Gate setup workflows
johnnygreco Aug 5, 2026
466215b
docs(egress-gate): clarify gateway restart workflow
johnnygreco Aug 6, 2026
ec744bc
fix(egress-gate): address final review findings
johnnygreco Aug 6, 2026
decc13d
refactor(egress-gate): simplify final review fixes
johnnygreco Aug 6, 2026
fcc1100
refactor(egress-gate): trust inherited config defaults
johnnygreco Aug 6, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/docs-preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@ jobs:
'scripts/build-docs.sh',
'scripts/publish-agent-markdown.py',
'scripts/render-dev-notes.py',
'scripts/stage-privacy-guard-docs.py',
'scripts/stage-egress-gate-docs.py',
'tests/test_agent_markdown.py',
'tests/test_docs_404.py',
'tests/test_render_dev_notes.py',
'tests/test_stage_privacy_guard_docs.py',
'tests/test_stage_egress_gate_docs.py',
'zensical.toml',
]);
const docsChanged = files.some(
({ filename }) => filename.startsWith('docs/') ||
filename.startsWith('overrides/') ||
filename.startsWith('projects/privacy-guard/docs/') ||
filename.startsWith('projects/egress-gate/docs/') ||
exactInputs.has(filename),
);
operation = docsChanged ? 'deploy' : 'remove';
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docs-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ jobs:
'scripts/build-docs.sh',
'scripts/publish-agent-markdown.py',
'scripts/render-dev-notes.py',
'scripts/stage-privacy-guard-docs.py',
'scripts/stage-egress-gate-docs.py',
'tests/test_agent_markdown.py',
'tests/test_docs_404.py',
'tests/test_render_dev_notes.py',
'tests/test_stage_privacy_guard_docs.py',
'tests/test_stage_egress_gate_docs.py',
'zensical.toml',
]);
const docsChanged = files.some(
({ filename }) => filename.startsWith('docs/') ||
filename.startsWith('overrides/') ||
filename.startsWith('projects/privacy-guard/docs/') ||
filename.startsWith('projects/egress-gate/docs/') ||
exactInputs.has(filename),
);
core.setOutput('operation', docsChanged ? 'deploy' : 'remove');
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Check navigation JavaScript syntax
run: node --check docs/javascripts/navigation-drawer.js

- name: Test navigation JavaScript behavior
run: node tests/navigation-drawer.test.js

- name: Build documentation
run: scripts/build-docs.sh

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Privacy Guard
name: Egress Gate

"on":
pull_request:
Expand All @@ -11,12 +11,12 @@ permissions:
contents: read

concurrency:
group: privacy-guard-${{ github.workflow }}-${{ github.ref }}
group: egress-gate-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Check Privacy Guard (Python ${{ matrix.python-version }})
name: Check Egress Gate (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -26,7 +26,7 @@ jobs:
- "3.14"
defaults:
run:
working-directory: projects/privacy-guard
working-directory: projects/egress-gate
steps:
- name: Checkout
uses: actions/checkout@v7
Expand All @@ -41,8 +41,8 @@ jobs:

- name: Configure isolated uv paths
run: |
echo "UV_CACHE_DIR=$RUNNER_TEMP/privacy-guard-uv-cache" >> "$GITHUB_ENV"
echo "UV_PROJECT_ENVIRONMENT=$RUNNER_TEMP/privacy-guard-venv" >> "$GITHUB_ENV"
echo "UV_CACHE_DIR=$RUNNER_TEMP/egress-gate-uv-cache" >> "$GITHUB_ENV"
echo "UV_PROJECT_ENVIRONMENT=$RUNNER_TEMP/egress-gate-venv" >> "$GITHUB_ENV"

- name: Install locked dependencies
run: uv sync --frozen
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ lib/
# Static site and docs output
public/
site/
docs/documentation/privacy-guard/
docs/documentation/egress-gate/
.docusaurus/
.vitepress/cache/
.vitepress/dist/
Expand Down
4 changes: 2 additions & 2 deletions docs/development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ scripts/build-docs.sh
```

`scripts/build-docs.sh` recreates `.venv-docs`, installs the pinned toolchain,
stages canonical Privacy Guard documentation from
`projects/privacy-guard/docs/`, renders Dev Notes metadata, and runs
stages canonical Egress Gate documentation from
`projects/egress-gate/docs/`, renders Dev Notes metadata, and runs
`zensical build --clean --strict`. Do not report success unless it completes
without issues.

Expand Down
3 changes: 2 additions & 1 deletion docs/documentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ agent_markdown: true
Technical documentation and references for installing, using, and extending
OpenShell Research projects.

- [Privacy Guard](privacy-guard/index.md): middleware for protecting sensitive data in OpenShell.
- [Egress Gate](egress-gate/index.md): extensible middleware for applying gates
to outgoing HTTP requests.
123 changes: 87 additions & 36 deletions docs/javascripts/navigation-drawer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
(() => {
const drawerStateKey = "openshell.navigationDrawerOpen";
const modalDrawerQuery = "(max-width: 63.99rem)";
let cleanup = () => {};

function enhanceNavigationDrawer() {
Expand All @@ -7,32 +9,19 @@
const toggle = document.querySelector("#__drawer");
const sidebar = document.querySelector(".md-sidebar--primary");
const overlay = document.querySelector('.md-overlay[for="__drawer"]');
const legacyControl = document.querySelector(
'.md-header__button[for="__drawer"]',
);

if (!(toggle instanceof HTMLInputElement) || !(sidebar instanceof HTMLElement)) {
cleanup = () => {};
return;
}

let button = document.querySelector(".openshell-drawer-button");
if (!(button instanceof HTMLButtonElement) && legacyControl instanceof HTMLElement) {
button = document.createElement("button");
button.type = "button";
button.className = legacyControl.className;
button.classList.add("openshell-drawer-button");
button.innerHTML = legacyControl.innerHTML;
legacyControl.replaceWith(button);
}

if (!(button instanceof HTMLButtonElement)) {
const modalDrawer = window.matchMedia(modalDrawerQuery);
const button = document.querySelector(".openshell-drawer-button");

if (
!(toggle instanceof HTMLInputElement) ||
!(sidebar instanceof HTMLElement) ||
!(button instanceof HTMLElement)
) {
cleanup = () => {};
return;
}

sidebar.id = "primary-navigation";
sidebar.setAttribute("role", "dialog");
sidebar.setAttribute("aria-label", "Primary navigation");
button.setAttribute("aria-controls", sidebar.id);

Expand Down Expand Up @@ -61,26 +50,42 @@
const focusableElements = () =>
Array.from(
sidebar.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])',
'a[href], button:not([disabled]), input:not([disabled]):not(.md-toggle), [tabindex]:not([tabindex="-1"])',
),
).filter((element) => element instanceof HTMLElement && !element.hidden);
).filter(
(element) =>
element instanceof HTMLElement &&
element.tabIndex >= 0 &&
element.getClientRects().length > 0 &&
window.getComputedStyle(element).visibility === "visible" &&
!element.closest("[inert]"),
);

const synchronize = ({ moveFocus = false, restoreFocus = false } = {}) => {
const isOpen = toggle.checked;
const isModal = modalDrawer.matches;
document.documentElement.dataset.navigationDrawer = isOpen ? "open" : "closed";
button.setAttribute("aria-expanded", String(isOpen));
button.setAttribute("aria-label", isOpen ? "Close navigation" : "Open navigation");
sidebar.setAttribute("aria-hidden", String(!isOpen));
if (isOpen) {
if (isModal) {
sidebar.setAttribute("role", "dialog");
sidebar.setAttribute("aria-label", "Primary navigation");
} else {
sidebar.removeAttribute("role");
sidebar.removeAttribute("aria-label");
}
if (isOpen && isModal) {
sidebar.setAttribute("aria-modal", "true");
} else {
sidebar.removeAttribute("aria-modal");
}
sidebar.inert = !isOpen;
backgroundElements.forEach((wasInert, element) => {
element.inert = isOpen || wasInert;
element.inert = (isOpen && isModal) || wasInert;
});

if (isOpen && moveFocus) {
if (isOpen && isModal && moveFocus) {
focusableElements()[0]?.focus();
} else if (!isOpen && restoreFocus) {
returnFocus.focus();
Expand All @@ -92,27 +97,42 @@
returnFocus = button;
}
toggle.checked = isOpen;
writeDrawerState(isOpen);
synchronize(options);
};

const onButtonClick = () => {
const onButtonClick = (event) => {
event.preventDefault();
setOpen(!toggle.checked, {
moveFocus: !toggle.checked,
restoreFocus: toggle.checked,
});
};

const onToggleChange = () => synchronize();
const onOverlayClick = (event) => {
event.preventDefault();
setOpen(false, { restoreFocus: true });
const onToggleChange = () => {
writeDrawerState(toggle.checked);
synchronize();
};
const onSidebarClick = (event) => {
if (event.target instanceof Element && event.target.closest("a[href]")) {
const link = event.target instanceof Element && event.target.closest("a[href]");
if (link && modalDrawer.matches) {
setOpen(false);
}
};
const onOverlayClick = (event) => {
event.preventDefault();
setOpen(false, { restoreFocus: true });
};
const onKeyDown = (event) => {
if (
document.activeElement === button &&
(event.key === " " || event.key === "Enter")
) {
event.preventDefault();
onButtonClick(event);
return;
}

if (!toggle.checked) return;

if (event.key === "Escape") {
Expand All @@ -121,7 +141,7 @@
return;
}

if (event.key !== "Tab") return;
if (event.key !== "Tab" || !modalDrawer.matches) return;

const focusable = focusableElements();
if (!focusable.length) {
Expand All @@ -142,23 +162,54 @@

button.addEventListener("click", onButtonClick);
toggle.addEventListener("change", onToggleChange);
overlay?.addEventListener("click", onOverlayClick);
sidebar.addEventListener("click", onSidebarClick);
overlay?.addEventListener("click", onOverlayClick);
const onDrawerModeChange = () => {
const shouldMoveFocus =
toggle.checked && modalDrawer.matches && !sidebar.contains(document.activeElement);
synchronize({ moveFocus: shouldMoveFocus });
};
modalDrawer.addEventListener("change", onDrawerModeChange);
document.addEventListener("keydown", onKeyDown);
synchronize();
document.documentElement.classList.add("openshell-drawer-restoring");
toggle.checked = readDrawerState();
synchronize({ moveFocus: toggle.checked && modalDrawer.matches });
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
document.documentElement.classList.remove("openshell-drawer-restoring");
});
});

cleanup = () => {
button.removeEventListener("click", onButtonClick);
toggle.removeEventListener("change", onToggleChange);
overlay?.removeEventListener("click", onOverlayClick);
sidebar.removeEventListener("click", onSidebarClick);
overlay?.removeEventListener("click", onOverlayClick);
modalDrawer.removeEventListener("change", onDrawerModeChange);
document.removeEventListener("keydown", onKeyDown);
document.documentElement.classList.remove("openshell-drawer-restoring");
backgroundElements.forEach((wasInert, element) => {
element.inert = wasInert;
});
};
}

const readDrawerState = () => {
try {
return window.sessionStorage.getItem(drawerStateKey) === "true";
} catch {
return false;
}
};

const writeDrawerState = (isOpen) => {
try {
window.sessionStorage.setItem(drawerStateKey, String(isOpen));
} catch {
// Keep the drawer usable when browser storage is unavailable.
}
};

if (window.document$?.subscribe) {
window.document$.subscribe(enhanceNavigationDrawer);
} else if (document.readyState === "loading") {
Expand Down
Loading
Loading