slaunt 0.1.17 → 0.1.19
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.
- package/README.md +17 -122
- package/bin/slaunt-hooks.js +99 -0
- package/package.json +9 -59
- package/release/SlauntKillBar.zip +0 -0
- package/release/manifest.json +7 -0
- package/scripts/killbar-portability-check.js +67 -0
- package/scripts/package-killbar-macos.sh +38 -0
- package/src/approval-manager.js +53 -0
- package/src/cli-auth.js +230 -0
- package/src/compact-wordmark.js +13 -0
- package/src/desktop/broker-daemon.js +18 -0
- package/src/desktop/broker-grants.js +37 -0
- package/src/desktop/broker-hook-command.js +23 -0
- package/src/desktop/broker-keychain.js +29 -0
- package/src/desktop/broker-launch-agent.js +34 -0
- package/src/desktop/broker-service.js +40 -0
- package/src/desktop/chat-capture-command.js +7 -0
- package/src/desktop/chat-hook-capture.js +445 -0
- package/src/desktop/claude-lifecycle.js +143 -0
- package/src/desktop/client-identity.js +19 -0
- package/src/desktop/native-claude-broker.js +34 -0
- package/src/desktop/native-claude-connection.js +42 -0
- package/src/desktop/tool-guard-command.js +13 -0
- package/src/desktop/tool-guard.js +225 -0
- package/src/discovery/catalog.js +734 -0
- package/src/full-screen.js +86 -0
- package/src/helper-release.js +27 -0
- package/src/hidden-oauth.js +70 -0
- package/src/independent-grants.js +38 -0
- package/src/install-flow.js +71 -0
- package/src/killbar-background-install.js +47 -0
- package/src/killbar-enrollment.js +74 -0
- package/src/killbar-launch-agent.js +10 -0
- package/src/mcp-identity.js +6 -0
- package/src/native-config.js +150 -0
- package/src/native-oauth.js +33 -0
- package/src/oauth-cohort.js +44 -0
- package/src/pty-wrapper.py +57 -0
- package/src/read-json.js +8 -0
- package/src/setup/client-installer.js +93 -0
- package/src/setup/codex-guard-supervisor.js +34 -0
- package/src/setup/hook-installer.js +496 -0
- package/src/setup/managed-policy.js +26 -0
- package/src/setup/mcp-config-installer.js +204 -0
- package/src/setup-flow.js +66 -0
- package/src/tui.js +205 -0
- package/test/approval-manager.test.js +27 -0
- package/test/broker-grants.test.js +14 -0
- package/test/broker-keychain.test.js +14 -0
- package/test/broker-launch-agent.test.js +15 -0
- package/test/broker-service.test.js +42 -0
- package/test/cli-auth-recovery.test.js +15 -0
- package/test/codex-guard-supervisor.test.js +25 -0
- package/test/full-screen.test.js +49 -0
- package/test/helper-release.test.js +22 -0
- package/test/hidden-oauth.test.js +56 -0
- package/test/hook-installer.test.js +232 -0
- package/test/independent-grants.test.js +16 -0
- package/test/install-flow.test.js +24 -0
- package/test/killbar-background-install.test.js +15 -0
- package/test/killbar-enrollment.test.js +115 -0
- package/test/killbar-launch-agent.test.js +12 -0
- package/test/killbar-portability.test.js +17 -0
- package/test/managed-policy.test.js +18 -0
- package/test/native-claude-broker.test.js +19 -0
- package/test/native-claude-connection.test.js +17 -0
- package/test/native-claude-hooks.test.js +9 -0
- package/test/native-config.test.js +3 -0
- package/test/native-integration.cjs +29 -0
- package/test/native-oauth.test.js +23 -0
- package/test/oauth-cohort.test.js +26 -0
- package/test/tui.test.js +100 -0
- package/LICENSE +0 -201
- package/dist/audit/risk-engine.d.ts +0 -4
- package/dist/audit/risk-engine.d.ts.map +0 -1
- package/dist/audit/risk-engine.js +0 -174
- package/dist/audit/risk-engine.js.map +0 -1
- package/dist/audit/run.d.ts +0 -18
- package/dist/audit/run.d.ts.map +0 -1
- package/dist/audit/run.js +0 -68
- package/dist/audit/run.js.map +0 -1
- package/dist/classification/builtin-catalog.d.ts +0 -3
- package/dist/classification/builtin-catalog.d.ts.map +0 -1
- package/dist/classification/builtin-catalog.js +0 -283
- package/dist/classification/builtin-catalog.js.map +0 -1
- package/dist/classification/catalog.d.ts +0 -10
- package/dist/classification/catalog.d.ts.map +0 -1
- package/dist/classification/catalog.js +0 -194
- package/dist/classification/catalog.js.map +0 -1
- package/dist/classification/classifier.d.ts +0 -3
- package/dist/classification/classifier.d.ts.map +0 -1
- package/dist/classification/classifier.js +0 -64
- package/dist/classification/classifier.js.map +0 -1
- package/dist/classification/public-catalog-config.d.ts +0 -3
- package/dist/classification/public-catalog-config.d.ts.map +0 -1
- package/dist/classification/public-catalog-config.js +0 -5
- package/dist/classification/public-catalog-config.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -239
- package/dist/cli.js.map +0 -1
- package/dist/config/discover.d.ts +0 -8
- package/dist/config/discover.d.ts.map +0 -1
- package/dist/config/discover.js +0 -163
- package/dist/config/discover.js.map +0 -1
- package/dist/config/install.d.ts +0 -10
- package/dist/config/install.d.ts.map +0 -1
- package/dist/config/install.js +0 -143
- package/dist/config/install.js.map +0 -1
- package/dist/config/paths.d.ts +0 -24
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/paths.js +0 -90
- package/dist/config/paths.js.map +0 -1
- package/dist/demo.d.ts +0 -4
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -99
- package/dist/demo.js.map +0 -1
- package/dist/explanation/local.d.ts +0 -24
- package/dist/explanation/local.d.ts.map +0 -1
- package/dist/explanation/local.js +0 -696
- package/dist/explanation/local.js.map +0 -1
- package/dist/mcp/introspect.d.ts +0 -18
- package/dist/mcp/introspect.d.ts.map +0 -1
- package/dist/mcp/introspect.js +0 -236
- package/dist/mcp/introspect.js.map +0 -1
- package/dist/mcp/oauth.d.ts +0 -14
- package/dist/mcp/oauth.d.ts.map +0 -1
- package/dist/mcp/oauth.js +0 -257
- package/dist/mcp/oauth.js.map +0 -1
- package/dist/presentation/plain-language.d.ts +0 -12
- package/dist/presentation/plain-language.d.ts.map +0 -1
- package/dist/presentation/plain-language.js +0 -120
- package/dist/presentation/plain-language.js.map +0 -1
- package/dist/privacy/redaction.d.ts +0 -2
- package/dist/privacy/redaction.d.ts.map +0 -1
- package/dist/privacy/redaction.js +0 -50
- package/dist/privacy/redaction.js.map +0 -1
- package/dist/report/html.d.ts +0 -4
- package/dist/report/html.d.ts.map +0 -1
- package/dist/report/html.js +0 -101
- package/dist/report/html.js.map +0 -1
- package/dist/tui/analysis-path.d.ts +0 -17
- package/dist/tui/analysis-path.d.ts.map +0 -1
- package/dist/tui/analysis-path.js +0 -112
- package/dist/tui/analysis-path.js.map +0 -1
- package/dist/tui/render.d.ts +0 -4
- package/dist/tui/render.d.ts.map +0 -1
- package/dist/tui/render.js +0 -262
- package/dist/tui/render.js.map +0 -1
- package/dist/tui/splash.d.ts +0 -15
- package/dist/tui/splash.d.ts.map +0 -1
- package/dist/tui/splash.js +0 -130
- package/dist/tui/splash.js.map +0 -1
- package/dist/tui/stream.d.ts +0 -6
- package/dist/tui/stream.d.ts.map +0 -1
- package/dist/tui/stream.js +0 -56
- package/dist/tui/stream.js.map +0 -1
- package/dist/types.d.ts +0 -132
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -8
- package/dist/version.js.map +0 -1
- package/models/APACHE-2.0.txt +0 -202
- package/models/MODEL.json +0 -29
- package/models/NOTICE.md +0 -23
package/README.md
CHANGED
|
@@ -1,138 +1,33 @@
|
|
|
1
|
-
# Slaunt
|
|
1
|
+
# Slaunt native OAuth installer — development build
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package contains the client-side one-sign-in OAuth handoff for Codex and Claude Code. Each native client stores its own OAuth credentials; bootstrap credentials stay in CLI memory during setup. The Desktop vault is not used for the native OAuth handoff.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
npx slaunt audit
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
No account or database configuration is required.
|
|
10
|
-
|
|
11
|
-
Slaunt discovers local MCP configurations—including the active Claude Code project scope—requests each server's declared tool list, classifies those tools, and identifies risky permissions or combinations. It then writes a private HTML report on your machine.
|
|
12
|
-
|
|
13
|
-
Every discovered server also receives a grounded explanation. The npm install
|
|
14
|
-
includes a quantized Apache-2.0 TinyLlama 1.1B model through two pinned data
|
|
15
|
-
dependencies and runs it locally through `node-llama-cpp` with a fixed
|
|
16
|
-
evidence-only prompt containing the server's declared `tools/list` inventory.
|
|
17
|
-
The model parts are reassembled into a user cache and SHA-256 verified before
|
|
18
|
-
loading. Slaunt probes the selected native backend before generating; if a GPU
|
|
19
|
-
backend is present but cannot create a context, it retries with a cached or
|
|
20
|
-
locally built CPU backend. The first CPU recovery may take a little longer while
|
|
21
|
-
`node-llama-cpp` builds its backend. If no usable runtime is available, the
|
|
22
|
-
report still explains every server with a deterministic local fallback and marks
|
|
23
|
-
that source explicitly. Generated text must pass a local completeness and
|
|
24
|
-
evidence-grounding check; malformed model output is discarded rather than shown
|
|
25
|
-
in the report. No server configuration values or audit data are sent to a
|
|
26
|
-
remote model.
|
|
27
|
-
|
|
28
|
-
Reports count actual client/scope bindings separately from unique server names,
|
|
29
|
-
so the same server name configured for Claude and Codex is not silently merged.
|
|
30
|
-
|
|
31
|
-
The audit-only release does not include `slaunt connect`.
|
|
32
|
-
|
|
33
|
-
## What the audit checks
|
|
5
|
+
The Codex hook configuration calls `tool_authorize_codex` through the configured remote Slaunt MCP server. It passes only session, turn, tool name and tool-use metadata. The companion Central development package contains the authenticated server-side decision. For the default Codex configuration no local command hook is required. Optional legacy chat-capture and the standalone command supervisor are separate development components.
|
|
34
6
|
|
|
35
|
-
|
|
36
|
-
- Claude Code user-level and active project-scoped MCP server entries
|
|
37
|
-
- MCP servers declared in JSON or TOML
|
|
38
|
-
- Tools exposed by each server
|
|
39
|
-
- Sensitive capabilities such as shell execution, secret access, code changes, and production deployment
|
|
40
|
-
- Risky combinations across servers and clients
|
|
41
|
-
- Unknown tools that still need classification review
|
|
7
|
+
**Not deployed or verified for production installation.** The CLI and persistent Claude broker are wired locally, but production setup remains disabled: the companion Central changes and migration 031 are not deployed, and the complete native OAuth and Keychain flow has not been verified on a clean Mac. OAuth retries use proof-bound receipts and recover interrupted Supabase refreshes under a database lease. Codex can continue a tool call when an MCP hook is unavailable or fails; users accepted that tradeoff for this flow. A managed Codex policy prevents disabling the hook through Codex's hook browser, subject to the organization's managed configuration and client support.
|
|
42
8
|
|
|
43
|
-
|
|
44
|
-
`tools/call` or any other MCP tool.
|
|
9
|
+
`src/setup/managed-policy.js` can generate a Codex managed MCP policy with `createCodexManagedPolicy()`. It does not install or activate the policy. Claude policy generation still requires a separately deployed guard executable. Claude command guards use `onFailure: "block"` and require Claude Code 2.1.295 or later.
|
|
45
10
|
|
|
46
|
-
|
|
47
|
-
flow. The interactive audit asks once before opening sign-in pages; use
|
|
48
|
-
`--authorize-http-servers` for an explicit non-interactive choice. Endpoint-scoped
|
|
49
|
-
OAuth state is cached locally with restrictive file permissions so a later audit
|
|
50
|
-
can reuse the authorized session without another sign-in.
|
|
11
|
+
The local OAuth integration uses installed native clients in isolated temporary homes and a simulated identity provider. The direct authorization hook has server-side runtime tests; an actual Codex tool call against production and a real authenticated user has not been verified.
|
|
51
12
|
|
|
52
|
-
|
|
13
|
+
Requirements for local tests: Node.js 20 or newer; Python 3 and both native client CLIs for the native integration test.
|
|
53
14
|
|
|
54
15
|
```sh
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Inspect configuration without starting local MCP processes
|
|
59
|
-
npx slaunt audit --no-server-starts
|
|
60
|
-
|
|
61
|
-
# Run non-interactively and allow declared tool-list requests
|
|
62
|
-
npx slaunt audit --yes
|
|
63
|
-
|
|
64
|
-
# Explicitly authorize protected HTTP/SSE servers for tools/list
|
|
65
|
-
npx slaunt audit --authorize-http-servers --quick
|
|
66
|
-
|
|
67
|
-
# Skip the paced terminal presentation
|
|
68
|
-
npx slaunt audit --quick
|
|
69
|
-
|
|
70
|
-
# Use only the bundled classification rules
|
|
71
|
-
npx slaunt audit --offline
|
|
72
|
-
|
|
73
|
-
# Produce redacted JSON without a report
|
|
74
|
-
npx slaunt audit --json --no-report --no-server-starts
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
The interactive terminal view is a compact graphical evidence dashboard: one
|
|
78
|
-
posture panel, one client/server access map, and one wrapped explanation stream.
|
|
79
|
-
Inventory gaps are grouped once, and long explanations wrap across lines rather
|
|
80
|
-
than being repeated in separate warning panels. Use `--quick` when a
|
|
81
|
-
non-animated report is easier to scan.
|
|
82
|
-
|
|
83
|
-
Run `npx slaunt audit --help` for all options. Node.js 20.17 or newer is required.
|
|
84
|
-
|
|
85
|
-
## Privacy and consent
|
|
86
|
-
|
|
87
|
-
- Tool names, descriptions, schemas, configuration values, findings, and reports stay local.
|
|
88
|
-
- Slaunt downloads approved classification rules from a read-only Supabase catalog. It does not upload audit data.
|
|
89
|
-
- Starting a configured stdio server requires interactive consent or `--allow-server-starts`.
|
|
90
|
-
- Protected HTTP/SSE inventory requires the interactive browser prompt or explicit `--authorize-http-servers`.
|
|
91
|
-
- OAuth client state and tokens, when authorized, stay in a local endpoint-scoped cache with 0700/0600 permissions.
|
|
92
|
-
- Stdio starts receive configured variables and the MCP SDK's safe defaults; the full parent environment is passed only with explicit `--inherit-parent-env`.
|
|
93
|
-
- Slaunt MCP installation happens only with `--install-slaunt`.
|
|
94
|
-
- The HTML report opens only with `--open-report`.
|
|
95
|
-
- Environment variables and HTTP header values are redacted from JSON and reports.
|
|
96
|
-
|
|
97
|
-
If the remote catalog is unavailable, Slaunt uses a seven-day local cache and then its bundled rules. `--offline` disables the refresh entirely.
|
|
98
|
-
|
|
99
|
-
The bundled model is the default. To use an already-running local Ollama
|
|
100
|
-
model instead, set `SLAUNT_AUDIT_LLM_URL` (it must resolve to `localhost`,
|
|
101
|
-
`127.0.0.1`, or `::1`) and optionally `SLAUNT_AUDIT_LLM_MODEL`.
|
|
102
|
-
`SLAUNT_AUDIT_LLM_TIMEOUT_MS` controls either local path. The model manifest
|
|
103
|
-
and attribution are in `models/`; `npm run verify:model` checks the pinned
|
|
104
|
-
size and SHA-256 before publication.
|
|
105
|
-
|
|
106
|
-
## How classification works
|
|
107
|
-
|
|
108
|
-
Approved database rules and bundled rules map tool declarations to capabilities and risk levels. Matching happens locally. Slaunt then checks direct authority and cross-server attack paths, such as a client that can execute shell commands and read secrets.
|
|
109
|
-
|
|
110
|
-
Unknown does not mean malicious. It means no approved rule matched the tool, so Slaunt treats it as potentially sensitive until reviewed.
|
|
111
|
-
|
|
112
|
-
## Optional Slaunt MCP installation
|
|
113
|
-
|
|
114
|
-
```sh
|
|
115
|
-
npx slaunt audit --install-slaunt
|
|
16
|
+
npm ci --ignore-scripts
|
|
17
|
+
npm test
|
|
18
|
+
npm run test:integration
|
|
116
19
|
```
|
|
117
20
|
|
|
118
|
-
|
|
21
|
+
## Interactive setup
|
|
119
22
|
|
|
120
|
-
|
|
23
|
+
Run `node bin/slaunt-hooks.js install` in an interactive terminal for a centered full-screen setup with an animated splash, logo, selection and progress. The layout adapts to terminal size. The original terminal screen is restored on exit, with the result printed afterward. Choose Codex, Claude Code, or both with the arrow keys or j/k. The recommended default is both clients. Pass `--client codex`, `--client claude`, or `--client both` to skip the picker. Escape, Ctrl-C, or Ctrl-D cancels the picker. The installer displays its current availability and exits without changing settings while the production integration remains gated.
|
|
121
24
|
|
|
122
|
-
|
|
123
|
-
credentials, non-MCP agents, hidden provider scopes, client approval policy, or
|
|
124
|
-
whether a tool has been used. It performs `initialize` and `tools/list` only.
|
|
125
|
-
If a server cannot be inspected, its tool inventory remains unavailable; Slaunt
|
|
126
|
-
does not treat that server as safe.
|
|
25
|
+
Set `SLAUNT_REDUCED_MOTION=1` to disable animation, or set `NO_COLOR` to disable color. Small terminals use a compact terminal-cell rendering of the workspace Neue Haas wordmark; the ASCII logo remains on larger screens. Noninteractive and `TERM=dumb` terminals receive plain output and select both clients by default; CI skips interactive prompts and animation.
|
|
127
26
|
|
|
128
|
-
##
|
|
27
|
+
## Packaged KillBar helper
|
|
129
28
|
|
|
130
|
-
|
|
131
|
-
npm ci
|
|
132
|
-
npm run check
|
|
133
|
-
npm test
|
|
134
|
-
```
|
|
29
|
+
The development npm package includes an ad hoc-signed Apple Silicon helper as `release/SlauntKillBar.zip`. The installer checks the archive SHA-256, extracts it to an owner-only temporary directory, verifies its code signature and pinned code-directory hash, and verifies the installed copy before registering a launch agent. The archive and manifest are in the same package: their hashes detect accidental or partial modification but do not authenticate the publisher if an attacker can replace the entire package. Distribution must use a trusted npm release and account.
|
|
135
30
|
|
|
136
|
-
The
|
|
31
|
+
The helper is not Developer ID signed or notarized. A local extraction and bridge-version test passed, but this does not establish Gatekeeper behavior for every download method, enrollment, Keychain access, or startup after login on another Mac. Intel Macs are unsupported by this artifact. The package remains private and has not been published.
|
|
137
32
|
|
|
138
|
-
|
|
33
|
+
The setup flow checks the authenticated workspace when enrolling the helper, stores a separate guard grant in the helper's Keychain item, and registers per-user launch agents for KillBar and the Claude broker. The broker maintains separate Central connections for Claude sessions and denies guarded calls when authorization is unavailable. Existing installations are not replaced. Live production enrollment, Keychain access, background health, restart, and command delivery have not been verified.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const {spawn}=require('node:child_process');
|
|
4
|
+
const {ISSUER}=require('../src/setup-flow');
|
|
5
|
+
const {readJson}=require('../src/read-json');
|
|
6
|
+
const {createTerminal}=require('../src/tui');
|
|
7
|
+
const {installFlow}=require('../src/install-flow');
|
|
8
|
+
|
|
9
|
+
const MACOS_CODEX_APP = '/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
|
|
10
|
+
function probeExecutable(command, {signal, env}) {
|
|
11
|
+
if (signal?.aborted) return Promise.resolve('aborted');
|
|
12
|
+
return new Promise(resolve=>{
|
|
13
|
+
let child;try {child=spawn(command,['--version'],{stdio:'ignore',env});}catch{return resolve('failed');}
|
|
14
|
+
let finished=false;const finish=value=>{if(finished)return;finished=true;clearTimeout(timer);signal?.removeEventListener('abort',abort);resolve(value);};
|
|
15
|
+
const abort=()=>{child.kill('SIGKILL');finish('aborted');};
|
|
16
|
+
const timer=setTimeout(abort,10000);
|
|
17
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
18
|
+
if(signal?.aborted)abort();
|
|
19
|
+
child.once('error',error=>finish(error.code==='ENOENT'?'missing':'failed'));
|
|
20
|
+
child.once('close',code=>finish(code===0?'found':'failed'));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function resolveClient(command, {signal, env=process.env, platform=process.platform, appCodexPath=MACOS_CODEX_APP} = {}) {
|
|
24
|
+
const result=await probeExecutable(command,{signal,env});
|
|
25
|
+
if(result==='found') return command;
|
|
26
|
+
// The macOS app bundles Codex without adding it to other terminal sessions' PATH.
|
|
27
|
+
if(result==='missing' && command==='codex' && platform==='darwin' && !signal?.aborted)
|
|
28
|
+
return await probeExecutable(appCodexPath,{signal,env})==='found' ? appCodexPath : null;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function checkClient(command, options) { return Boolean(await resolveClient(command, options)); }
|
|
32
|
+
|
|
33
|
+
async function main(args=process.argv.slice(2),dependencies={}) {
|
|
34
|
+
const terminal=dependencies.terminal||createTerminal();
|
|
35
|
+
const output=dependencies.output||process.stdout;
|
|
36
|
+
const env=dependencies.env||process.env;
|
|
37
|
+
const fetchImpl=dependencies.fetch||fetch;
|
|
38
|
+
const probe=dependencies.probe||resolveClient;
|
|
39
|
+
if(args.length===0||args[0]==='--help'){output.write('Usage: slaunt-hooks install [--client codex|claude|both]\nChecks setup availability for Codex and Claude Code.\n');return 0;}
|
|
40
|
+
if(args[0]!=='install'||(args.length!==1&&!(args.length===3&&args[1]==='--client'&&['codex','claude','both'].includes(args[2])))){terminal.fail('Invalid arguments.','Run slaunt-hooks --help.');return 2;}
|
|
41
|
+
const controller=new AbortController();
|
|
42
|
+
const probes=new Map();
|
|
43
|
+
const probeOnce=client=>{
|
|
44
|
+
if(!probes.has(client)) probes.set(client,Promise.resolve().then(()=>probe(client,{signal:controller.signal,env})));
|
|
45
|
+
return probes.get(client);
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
await terminal.enter?.();
|
|
49
|
+
terminal.banner();
|
|
50
|
+
if(env.CODEX_HOME||env.CLAUDE_CONFIG_DIR){terminal.fail('Custom client directories are not supported.','No client settings were changed.');return 1;}
|
|
51
|
+
if(!args[2] && terminal.interactive && terminal.setClientAvailability){
|
|
52
|
+
for(const client of ['codex','claude']){
|
|
53
|
+
terminal.setClientAvailability(client,'checking');
|
|
54
|
+
void probeOnce(client).then(found=>{if(!controller.signal.aborted)terminal.setClientAvailability(client,found?'installed':'missing');},()=>{if(!controller.signal.aborted)terminal.setClientAvailability(client,'missing');});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const selection=args[2]||await terminal.selectClients();
|
|
58
|
+
if(!selection){terminal.fail('Setup cancelled.','No client settings were changed.');return 130;}
|
|
59
|
+
terminal.setSelection?.(selection);
|
|
60
|
+
const clients=selection==='both'?['codex','claude']:[selection];
|
|
61
|
+
const executablePaths={};
|
|
62
|
+
for(const client of clients){const label=client==='codex'?'Codex':'Claude Code';terminal.progress(`Checking ${label}`);const found=await probeOnce(client);if(!found){terminal.fail(`${label} could not be found or started.`,'Check that the client is installed and available in this terminal, then try again. No settings were changed.');return 1;}executablePaths[client]=typeof found==='string'?found:client;terminal.done(`${label} found`);}
|
|
63
|
+
terminal.progress('Checking Slaunt setup');
|
|
64
|
+
let metadata;try {const res=await fetchImpl(new URL('/.well-known/oauth-authorization-server',ISSUER),{redirect:'error',signal:AbortSignal.timeout(10000)});if(!res.ok)throw new Error();metadata=await readJson(res,32768);}catch{terminal.fail('Could not reach Slaunt setup.','Check your connection and try again. No settings were changed.');return 1;}
|
|
65
|
+
if(metadata.issuer!==ISSUER||metadata.slaunt_cli_setup_supported!==true){terminal.fail('Setup is not yet enabled on Slaunt.','No client settings were changed.');return 1;}
|
|
66
|
+
terminal.done('Slaunt setup available');
|
|
67
|
+
try {
|
|
68
|
+
const result=await (dependencies.installFlow||installFlow)({clients,executablePaths,terminal,fetchImpl});
|
|
69
|
+
if(result?.installed!==true)throw new Error('setup-incomplete');
|
|
70
|
+
terminal.success?.('Setup complete.','Selected clients and KillBar are connected.');
|
|
71
|
+
return 0;
|
|
72
|
+
} catch(error) {
|
|
73
|
+
const messages={'killbar-release-missing':'KillBar is missing from this installer.','killbar-release-unavailable':'KillBar release metadata is invalid.','killbar-release-untrusted':'KillBar could not be verified.','killbar-platform-unsupported':'This KillBar package requires an Apple Silicon Mac.','killbar-helper-unavailable':'KillBar could not be verified.','killbar-helper-untrusted':'KillBar could not be verified.','claude-guard-unavailable':'Claude authorization is not available in this installer.'};
|
|
74
|
+
const clientLabel=error?.setupClient==='codex'?'Codex':error?.setupClient==='claude'?'Claude Code':null;
|
|
75
|
+
if(clientLabel && error?.message==='client-configuration-conflict') messages[error.message]=`${clientLabel} already has a conflicting Slaunt configuration.`;
|
|
76
|
+
if(clientLabel && error?.message==='client-configuration-failed') messages[error.message]=`${clientLabel} configuration could not be updated.`;
|
|
77
|
+
if(error?.message==='cohort-failed') {
|
|
78
|
+
if(error.setupStage==='native-login' && clientLabel) {
|
|
79
|
+
const nativeMessages={launch:'could not be started',request:'did not provide a valid authorization request', 'waiting-approval':'exited while waiting for authorization', callback:'could not complete the authorization handoff', 'client-exit':'did not complete authorization after the callback'};
|
|
80
|
+
messages[error.message]=`${clientLabel} ${nativeMessages[error.nativeStage] || 'could not complete its Slaunt authorization'}.`;
|
|
81
|
+
}
|
|
82
|
+
else if(error.setupStage==='authorization') {
|
|
83
|
+
const details={'sign-in':'Slaunt sign-in could not be completed.','workspace':'Slaunt workspace identity could not be verified.','enrollment':'KillBar enrollment could not be completed.','client-grants':'Slaunt could not issue client authorization grants.'};
|
|
84
|
+
messages[error.message]=details[error.setupDetail] || 'Slaunt authorization or enrollment could not be completed.';
|
|
85
|
+
}
|
|
86
|
+
else if(error.setupStage==='timeout') messages[error.message]='Slaunt authorization timed out.';
|
|
87
|
+
}
|
|
88
|
+
terminal.fail(messages[error?.message]||'Setup could not be completed.',error?.setupMayHaveChanged?'Some settings may have changed; review them before retrying.':'No client settings were changed.');return 1;
|
|
89
|
+
}
|
|
90
|
+
} finally {controller.abort();terminal.stop?.();terminal.close?.();}
|
|
91
|
+
}
|
|
92
|
+
if(require.main===module) {
|
|
93
|
+
const terminal=createTerminal();
|
|
94
|
+
const onInterrupt=()=>{terminal.close();process.exit(130);};
|
|
95
|
+
const onTerminate=()=>{terminal.close();process.exit(143);};
|
|
96
|
+
process.once('SIGINT',onInterrupt);process.once('SIGTERM',onTerminate);
|
|
97
|
+
void main(process.argv.slice(2),{terminal}).then(code=>{process.exitCode=code;},()=>{terminal.close();console.error('Setup failed. Review client settings before retrying.');process.exitCode=1;}).finally(()=>{process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onTerminate);});
|
|
98
|
+
}
|
|
99
|
+
module.exports={main,checkClient,resolveClient};
|
package/package.json
CHANGED
|
@@ -1,70 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slaunt",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Privacy-first MCP agent access auditor for Claude and Codex",
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "0.1.19",
|
|
6
4
|
"bin": {
|
|
7
|
-
"slaunt": "
|
|
5
|
+
"slaunt": "bin/slaunt-hooks.js",
|
|
6
|
+
"slaunt-hooks": "bin/slaunt-hooks.js"
|
|
8
7
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"models/MODEL.json",
|
|
12
|
-
"models/NOTICE.md",
|
|
13
|
-
"models/APACHE-2.0.txt",
|
|
14
|
-
"README.md",
|
|
15
|
-
"LICENSE"
|
|
16
|
-
],
|
|
17
8
|
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"test": "npm run build && vitest run",
|
|
21
|
-
"test:watch": "vitest",
|
|
22
|
-
"audit:demo": "npm run build && node dist/cli.js audit --demo",
|
|
23
|
-
"verify:model": "node scripts/verify-model.mjs",
|
|
24
|
-
"fetch:model": "node scripts/fetch-model.mjs",
|
|
25
|
-
"prepublishOnly": "npm run check && npm test && npm run verify:model"
|
|
9
|
+
"test": "node --test test/*.test.js",
|
|
10
|
+
"test:integration": "node test/native-integration.cjs"
|
|
26
11
|
},
|
|
27
12
|
"engines": {
|
|
28
|
-
"node": ">=20
|
|
13
|
+
"node": ">=20"
|
|
29
14
|
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"mcp",
|
|
32
|
-
"security",
|
|
33
|
-
"audit",
|
|
34
|
-
"claude",
|
|
35
|
-
"codex",
|
|
36
|
-
"agent"
|
|
37
|
-
],
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/kerodem/slaunt-audit.git"
|
|
41
|
-
},
|
|
42
|
-
"bugs": {
|
|
43
|
-
"url": "https://github.com/kerodem/slaunt-audit/issues"
|
|
44
|
-
},
|
|
45
|
-
"homepage": "https://github.com/kerodem/slaunt-audit#readme",
|
|
46
|
-
"license": "MIT",
|
|
47
15
|
"dependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@modelcontextprotocol/sdk": "1.
|
|
50
|
-
"
|
|
51
|
-
"node-llama-cpp": "3.21.1",
|
|
52
|
-
"open": "11.0.0",
|
|
53
|
-
"ora": "9.4.1",
|
|
54
|
-
"slaunt-model-tinyllama-1b-part1": "0.1.0",
|
|
55
|
-
"slaunt-model-tinyllama-1b-part2": "0.1.0",
|
|
56
|
-
"smol-toml": "1.7.1",
|
|
57
|
-
"zod": "4.4.3"
|
|
58
|
-
},
|
|
59
|
-
"devDependencies": {
|
|
60
|
-
"@types/node": "24.10.1",
|
|
61
|
-
"typescript": "7.0.2",
|
|
62
|
-
"vitest": "4.1.10"
|
|
63
|
-
},
|
|
64
|
-
"overrides": {
|
|
65
|
-
"fast-uri": "3.1.6",
|
|
66
|
-
"hono": "4.13.5",
|
|
67
|
-
"@hono/node-server": "2.1.1",
|
|
68
|
-
"qs": "6.16.0"
|
|
16
|
+
"@electron/asar": "3.4.1",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
18
|
+
"smol-toml": "1.9.1"
|
|
69
19
|
}
|
|
70
20
|
}
|
|
Binary file
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Cross-machine acceptance probe. Never starts a persistent agent or enrolls a device.
|
|
4
|
+
const fs=require('node:fs');
|
|
5
|
+
const os=require('node:os');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const {spawnSync}=require('node:child_process');
|
|
8
|
+
|
|
9
|
+
function run(command,args,options={}) {
|
|
10
|
+
const result=spawnSync(command,args,{encoding:'utf8',timeout:30000,maxBuffer:1024*1024,...options});
|
|
11
|
+
if(result.error || result.status!==0) throw new Error('command failed');
|
|
12
|
+
return result.stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
if(args.length!==4 || args[0]!=='--app' || args[2]!=='--team' || !path.isAbsolute(args[1]) || !/^[A-Z0-9]{10}$/.test(args[3])) throw new Error('Usage: node scripts/killbar-portability-check.js --app /absolute/path/SlauntKillBar.app --team TRUSTED_TEAM_ID');
|
|
16
|
+
return {app:args[1],team:args[3]};
|
|
17
|
+
}
|
|
18
|
+
function test({app,team},{platform=process.platform,runCommand=run,tempRoot=os.tmpdir(),write=console.log}={}) {
|
|
19
|
+
const checks=[];
|
|
20
|
+
function check(name,fn){try{fn();checks.push({name,result:'PASS'});}catch{checks.push({name,result:'FAIL'});}}
|
|
21
|
+
function pending(name){checks.push({name,result:'NOT VERIFIED'});}
|
|
22
|
+
if(platform!=='darwin') {checks.push({name:'macOS platform',result:'FAIL'});return checks;}
|
|
23
|
+
let tmp;
|
|
24
|
+
try {
|
|
25
|
+
tmp=fs.mkdtempSync(path.join(tempRoot,'slaunt-other-mac-'));
|
|
26
|
+
const pkg=path.join(tmp,'package'),dest=path.join(tmp,'install'),extract=path.join(tmp,'extract');
|
|
27
|
+
fs.mkdirSync(pkg);fs.mkdirSync(dest);fs.mkdirSync(extract);
|
|
28
|
+
let packed=false,installed=false,unpacked=false,verified=false,bridge=false;
|
|
29
|
+
const archive=path.join(pkg,'killbar.zip');
|
|
30
|
+
const requirement=`=identifier "ai.slaunt.killbar" and anchor apple generic and certificate leaf[subject.OU] = "${team}"`;
|
|
31
|
+
check('Source signature matches trusted team',()=>runCommand('/usr/bin/codesign',['--verify','--deep','--strict','-R',requirement,app]));
|
|
32
|
+
if(checks.at(-1).result==='PASS') check('Create npm delivery fixture',()=>{
|
|
33
|
+
runCommand('/usr/bin/ditto',['-c','-k','--sequesterRsrc','--keepParent',app,archive]);
|
|
34
|
+
fs.writeFileSync(path.join(pkg,'package.json'),JSON.stringify({name:'slaunt-killbar-acceptance-fixture',version:'1.0.0',private:true,files:['killbar.zip']}));
|
|
35
|
+
runCommand('npm',['pack',pkg,'--pack-destination',tmp,'--offline','--ignore-scripts','--cache',path.join(tmp,'cache'),'--silent']);packed=true;
|
|
36
|
+
}); else pending('Create npm delivery fixture');
|
|
37
|
+
if(packed) check('Install npm fixture',()=>{
|
|
38
|
+
runCommand('npm',['install',path.join(tmp,'slaunt-killbar-acceptance-fixture-1.0.0.tgz'),'--prefix',dest,'--cache',path.join(tmp,'cache'),'--offline','--ignore-scripts','--no-audit','--no-fund','--silent']);installed=true;
|
|
39
|
+
}); else pending('Install npm fixture');
|
|
40
|
+
const copied=path.join(extract,'SlauntKillBar.app');
|
|
41
|
+
if(installed) check('Extract helper',()=>{runCommand('/usr/bin/ditto',['-x','-k',path.join(dest,'node_modules/slaunt-killbar-acceptance-fixture/killbar.zip'),extract]);unpacked=true;}); else pending('Extract helper');
|
|
42
|
+
if(unpacked) check('Extracted signature matches trusted team',()=>{runCommand('/usr/bin/codesign',['--verify','--deep','--strict','-R',requirement,copied]);verified=true;}); else pending('Extracted signature matches trusted team');
|
|
43
|
+
if(verified) check('Gatekeeper distribution assessment',()=>runCommand('/usr/sbin/spctl',['--assess','--type','execute',copied])); else pending('Gatekeeper distribution assessment');
|
|
44
|
+
const bin=path.join(copied,'Contents/MacOS/SlauntKillBar');
|
|
45
|
+
if(verified) check('Helper executes from npm fixture',()=>{const v=JSON.parse(runCommand(bin,['--desktop-bridge-version']));if(v.protocol_version!==1)throw new Error();bridge=true;}); else pending('Helper executes from npm fixture');
|
|
46
|
+
if(bridge) check('Local status protocol responds',()=>{const status=JSON.parse(runCommand(bin,['--desktop-control-status']));if(status.protocol_version!==1 || typeof status.enrolled!=='boolean' || status.requires_restart===true) throw new Error();}); else pending('Local status protocol responds');
|
|
47
|
+
pending('Clean-device enrollment and Keychain access');
|
|
48
|
+
// The installer and an observable service health protocol do not exist yet.
|
|
49
|
+
// A running process or a launchd entry alone cannot prove login restart or command processing.
|
|
50
|
+
pending('Clean macOS VM or independent machine');
|
|
51
|
+
pending('Actual published npm package');
|
|
52
|
+
pending('Background service starts and stays healthy');
|
|
53
|
+
pending('Service starts after logout/login or restart');
|
|
54
|
+
pending('Server command delivery and acknowledgement');
|
|
55
|
+
} catch {checks.push({name:'Test setup',result:'FAIL'});}
|
|
56
|
+
finally {if(tmp)fs.rmSync(tmp,{recursive:true,force:true});}
|
|
57
|
+
return checks;
|
|
58
|
+
}
|
|
59
|
+
function main(args=process.argv.slice(2)) {
|
|
60
|
+
let options;try{options=parseArgs(args);}catch(e){console.error(e.message);return 2;}
|
|
61
|
+
const checks=test(options);for(const c of checks) console.log(`${c.result.padEnd(12)} ${c.name}`);
|
|
62
|
+
const failed=checks.some(c=>c.result==='FAIL'),pending=checks.some(c=>c.result==='NOT VERIFIED');
|
|
63
|
+
console.log(failed?'Result: FAIL':pending?'Result: INCOMPLETE':'Result: PASS');
|
|
64
|
+
return failed?1:pending?2:0;
|
|
65
|
+
}
|
|
66
|
+
if(require.main===module)process.exitCode=main();
|
|
67
|
+
module.exports={test,parseArgs,main};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Stage an already signed and notarized KillBar app for the npm release.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
if [ "$#" -ne 3 ]; then
|
|
5
|
+
echo 'Usage: package-killbar-macos.sh <signed-app> <expected-team-id> <output-zip>' >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
app=$1
|
|
9
|
+
team=$2
|
|
10
|
+
output=$3
|
|
11
|
+
if [[ ! "$team" =~ ^[A-Z0-9]{10}$ ]] || [[ "$app" != /* ]] || [[ "$output" != /* ]]; then
|
|
12
|
+
echo 'Use absolute paths and the expected 10-character Apple team ID.' >&2
|
|
13
|
+
exit 2
|
|
14
|
+
fi
|
|
15
|
+
if [ ! -d "$app" ] || [ -e "$output" ]; then
|
|
16
|
+
echo 'The source app must exist and the output must not already exist.' >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
requirement="=identifier \"ai.slaunt.killbar\" and anchor apple generic and certificate leaf[subject.OU] = \"$team\""
|
|
20
|
+
/usr/bin/codesign --verify --deep --strict -R "$requirement" "$app"
|
|
21
|
+
/usr/sbin/spctl --assess --type execute "$app"
|
|
22
|
+
/usr/bin/xcrun stapler validate "$app"
|
|
23
|
+
tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/slaunt-killbar-release.XXXXXXXX")
|
|
24
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
25
|
+
/usr/bin/codesign -d --entitlements :- "$app" > "$tmp/entitlements.plist" 2>/dev/null
|
|
26
|
+
expected="$team.ai.slaunt.killbar"
|
|
27
|
+
actual=$(/usr/libexec/PlistBuddy -c 'Print :com.apple.application-identifier' "$tmp/entitlements.plist")
|
|
28
|
+
group=$(/usr/libexec/PlistBuddy -c 'Print :keychain-access-groups:0' "$tmp/entitlements.plist")
|
|
29
|
+
if [ "$actual" != "$expected" ] || [ "$group" != "$expected" ]; then
|
|
30
|
+
echo 'The signed application identifier or Keychain access group does not match.' >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
/usr/bin/ditto "$app" "$tmp/SlauntKillBar.app"
|
|
34
|
+
/usr/bin/codesign --verify --deep --strict -R "$requirement" "$tmp/SlauntKillBar.app"
|
|
35
|
+
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "$tmp/SlauntKillBar.app" "$tmp/SlauntKillBar.zip"
|
|
36
|
+
# Exclusive creation prevents overwriting an existing release artifact.
|
|
37
|
+
( set -o noclobber; cat "$tmp/SlauntKillBar.zip" > "$output" )
|
|
38
|
+
echo 'Verified KillBar archive created.'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const crypto = require('node:crypto');
|
|
3
|
+
const { validateRequest, validateCallback } = require('./hidden-oauth');
|
|
4
|
+
const hash = value => crypto.createHash('sha256').update(value).digest();
|
|
5
|
+
const secret = () => crypto.randomBytes(32).toString('base64url');
|
|
6
|
+
const equal = (digest, value) => typeof value === 'string' && value.length <= 128 && crypto.timingSafeEqual(digest, hash(value));
|
|
7
|
+
// Prototype only: in-memory state is intentionally not suitable for deployment.
|
|
8
|
+
// validateClient must check the server's actual registered client and redirect.
|
|
9
|
+
// issueCallback must create independent refreshable grants for each request.
|
|
10
|
+
function createApprovalManager({ issuer, validateClient, issueCallback, now = Date.now, ttlMs = 120000, maxPending = 1000 }) {
|
|
11
|
+
if (typeof validateClient !== 'function' || typeof issueCallback !== 'function') throw new Error('approval-dependencies-required');
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
function clearExpired() { for (const [id, entry] of entries) if (entry.expiresAt <= now()) entries.delete(id); }
|
|
14
|
+
async function create(requests) {
|
|
15
|
+
clearExpired();
|
|
16
|
+
if (!Array.isArray(requests) || requests.length < 1 || requests.length > 2 || entries.size >= maxPending) throw new Error('invalid-approval-request');
|
|
17
|
+
const clients = new Set();
|
|
18
|
+
const validated = [];
|
|
19
|
+
for (const item of requests) {
|
|
20
|
+
if (!['codex','claude'].includes(item?.client) || clients.has(item.client) || typeof item.authorizationUrl !== 'string' || item.authorizationUrl.length > 8192) throw new Error('invalid-approval-request');
|
|
21
|
+
clients.add(item.client);
|
|
22
|
+
const request = validateRequest(item.authorizationUrl, issuer);
|
|
23
|
+
if (!await validateClient({client:item.client, request:request.url})) throw new Error('unregistered-client');
|
|
24
|
+
validated.push({ client:item.client, request });
|
|
25
|
+
}
|
|
26
|
+
const id=secret(), approveSecret=secret(), redeemSecret=secret();
|
|
27
|
+
entries.set(id,{requests:validated, approveHash:hash(approveSecret), redeemHash:hash(redeemSecret), expiresAt:now()+ttlMs, state:'pending'});
|
|
28
|
+
return { id, approveSecret, redeemSecret, expiresAt:now()+ttlMs };
|
|
29
|
+
}
|
|
30
|
+
function approve({id, approveSecret, principal, accepted}) {
|
|
31
|
+
clearExpired();
|
|
32
|
+
const entry=entries.get(id);
|
|
33
|
+
if (!entry || entry.state !== 'pending' || !equal(entry.approveHash, approveSecret) || !principal || typeof principal !== 'string') throw new Error('invalid-approval');
|
|
34
|
+
if (accepted !== true) { entries.delete(id); return { approved:false }; }
|
|
35
|
+
entry.principal=principal; entry.state='approved'; entry.approveHash=null;
|
|
36
|
+
return { approved:true, clients:entry.requests.map(x=>x.client) };
|
|
37
|
+
}
|
|
38
|
+
async function redeem({id, redeemSecret}) {
|
|
39
|
+
clearExpired();
|
|
40
|
+
const entry=entries.get(id);
|
|
41
|
+
if (!entry || entry.state !== 'approved' || !equal(entry.redeemHash, redeemSecret)) throw new Error('invalid-redemption');
|
|
42
|
+
// Claim before awaiting external code; retries must start a new approval.
|
|
43
|
+
entry.state='consumed'; entries.delete(id);
|
|
44
|
+
const callbacks = {};
|
|
45
|
+
for (const item of entry.requests) {
|
|
46
|
+
const result = await issueCallback({ client:item.client, request:item.request.url, principal:entry.principal });
|
|
47
|
+
callbacks[item.client] = validateCallback(result, item.request);
|
|
48
|
+
}
|
|
49
|
+
return callbacks;
|
|
50
|
+
}
|
|
51
|
+
return {create,approve,redeem};
|
|
52
|
+
}
|
|
53
|
+
module.exports = {createApprovalManager};
|