From 1240a0cf4fd8c33604e869d072fc483352de93e7 Mon Sep 17 00:00:00 2001 From: johnnyfish Date: Thu, 7 May 2026 21:03:39 +0300 Subject: [PATCH] feat: fetch gateway skill from OneCLI API with static fallback --- .../skills/onecli-gateway/SKILL.fallback.md | 85 +++++++++++++++++++ container/skills/onecli-gateway/SKILL.md | 67 --------------- package.json | 2 +- pnpm-lock.yaml | 10 +-- src/container-runner.ts | 23 ++++- 5 files changed, 111 insertions(+), 76 deletions(-) create mode 100644 container/skills/onecli-gateway/SKILL.fallback.md delete mode 100644 container/skills/onecli-gateway/SKILL.md diff --git a/container/skills/onecli-gateway/SKILL.fallback.md b/container/skills/onecli-gateway/SKILL.fallback.md new file mode 100644 index 000000000..2e77d4503 --- /dev/null +++ b/container/skills/onecli-gateway/SKILL.fallback.md @@ -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. diff --git a/container/skills/onecli-gateway/SKILL.md b/container/skills/onecli-gateway/SKILL.md deleted file mode 100644 index 0c22c3e3a..000000000 --- a/container/skills/onecli-gateway/SKILL.md +++ /dev/null @@ -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. diff --git a/package.json b/package.json index 3f4794ced..babfd620a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f740338e..902b6ae2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': {} diff --git a/src/container-runner.ts b/src/container-runner.ts index 27b0f5c6c..26af37930 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -132,7 +132,7 @@ async function spawnContainer(session: Session): Promise { // 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 { 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);