aems-agent 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ name: Build AEMS Agent
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: "Version tag (for manual runs)"
11
+ required: false
12
+ default: "dev"
13
+
14
+ permissions:
15
+ contents: write
16
+ id-token: write
17
+
18
+ jobs:
19
+ build-windows:
20
+ runs-on: windows-latest
21
+ env:
22
+ WIN_CODESIGN_CERT_PFX_BASE64: ${{ secrets.WIN_CODESIGN_CERT_PFX_BASE64 }}
23
+ WIN_CODESIGN_CERT_PASSWORD: ${{ secrets.WIN_CODESIGN_CERT_PASSWORD }}
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.11"
31
+
32
+ - name: Install dependencies
33
+ run: |
34
+ pip install pyinstaller
35
+ pip install -e ".[dev]"
36
+
37
+ - name: Install NSIS
38
+ run: choco install nsis -y
39
+
40
+ - name: Add NSIS to PATH
41
+ run: echo "C:\Program Files (x86)\NSIS" >> $env:GITHUB_PATH
42
+
43
+ - name: Build with PyInstaller and NSIS installer
44
+ run: python packaging/build.py --platform windows
45
+
46
+ - name: Verify installer exists
47
+ run: |
48
+ if (Test-Path "dist\aems-agent-setup.exe") {
49
+ Write-Host "Installer built: dist\aems-agent-setup.exe"
50
+ (Get-Item "dist\aems-agent-setup.exe").Length
51
+ } else {
52
+ Write-Host "::warning::Installer not found. Running NSIS directly..."
53
+ & "C:\Program Files (x86)\NSIS\makensis.exe" /DDIST_DIR=dist\aems-agent /DOUTPUT_DIR=dist packaging\windows\installer.nsi
54
+ }
55
+
56
+ - name: Check Windows signing credentials
57
+ id: win_sign_check
58
+ run: |
59
+ if ($env:WIN_CODESIGN_CERT_PFX_BASE64 -and $env:WIN_CODESIGN_CERT_PASSWORD) {
60
+ echo "available=true" >> $env:GITHUB_OUTPUT
61
+ Write-Host "Code signing credentials found, will sign installer."
62
+ } else {
63
+ echo "available=false" >> $env:GITHUB_OUTPUT
64
+ Write-Host "::warning::Code signing credentials not configured. Installer will be unsigned."
65
+ }
66
+
67
+ - name: Sign Windows installer (Authenticode)
68
+ if: steps.win_sign_check.outputs.available == 'true'
69
+ run: |
70
+ $pfxPath = Join-Path $env:RUNNER_TEMP "codesign.pfx"
71
+ [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WIN_CODESIGN_CERT_PFX_BASE64))
72
+ $signtool = (Get-Command signtool.exe).Source
73
+ & $signtool sign /fd SHA256 /f $pfxPath /p $env:WIN_CODESIGN_CERT_PASSWORD /tr http://timestamp.digicert.com /td SHA256 dist\aems-agent-setup.exe
74
+ & $signtool verify /pa /v dist\aems-agent-setup.exe
75
+
76
+ - name: Upload artifact
77
+ uses: actions/upload-artifact@v4
78
+ with:
79
+ name: aems-agent-windows
80
+ path: |
81
+ dist/aems-agent-setup.exe
82
+ dist/aems-agent/
83
+
84
+ build-macos:
85
+ runs-on: macos-latest
86
+ env:
87
+ MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
88
+ MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
89
+ MACOS_DEVELOPER_IDENTITY: ${{ secrets.MACOS_DEVELOPER_IDENTITY }}
90
+ APPLE_ID: ${{ secrets.APPLE_ID }}
91
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
92
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
93
+ steps:
94
+ - uses: actions/checkout@v4
95
+
96
+ - name: Set up Python
97
+ uses: actions/setup-python@v5
98
+ with:
99
+ python-version: "3.11"
100
+
101
+ - name: Install dependencies
102
+ run: |
103
+ pip install pyinstaller
104
+ pip install -e ".[dev]"
105
+
106
+ - name: Build with PyInstaller
107
+ run: python packaging/build.py --platform macos
108
+
109
+ - name: Check macOS signing credentials
110
+ id: mac_sign_check
111
+ run: |
112
+ ALL_PRESENT=true
113
+ for v in MACOS_CERT_P12_BASE64 MACOS_CERT_PASSWORD MACOS_DEVELOPER_IDENTITY APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
114
+ if [ -z "${!v}" ]; then
115
+ ALL_PRESENT=false
116
+ break
117
+ fi
118
+ done
119
+ echo "available=$ALL_PRESENT" >> "$GITHUB_OUTPUT"
120
+ if [ "$ALL_PRESENT" = "false" ]; then
121
+ echo "::warning::macOS signing/notarization credentials not configured. DMG will be unsigned."
122
+ else
123
+ echo "Signing credentials found, will sign and notarize."
124
+ fi
125
+
126
+ - name: Import Developer ID certificate
127
+ if: steps.mac_sign_check.outputs.available == 'true'
128
+ run: |
129
+ KEYCHAIN_PATH="$RUNNER_TEMP/aems-signing.keychain-db"
130
+ KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
131
+ CERT_PATH="$RUNNER_TEMP/developer_id.p12"
132
+ python - <<'PY'
133
+ import base64
134
+ import os
135
+ from pathlib import Path
136
+
137
+ data = os.environ["MACOS_CERT_P12_BASE64"]
138
+ out = Path(os.environ["RUNNER_TEMP"]) / "developer_id.p12"
139
+ out.write_bytes(base64.b64decode(data))
140
+ PY
141
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
142
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
143
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
144
+ security list-keychains -d user -s "$KEYCHAIN_PATH"
145
+ security default-keychain -s "$KEYCHAIN_PATH"
146
+ security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
147
+ security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
148
+
149
+ - name: Sign macOS app bundle and DMG
150
+ if: steps.mac_sign_check.outputs.available == 'true'
151
+ run: |
152
+ codesign --force --deep --options runtime --timestamp --sign "$MACOS_DEVELOPER_IDENTITY" "dist/AEMS Agent.app"
153
+ codesign --verify --deep --strict --verbose=2 "dist/AEMS Agent.app"
154
+ codesign --force --timestamp --sign "$MACOS_DEVELOPER_IDENTITY" "dist/AEMS-Agent.dmg"
155
+ codesign --verify --verbose=2 "dist/AEMS-Agent.dmg"
156
+
157
+ - name: Notarize and staple macOS DMG
158
+ if: steps.mac_sign_check.outputs.available == 'true'
159
+ run: |
160
+ xcrun notarytool submit "dist/AEMS-Agent.dmg" \
161
+ --apple-id "$APPLE_ID" \
162
+ --password "$APPLE_APP_SPECIFIC_PASSWORD" \
163
+ --team-id "$APPLE_TEAM_ID" \
164
+ --wait
165
+ xcrun stapler staple "dist/AEMS-Agent.dmg"
166
+ spctl --assess --type open --context context:primary-signature --verbose=4 "dist/AEMS-Agent.dmg"
167
+
168
+ - name: Upload artifact
169
+ uses: actions/upload-artifact@v4
170
+ with:
171
+ name: aems-agent-macos
172
+ path: |
173
+ dist/AEMS-Agent.dmg
174
+ dist/AEMS Agent.app/
175
+
176
+ build-linux:
177
+ runs-on: ubuntu-latest
178
+ steps:
179
+ - uses: actions/checkout@v4
180
+
181
+ - name: Set up Python
182
+ uses: actions/setup-python@v5
183
+ with:
184
+ python-version: "3.11"
185
+
186
+ - name: Install dependencies
187
+ run: |
188
+ pip install pyinstaller
189
+ pip install -e ".[dev]"
190
+
191
+ - name: Build with PyInstaller
192
+ run: python packaging/build.py --platform linux
193
+
194
+ - name: Upload artifact
195
+ uses: actions/upload-artifact@v4
196
+ with:
197
+ name: aems-agent-linux
198
+ path: dist/aems-agent/
199
+
200
+ release:
201
+ needs: [build-windows, build-macos, build-linux]
202
+ runs-on: ubuntu-latest
203
+ if: startsWith(github.ref, 'refs/tags/v')
204
+ steps:
205
+ - uses: actions/checkout@v4
206
+
207
+ - name: Download all artifacts
208
+ uses: actions/download-artifact@v4
209
+ with:
210
+ path: artifacts/
211
+
212
+ - name: Collect release assets
213
+ id: assets
214
+ run: |
215
+ ASSETS=""
216
+
217
+ if [ -f artifacts/aems-agent-windows/aems-agent-setup.exe ]; then
218
+ ASSETS="$ASSETS artifacts/aems-agent-windows/aems-agent-setup.exe"
219
+ fi
220
+ if [ -f artifacts/aems-agent-macos/AEMS-Agent.dmg ]; then
221
+ ASSETS="$ASSETS artifacts/aems-agent-macos/AEMS-Agent.dmg"
222
+ fi
223
+ if [ -d artifacts/aems-agent-linux ]; then
224
+ tar -czf artifacts/aems-agent-linux.tar.gz -C artifacts aems-agent-linux
225
+ ASSETS="$ASSETS artifacts/aems-agent-linux.tar.gz"
226
+ fi
227
+
228
+ : > artifacts/sha256sums.txt
229
+ for f in $ASSETS; do
230
+ sha256sum "$f" >> artifacts/sha256sums.txt
231
+ done
232
+
233
+ VERSION="${GITHUB_REF#refs/tags/v}"
234
+ cat > artifacts/release-manifest.json <<EOF
235
+ {
236
+ "name": "aems-agent",
237
+ "version": "${VERSION}",
238
+ "generated_at_utc": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
239
+ "checksum_file": "sha256sums.txt"
240
+ }
241
+ EOF
242
+
243
+ ASSETS="$ASSETS artifacts/sha256sums.txt artifacts/release-manifest.json"
244
+ ASSETS=$(echo "$ASSETS" | xargs)
245
+ echo "files=$ASSETS" >> "$GITHUB_OUTPUT"
246
+
247
+ - name: Install cosign
248
+ uses: sigstore/cosign-installer@v3.7.0
249
+
250
+ - name: Sign release manifest (keyless)
251
+ run: |
252
+ cosign sign-blob --yes artifacts/release-manifest.json \
253
+ --output-signature artifacts/release-manifest.sig \
254
+ --output-certificate artifacts/release-manifest.pem
255
+
256
+ - name: Append signature assets
257
+ id: signed_assets
258
+ run: |
259
+ FILES="${{ steps.assets.outputs.files }} artifacts/release-manifest.sig artifacts/release-manifest.pem"
260
+ FILES=$(echo "$FILES" | xargs)
261
+ echo "files=$FILES" >> "$GITHUB_OUTPUT"
262
+
263
+ - name: Create Release
264
+ env:
265
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
266
+ run: |
267
+ VERSION="${GITHUB_REF#refs/tags/v}"
268
+ gh release create "v${VERSION}" \
269
+ --title "AEMS Agent v${VERSION}" \
270
+ --notes "AEMS Local Bridge Agent v${VERSION} - Windows (.exe), macOS (.dmg), Linux (.tar.gz), plus signed manifest/checksums." \
271
+ ${{ steps.signed_assets.outputs.files }}
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest, macos-latest]
15
+ python-version: ['3.10', '3.11', '3.12']
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Run tests
28
+ run: python -m pytest -v --tb=short
29
+
30
+ lint:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v5
37
+ with:
38
+ python-version: '3.11'
39
+
40
+ - name: Install dependencies
41
+ run: pip install -e ".[dev]"
42
+
43
+ - name: Check formatting (black)
44
+ run: black --check src/ tests/
45
+
46
+ - name: Lint (ruff)
47
+ run: ruff check src/ tests/
@@ -0,0 +1,41 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ venv/
14
+ .venv/
15
+ ENV/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Testing
29
+ .pytest_cache/
30
+ .coverage
31
+ htmlcov/
32
+ .mypy_cache/
33
+
34
+ # PyInstaller
35
+ *.spec.bak
36
+
37
+ # Logs
38
+ *.log
39
+
40
+ # Config (user-specific)
41
+ config/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 AEMS Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: aems-agent
3
+ Version: 0.2.0
4
+ Summary: AEMS Local Bridge Agent — local filesystem access for exam PDFs
5
+ Author: AEMS Team
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: cryptography>=43.0.0
10
+ Requires-Dist: fastapi>=0.104.0
11
+ Requires-Dist: httpx>=0.25.0
12
+ Requires-Dist: pydantic>=2.0.0
13
+ Requires-Dist: pyjwt[crypto]>=2.9.0
14
+ Requires-Dist: typer>=0.9.0
15
+ Requires-Dist: uvicorn[standard]>=0.24.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: black>=23.0.0; extra == 'dev'
18
+ Requires-Dist: httpx>=0.25.0; extra == 'dev'
19
+ Requires-Dist: mypy>=1.5.0; extra == 'dev'
20
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
21
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
22
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
24
+ Provides-Extra: tray
25
+ Requires-Dist: pillow>=10.0.0; extra == 'tray'
26
+ Requires-Dist: pystray>=0.19.0; extra == 'tray'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # AEMS Local Bridge Agent
30
+
31
+ A lightweight companion service that runs on `localhost` and provides REST API access to the local filesystem, enabling the [AEMS](https://github.com/artkula/aems) web app to read/write exam PDFs to a user-chosen folder.
32
+
33
+ ## Installation
34
+
35
+ ### pip (recommended for development)
36
+
37
+ ```bash
38
+ pip install aems-agent
39
+ ```
40
+
41
+ ### Binary installers
42
+
43
+ Download pre-built installers from [Releases](https://github.com/artkula/aems-agent/releases):
44
+
45
+ | Platform | File | Notes |
46
+ |----------|------|-------|
47
+ | Windows | `aems-agent-setup.exe` | Installs to `%LOCALAPPDATA%\AEMS Agent` |
48
+ | macOS | `AEMS-Agent.dmg` | Drag to Applications |
49
+ | Linux | `aems-agent-linux.tar.gz` | Extract and run `./aems-agent run` |
50
+
51
+ ## Usage
52
+
53
+ ```bash
54
+ # Start the agent (default: http://127.0.0.1:61234)
55
+ aems-agent run
56
+
57
+ # Start with system tray icon
58
+ aems-agent run --tray
59
+
60
+ # Custom port/host
61
+ aems-agent run --port 9000 --host 0.0.0.0
62
+
63
+ # Enforce runtime license policy
64
+ aems-agent run --license-policy warn
65
+ aems-agent run --license-policy soft-block
66
+ aems-agent run --license-policy hard-block
67
+
68
+ # Show auth token
69
+ aems-agent token
70
+
71
+ # Set exam storage directory
72
+ aems-agent set-path /path/to/exams
73
+
74
+ # Show config directory location
75
+ aems-agent config-dir
76
+
77
+ # Store a license JWT token
78
+ aems-agent license-store "<jwt-token>"
79
+
80
+ # Validate token signature + claims + heartbeat
81
+ aems-agent license-check \
82
+ --license-url https://license.domain.com \
83
+ --issuer https://license.domain.com \
84
+ --audience aems-agent
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ Config files are stored in a platform-specific directory:
90
+
91
+ | Platform | Path |
92
+ |----------|------|
93
+ | Windows | `%APPDATA%\AEMS\agent\` |
94
+ | macOS | `~/.config/aems/agent/` |
95
+ | Linux | `~/.config/aems/agent/` (or `$XDG_CONFIG_HOME/aems/agent/`) |
96
+
97
+ Files:
98
+ - `config.json` - storage path, port, allowed origins, license settings
99
+ - `auth_token` - bearer token for API authentication
100
+ - `license.jwt` - stored license token
101
+ - `agent.log` - rotating log file
102
+
103
+ Runtime license policy modes:
104
+ - `warn`: agent starts and logs validation failures.
105
+ - `soft-block`: agent starts in limited mode when license is invalid. Write operations (`PUT/DELETE /files/*`, `PUT /config/path`) are blocked until license becomes valid again.
106
+ - `hard-block`: startup fails and exits non-zero when license is invalid/revoked/grace-expired; runtime checks also terminate the process non-zero on hard-block failures.
107
+
108
+ ## API Endpoints
109
+
110
+ | Method | Path | Auth | Description |
111
+ |--------|------|------|-------------|
112
+ | GET | `/status` | No | Alive check |
113
+ | GET | `/health` | Yes | Detailed health with disk info |
114
+ | GET/PUT | `/config/path` | Yes | Get/set storage path |
115
+ | GET | `/files/{assignment_id}` | Yes | List submissions |
116
+ | GET/PUT/DELETE | `/files/{aid}/{sid}` | Yes | Manage submission PDFs |
117
+ | GET/PUT | `/files/{aid}/{sid}/annotated` | Yes | Manage annotated PDFs |
118
+ | POST | `/pair/initiate` | No | Start browser pairing |
119
+ | POST | `/pair/complete` | No | Complete pairing |
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ git clone https://github.com/artkula/aems-agent.git
125
+ cd aems-agent
126
+ python -m pip install -e ".[dev]"
127
+ python -m pytest -v
128
+ ```
129
+
130
+ ## Release Trust and Verification
131
+
132
+ Release pipeline:
133
+ - `.github/workflows/build.yml`
134
+ - Windows tagged releases are Authenticode-signed.
135
+ - macOS tagged releases are code-signed, notarized, and stapled.
136
+ - `release-manifest.json` and `sha256sums.txt` are signed with cosign as supplemental integrity proof.
137
+
138
+ Verification examples:
139
+
140
+ Windows:
141
+ ```powershell
142
+ Get-AuthenticodeSignature .\aems-agent-setup.exe | Format-List
143
+ ```
144
+
145
+ macOS:
146
+ ```bash
147
+ codesign --verify --deep --strict --verbose=2 "AEMS Agent.app"
148
+ spctl --assess --type open --context context:primary-signature --verbose=4 "AEMS-Agent.dmg"
149
+ ```
150
+
151
+ Cosign manifest verification:
152
+ ```bash
153
+ cosign verify-blob \
154
+ --certificate release-manifest.pem \
155
+ --signature release-manifest.sig \
156
+ release-manifest.json
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,133 @@
1
+ # AEMS Local Bridge Agent
2
+
3
+ A lightweight companion service that runs on `localhost` and provides REST API access to the local filesystem, enabling the [AEMS](https://github.com/artkula/aems) web app to read/write exam PDFs to a user-chosen folder.
4
+
5
+ ## Installation
6
+
7
+ ### pip (recommended for development)
8
+
9
+ ```bash
10
+ pip install aems-agent
11
+ ```
12
+
13
+ ### Binary installers
14
+
15
+ Download pre-built installers from [Releases](https://github.com/artkula/aems-agent/releases):
16
+
17
+ | Platform | File | Notes |
18
+ |----------|------|-------|
19
+ | Windows | `aems-agent-setup.exe` | Installs to `%LOCALAPPDATA%\AEMS Agent` |
20
+ | macOS | `AEMS-Agent.dmg` | Drag to Applications |
21
+ | Linux | `aems-agent-linux.tar.gz` | Extract and run `./aems-agent run` |
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Start the agent (default: http://127.0.0.1:61234)
27
+ aems-agent run
28
+
29
+ # Start with system tray icon
30
+ aems-agent run --tray
31
+
32
+ # Custom port/host
33
+ aems-agent run --port 9000 --host 0.0.0.0
34
+
35
+ # Enforce runtime license policy
36
+ aems-agent run --license-policy warn
37
+ aems-agent run --license-policy soft-block
38
+ aems-agent run --license-policy hard-block
39
+
40
+ # Show auth token
41
+ aems-agent token
42
+
43
+ # Set exam storage directory
44
+ aems-agent set-path /path/to/exams
45
+
46
+ # Show config directory location
47
+ aems-agent config-dir
48
+
49
+ # Store a license JWT token
50
+ aems-agent license-store "<jwt-token>"
51
+
52
+ # Validate token signature + claims + heartbeat
53
+ aems-agent license-check \
54
+ --license-url https://license.domain.com \
55
+ --issuer https://license.domain.com \
56
+ --audience aems-agent
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ Config files are stored in a platform-specific directory:
62
+
63
+ | Platform | Path |
64
+ |----------|------|
65
+ | Windows | `%APPDATA%\AEMS\agent\` |
66
+ | macOS | `~/.config/aems/agent/` |
67
+ | Linux | `~/.config/aems/agent/` (or `$XDG_CONFIG_HOME/aems/agent/`) |
68
+
69
+ Files:
70
+ - `config.json` - storage path, port, allowed origins, license settings
71
+ - `auth_token` - bearer token for API authentication
72
+ - `license.jwt` - stored license token
73
+ - `agent.log` - rotating log file
74
+
75
+ Runtime license policy modes:
76
+ - `warn`: agent starts and logs validation failures.
77
+ - `soft-block`: agent starts in limited mode when license is invalid. Write operations (`PUT/DELETE /files/*`, `PUT /config/path`) are blocked until license becomes valid again.
78
+ - `hard-block`: startup fails and exits non-zero when license is invalid/revoked/grace-expired; runtime checks also terminate the process non-zero on hard-block failures.
79
+
80
+ ## API Endpoints
81
+
82
+ | Method | Path | Auth | Description |
83
+ |--------|------|------|-------------|
84
+ | GET | `/status` | No | Alive check |
85
+ | GET | `/health` | Yes | Detailed health with disk info |
86
+ | GET/PUT | `/config/path` | Yes | Get/set storage path |
87
+ | GET | `/files/{assignment_id}` | Yes | List submissions |
88
+ | GET/PUT/DELETE | `/files/{aid}/{sid}` | Yes | Manage submission PDFs |
89
+ | GET/PUT | `/files/{aid}/{sid}/annotated` | Yes | Manage annotated PDFs |
90
+ | POST | `/pair/initiate` | No | Start browser pairing |
91
+ | POST | `/pair/complete` | No | Complete pairing |
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ git clone https://github.com/artkula/aems-agent.git
97
+ cd aems-agent
98
+ python -m pip install -e ".[dev]"
99
+ python -m pytest -v
100
+ ```
101
+
102
+ ## Release Trust and Verification
103
+
104
+ Release pipeline:
105
+ - `.github/workflows/build.yml`
106
+ - Windows tagged releases are Authenticode-signed.
107
+ - macOS tagged releases are code-signed, notarized, and stapled.
108
+ - `release-manifest.json` and `sha256sums.txt` are signed with cosign as supplemental integrity proof.
109
+
110
+ Verification examples:
111
+
112
+ Windows:
113
+ ```powershell
114
+ Get-AuthenticodeSignature .\aems-agent-setup.exe | Format-List
115
+ ```
116
+
117
+ macOS:
118
+ ```bash
119
+ codesign --verify --deep --strict --verbose=2 "AEMS Agent.app"
120
+ spctl --assess --type open --context context:primary-signature --verbose=4 "AEMS-Agent.dmg"
121
+ ```
122
+
123
+ Cosign manifest verification:
124
+ ```bash
125
+ cosign verify-blob \
126
+ --certificate release-manifest.pem \
127
+ --signature release-manifest.sig \
128
+ release-manifest.json
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT