feat: fetch gateway skill from OneCLI API with static fallback

This commit is contained in:
johnnyfish
2026-05-07 21:03:39 +03:00
parent 4305c6a87d
commit 1240a0cf4f
5 changed files with 111 additions and 76 deletions
@@ -0,0 +1,85 @@
---
name: onecli-gateway
description: >-
OneCLI Gateway: transparent HTTPS proxy that injects stored credentials
into outbound calls. You MUST use this skill when the user asks you to
read emails, check calendar, access GitHub repos, create issues, check
Stripe payments, or interact with ANY external service or API. Do NOT
use browser extensions or OAuth CLI tools. Make HTTP requests directly;
the gateway injects credentials automatically.
compatibility: Requires HTTPS_PROXY set in environment (automatic when launched via `onecli run`)
metadata:
author: onecli
version: "0.5.0"
---
# OneCLI Gateway
Your outbound HTTPS traffic is transparently proxied through the OneCLI
gateway, which injects stored credentials at the proxy boundary. You never
see or handle credential values directly.
## How to Access External Services
You have direct HTTP access to external APIs. OAuth apps (Gmail, GitHub,
Google Calendar, Google Drive, etc.) and API key services are all available
through the gateway. Just make the request directly; the gateway injects
credentials if the app is connected. If not, it returns an error with a
connect URL you can present to the user.
## Making Requests
Call the real API URL. The gateway intercepts the request and injects
credentials automatically.
```bash
curl -s "https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=5"
curl -s "https://api.github.com/user/repos?per_page=10"
curl -s "https://api.stripe.com/v1/charges?limit=5"
```
Standard HTTP clients (curl, fetch, requests, axios, Go net/http, git) all
honor the `HTTPS_PROXY` environment variable automatically. You do not need
to set any auth headers.
## Credential Stubs for MCP Servers
Some MCP servers need local credential files to start. Stubs for connected
apps are pre-written automatically. Files containing `"onecli-managed"`
values are managed by OneCLI — do NOT modify or delete them.
If an MCP server won't start due to missing credentials, create stubs
**before** starting it. Use `"onecli-managed"` as the placeholder for all
secret values, with file permissions `0600`. See the guide at:
https://www.onecli.sh/docs/guides/credential-stubs/general-app
## When a Request Fails
If you get a 401, 403, or a gateway error (e.g., `app_not_connected`):
**Step 1 — Show the user a connect link.** Use the `connect_url` from the
error response:
> To connect [service], open this link:
> [connect_url from the error response]
If there is no `connect_url` in the error, tell the user to open the
OneCLI dashboard and connect the service there.
**Step 2 — Retry after the user connects.** Let the user know you will
retry once they have connected. When they confirm, retry the original
request. If the retry still fails, ask if they need help with the setup.
## Rules
- **Never** say "I don't have access to X" without first making the HTTP
request through the proxy.
- **Never** use browser extensions, gcloud, or manual auth flows. The
gateway handles credentials for you.
- **Never** ask the user for API keys or tokens directly. Direct them to
connect the service in the OneCLI dashboard.
- **Never** suggest the user open Gmail/Calendar/GitHub in their browser
when they ask you to read or interact with those services. You have API
access. Use it.
- If the gateway returns a policy error (403 with a JSON body), respect
the block. Do not retry or circumvent it.
-67
View File
@@ -1,67 +0,0 @@
---
name: onecli-gateway
description: >-
Handle credentials and authentication for external services. Use when you
hit a 401, 403, or app_not_connected error, or when the user asks you to
access an external service (Gmail, GitHub, Slack, Calendar, Stripe, etc.).
Do NOT use browser extensions or manual auth flows — make HTTP requests
directly; the OneCLI proxy injects credentials automatically.
---
# OneCLI Gateway: Credentials & Authentication
Your container routes all HTTPS traffic through the OneCLI proxy, which
injects stored credentials (API keys, OAuth tokens) at the proxy boundary.
You never see or handle credential values directly.
## Making Requests
Call the real API URL. The proxy intercepts and injects credentials automatically.
```bash
curl -s "https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=5"
curl -s "https://api.github.com/user/repos?per_page=10"
curl -s "https://api.stripe.com/v1/charges?limit=5"
```
Any HTTP client (curl, fetch, axios, Python requests, Go net/http, git) honors
`HTTPS_PROXY` automatically. You do not need to set auth headers.
If a tool or library validates credentials locally before making the request,
pass any placeholder value (a fake string). The proxy replaces it with real
credentials at request time.
## When a Request Fails (401 / 403 / app_not_connected)
### Step 1 — Show the user a connect link
If the error response includes a `connect_url`, share it directly:
> To connect [service], open this link:
> [connect_url from the error response]
If there's no `connect_url`, tell the user to open the OneCLI dashboard and
connect the service there.
Do NOT ask the user for API keys or tokens. Do NOT suggest pasting credentials
into chat. The fix is always connecting the service in OneCLI.
### Step 2 — Retry after the user connects
After showing the link, let the user know you'll retry once they've connected.
When they confirm (or after a reasonable pause), retry the original request.
If the retry still fails, ask the user if they need help with the OneCLI setup.
## Rules
- **Never** say "I don't have access to X" without first making the HTTP
request through the proxy.
- **Never** use browser extensions, gcloud, or manual auth flows. The proxy
handles credentials for you.
- **Never** ask the user for API keys, tokens, or passwords directly.
- **Never** suggest the user open Gmail/Calendar/GitHub in their browser
when they ask you to read or interact with those services. You have API
access — use it.
- If the proxy returns a policy error (403 with a JSON body), respect the
block. Do not retry or circumvent it.
+1 -1
View File
@@ -26,7 +26,7 @@
"dependencies": {
"@clack/core": "^1.2.0",
"@clack/prompts": "^1.2.0",
"@onecli-sh/sdk": "^0.3.1",
"@onecli-sh/sdk": "^0.5.0",
"better-sqlite3": "11.10.0",
"chat": "^4.24.0",
"cron-parser": "5.5.0",
+5 -5
View File
@@ -15,8 +15,8 @@ importers:
specifier: ^1.2.0
version: 1.2.0
'@onecli-sh/sdk':
specifier: ^0.3.1
version: 0.3.1
specifier: ^0.5.0
version: 0.5.0
better-sqlite3:
specifier: 11.10.0
version: 11.10.0
@@ -303,8 +303,8 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@onecli-sh/sdk@0.3.1':
resolution: {integrity: sha512-oMSa4DUCVS52vec41nFOg3XdCBTbMVEZdCFCsaUd9sRXVorCPWd3VyZq4giXsmk4g09DA/zLjsnrY7l6G94Ulg==}
'@onecli-sh/sdk@0.5.0':
resolution: {integrity: sha512-oe5Yx9o98v6N1PgzcCR7nULHHqcqKWNJIDOHGOSNX+l20mLlZpFUqfKPeFmsojBNRQMoqbvZQKUlFMp6gVuYBA==}
engines: {node: '>=20'}
'@oxc-project/types@0.124.0':
@@ -1665,7 +1665,7 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@onecli-sh/sdk@0.3.1': {}
'@onecli-sh/sdk@0.5.0': {}
'@oxc-project/types@0.124.0': {}
+20 -3
View File
@@ -132,7 +132,7 @@ async function spawnContainer(session: Session): Promise<void> {
// buildMounts and buildContainerArgs so side effects (mkdir, etc.) fire once.
const { provider, contribution } = resolveProviderContribution(session, agentGroup, containerConfig);
const mounts = buildMounts(agentGroup, session, containerConfig, contribution);
const mounts = await buildMounts(agentGroup, session, containerConfig, contribution);
const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`;
// OneCLI agent identifier is always the agent group id — stable across
// sessions and reversible via getAgentGroup() for approval routing.
@@ -239,12 +239,12 @@ function resolveProviderContribution(
return { provider, contribution };
}
function buildMounts(
async function buildMounts(
agentGroup: AgentGroup,
session: Session,
containerConfig: import('./container-config.js').ContainerConfig,
providerContribution: ProviderContainerContribution,
): VolumeMount[] {
): Promise<VolumeMount[]> {
const projectRoot = process.cwd();
// Per-group filesystem state lives forever after first creation. Init is
@@ -252,6 +252,23 @@ function buildMounts(
// is a no-op for groups that have spawned before.
initGroupFilesystem(agentGroup);
// Fetch the latest gateway skill from the API; fall back to the static copy.
const skillDir = path.join(projectRoot, 'container', 'skills', 'onecli-gateway');
const skillPath = path.join(skillDir, 'SKILL.md');
const fallbackPath = path.join(skillDir, 'SKILL.fallback.md');
try {
const skill = await onecli.getGatewaySkill();
const existing = fs.existsSync(skillPath) ? fs.readFileSync(skillPath, 'utf8') : '';
if (skill && skill !== existing) {
fs.writeFileSync(skillPath, skill);
}
} catch {
if (!fs.existsSync(skillPath) && fs.existsSync(fallbackPath)) {
fs.copyFileSync(fallbackPath, skillPath);
}
log.warn('Could not fetch gateway skill from OneCLI API; using static fallback');
}
// Sync skill symlinks based on container.json selection before mounting.
const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared');
syncSkillSymlinks(claudeDir, containerConfig);