Use this guide to validate kernel/plugin changes quickly. Prefer the mocked Jest tests first; use the CLI harness only when you need to exercise HTTP plugins.
- Do NOT use Perplexity or any web search for UbiquityOS internals, configs, or behavior; there is no public documentation.
- For UbiquityOS research, rely on local code, tests, and docs in this repository (use
rgand read source). - If the question is about non-UbiquityOS dependencies or general tooling, external research is allowed; otherwise stay local.
- Never use
UBQ_in secret/property/key names; useUOS_instead. - When defining TypeBox schemas, do not mark fields as optional if a default is provided; keep them required with defaults.
- Never implement AI behavior based on keyword/regex triggers (e.g.,
if (text.includes("install")) …) to pick tools/plugins. If the model is making a decision, that decision must be prompt-driven. - Explicit user-facing commands are fine as entrypoints (e.g.,
/help,/config,@UbiquityOS agent) — but what happens after the entrypoint must be determined by instructions + context, not brittle string matching. - Modern LLMs are capable of correct decisions when we (1) provide effective instructions and (2) distill the relevant context; prefer improving prompts/context over adding special-case code paths.
- Keep prompts lightweight by default; only include heavier context (full plugin defaults/schemas) on demand.
This is a Deno-based project; the runtime server is Deno, and Bun is only used as a task runner for scripts/tests.
bun install
# Only needed if you're working on plugin submodules under lib/plugins/
bun run setup:pluginsIf you're editing lib/plugin-sdk locally and want other packages/plugins to use it before publishing to npm:
# 1) Build the SDK (exports point at dist/)
cd lib/plugin-sdk
bun install
bun run build
# 2) Register it globally for bun link
bun link
# 3) Link it into a consumer package (example: lib/plugins/hello-world-plugin)
cd ../plugins/hello-world-plugin
bun install
bun link @ubiquity-os/plugin-sdkTo undo:
cd lib/plugins/hello-world-plugin
bun unlink @ubiquity-os/plugin-sdk
cd ../plugin-sdk
bun unlinkThe Jest suite mocks external dependencies (GitHub API, LLM calls, plugin manifests/dispatch) via MSW.
# List all tests
bun run jest:test -- --listTests
# Run the kernel-focused suite
bun run jest:test -- tests/kernel.test.ts
# Run a single test by name
bun run jest:test -- tests/kernel.test.ts -t "process /hello"Note: bun test is not used for this suite (tests rely on Jest-only APIs like jest.requireActual).
scripts/test-command.ts is a local harness that:
- Downloads a repo config from GitHub (
.github/.ubiquity-os.config.dev.ymlthen.github/.ubiquity-os.config.yml) - Caches it to
.test-cache/config.<org>.<repo>.yml(first run downloads + exits; rerun to execute) - Fetches each HTTP plugin's
manifest.json - For slash commands (
/hello), POSTs a kernel-shaped payload to the matching plugin URL
Example:
# Terminal A: start the local hello-world HTTP plugin (127.0.0.1:9090)
bun run plugin:hello-world
# Terminal B: provide a token so the harness can download config
export GITHUB_TOKEN=***redacted***
# First run caches config and exits; run again to dispatch
# (target repo must have a .github/.ubiquity-os.config*.yml that includes http://127.0.0.1:9090 under plugins:)
bun run scripts/test-command.ts hello https://github.com/0x4007/ubiquity-os-sandbox/issues/2Limitations:
- Only HTTP plugins (config keys that are URLs) are supported; GitHub Action plugins are not executed by this tool.
/helpand@UbiquityOS …routing is not implemented (slash-command dispatch only).- The harness currently uses a mock plugin
authToken, so plugins that call GitHub typically return401 Unauthorized(expected). To fully exercise a plugin, updatescripts/test-command.tsto pass a real token and use a sandbox repo/issue.
For complete autonomous testing of plugin commands, use the simplified CLI with GitHub CLI verification:
# Install GitHub CLI and authenticate
gh auth login
# Set up environment variables
export APP_ID=your_github_app_id
export APP_PRIVATE_KEY="$(cat path/to/private-key.pem)"- Start your plugin locally (if testing HTTP plugins):
# Example for hello-world plugin
bun run plugin:hello-world # Runs on http://127.0.0.1:9090- Execute command via test harness:
# Simple syntax: command + GitHub URL
bun run test-command hello https://github.com/0x4007/ubiquity-os-sandbox/issues/2- Verify comment was posted:
# Check the latest comment on the issue
gh issue view 2 --repo 0x4007/ubiquity-os-sandbox --comments --json comments | jq '.comments[-1].body'- Full autonomous verification:
# One-liner to test and verify
bun run scripts/test-command.ts hello https://github.com/0x4007/ubiquity-os-sandbox/issues/2 && \
sleep 3 && \
echo "🔍 Verifying comment was posted..." && \
gh issue view 2 --repo 0x4007/ubiquity-os-sandbox --comments --json comments | jq -r '.comments[-1].body'# Test help command
bun run test-command help https://github.com/0x4007/ubiquity-os-sandbox/issues/3 && \
sleep 3 && \
gh issue view 3 --repo 0x4007/ubiquity-os-sandbox --comments --json comments | jq -r '.comments[-1].body'
# Test wallet command
bun run test-command wallet https://github.com/0x4007/ubiquity-os-sandbox/issues/4 && \
sleep 3 && \
gh issue view 4 --repo 0x4007/ubiquity-os-sandbox --comments --json comments | jq -r '.comments[-1].body'Create test-plugin-e2e.sh for automated testing:
#!/bin/bash
set -e
COMMAND=$1
ISSUE_URL=$2
REPO=$(echo $ISSUE_URL | sed 's|https://github.com/\([^/]*\)/\([^/]*\)/issues/\([0-9]*\)|\1/\2|')
ISSUE_NUM=$(echo $ISSUE_URL | sed 's|https://github.com/\([^/]*\)/\([^/]*\)/issues/\([0-9]*\)|\3|')
echo "🧪 Testing /$COMMAND on $REPO issue #$ISSUE_NUM"
# Execute command
bun run test-command $COMMAND $ISSUE_URL
# Wait for processing
sleep 5
# Verify comment was posted
echo "🔍 Verifying comment..."
COMMENT=$(gh issue view $ISSUE_NUM --repo $REPO --comments --json comments | jq -r '.comments[-1].body')
if [[ $COMMENT == *"Error"* ]] || [[ $COMMENT == *"failed"* ]]; then
echo "❌ Test FAILED - Command returned error"
echo "Comment: $COMMENT"
exit 1
else
echo "✅ Test PASSED - Command executed successfully"
echo "Comment preview: ${COMMENT:0:100}..."
fiUsage:
chmod +x test-plugin-e2e.sh
./test-plugin-e2e.sh hello https://github.com/0x4007/ubiquity-os-sandbox/issues/2The kernel loads and merges plugin config from:
- Repo config:
.github/.ubiquity-os.config.yml(production) or.github/.ubiquity-os.config.dev.yml(development) - Org repo config:
<OWNER>/.ubiquity-osusing the same paths
To accelerate debugging/feedback loops, use the internal dashboard logs fetcher:
# Beta (develop) logs, last hour
deno task dash-logs:beta
deno task dash-logs --project-id=7f8de540-0885-4313-84c8-d3d6b3a40a49 --deployment-id=ktca8xhgvq5d --since=1h --format=ndjson
# Production logs, last hour (deployment-id=latest works)
deno task dash-logs --project-id=ac40defb-c3ad-4253-a39b-34bf9731217a --deployment-id=latest --since=1h --format=ndjson
# AI-friendly prod logs (NDJSON) and human-friendly prod logs (table)
deno task dash-logs:ai
deno task dash-logs:humanAuth uses the dashboard cookie token (no public API). Set DENO_DEPLOY_TOKEN in the shell, or pass --token=... explicitly.
Findings:
- The dashboard logs endpoint is internal (
dash.deno.com/_api/.../query_logs) and has no public API docs; it is accessed via the dashboard cookie token (token=...). - The
messagefield is sometimes a JSON string and sometimes plain text; keep raw NDJSON for AI parsing and avoid assuming schema.
For fast beta debugging with a feedback loop, prefer deno task dash-logs:beta (NDJSON) and deno task dash-logs:human (table) when a human needs to scan logs quickly.
# Port 9090 already in use
lsof -nP -iTCP:9090 -sTCP:LISTEN
# Reset Jest cache (useful after module-level mock changes)
bun run jest:test -- --clearCachePlugins can securely call the ai.ubq.fi LLM endpoint using inherited GitHub authentication—no manual tokens needed. The kernel dispatches with short-lived installation tokens, and the API verifies repo access.
The LLM functionality is integrated into @ubiquity-os/plugin-sdk as callLlm():
- HTTP Plugins: Import
callLlmfrom plugin-sdk. - GitHub Actions Plugins: Use the composite action.
- Kernel: Direct import for internal use.
HTTP Plugin:
import { PluginInput, createPlugin, callLlm } from "@ubiquity-os/plugin-sdk";
export default createPlugin({
async onCommand(input: PluginInput) {
const result = await callLlm({ messages: [{ role: "user", content: "Hello!" }] }, input);
// result: ChatCompletion or AsyncIterable<ChatCompletionChunk>
},
});GitHub Actions Plugin:
- uses: ubiquity-os/ubiquity-os-kernel/.github/actions/llm-call@main
with:
auth-token: ${{ inputs.authToken }}
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
messages: '[{"role":"user","content":"Query"}]'Plugins inherit authToken (GitHub app installation token) from kernel dispatch. The API (ai.ubq.fi/serve.ts) verifies:
- Token Format: Must start with 'gh' and include
X-GitHub-Owner/Repoheaders. - Repo Access Check: Uses Octokit to list authenticated repos (paginated to handle >100 repos), confirms token grants access to the exact owner/repo.
- Caching: Verifications cached for 5 min per token+repo to reduce API calls.
- Installation Scoping: Tokens are per-installation; repo check implicitly verifies installation validity. Optional explicit
X-GitHub-Installation-Idheader for extra assurance.
// In requireClientAuth()
if (token.startsWith("gh") && req.headers.has("X-GitHub-Owner") && req.headers.has("X-GitHub-Repo")) {
const owner = req.headers.get("X-GitHub-Owner")!;
const repo = req.headers.get("X-GitHub-Repo")!;
const cacheKey = await sha256Base64Url(token + owner + repo);
if (cached) return null; // Allow
const octokit = new Octokit({ auth: token });
let page = 1,
hasAccess = false;
while (!hasAccess) {
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser({ per_page: 100, page });
hasAccess = repos.some((r) => r.owner.login === owner && r.name === repo);
if (hasAccess || repos.length < 100) break;
page++; // Paginate
}
if (hasAccess) {
cache.set(cacheKey, Date.now() + 5 * 60_000);
return null; // Allow
}
return openaiError(401, "Invalid GitHub token for repo", "invalid_auth_for_repo");
}- Pagination: Loops through repo pages to avoid false negatives for large orgs.
- No Storage: Tokens verified on-the-fly; not persisted.
- Fallback: Existing API keys/KV auth still work.
Use the test harness with LLM-enabled plugins:
# Test plugin that calls LLM
bun run test-command llm-query https://github.com/0x4007/ubiquity-os-sandbox/issues/5
# Verify response in issue commentslib/plugin-sdk/src/llm/index.ts- Core callLlm function.github/actions/llm-call/action.yml- Composite actionlib/ai.ubq.fi/serve.ts- API with GitHub verificationsrc/github/handlers/index.ts- Dispatch with authTokenlib/plugin-sdk/src/signature.ts- PluginInput types
sequenceDiagram
participant User
participant GitHub
participant Kernel
participant Plugin
participant ai.ubq.fi
User->>GitHub: Comments "/llm <prompt>"
GitHub->>Kernel: Webhook event
Kernel->>Kernel: Generate installation token
Kernel->>Plugin: Dispatch with authToken + owner/repo
Plugin->>ai.ubq.fi: callLlm() with inherited auth
ai.ubq.fi->>ai.ubq.fi: Verify token has repo access
ai.ubq.fi->>Plugin: LLM response
Plugin->>GitHub: Post comment with AI greeting
scripts/test-command.tsscripts/setup-plugins.tssrc/github/utils/config.tssrc/github/utils/plugins.tssrc/github/utils/workflow-dispatch.tstests/kernel.test.ts