feat: fetch gateway skill from OneCLI API with static fallback
This commit is contained in:
85
container/skills/onecli-gateway/SKILL.fallback.md
Normal file
85
container/skills/onecli-gateway/SKILL.fallback.md
Normal file
@@ -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.
|
||||||
@@ -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.
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/core": "^1.2.0",
|
"@clack/core": "^1.2.0",
|
||||||
"@clack/prompts": "^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",
|
"better-sqlite3": "11.10.0",
|
||||||
"chat": "^4.24.0",
|
"chat": "^4.24.0",
|
||||||
"cron-parser": "5.5.0",
|
"cron-parser": "5.5.0",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
'@onecli-sh/sdk':
|
'@onecli-sh/sdk':
|
||||||
specifier: ^0.3.1
|
specifier: ^0.5.0
|
||||||
version: 0.3.1
|
version: 0.5.0
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: 11.10.0
|
specifier: 11.10.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
@@ -303,8 +303,8 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
'@onecli-sh/sdk@0.3.1':
|
'@onecli-sh/sdk@0.5.0':
|
||||||
resolution: {integrity: sha512-oMSa4DUCVS52vec41nFOg3XdCBTbMVEZdCFCsaUd9sRXVorCPWd3VyZq4giXsmk4g09DA/zLjsnrY7l6G94Ulg==}
|
resolution: {integrity: sha512-oe5Yx9o98v6N1PgzcCR7nULHHqcqKWNJIDOHGOSNX+l20mLlZpFUqfKPeFmsojBNRQMoqbvZQKUlFMp6gVuYBA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
'@oxc-project/types@0.124.0':
|
'@oxc-project/types@0.124.0':
|
||||||
@@ -1665,7 +1665,7 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@onecli-sh/sdk@0.3.1': {}
|
'@onecli-sh/sdk@0.5.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.124.0': {}
|
'@oxc-project/types@0.124.0': {}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ async function spawnContainer(session: Session): Promise<void> {
|
|||||||
// buildMounts and buildContainerArgs so side effects (mkdir, etc.) fire once.
|
// buildMounts and buildContainerArgs so side effects (mkdir, etc.) fire once.
|
||||||
const { provider, contribution } = resolveProviderContribution(session, agentGroup, containerConfig);
|
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()}`;
|
const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`;
|
||||||
// OneCLI agent identifier is always the agent group id — stable across
|
// OneCLI agent identifier is always the agent group id — stable across
|
||||||
// sessions and reversible via getAgentGroup() for approval routing.
|
// sessions and reversible via getAgentGroup() for approval routing.
|
||||||
@@ -239,12 +239,12 @@ function resolveProviderContribution(
|
|||||||
return { provider, contribution };
|
return { provider, contribution };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMounts(
|
async function buildMounts(
|
||||||
agentGroup: AgentGroup,
|
agentGroup: AgentGroup,
|
||||||
session: Session,
|
session: Session,
|
||||||
containerConfig: import('./container-config.js').ContainerConfig,
|
containerConfig: import('./container-config.js').ContainerConfig,
|
||||||
providerContribution: ProviderContainerContribution,
|
providerContribution: ProviderContainerContribution,
|
||||||
): VolumeMount[] {
|
): Promise<VolumeMount[]> {
|
||||||
const projectRoot = process.cwd();
|
const projectRoot = process.cwd();
|
||||||
|
|
||||||
// Per-group filesystem state lives forever after first creation. Init is
|
// 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.
|
// is a no-op for groups that have spawned before.
|
||||||
initGroupFilesystem(agentGroup);
|
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.
|
// Sync skill symlinks based on container.json selection before mounting.
|
||||||
const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared');
|
const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared');
|
||||||
syncSkillSymlinks(claudeDir, containerConfig);
|
syncSkillSymlinks(claudeDir, containerConfig);
|
||||||
|
|||||||
Reference in New Issue
Block a user