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
11 changes: 0 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install the core dev extra
run: |
Expand Down Expand Up @@ -56,7 +55,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install torch CPU wheels
run: |
Expand Down Expand Up @@ -89,7 +87,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install torch CPU wheels
run: |
Expand Down Expand Up @@ -141,7 +138,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install torch CPU wheels
run: |
Expand Down Expand Up @@ -182,7 +178,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install torch CPU wheels
run: |
Expand Down Expand Up @@ -243,7 +238,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install torch CPU wheels
run: |
Expand Down Expand Up @@ -310,7 +304,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install docs dependencies
run: |
Expand All @@ -337,7 +330,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install the renderer PyPI itself uses
run: |
Expand All @@ -362,8 +354,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: client/package-lock.json

- name: Install client dependencies
working-directory: client
Expand Down Expand Up @@ -405,7 +395,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Build the core wheel with no extras
run: |
Expand Down
112 changes: 112 additions & 0 deletions .github/workflows/hub-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Hub artifact verification (sandbox)

# The community-upload verification sandbox (issue #48; the design of
# record is plans/hub_accounts_plan.md §7). Dispatched by
# spikeforge-hub-api once a committed upload enters its `verifying`
# state. That hub-api endpoint (spikeforge-hub-api#4) is not built as of
# this writing, so the `hub-artifact-verify` event type and the
# client_payload shape below are this repo's half of a contract the two
# repositories have to agree on -- see the pull request that introduced
# this file for the coordination note.
#
# SECURITY -- read before changing `runs-on` or the egress allowlist:
#
# This job runs `torch.load` on bytes a stranger uploaded. It MUST stay
# on a GitHub-hosted, ephemeral runner. It must NEVER run on the
# self-hosted `spikeforge-ci` runner that `deploy-hetzner.yml` uses --
# that runner holds the Hetzner deploy key, and running untrusted-content
# code on a persistent self-hosted runner is named in the plan (§7.2) as
# the single most predictable way this whole design could be
# compromised. This is deliberately the first workflow in this repository
# that does not use `spikeforge-ci`.
on:
repository_dispatch:
types: [hub-artifact-verify]

permissions:
contents: read

# One verification at a time per version; a second dispatch for the same
# version (a retry) should not race the first, but must not cancel it
# silently either -- a cancelled run still owes the hub API a report, or
# the version is stuck in `verifying` forever (the exact bug
# spikeforge-hub-api#4 exists to fix).
concurrency:
group: hub-verify-${{ github.event.client_payload.version_id }}
cancel-in-progress: false

env:
# CPU wheels keep this light; mirrors ci.yml's TORCH_INDEX_URL.
TORCH_INDEX_URL: https://download.pytorch.org/whl/cpu

jobs:
verify:
# GitHub-hosted only -- see the header comment. Do not add
# `spikeforge-ci` or any other self-hosted label to this job.
runs-on: ubuntu-latest
# A wall-clock ceiling independent of any one step's own timeout, per
# plans/hub_accounts_plan.md §7.2 ("a wall-clock timeout").
timeout-minutes: 20
steps:
# Restricts this job's DNS/network egress to exactly what it needs:
# GitHub's own checkout/runner endpoints, PyPI + the CPU wheel
# index for installing dependencies, and the two hub-api endpoints
# this job talks to (the artifact CDN and the verification
# callback) -- plans/hub_accounts_plan.md §7.2's "no network egress
# beyond the two endpoints this job actually needs", extended by
# the toolchain-setup endpoints every job on this runner requires
# regardless. If this allowlist ever needs to change, flip
# `egress-policy` to `audit` first, read the resulting job summary
# for what was actually contacted, then return it to `block`.
- name: Harden the runner's network egress
uses: step-security/harden-runner@v2
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
codeload.github.com:443
objects.githubusercontent.com:443
results-receiver.actions.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
download.pytorch.org:443
hub.spikeforge.net:443
cdn.spikeforge.net:443

- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
# No `cache: pip` here, deliberately: this job's whole point is
# to be a disposable sandbox for untrusted content, and a pip
# cache is one more thing that could be poisoned across runs.

- name: Install torch CPU wheels
run: |
python -m pip install --upgrade pip
pip install torch torchvision \
--index-url ${{ env.TORCH_INDEX_URL }}

- name: Install core + targets + hub editable
run: |
pip install -e "./packages/spikeforge[dev,nir]" \
-e ./packages/spikeforge-targets \
-e ./packages/spikeforge-hub

- name: Run the verification pipeline and post the signed report
env:
PYTHONPATH: ${{ github.workspace }}/scripts
SPIKEFORGE_HUB_VERIFY_VERSION_ID: >-
${{ github.event.client_payload.version_id }}
SPIKEFORGE_HUB_VERIFY_ARTIFACT_URL: >-
${{ github.event.client_payload.artifact_url }}
# Fixed to this repository's own configuration -- never read
# from the dispatch payload. The callback destination must not
# be data a compromised or malformed dispatch could redirect;
# see hub_verify/cli.py's module docstring.
SPIKEFORGE_HUB_API_BASE_URL: ${{ vars.SPIKEFORGE_HUB_API_BASE_URL }}
SPIKEFORGE_HUB_VERIFICATION_SECRET: >-
${{ secrets.SPIKEFORGE_HUB_VERIFICATION_SECRET }}
run: python -m hub_verify.cli
18 changes: 11 additions & 7 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2B31L3PSWD"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', 'G-2B31L3PSWD');
// Analytics belongs to the public website, never the local desktop app.
if (location.hostname === "dash.spikeforge.net") {
var analytics = document.createElement("script");
analytics.async = true;
analytics.src = "https://www.googletagmanager.com/gtag/js?id=G-2B31L3PSWD";
document.head.appendChild(analytics);
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag("js", new Date());
gtag("config", "G-2B31L3PSWD");
}
</script>
<title>Spikeforge Dashboard — Explore Spiking Neural Networks</title>
<meta name="description" content="Explore spiking neural networks in the Spikeforge browser dashboard with live model introspection and visualization.">
Expand Down
127 changes: 49 additions & 78 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { lazy, Suspense, useCallback, useRef } from "react";

import { AnalysisPanels } from "./components/AnalysisPanels";
import { Controls } from "./components/Controls";
import { AppHeader } from "./components/AppHeader";
import { DownloadProgress } from "./components/DownloadProgress";
import { EnergyPanel } from "./components/EnergyPanel";
import { HubPanel } from "./components/HubPanel";
import { LoadedModelPanel } from "./components/LoadedModelPanel";
import { ModelPanel } from "./components/ModelPanel";
import { Section } from "./components/Stepper";
import { ModelWorkspace } from "./components/ModelWorkspace";
import { NavRail } from "./components/NavRail";
import { StatusBar } from "./components/StatusBar";
import { TabBar } from "./components/TabBar";
import { TabPanel } from "./components/TabPanel";
import { TargetsPanel } from "./components/TargetsPanel";
import { TopBar } from "./components/TopBar";
import { TourCard } from "./components/TourCard";
import { TrainControls } from "./components/TrainControls";
import { TrainingPanel } from "./components/TrainingPanel";
import { ViewerPanels } from "./components/ViewerPanels";
import { useEncodeConfig } from "./hooks/useEncodeConfig";
Expand Down Expand Up @@ -122,78 +118,53 @@ export default function App() {

return (
<div className="app">
<header className="app-header">
<TopBar
mode={training.state.config.mode}
onModeChange={(mode) => training.patch({ mode })}
lessons={LESSONS}
tourOpen={tour.menuOpen}
onToggleTours={tour.toggleMenu}
onOpenTour={tour.openLesson}
/>

<TabBar
tabs={TABS}
active={tabs.active}
busy={busy}
onSelect={tabs.select}
/>
<NavRail
tabs={TABS}
active={tabs.active}
busy={busy}
onSelect={tabs.select}
/>

{training.state.loaded && (
<div className="loaded-model-bar">
<LoadedModelPanel loaded={training.state.loaded} />
</div>
)}
</header>
<AppHeader
mode={training.state.config.mode}
onModeChange={(mode) => training.patch({ mode })}
active={tabs.active}
loaded={training.state.loaded}
dataset={config.dataset}
tourOpen={tour.menuOpen}
onToggleTours={tour.toggleMenu}
onOpenTour={tour.openLesson}
/>

<main className="app-main">
<TabPanel id="model" active={tabs.active}>
<div className="tab-cols tab-cols-model">
<div className="tab-col">
<ModelPanel
models={training.state.models}
current={training.state.loaded?.name ?? null}
connected={ws.connected}
busy={training.state.running}
loading={viewer.modelLoading}
readOnly={readOnly}
onNew={actions.newModel}
onLoad={actions.loadModel}
onSave={actions.saveModel}
/>

<div className="panel">
<Section
title={t("section.train")}
hint={t("section.train.hint")}
>
<TrainControls
running={training.state.running}
connected={ws.connected}
readOnly={readOnly}
onTrain={actions.train}
onStop={actions.stopTrain}
/>
</Section>
</div>
</div>

<div className="tab-col">
<Controls
config={config}
model={training.state.config}
datasets={training.state.datasets}
gpuAvailable={viewer.gpuAvailable}
topologies={training.state.topologies}
neurons={training.state.neurons}
surrogates={training.state.surrogates}
locked={viewer.locked}
onChange={patchConfig}
onModelChange={training.patch}
onSelectSample={actions.selectSample}
/>
</div>
</div>
<ModelWorkspace
config={config}
model={training.state.config}
datasets={training.state.datasets}
models={training.state.models}
currentModel={training.state.loaded?.name ?? null}
loaded={training.state.loaded}
sample={viewer.state.sample}
eventFrame={viewer.state.eventFrame}
topologies={training.state.topologies}
neurons={training.state.neurons}
surrogates={training.state.surrogates}
gpuAvailable={viewer.gpuAvailable}
connected={ws.connected}
busy={training.state.running}
loading={viewer.modelLoading}
readOnly={readOnly}
locked={viewer.locked}
onPatchConfig={patchConfig}
onModelChange={training.patch}
onSelectSample={actions.selectSample}
onNew={actions.newModel}
onLoad={actions.loadModel}
onSave={actions.saveModel}
onTrain={actions.train}
onStopTrain={actions.stopTrain}
/>
</TabPanel>

<TabPanel id="viewer" active={tabs.active}>
Expand Down Expand Up @@ -236,7 +207,7 @@ export default function App() {
</TabPanel>

<TabPanel id="training" active={tabs.active}>
<div className="tab-cols">
<div className="tab-cols tab-cols-training">
<div className="tab-col">
<TrainingPanel
loss={training.state.loss}
Expand Down Expand Up @@ -268,13 +239,13 @@ export default function App() {
</TabPanel>

<TabPanel id="hub" active={tabs.active}>
<div className="tab-col narrow">
<div className="tab-col hub-col">
<HubPanel hub={hub} />
</div>
</TabPanel>

<TabPanel id="deploy" active={tabs.active}>
<div className="tab-cols">
<div className="tab-cols tab-cols-deploy">
<div className="tab-col">
<TargetsPanel
list={viewer.state.targetList}
Expand Down
Loading
Loading