diff --git a/plugins/box/.codex-plugin/plugin.json b/plugins/box/.codex-plugin/plugin.json
index 73a3f3b9..3ceb7509 100644
--- a/plugins/box/.codex-plugin/plugin.json
+++ b/plugins/box/.codex-plugin/plugin.json
@@ -9,6 +9,7 @@
"repository": "https://github.com/openai/plugins",
"license": "MIT",
"keywords": [],
+ "skills": "./skills/",
"apps": "./.app.json",
"interface": {
"displayName": "Box",
@@ -21,6 +22,8 @@
"defaultPrompt": [
"Find a Box file and summarize the key points"
],
+ "composerIcon": "./assets/box-small.svg",
+ "logo": "./assets/box.svg",
"screenshots": []
}
}
diff --git a/plugins/box/assets/box-small.svg b/plugins/box/assets/box-small.svg
new file mode 100644
index 00000000..96438f92
--- /dev/null
+++ b/plugins/box/assets/box-small.svg
@@ -0,0 +1,3 @@
+
diff --git a/plugins/box/assets/box.svg b/plugins/box/assets/box.svg
new file mode 100644
index 00000000..1f404801
--- /dev/null
+++ b/plugins/box/assets/box.svg
@@ -0,0 +1,3 @@
+
diff --git a/plugins/box/skills/box/README.md b/plugins/box/skills/box/README.md
new file mode 100644
index 00000000..991e353b
--- /dev/null
+++ b/plugins/box/skills/box/README.md
@@ -0,0 +1,56 @@
+# Box Content API — Codex Skill
+
+An [OpenAI Codex](https://openai.com/index/openai-codex/) skill that helps Codex build and troubleshoot Box integrations: uploads, folders, downloads, shared links, collaborations, search, metadata, webhooks, and Box AI retrieval.
+
+## Installation
+
+Copy or clone this folder into your Codex skills directory:
+
+```bash
+# Example: install into the default Codex skills location
+cp -r box-content-api ~/.codex/skills/
+```
+
+Once installed, invoke the skill in any Codex conversation with `$box-content-api`.
+
+## What's included
+
+```
+├── SKILL.md # Entry point — workflow, guardrails, and verification
+├── agents/openai.yaml # UI metadata for skill lists and chips
+├── references/
+│ ├── auth-and-setup.md # Auth paths, SDK vs REST, codebase inspection
+│ ├── box-cli.md # CLI-first local verification
+│ ├── workflows.md # Quick router when the task is ambiguous
+│ ├── content-workflows.md # Uploads, folders, shared links, collaborations, metadata, moves
+│ ├── bulk-operations.md # Batch moves, folder restructuring, serial execution, rate limits
+│ ├── webhooks-and-events.md # Webhook setup, events, idempotency
+│ ├── ai-and-retrieval.md # Search-first retrieval and Box AI
+│ └── troubleshooting.md # Common failure modes and debugging
+├── scripts/
+│ ├── box_cli_smoke.py # Smoke tests via Box CLI
+│ └── box_rest.py # Smoke tests via Box REST API (stdlib only)
+└── examples/
+ └── box-content-api-prompts.md # Example prompts
+```
+
+## Prerequisites
+
+- **Python 3.10+** — both scripts use only the standard library.
+- **Box CLI** (optional) — install from [developer.box.com/guides/cli](https://developer.box.com/guides/cli) for CLI-first verification. If unavailable, the skill falls back to `scripts/box_rest.py` with a `BOX_ACCESS_TOKEN`.
+
+## Quick smoke test
+
+```bash
+# With Box CLI installed and authenticated:
+python3 scripts/box_cli_smoke.py check-auth
+python3 scripts/box_cli_smoke.py list-folder-items 0 --max-items 5
+
+# With a bearer token instead:
+export BOX_ACCESS_TOKEN="your-token"
+python3 scripts/box_rest.py get-item --item-type folder --item-id 0
+```
+
+## License
+
+See [LICENSE](LICENSE) if present, or contact the repository owner.
diff --git a/plugins/box/skills/box/SKILL.md b/plugins/box/skills/box/SKILL.md
new file mode 100644
index 00000000..18579a5e
--- /dev/null
+++ b/plugins/box/skills/box/SKILL.md
@@ -0,0 +1,103 @@
+---
+name: box-content-api
+description: Build and troubleshoot Box integrations for uploads, folders, folder listings, downloads and previews, shared links, collaborations, search, metadata, event-driven automations, and Box AI retrieval flows. Use when Codex needs to add Box APIs or SDKs to an app, wire Box-backed document workflows, organize or share content, react to new files, or fetch Box content for search, summarization, extraction, or question-answering.
+---
+
+# Box Content API
+
+## Overview
+
+Implement Box content workflows in application code. Reuse the repository's existing auth and HTTP or SDK stack whenever possible, identify the acting Box identity before coding, and make the smallest end-to-end path work before layering on sharing, metadata, webhooks, or AI.
+
+## Route The Request
+
+| If the user needs... | Primary object | Read first | Pair with | Minimal verification |
+| --- | --- | --- | --- | --- |
+| Local verification, manual smoke tests, or quick inspection from Codex without app code changes | Current CLI environment | `references/box-cli.md` | `references/auth-and-setup.md` | `scripts/box_cli_smoke.py check-auth` then a read command |
+| Uploads, folders, listings, downloads, shared links, collaborations, or metadata | File or folder | `references/content-workflows.md` | `references/auth-and-setup.md` | Read-after-write call using the same actor |
+| Organizing, reorganizing, or batch-moving files across folders; bulk metadata tagging; migrating folder structures | File set or folder tree | `references/bulk-operations.md` | `references/auth-and-setup.md`, `references/content-workflows.md`, `references/ai-and-retrieval.md` | Inventory source, verify move count matches plan |
+| Event-driven ingestion, new-file triggers, or webhook debugging | Webhook or events feed | `references/webhooks-and-events.md` | `references/auth-and-setup.md`, `references/troubleshooting.md` | Signature check plus duplicate-delivery test |
+| Search, document retrieval, summarization, extraction, or Box AI | Search result set or file content | `references/ai-and-retrieval.md` | `references/auth-and-setup.md` | Retrieval-quality check before answer formatting |
+| 401, 403, 404, 409, 429, missing content, or wrong-actor bugs | Existing request path | `references/troubleshooting.md` | `references/auth-and-setup.md` | Reproduce with the exact actor, object ID, and endpoint |
+| Unsure which workflow applies | Unknown | `references/workflows.md` | `references/auth-and-setup.md` | Choose the smallest Box object/action pair first |
+
+## Workflow
+
+Follow these steps in order when coding against Box.
+
+1. Inspect the repository for existing Box auth, SDK or HTTP client, env vars, webhook handlers, Box ID persistence, and tests.
+2. Determine the acting identity before choosing endpoints: connected user, enterprise service account, app user, or platform-provided token.
+3. Identify the primary Box object and choose the matching reference from the routing table above.
+4. Confirm whether the task changes access or data exposure. Shared links, collaborations, auth changes, large-scale downloads, and broad AI retrieval all need explicit user confirmation before widening access or scope.
+5. Read only the matching reference files:
+ - Auth setup, actor selection, SDK vs REST: `references/auth-and-setup.md`
+ - Box CLI local verification: `references/box-cli.md`
+ - Workflow router: `references/workflows.md`
+ - Content operations: `references/content-workflows.md`
+ - Bulk file organization, batch moves, folder restructuring: `references/bulk-operations.md`
+ - Webhooks and events: `references/webhooks-and-events.md`
+ - AI and retrieval: `references/ai-and-retrieval.md`
+ - Debugging and failure modes: `references/troubleshooting.md`
+6. Implement the smallest end-to-end flow that proves the integration works.
+7. Add a runnable verification step. Prefer the repository's tests first; otherwise use `scripts/box_cli_smoke.py` when Box CLI is available and authenticated, and `scripts/box_rest.py` as a fallback.
+8. Summarize the deliverable with auth context, Box IDs, env vars or config, and the exact verification command or test.
+
+## Guardrails
+
+- Preserve the existing Box auth model unless the user explicitly asks to change it.
+- Check the current official Box docs before introducing a new auth path, changing auth scope, or changing Box AI behavior.
+- Prefer an official Box SDK when the codebase already uses one or the target language has a maintained SDK. Otherwise use direct REST calls with explicit request and response handling.
+- Keep access tokens, client secrets, private keys, and webhook secrets in env vars or the project's secret manager.
+- Distinguish file IDs, folder IDs, shared links, metadata template identifiers, and collaboration IDs.
+- Treat shared links, collaborations, and metadata writes as permission-sensitive changes. Confirm audience, scope, and least privilege before coding or applying them.
+- Require explicit confirmation before widening external access, switching the acting identity, or retrieving more document content than the task truly needs.
+- When a task requires understanding document content — classification, extraction, categorization — use Box AI (Q&A, extract) as the first method attempted. Box AI operates server-side and does not require downloading file bodies. Fall back to metadata inspection, previews, or local analysis only if Box AI is unavailable, not authorized, or returns an error on the first attempt.
+- Pace Box AI calls at least 1–2 seconds apart. For content-based classification of many files, classify a small sample first to validate the prompt and discover whether cheaper signals (filename, extension, metadata) can sort the remaining files without additional AI calls.
+- Avoid downloading file bodies or routing content through external AI pipelines when Box-native methods (Box AI, search, metadata, previews) can answer the question server-side.
+- Request only the fields the application actually needs, and persist returned Box IDs instead of reconstructing paths later.
+- Run Box CLI commands strictly one at a time. The CLI does not support concurrent invocations and parallel calls cause auth conflicts and dropped operations. For bulk work (organizing, batch moves, batch metadata), default to REST over CLI.
+- Make webhook and event consumers idempotent. Box delivery and retry paths can produce duplicates.
+- Keep AI retrieval narrow for search and Q&A tasks. Search and filter first, then retrieve only the files needed for the answer. This does not apply to Box AI classification — when classifying documents, Box AI should be tried first per the content-understanding guardrail above.
+- Do not use `box configure:environments:get --current` as a routine auth check because it can print sensitive environment details.
+
+## Verification
+
+- Prefer the repository's existing tests, scripts, or app flows when they already cover the changed Box behavior.
+- If no better verification path exists, prefer `scripts/box_cli_smoke.py` when `box` is installed and authenticated. Fall back to `scripts/box_rest.py` with `BOX_ACCESS_TOKEN` when CLI auth is unavailable or the task specifically needs direct bearer-token verification.
+- Confirm CLI auth with `box users:get me --json` or `scripts/box_cli_smoke.py check-auth`.
+- Verify mutations with a read-after-write call using the same actor, and record the object ID.
+- For webhooks, test the minimal happy path, duplicate delivery, and signature failure handling.
+- For AI flows, test retrieval quality separately from answer formatting.
+
+Example smoke checks:
+
+```bash
+python3 scripts/box_cli_smoke.py check-auth
+python3 scripts/box_cli_smoke.py get-folder 0 --fields id name item_collection
+python3 scripts/box_cli_smoke.py list-folder-items 0 --max-items 20
+python3 scripts/box_cli_smoke.py search "invoice" --limit 10
+python3 scripts/box_rest.py get-item --item-type folder --item-id 0 --fields id name item_collection
+```
+
+## Deliverable
+
+The final answer should include:
+
+- Acting auth context used for the change
+- Box object type and IDs touched
+- Env vars, secrets, or config expected by the integration
+- Files or endpoints added or changed
+- Exact verification command, script, or test path
+- Any permission-sensitive assumptions that still need confirmation
+
+## References
+
+- `references/auth-and-setup.md`: auth path selection, SDK vs REST choice, existing-codebase inspection, and current Box doc anchors
+- `references/box-cli.md`: CLI-first local auth, smoke-test commands, and safe verification patterns
+- `references/workflows.md`: quick workflow router when the task is ambiguous
+- `references/content-workflows.md`: uploads, folders, listings, downloads, shared links, collaborations, metadata, and file moves
+- `references/bulk-operations.md`: organizing files at scale, batch moves, folder hierarchy creation, serial execution, and rate-limit handling
+- `references/webhooks-and-events.md`: webhook setup, event-feed usage, idempotency, and verification
+- `references/ai-and-retrieval.md`: search-first retrieval, Box AI usage, and external AI guardrails
+- `references/troubleshooting.md`: common failure modes and a debugging checklist
+- `examples/box-content-api-prompts.md`: example prompts for realistic use cases
diff --git a/plugins/box/skills/box/agents/openai.yaml b/plugins/box/skills/box/agents/openai.yaml
new file mode 100644
index 00000000..3c3b8db4
--- /dev/null
+++ b/plugins/box/skills/box/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "Box Content API"
+ short_description: "Implement Box content flows safely"
+ icon_small: "../../../assets/box-small.svg"
+ icon_large: "../../../assets/box.svg"
+ default_prompt: "Use $box-content-api to identify the acting Box auth context, prefer Box CLI for local verification when available, implement the smallest Box flow needed, and return Box IDs plus a verification command."
diff --git a/plugins/box/skills/box/examples/box-content-api-prompts.md b/plugins/box/skills/box/examples/box-content-api-prompts.md
new file mode 100644
index 00000000..152bd870
--- /dev/null
+++ b/plugins/box/skills/box/examples/box-content-api-prompts.md
@@ -0,0 +1,7 @@
+# Example Prompts
+
+- "Use $box-content-api to add the smallest possible endpoint that uploads a generated PDF into a configured Box folder, then tell me which folder ID and file ID were used to verify it."
+- "Use $box-content-api to verify my current Box CLI auth context, list the root folder items with CLI-first verification, and tell me which actor the command is running as."
+- "Use $box-content-api to debug why this Box folder listing returns 404 in production but works locally; identify the acting auth context and the exact object ID mismatch."
+- "Use $box-content-api to wire a webhook handler for new files in a folder, make it idempotent, and include a duplicate-delivery verification step."
+- "Use $box-content-api to build a search-first retrieval flow over Box content for invoice lookup, and only download file content if the selected result actually needs it."
diff --git a/plugins/box/skills/box/references/ai-and-retrieval.md b/plugins/box/skills/box/references/ai-and-retrieval.md
new file mode 100644
index 00000000..f0666d79
--- /dev/null
+++ b/plugins/box/skills/box/references/ai-and-retrieval.md
@@ -0,0 +1,96 @@
+# AI and Retrieval
+
+## Table of Contents
+
+- Search-first strategy
+- Content understanding preference order
+- Choose Box AI vs external AI
+- Retrieval guardrails
+- Verification checklist
+- Primary docs
+
+## Search-first strategy
+
+- Use Box search before recursive folder traversal or bulk download.
+- Narrow the candidate set with ancestor folders, object type, filenames, owners, or metadata filters whenever possible.
+- Return stable IDs and lightweight metadata first, then retrieve content only for the final shortlist.
+
+## Content understanding preference order
+
+When the task requires understanding what a document contains (classification, extraction, summarization, Q&A), prefer Box-native methods first:
+
+1. **Box AI Q&A or Extract** — keeps content server-side, no downloads needed.
+2. **Metadata inspection** — check existing Box metadata templates or properties.
+3. **Previews or thumbnails** — lightweight visual inspection without downloading the full file.
+4. **Local analysis (OCR, agent-side parsing)** — download and process locally only when the above methods are unavailable, not authorized, or insufficient.
+
+If the first Box AI call fails with a 403 or feature-not-available error, switch to the next method immediately rather than retrying AI for the remaining files.
+
+### Box AI via CLI
+
+**Before the first AI call**, run `box ai:ask --help` to confirm the command exists in the installed CLI version.
+
+Ask a question about a file's content:
+
+```bash
+box ai:ask --items=id=,type=file \
+ --prompt "Summarize this document in one sentence." \
+ --json --no-color
+```
+
+Extract key-value pairs via a freeform prompt:
+
+```bash
+box ai:extract --items=id=,type=file \
+ --prompt "document_type, vendor_name, date" \
+ --json --no-color
+```
+
+Extract with typed fields or a metadata template:
+
+```bash
+box ai:extract-structured --items=id=,type=file \
+ --fields "key=document_type,type=enum,options=invoice;receipt;contract;other" \
+ --json --no-color
+```
+
+Reference: https://github.com/box/boxcli/blob/main/docs/ai.md
+
+An "Unexpected Error" with no HTTP body and exit code 2 may indicate the CLI version does not support AI commands, Box AI is not enabled for the account, or the file type is not supported. Run `box ai:ask --help` to verify the command exists, and try with a known-supported file type (PDF, DOCX) before falling back.
+
+### Box AI pacing
+
+Box AI endpoints have tighter per-user/per-app rate limits than standard content API calls. Pace AI calls at least 1–2 seconds apart. For bulk classification workflows, use the sample-first strategy described in `references/bulk-operations.md` to minimize the total number of AI calls.
+
+## Choose Box AI vs external AI
+
+- Prefer Box AI when the task maps directly to Box-native document question answering, extraction, or summarization.
+- Use an external AI pipeline only when the product needs model behavior that Box AI does not provide or the application already owns the reasoning layer.
+- Check the current official Box AI docs before changing prompts, capabilities, or supported object flows.
+
+## Retrieval guardrails
+
+- Avoid pulling raw file bodies when metadata, previews, or Box-native answers are enough.
+- Keep retrieval scoped to the smallest relevant set of files.
+- Preserve traceability with file IDs, names, shared links, or citations when the product needs auditability.
+- Confirm with the user before broad retrieval across large folders or sensitive content sets.
+
+## Verification checklist
+
+- Retrieval quality:
+ - Confirm the search filters and candidate set contain the intended documents.
+- Answer grounding:
+ - Confirm the final answer can point back to the specific file IDs or names used.
+- Access control:
+ - Confirm the acting identity can only see the content the product is supposed to expose.
+
+## Primary docs
+
+- Search reference:
+ - https://developer.box.com/reference/get-search/
+- Box AI guides:
+ - https://developer.box.com/guides/box-ai/
+- Box AI with objects:
+ - https://developer.box.com/guides/box-ai/use-box-ai-with-box-objects/
+- Box CLI AI commands:
+ - https://github.com/box/boxcli/blob/main/docs/ai.md
diff --git a/plugins/box/skills/box/references/auth-and-setup.md b/plugins/box/skills/box/references/auth-and-setup.md
new file mode 100644
index 00000000..e7981ce3
--- /dev/null
+++ b/plugins/box/skills/box/references/auth-and-setup.md
@@ -0,0 +1,94 @@
+# Auth and Setup
+
+## Table of Contents
+
+- Actor selection checklist
+- CLI-first local testing
+- Choosing the auth path
+- Choosing SDK vs REST
+- Inspecting an existing codebase
+- Common secrets and config
+- Official Box starting points
+
+## Actor selection checklist
+
+Choose the acting identity before you choose endpoints or debug errors:
+
+- Connected user: use when the product acts on behalf of an end user who linked their Box account.
+- Enterprise service account: use when the backend runs unattended against enterprise-managed content.
+- App user: use when the product provisions managed Box identities per tenant or workflow.
+- Existing token from the platform: use when the surrounding app already resolved auth and passes the token into the Box layer.
+
+Always capture which actor you are using in logs, test output, and the final answer. Many Box bugs are actually actor mismatches.
+
+## CLI-first local testing
+
+When the task is a local smoke test, quick inspection, or one-off verification from Codex, prefer Box CLI before raw REST if `box` is already installed and authenticated.
+
+- Check CLI auth safely with `box users:get me --json`.
+- If CLI auth is missing:
+ - Fastest OAuth path: `box login -d`
+ - Use your own Box app: `box login --platform-app`
+ - Use an app config file: `box configure:environments:add PATH`
+- Use `--as-user ` when you need to verify behavior as a managed user or another actor allowed by the current Box environment.
+- Use `-t ` only when the task explicitly requires a direct bearer token instead of the current CLI environment.
+- Avoid `box configure:environments:get --current` as a routine auth check because it can print sensitive environment details.
+- Prefer the bundled `scripts/box_cli_smoke.py` wrapper when you want deterministic CLI-based verification from the skill.
+
+## Choosing the auth path
+
+- Reuse the repository's existing Box auth flow if one already exists.
+- Use a user-auth flow when end users connect their own Box accounts and the app acts as that user.
+- Use the enterprise or server-side pattern already approved for the Box app when the backend runs unattended or manages enterprise content.
+- Treat impersonation, app-user usage, token exchange, or downscoping as advanced changes. Add them only when the product requirements clearly demand them.
+- Verify the exact flow against the current auth guides before introducing a new auth path or changing scopes.
+
+## Choosing SDK vs REST
+
+- Use an official Box SDK when the target language already has one in the codebase or the team prefers SDK-managed models and pagination.
+- Use direct REST calls when the project already centers on a generic HTTP client, only a few endpoints are needed, or SDK support does not match the feature set.
+- Avoid mixing SDK abstractions and handwritten REST calls for the same feature unless there is a clear gap.
+- Preserve the project's existing retry, logging, and error-normalization patterns.
+
+## Inspecting an existing codebase
+
+Search for:
+
+- `box`
+- `BOX_`
+- `client_id`
+- `client_secret`
+- `enterprise`
+- `shared_link`
+- `webhook`
+- `metadata`
+
+Confirm:
+
+- Where access tokens are issued, refreshed, or injected
+- Whether requests are user-scoped, service-account-scoped, or app-user-scoped
+- Whether the codebase already has pagination, retry, and rate-limit helpers
+- Whether webhook verification already exists
+- Whether file and folder IDs are persisted in a database, config, or user settings
+
+## Common secrets and config
+
+- Client ID and client secret
+- Private key material or app config used by the approved Box auth flow
+- Enterprise ID, user ID, or app-user identifiers when relevant
+- Webhook signing secrets
+- Default folder IDs
+- Metadata template identifiers and field names
+- Shared link defaults such as access level or expiration policy
+- Box CLI environment names or `--as-user` conventions when the team uses CLI-based operations
+
+## Official Box starting points
+
+- Developer guides: https://developer.box.com/guides
+- API reference root: https://developer.box.com/reference
+- SDK overview: https://developer.box.com/guides/tooling/sdks/
+- Authentication guides: https://developer.box.com/guides/authentication/
+- CLI guides: https://developer.box.com/guides/cli
+- CLI OAuth quick start: https://developer.box.com/guides/cli/quick-start
+
+Check the current Box docs before introducing a new auth model, changing scopes, or changing Box AI behavior, because auth guidance and SDK coverage can evolve independently from the content endpoints.
diff --git a/plugins/box/skills/box/references/box-cli.md b/plugins/box/skills/box/references/box-cli.md
new file mode 100644
index 00000000..bd83833d
--- /dev/null
+++ b/plugins/box/skills/box/references/box-cli.md
@@ -0,0 +1,103 @@
+# Box CLI
+
+## Table of Contents
+
+- When to use CLI-first mode
+- Safe auth checks
+- Authentication paths
+- Common verification commands
+- Actor controls
+- Guardrails
+
+## When to use CLI-first mode
+
+Use Box CLI first when:
+
+- Codex needs a quick local smoke test without changing application code
+- The operator already has a working Box CLI environment
+- You want to verify behavior as the current CLI actor or with `--as-user`
+
+Use `scripts/box_rest.py` instead when:
+
+- The repository already uses token-based REST verification
+- The task requires a raw bearer token from the surrounding platform
+- Box CLI is not installed or not authenticated
+
+## Safe auth checks
+
+Use these commands to confirm CLI availability and auth without printing secrets:
+
+```bash
+command -v box
+box --version
+box users:get me --json
+```
+
+Prefer the bundled wrapper:
+
+```bash
+python3 scripts/box_cli_smoke.py check-auth
+```
+
+Do not use `box configure:environments:get --current` as a routine check because it can print sensitive environment details.
+
+## Authentication paths
+
+- Fastest OAuth flow with the official Box CLI app:
+ - `box login -d`
+- OAuth with your own Box app:
+ - `box login --platform-app`
+- Add an environment from an app config file:
+ - `box configure:environments:add PATH`
+
+After login or environment setup, re-run `box users:get me --json` to confirm the CLI can make authenticated calls.
+
+## Common verification commands
+
+Read-only checks:
+
+```bash
+box users:get me --json
+box folders:get 0 --json --fields id,name,item_collection
+box folders:items 0 --json --max-items 20
+box search "invoice" --json --limit 10
+```
+
+Write checks:
+
+```bash
+box folders:create 0 "codex-smoke-test" --json
+box files:upload ./artifact.pdf --parent-id 0 --json
+box shared-links:create 12345 file --access company --json
+```
+
+Wrapper equivalents:
+
+```bash
+python3 scripts/box_cli_smoke.py get-folder 0 --fields id name item_collection
+python3 scripts/box_cli_smoke.py list-folder-items 0 --max-items 20
+python3 scripts/box_cli_smoke.py search "invoice" --limit 10
+python3 scripts/box_cli_smoke.py create-folder 0 "codex-smoke-test"
+```
+
+## Actor controls
+
+- Use `--as-user ` to verify behavior as a different allowed Box user.
+- Use `-t ` only when the task explicitly requires a direct bearer token instead of the current CLI environment.
+- Always report which actor was used for the verification command.
+
+## Guardrails
+
+- Do not paste or echo client secrets, private keys, or raw access tokens into the conversation.
+- Prefer read commands before write commands.
+- For shared links and collaborations, confirm scope and audience before creating or widening access.
+- After any write, follow up with a read command against the same object and actor.
+
+## Official docs
+
+- CLI overview:
+ - https://developer.box.com/guides/cli
+- CLI OAuth quick start:
+ - https://developer.box.com/guides/cli/quick-start
+- CLI options and `--as-user`:
+ - https://developer.box.com/guides/cli/quick-start/options-and-bulk-commands/
diff --git a/plugins/box/skills/box/references/bulk-operations.md b/plugins/box/skills/box/references/bulk-operations.md
new file mode 100644
index 00000000..4a2519f1
--- /dev/null
+++ b/plugins/box/skills/box/references/bulk-operations.md
@@ -0,0 +1,229 @@
+# Bulk Operations
+
+## Table of Contents
+
+- When this applies
+- Constraints
+- Workflow: inventory, classify, plan, execute, verify
+- Step 1 — Inventory
+- Step 2 — Classify (when content-based sorting is needed)
+- Step 3 — Plan the target hierarchy
+- Step 4 — Create folders
+- Step 5 — Move files
+- Step 6 — Verify
+- Rate-limit and backoff handling
+- REST vs CLI for bulk work
+- Partial failure recovery
+
+Read `references/auth-and-setup.md` first when the acting identity or SDK vs REST choice is unclear.
+
+## When this applies
+
+Use this reference when the task involves more than a handful of files or folders in a single operation:
+
+- Organizing or reorganizing files across folders (by type, date, project, etc.)
+- Batch-moving files from a flat folder into a structured hierarchy
+- Creating a folder tree for a classification or filing scheme
+- Bulk-tagging files with metadata
+- Migrating content between folder structures
+
+## Constraints
+
+### Box CLI must run serially
+
+The Box CLI does not support concurrent invocations against the same environment. Launching multiple CLI processes in parallel causes auth conflicts, dropped operations, and unpredictable errors. **Always run CLI commands one at a time, waiting for each to complete before starting the next.**
+
+### Box API rate limits
+
+Box enforces per-user and per-app rate limits. Bulk operations that send requests too quickly will receive `429 Too Many Requests` responses. The response includes a `Retry-After` header with the number of seconds to wait. See [Rate-limit and backoff handling](#rate-limit-and-backoff-handling) below.
+
+### Folder name uniqueness
+
+Box enforces unique names within a parent folder. Creating a folder that already exists returns a `409 Conflict`. Check for existing folders before creating, or handle 409 by looking up the existing folder and reusing its ID.
+
+## Workflow: inventory, classify, plan, execute, verify
+
+Bulk operations follow this pattern. Do not skip ahead — moving files without a verified plan leads to misplaced content that is painful to undo.
+
+```
+Inventory → Classify (if needed) → Plan → Execute (serial) → Verify
+```
+
+Skip the classify step when files can be sorted by filename, extension, or existing metadata alone.
+
+## Step 1 — Inventory
+
+List everything in the source folder(s). Paginate fully — do not assume a single page covers all items.
+
+```bash
+# CLI — list up to 1000 items
+python3 scripts/box_cli_smoke.py list-folder-items --max-items 1000 --fields id name type
+
+# REST — paginate with offset
+python3 scripts/box_rest.py get-folder-items --folder-id --limit 1000 --fields id name type
+```
+
+For folders with more items than one page returns, increment the offset and repeat until all items are captured.
+
+Capture each item's `id`, `name`, and `type` into a working list before proceeding.
+
+## Step 2 — Classify (when content-based sorting is needed)
+
+Skip this step if files can be categorized by filename, extension, or existing metadata. Use it when the documents are unstructured and their content determines the category — for example, a folder of mixed invoices, receipts, contracts, and reports that all share the same file type.
+
+### Preference order for content understanding
+
+1. **Box AI Q&A or Extract** (preferred) — ask Box AI to classify or extract structured fields from each file. This keeps content server-side, requires no downloads, and leverages Box's own document understanding.
+2. **Metadata inspection** — check existing Box metadata templates or properties already applied to the files.
+3. **Previews or thumbnails** — use Box preview representations for lightweight visual inspection without downloading the full file.
+4. **Local analysis (OCR, agent-side parsing)** — download the file and process it locally. Use only when Box AI is unavailable, not authorized, or insufficient for the document type.
+
+### Sample-first strategy
+
+Do not classify every file up front. Box AI calls are slower than metadata reads and have tighter rate limits.
+
+1. **Pick a small sample** (5–10 files) that appear representative of the mix.
+2. **Classify the sample** using Box AI to discover the category set and validate the prompt.
+3. **Check for cheaper signals.** After seeing the sample results, determine whether filename patterns, extensions, or metadata can sort some or all of the remaining files without additional AI calls.
+4. **Classify the remainder** — use AI only for files that cannot be sorted by cheaper signals. Pace AI calls at least 1–2 seconds apart.
+5. **Record each classification** (file ID → category) as it completes so an interrupted run can resume without re-classifying finished files.
+
+### Box AI classification via CLI
+
+**Before the first AI call**, run `box ai:ask --help` to confirm the command exists in the installed CLI version and to check for any flag changes.
+
+Use `box ai:ask` to classify a single file by asking a direct question:
+
+```bash
+box ai:ask --items=id=,type=file \
+ --prompt "What type of document is this? Reply with exactly one of: invoice, receipt, contract, report, other." \
+ --json --no-color
+```
+
+Use `box ai:extract` when you need key-value extraction via a freeform prompt:
+
+```bash
+box ai:extract --items=id=,type=file \
+ --prompt "document_type, vendor_name, date" \
+ --json --no-color
+```
+
+Use `box ai:extract-structured` when you have a metadata template or want typed fields with options:
+
+```bash
+box ai:extract-structured --items=id=,type=file \
+ --fields "key=document_type,type=enum,options=invoice;receipt;contract;report;other" \
+ --json --no-color
+```
+
+Reference: https://github.com/box/boxcli/blob/main/docs/ai.md
+
+### Handling failures during classification
+
+- **Exit code 2 or "Unexpected Error" with no HTTP body** can mean the installed CLI version does not have AI commands, Box AI is not enabled for the account, or the file type is not supported. Run `box ai:ask --help` to verify the command exists. If the command exists but still fails, try a known-supported file type (PDF, DOCX) to distinguish account-level unavailability from file-type incompatibility.
+- If the first AI call returns a 403, feature-not-available, or similar authorization error, stop attempting AI classification for the remaining files and switch to the next method in the preference order immediately.
+- If an individual file fails (unsupported format, empty content, timeout), log it and continue. Classify it manually or by fallback method after the batch finishes.
+- On 429, wait for the `Retry-After` period and retry the same file before moving to the next one.
+- Box AI support for file types varies by account tier. Image files (`.jpg`, `.png`) may not be supported for text-based Q&A. If the sample files are images, try `box ai:extract` first or check whether the account has image-understanding capabilities before falling back to local OCR.
+
+## Step 3 — Plan the target hierarchy
+
+Decide the target folder structure before creating or moving anything.
+
+1. Define the classification rule (by file-name pattern, extension, date, metadata, or content).
+2. Map each inventoried item to its target folder path.
+3. Identify which target folders already exist and which need to be created.
+4. Write the plan as a structured list or table — folder path, folder ID (if existing), and the file IDs that belong there.
+
+Example plan:
+
+```
+Target folder | Parent ID | Needs creation | File IDs
+-----------------------|-----------|----------------|------------------
+/SEC Filings/10-K | 0 | yes | 111, 112, 113 ...
+/SEC Filings/10-Q | 0 | yes | 211, 212, 213 ...
+/Research/AI | 0 | yes | 311, 312, 313 ...
+```
+
+Confirm the plan with the user before executing if the operation is large or the classification is ambiguous.
+
+## Step 4 — Create folders
+
+Create target folders **one at a time, serially**. After each creation, record the returned folder ID — you need it for moves.
+
+```bash
+# CLI
+python3 scripts/box_cli_smoke.py create-folder "SEC Filings"
+# then
+python3 scripts/box_cli_smoke.py create-folder "10-K"
+
+# REST
+python3 scripts/box_rest.py create-folder --parent-folder-id --name "SEC Filings"
+```
+
+Handle `409 Conflict` by listing the parent folder to find the existing folder's ID rather than failing the entire operation.
+
+Create parent folders before child folders. Process the tree top-down.
+
+## Step 5 — Move files
+
+Move files into their target folders **one at a time, serially**. Each move is a PUT that updates the file's parent.
+
+```bash
+# REST (preferred for bulk — more reliable than CLI for high-volume moves)
+python3 scripts/box_rest.py move-item --item-type file --item-id --parent-folder-id
+
+# CLI
+python3 scripts/box_cli_smoke.py move-item file --parent-id
+```
+
+After each successful move, record it. If a move fails, log the file ID and error and continue with the remaining files — do not abort the entire batch.
+
+### Pacing
+
+Insert a short delay between operations when working with large batches (100+ items). A 200–500ms pause between requests helps stay within rate limits without dramatically increasing total time.
+
+When using REST directly in application code (not via the scripts), implement proper 429 backoff instead of fixed delays.
+
+## Step 6 — Verify
+
+After all moves complete:
+
+1. List each target folder and confirm it contains the expected file IDs and count.
+2. List the source folder and confirm it is empty or contains only the items that were intentionally left behind.
+3. Report any items that failed to move and the error encountered.
+
+```bash
+python3 scripts/box_cli_smoke.py list-folder-items --max-items 1000 --fields id name
+```
+
+## Rate-limit and backoff handling
+
+When Box returns `429 Too Many Requests`:
+
+1. Read the `Retry-After` header (value in seconds).
+2. Wait that many seconds before retrying the same request.
+3. Do not retry other requests during the wait — the limit is typically per-user or per-app, so other requests will also be throttled.
+4. After a successful retry, resume normal pacing.
+
+In application code, implement exponential backoff with jitter starting at the `Retry-After` value. In script-based or CLI-based operations, a simple sleep-and-retry is sufficient.
+
+## REST vs CLI for bulk work
+
+| Factor | REST (`box_rest.py` or SDK) | CLI (`box_cli_smoke.py`) |
+| --- | --- | --- |
+| Concurrency safety | Can handle controlled concurrency with proper rate-limit handling | Must run serially — no parallel invocations |
+| Overhead per call | Lower — direct HTTP | Higher — process spawn per command |
+| Error handling | Structured JSON responses, easy to parse and retry | Exit codes and mixed output, harder to automate |
+| Best for | Bulk moves, batch metadata writes, any operation over ~50 items | Quick verification, small batches, interactive debugging |
+
+**Default to REST for bulk operations.** Fall back to CLI when REST auth is unavailable or the operator specifically prefers CLI-based workflows.
+
+## Partial failure recovery
+
+Bulk operations can fail partway through. Design for recovery:
+
+- Track which operations succeeded (keep a log of completed item IDs).
+- On failure, report what completed, what failed, and what remains.
+- Make the operation resumable: use the inventory list minus completed items as the input for a retry pass.
+- Moves are idempotent in practice — moving a file to a folder it is already in returns the file unchanged. Re-running a move pass is safe.
diff --git a/plugins/box/skills/box/references/content-workflows.md b/plugins/box/skills/box/references/content-workflows.md
new file mode 100644
index 00000000..e74822f8
--- /dev/null
+++ b/plugins/box/skills/box/references/content-workflows.md
@@ -0,0 +1,108 @@
+# Content Workflows
+
+## Table of Contents
+
+- Upload a file
+- Create folders
+- List folder items
+- Download or preview a file
+- Generate a shared link
+- Invite collaborators
+- Move a file or folder
+- Read or write metadata
+
+Read `references/auth-and-setup.md` first when the acting identity or SDK vs REST choice is unclear.
+
+For local or manual verification, prefer `scripts/box_cli_smoke.py` when Box CLI is available and authenticated. Fall back to `scripts/box_rest.py` when the task is token-first or Box CLI is unavailable.
+
+## Upload a file
+
+- Primary docs:
+ - https://developer.box.com/reference/post-files-content/
+- Use for local-disk uploads, form uploads, or pushing generated artifacts into Box.
+- Decide whether the input is a file path, in-memory upload, or generated artifact.
+- Set the destination folder ID first.
+- Treat file-name conflicts explicitly.
+- Start with standard upload; use chunked upload only when file size or resumable behavior requires it.
+- Minimal smoke check:
+ - Upload the file, then list the destination folder with the same actor and confirm returned `id` and `name`.
+
+## Create folders
+
+- Primary docs:
+ - https://developer.box.com/reference/post-folders/
+- Use for customer, project, case, employee, or workflow roots.
+- Decide the parent folder and canonical naming scheme before coding.
+- Handle duplicate-name conflicts intentionally.
+- Persist the returned folder ID instead of reconstructing paths later.
+- Minimal smoke check:
+ - Create the folder, then list the parent folder and confirm the child folder ID and name.
+
+## List folder items
+
+- Primary docs:
+ - https://developer.box.com/reference/get-folders-id-items/
+- Use for dashboards, file pickers, sync views, or post-upload verification.
+- Request only the fields the app actually needs.
+- Handle pagination instead of assuming a single page.
+- Filter server-side where practical before adding client-side transforms.
+- Minimal smoke check:
+ - Read the folder with a limited field set and confirm the app can process pagination metadata.
+
+## Download or preview a file
+
+- Primary docs:
+ - https://developer.box.com/reference/get-files-id-content/
+ - https://developer.box.com/guides/embed/ui-elements/preview/
+- Download when the app truly needs raw bytes for processing or export.
+- Use preview patterns when the app needs an embedded viewer.
+- Preserve filename, content type, and auth context in tests and logs.
+- Minimal smoke check:
+ - Fetch the file metadata first; only then download or preview the exact file ID you intend to use.
+
+## Generate a shared link
+
+- Primary docs:
+ - https://developer.box.com/reference/put-files-id/
+ - https://developer.box.com/reference/put-folders-id/
+- Use for external sharing, customer handoff, or quick verification outside the app.
+- Add or update `shared_link` on the target file or folder, not on an unrelated object.
+- Set access level, download permissions, and expiration intentionally.
+- Confirm the user explicitly wants the audience widened before enabling or broadening sharing.
+- Minimal smoke check:
+ - Read the file or folder after the update and confirm the resulting `shared_link` fields.
+
+## Invite collaborators
+
+- Primary docs:
+ - https://developer.box.com/reference/post-collaborations/
+- Use for team, vendor, or customer access to a shared workspace.
+- Prefer folder collaboration when multiple files should inherit the same access.
+- Choose the narrowest role that satisfies the request.
+- Verify the acting identity is allowed to invite collaborators before coding the flow.
+- Minimal smoke check:
+ - Create the collaboration, then fetch or list collaborations to confirm the collaborator and role.
+
+## Move a file or folder
+
+- Primary docs:
+ - https://developer.box.com/reference/put-files-id/ (update parent to move a file)
+ - https://developer.box.com/reference/put-folders-id/ (update parent to move a folder)
+- Use for reorganizing content, filing into project or category folders, or migrating between folder structures.
+- A move is a PUT on the item that sets `parent.id` to the new folder.
+- Moving a folder moves all of its contents recursively.
+- Handle name conflicts in the target folder — Box returns `409` if a same-named item already exists in the destination.
+- For bulk moves (more than a handful of items), read `references/bulk-operations.md` for the inventory-plan-execute-verify workflow, serial execution constraints, and rate-limit handling.
+- Minimal smoke check:
+ - Move the item, then list the target folder and confirm the item appears with the correct ID and name. Also list the source folder to confirm the item is gone.
+
+## Read or write metadata
+
+- Primary docs:
+ - https://developer.box.com/reference/post-files-id-metadata-global-properties/
+- Use for invoice IDs, customer names, case numbers, review states, or other business context.
+- Read the template definition or existing metadata instance before writing values.
+- Keep template identifiers and field names in config, not scattered through the codebase.
+- Validate keys and value types in code before calling Box.
+- Minimal smoke check:
+ - Write the metadata, then read the same instance back and confirm only the expected keys changed.
diff --git a/plugins/box/skills/box/references/troubleshooting.md b/plugins/box/skills/box/references/troubleshooting.md
new file mode 100644
index 00000000..497e03b4
--- /dev/null
+++ b/plugins/box/skills/box/references/troubleshooting.md
@@ -0,0 +1,95 @@
+# Troubleshooting
+
+## Table of Contents
+
+- Debugging checklist
+- 401 or 403
+- 404
+- 409
+- 429
+- Webhook verification failures
+- Search quality problems
+- CLI auth problems
+- Codex sandbox network access
+
+## Debugging checklist
+
+Before changing code, capture these facts:
+
+- Acting auth context
+- Exact endpoint and HTTP method
+- Box object type and ID
+- Minimal request payload
+- Response status and error body
+
+Most Box failures reduce to one of these mismatches: wrong actor, wrong object ID, wrong endpoint, or an access-control change that was never confirmed.
+
+When using Box CLI, run `box --help` before the first invocation of any subcommand to confirm it exists in the installed version and to verify flag names, required arguments, and supported options.
+
+## 401 or 403
+
+- Wrong auth context
+- Missing scope or app permission
+- Acting user does not have access to the target object
+- Token expired, downscoped, or issued for a different flow than expected
+
+## 404
+
+- Wrong file or folder ID
+- Object exists but is not visible to the current actor
+- Shared link or collaboration refers to a different object than expected
+
+## 409
+
+- File or folder name conflict on create or upload
+- Collaboration already exists
+- Metadata write conflicts with the expected template or instance state
+
+## 429
+
+- Rate limit or burst traffic
+- Missing backoff and retry handling
+- Excessive search or listing requests without pagination controls
+- Bulk operations (batch moves, folder creation, metadata writes) sending requests too quickly — read the `Retry-After` header and wait that many seconds before retrying
+- Parallel Box CLI invocations — the CLI must run serially; concurrent calls cause auth conflicts and can trigger rate limits faster than expected
+- For bulk workflows, add a 200–500ms pause between serial operations and implement proper `Retry-After` backoff; see `references/bulk-operations.md`
+
+## Webhook verification failures
+
+- Wrong signing secret
+- Request body mutated before signature verification
+- Timestamp tolerance or replay checks missing
+- The code logs the body before verification and accidentally changes normalization
+
+## Search quality problems
+
+- Missing ancestor-folder, type, owner, or metadata filters
+- Querying as the wrong actor
+- Expecting search to return content the current identity cannot see
+- Downloading too early instead of returning IDs and metadata first
+
+## CLI auth problems
+
+- `box` is installed but the current environment is not authorized
+- The command is running as the wrong CLI actor because `--as-user` was omitted or mis-set
+- A direct token passed with `-t` overrides the expected CLI environment
+- Someone used environment-inspection commands that print sensitive values instead of safe auth checks like `box users:get me --json`
+
+## Codex sandbox network access
+
+Box CLI commands that worked in a regular terminal fail inside Codex with `getaddrinfo ENOTFOUND api.box.com` or a generic "Unexpected Error" with no HTTP body. Auth checks like `box users:get me --json` may still pass because they use cached local credentials, making it look like auth works but API calls do not.
+
+**Cause:** Codex sandboxes block outbound network access by default. The CLI cannot reach `api.box.com`, `upload.box.com`, or any other Box endpoint.
+
+**Fix for Codex CLI:** Add to `~/.codex/config.toml`:
+
+```toml
+[sandbox_workspace_write]
+network_access = true
+```
+
+Then restart the Codex CLI session.
+
+**Fix for Codex web (cloud):** In the environment settings, turn agent internet access **On** and add `box.com` and `boxcloud.com` to the domain allowlist.
+
+**How to tell this is the problem:** If `box users:get me --json` succeeds but `box files:get --json` fails with a DNS or connection error, the sandbox is blocking outbound network access. The same commands will work in a regular terminal outside of Codex.
diff --git a/plugins/box/skills/box/references/webhooks-and-events.md b/plugins/box/skills/box/references/webhooks-and-events.md
new file mode 100644
index 00000000..f31d7722
--- /dev/null
+++ b/plugins/box/skills/box/references/webhooks-and-events.md
@@ -0,0 +1,39 @@
+# Webhooks and Events
+
+## Table of Contents
+
+- Choose webhooks vs events
+- Minimal implementation path
+- Verification checklist
+- Primary docs
+
+## Choose webhooks vs events
+
+- Use Box webhooks when the app needs push-based notifications for new or changed content.
+- Use the events APIs for catch-up syncs, polling-based integrations, or backfills after downtime.
+- Start with the smallest event consumer that can receive the signal, fetch the affected object metadata, and log or enqueue work.
+
+## Minimal implementation path
+
+1. Confirm which Box actor owns the webhook or event subscription.
+2. Store webhook signing secrets outside the codebase.
+3. Verify signatures before mutating request bodies.
+4. Persist enough event data to deduplicate duplicate deliveries and retries.
+5. Fetch the file or folder metadata after receiving the event rather than trusting the event payload alone.
+6. Hand off to downstream processing only after the idempotency key is recorded.
+
+## Verification checklist
+
+- Happy path: receive the event, verify the signature, fetch the file or folder metadata, and log the Box ID.
+- Duplicate delivery: send the same payload twice and confirm only one downstream action happens.
+- Signature failure: reject a payload with a bad signature and confirm no side effects occur.
+- Catch-up behavior: if the workflow also uses the events APIs, confirm the checkpoint or cursor is persisted.
+
+## Primary docs
+
+- Webhook guides:
+ - https://developer.box.com/guides/webhooks/
+- Webhook use cases:
+ - https://developer.box.com/guides/webhooks/use-cases/
+- Events API reference:
+ - https://developer.box.com/reference/resources/event/
diff --git a/plugins/box/skills/box/references/workflows.md b/plugins/box/skills/box/references/workflows.md
new file mode 100644
index 00000000..6dcb849e
--- /dev/null
+++ b/plugins/box/skills/box/references/workflows.md
@@ -0,0 +1,69 @@
+# Workflow Router
+
+## Table of Contents
+
+- Box CLI local verification
+- Content workflows
+- Webhooks and events
+- AI and retrieval
+- Troubleshooting
+
+Use this file when the task is ambiguous and you need to decide which targeted reference to open next.
+
+## Box CLI local verification
+
+Open `references/box-cli.md` for:
+
+- CLI-first smoke tests
+- Safe CLI auth checks
+- `--as-user` verification
+- Quick local reads and writes without changing app code
+
+## Content workflows
+
+Open `references/content-workflows.md` for:
+
+- Uploading files
+- Creating folders
+- Listing folder items
+- Downloading or previewing files
+- Creating shared links
+- Inviting collaborators
+- Reading or writing metadata
+
+## Bulk operations
+
+Open `references/bulk-operations.md` for:
+
+- Organizing or reorganizing files across folders
+- Batch-moving files into a structured hierarchy
+- Creating folder trees for classification schemes
+- Bulk metadata tagging
+- Serial execution constraints and rate-limit handling
+
+## Webhooks and events
+
+Open `references/webhooks-and-events.md` for:
+
+- Push-based notifications
+- Catch-up syncs with the events APIs
+- Signature verification
+- Idempotent event consumers
+
+## AI and retrieval
+
+Open `references/ai-and-retrieval.md` for:
+
+- Search-first retrieval
+- Box AI questions and summaries
+- External AI pipelines over Box content
+- Traceability and citation requirements
+
+## Troubleshooting
+
+Open `references/troubleshooting.md` for:
+
+- 401, 403, 404, 409, and 429 failures
+- Wrong-actor bugs
+- Search result mismatches
+- Webhook verification failures
diff --git a/plugins/box/skills/box/scripts/box_cli_smoke.py b/plugins/box/skills/box/scripts/box_cli_smoke.py
new file mode 100755
index 00000000..508835f6
--- /dev/null
+++ b/plugins/box/skills/box/scripts/box_cli_smoke.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+"""Minimal Box CLI smoke-test helper."""
+
+from __future__ import annotations
+
+import argparse
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+
+def ensure_box_cli() -> str:
+ box = shutil.which("box")
+ if not box:
+ raise SystemExit(
+ "Box CLI is not installed. Install it or fall back to scripts/box_rest.py."
+ )
+ return box
+
+
+def common_box_args(args: argparse.Namespace) -> list[str]:
+ command = ["--json", "--no-color"]
+ if args.token:
+ command.extend(["-t", args.token])
+ if args.as_user:
+ command.extend(["--as-user", args.as_user])
+ return command
+
+
+def run_box(subcommand: list[str]) -> int:
+ box = ensure_box_cli()
+ process = subprocess.run([box, *subcommand], text=True)
+ return process.returncode
+
+
+def handle_check_auth(args: argparse.Namespace) -> int:
+ return run_box(["users:get", "me", *common_box_args(args)])
+
+
+def handle_get_folder(args: argparse.Namespace) -> int:
+ command = ["folders:get", args.folder_id, *common_box_args(args)]
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def handle_list_folder_items(args: argparse.Namespace) -> int:
+ command = [
+ "folders:items",
+ args.folder_id,
+ *common_box_args(args),
+ "--max-items",
+ str(args.max_items),
+ ]
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def handle_search(args: argparse.Namespace) -> int:
+ command = ["search", args.query, *common_box_args(args), "--limit", str(args.limit)]
+ if args.item_type:
+ command.extend(["--type", args.item_type])
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ if args.ancestor_folder_ids:
+ command.extend(["--ancestor-folder-ids", ",".join(args.ancestor_folder_ids)])
+ if args.content_types:
+ command.extend(["--content-types", ",".join(args.content_types)])
+ return run_box(command)
+
+
+def handle_create_folder(args: argparse.Namespace) -> int:
+ command = ["folders:create", args.parent_id, args.name, *common_box_args(args)]
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def handle_upload_file(args: argparse.Namespace) -> int:
+ file_path = Path(args.path).expanduser().resolve()
+ if not file_path.exists():
+ raise SystemExit(f"File not found: {file_path}")
+ command = [
+ "files:upload",
+ str(file_path),
+ *common_box_args(args),
+ "--parent-id",
+ args.parent_id,
+ ]
+ if args.name:
+ command.extend(["--name", args.name])
+ if args.overwrite:
+ command.append("--overwrite")
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def handle_move_item(args: argparse.Namespace) -> int:
+ command = [
+ f"{args.item_type}s:move",
+ args.item_id,
+ args.parent_id,
+ *common_box_args(args),
+ ]
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def handle_create_shared_link(args: argparse.Namespace) -> int:
+ command = [
+ "shared-links:create",
+ args.item_id,
+ args.item_type,
+ *common_box_args(args),
+ ]
+ if args.access:
+ command.extend(["--access", args.access])
+ if args.can_download is not None:
+ command.append("--can-download" if args.can_download else "--no-can-download")
+ if args.unshared_at:
+ command.extend(["--unshared-at", args.unshared_at])
+ if args.fields:
+ command.extend(["--fields", ",".join(args.fields)])
+ return run_box(command)
+
+
+def parse_bool(value: str) -> bool:
+ lowered = value.lower()
+ if lowered == "true":
+ return True
+ if lowered == "false":
+ return False
+ raise argparse.ArgumentTypeError("Expected true or false.")
+
+
+def add_common_args(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument(
+ "--token",
+ help="Optional Box token to pass directly to the CLI.",
+ )
+ parser.add_argument(
+ "--as-user",
+ help="Optional user ID for Box CLI --as-user impersonation.",
+ )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Minimal Box CLI smoke-test helper.")
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ check_auth = subparsers.add_parser(
+ "check-auth",
+ help="Verify that Box CLI is installed and can access the current actor.",
+ )
+ add_common_args(check_auth)
+ check_auth.set_defaults(handler=handle_check_auth)
+
+ get_folder = subparsers.add_parser("get-folder", help="Fetch a Box folder.")
+ add_common_args(get_folder)
+ get_folder.add_argument("folder_id")
+ get_folder.add_argument("--fields", nargs="*")
+ get_folder.set_defaults(handler=handle_get_folder)
+
+ list_folder_items = subparsers.add_parser(
+ "list-folder-items", help="List items in a Box folder."
+ )
+ add_common_args(list_folder_items)
+ list_folder_items.add_argument("folder_id")
+ list_folder_items.add_argument("--max-items", type=int, default=20)
+ list_folder_items.add_argument("--fields", nargs="*")
+ list_folder_items.set_defaults(handler=handle_list_folder_items)
+
+ search = subparsers.add_parser("search", help="Search Box content.")
+ add_common_args(search)
+ search.add_argument("query")
+ search.add_argument("--limit", type=int, default=10)
+ search.add_argument("--type", dest="item_type", choices=["file", "folder", "web_link"])
+ search.add_argument("--ancestor-folder-ids", nargs="*")
+ search.add_argument("--content-types", nargs="*")
+ search.add_argument("--fields", nargs="*")
+ search.set_defaults(handler=handle_search)
+
+ create_folder = subparsers.add_parser("create-folder", help="Create a Box folder.")
+ add_common_args(create_folder)
+ create_folder.add_argument("parent_id")
+ create_folder.add_argument("name")
+ create_folder.add_argument("--fields", nargs="*")
+ create_folder.set_defaults(handler=handle_create_folder)
+
+ upload_file = subparsers.add_parser("upload-file", help="Upload a file to Box.")
+ add_common_args(upload_file)
+ upload_file.add_argument("path")
+ upload_file.add_argument("--parent-id", default="0")
+ upload_file.add_argument("--name")
+ upload_file.add_argument("--overwrite", action="store_true")
+ upload_file.add_argument("--fields", nargs="*")
+ upload_file.set_defaults(handler=handle_upload_file)
+
+ move_item = subparsers.add_parser(
+ "move-item", help="Move a file or folder to a different parent folder."
+ )
+ add_common_args(move_item)
+ move_item.add_argument("item_id")
+ move_item.add_argument("item_type", choices=["file", "folder"])
+ move_item.add_argument("--parent-id", required=True)
+ move_item.add_argument("--fields", nargs="*")
+ move_item.set_defaults(handler=handle_move_item)
+
+ create_shared_link = subparsers.add_parser(
+ "create-shared-link", help="Create or update a shared link with Box CLI."
+ )
+ add_common_args(create_shared_link)
+ create_shared_link.add_argument("item_id")
+ create_shared_link.add_argument("item_type", choices=["file", "folder"])
+ create_shared_link.add_argument("--access")
+ create_shared_link.add_argument("--can-download", type=parse_bool)
+ create_shared_link.add_argument("--unshared-at")
+ create_shared_link.add_argument("--fields", nargs="*")
+ create_shared_link.set_defaults(handler=handle_create_shared_link)
+
+ args = parser.parse_args()
+ return args.handler(args)
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/plugins/box/skills/box/scripts/box_rest.py b/plugins/box/skills/box/scripts/box_rest.py
new file mode 100755
index 00000000..6bb0efd9
--- /dev/null
+++ b/plugins/box/skills/box/scripts/box_rest.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+"""Minimal Box REST smoke-test helper using only the Python standard library."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import mimetypes
+import os
+import sys
+import uuid
+from pathlib import Path
+from typing import Any
+from urllib import error, parse, request
+
+
+DEFAULT_API_BASE = "https://api.box.com/2.0"
+DEFAULT_UPLOAD_BASE = "https://upload.box.com/api/2.0"
+
+
+def build_headers(token: str, extra: dict[str, str] | None = None) -> dict[str, str]:
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Accept": "application/json",
+ }
+ if extra:
+ headers.update(extra)
+ return headers
+
+
+def api_request(
+ method: str,
+ url: str,
+ token: str,
+ body: bytes | None = None,
+ headers: dict[str, str] | None = None,
+) -> Any:
+ req = request.Request(
+ url=url,
+ method=method,
+ data=body,
+ headers=build_headers(token, headers),
+ )
+ try:
+ with request.urlopen(req) as resp:
+ raw = resp.read()
+ content_type = resp.headers.get("Content-Type", "")
+ if "application/json" in content_type:
+ return json.loads(raw.decode("utf-8"))
+ return {"status": resp.status, "body": raw.decode("utf-8")}
+ except error.HTTPError as exc:
+ raw = exc.read().decode("utf-8", errors="replace")
+ try:
+ payload = json.loads(raw)
+ except json.JSONDecodeError:
+ payload = {"message": raw}
+ payload["_http_status"] = exc.code
+ raise SystemExit(
+ f"Box API error {exc.code}:\n{json.dumps(payload, indent=2, sort_keys=True)}"
+ )
+
+
+def dump_json(payload: Any) -> None:
+ json.dump(payload, sys.stdout, indent=2, sort_keys=True)
+ sys.stdout.write("\n")
+
+
+def encode_query(params: dict[str, Any]) -> str:
+ filtered = {}
+ for key, value in params.items():
+ if value is None:
+ continue
+ if isinstance(value, list):
+ filtered[key] = ",".join(str(item) for item in value)
+ else:
+ filtered[key] = value
+ return parse.urlencode(filtered)
+
+
+def get_token(cli_token: str | None) -> str:
+ token = cli_token or os.environ.get("BOX_ACCESS_TOKEN")
+ if not token:
+ raise SystemExit(
+ "Missing Box token. Set BOX_ACCESS_TOKEN or pass --token."
+ )
+ return token
+
+
+def parse_bool(value: str) -> bool:
+ lowered = value.lower()
+ if lowered == "true":
+ return True
+ if lowered == "false":
+ return False
+ raise argparse.ArgumentTypeError("Expected true or false.")
+
+
+def handle_get_item(args: argparse.Namespace) -> None:
+ query = encode_query({"fields": args.fields})
+ url = f"{args.base_url}/{args.item_type}s/{args.item_id}"
+ if query:
+ url = f"{url}?{query}"
+ dump_json(api_request("GET", url, args.token))
+
+
+def handle_get_folder_items(args: argparse.Namespace) -> None:
+ query = encode_query(
+ {
+ "limit": args.limit,
+ "offset": args.offset,
+ "fields": args.fields,
+ }
+ )
+ url = f"{args.base_url}/folders/{args.folder_id}/items"
+ if query:
+ url = f"{url}?{query}"
+ dump_json(api_request("GET", url, args.token))
+
+
+def handle_search(args: argparse.Namespace) -> None:
+ query = encode_query(
+ {
+ "query": args.query,
+ "limit": args.limit,
+ "offset": args.offset,
+ "type": args.type,
+ "fields": args.fields,
+ "ancestor_folder_ids": args.ancestor_folder_ids,
+ "content_types": args.content_types,
+ }
+ )
+ url = f"{args.base_url}/search?{query}"
+ dump_json(api_request("GET", url, args.token))
+
+
+def json_body(payload: dict[str, Any]) -> bytes:
+ return json.dumps(payload).encode("utf-8")
+
+
+def handle_create_folder(args: argparse.Namespace) -> None:
+ payload = {
+ "name": args.name,
+ "parent": {"id": args.parent_folder_id},
+ }
+ query = encode_query({"fields": args.fields})
+ url = f"{args.base_url}/folders"
+ if query:
+ url = f"{url}?{query}"
+ dump_json(
+ api_request(
+ "POST",
+ url,
+ args.token,
+ body=json_body(payload),
+ headers={"Content-Type": "application/json"},
+ )
+ )
+
+
+def _sanitize_filename(name: str) -> str:
+ """Escape characters that would break a Content-Disposition header value."""
+ return name.replace("\\", "\\\\").replace('"', '\\"').replace("\r", "").replace("\n", "")
+
+
+def multipart_upload(file_path: Path, attributes: dict[str, Any]) -> tuple[bytes, str]:
+ boundary = f"codex-box-{uuid.uuid4().hex}"
+ mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
+ safe_name = _sanitize_filename(file_path.name)
+ metadata_part = json.dumps(attributes).encode("utf-8")
+ file_bytes = file_path.read_bytes()
+ chunks = [
+ f"--{boundary}\r\n".encode("utf-8"),
+ b'Content-Disposition: form-data; name="attributes"\r\n',
+ b"Content-Type: application/json\r\n\r\n",
+ metadata_part,
+ b"\r\n",
+ f"--{boundary}\r\n".encode("utf-8"),
+ f'Content-Disposition: form-data; name="file"; filename="{safe_name}"\r\n'.encode(
+ "utf-8"
+ ),
+ f"Content-Type: {mime_type}\r\n\r\n".encode("utf-8"),
+ file_bytes,
+ b"\r\n",
+ f"--{boundary}--\r\n".encode("utf-8"),
+ ]
+ return b"".join(chunks), boundary
+
+
+def handle_upload_file(args: argparse.Namespace) -> None:
+ file_path = Path(args.file).expanduser().resolve()
+ if not file_path.exists():
+ raise SystemExit(f"File not found: {file_path}")
+ attributes = {
+ "name": args.name or file_path.name,
+ "parent": {"id": args.folder_id},
+ }
+ body, boundary = multipart_upload(file_path, attributes)
+ query = encode_query({"fields": args.fields})
+ url = f"{args.upload_base_url}/files/content"
+ if query:
+ url = f"{url}?{query}"
+ dump_json(
+ api_request(
+ "POST",
+ url,
+ args.token,
+ body=body,
+ headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
+ )
+ )
+
+
+def handle_move_item(args: argparse.Namespace) -> None:
+ payload = {"parent": {"id": args.parent_folder_id}}
+ query = encode_query({"fields": args.fields})
+ url = f"{args.base_url}/{args.item_type}s/{args.item_id}"
+ if query:
+ url = f"{url}?{query}"
+ dump_json(
+ api_request(
+ "PUT",
+ url,
+ args.token,
+ body=json_body(payload),
+ headers={"Content-Type": "application/json"},
+ )
+ )
+
+
+def handle_create_shared_link(args: argparse.Namespace) -> None:
+ shared_link: dict[str, Any] = {}
+ if args.access:
+ shared_link["access"] = args.access
+ if args.allow_download is not None:
+ shared_link["permissions"] = {"can_download": args.allow_download}
+ if args.unshared_at:
+ shared_link["unshared_at"] = args.unshared_at
+ payload = {"shared_link": shared_link}
+ dump_json(
+ api_request(
+ "PUT",
+ f"{args.base_url}/{args.item_type}s/{args.item_id}",
+ args.token,
+ body=json_body(payload),
+ headers={"Content-Type": "application/json"},
+ )
+ )
+
+
+def add_common_auth_args(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument(
+ "--token",
+ help="Box access token. Defaults to BOX_ACCESS_TOKEN.",
+ )
+ parser.add_argument(
+ "--base-url",
+ default=os.environ.get("BOX_API_BASE_URL", DEFAULT_API_BASE),
+ help=f"Box API base URL. Defaults to {DEFAULT_API_BASE}.",
+ )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Minimal Box REST smoke-test helper."
+ )
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ get_item = subparsers.add_parser(
+ "get-item", help="Fetch a Box file or folder."
+ )
+ add_common_auth_args(get_item)
+ get_item.add_argument("--item-type", required=True, choices=["file", "folder"])
+ get_item.add_argument("--item-id", required=True)
+ get_item.add_argument(
+ "--fields",
+ nargs="*",
+ help="Optional list of Box fields to request.",
+ )
+ get_item.set_defaults(handler=handle_get_item)
+
+ get_folder_items = subparsers.add_parser(
+ "get-folder-items", help="List items in a Box folder."
+ )
+ add_common_auth_args(get_folder_items)
+ get_folder_items.add_argument("--folder-id", required=True)
+ get_folder_items.add_argument("--limit", type=int, default=20)
+ get_folder_items.add_argument("--offset", type=int, default=0)
+ get_folder_items.add_argument(
+ "--fields",
+ nargs="*",
+ help="Optional list of Box fields to request.",
+ )
+ get_folder_items.set_defaults(handler=handle_get_folder_items)
+
+ search = subparsers.add_parser("search", help="Search Box content.")
+ add_common_auth_args(search)
+ search.add_argument("--query", required=True)
+ search.add_argument("--limit", type=int, default=10)
+ search.add_argument("--offset", type=int, default=0)
+ search.add_argument("--type", choices=["file", "folder", "web_link"])
+ search.add_argument("--ancestor-folder-ids", nargs="*")
+ search.add_argument("--content-types", nargs="*")
+ search.add_argument("--fields", nargs="*")
+ search.set_defaults(handler=handle_search)
+
+ create_folder = subparsers.add_parser(
+ "create-folder", help="Create a Box folder."
+ )
+ add_common_auth_args(create_folder)
+ create_folder.add_argument("--parent-folder-id", required=True)
+ create_folder.add_argument("--name", required=True)
+ create_folder.add_argument("--fields", nargs="*")
+ create_folder.set_defaults(handler=handle_create_folder)
+
+ upload_file = subparsers.add_parser("upload-file", help="Upload a file to Box.")
+ add_common_auth_args(upload_file)
+ upload_file.add_argument(
+ "--upload-base-url",
+ default=os.environ.get("BOX_UPLOAD_BASE_URL", DEFAULT_UPLOAD_BASE),
+ help=f"Box upload base URL. Defaults to {DEFAULT_UPLOAD_BASE}.",
+ )
+ upload_file.add_argument("--folder-id", required=True)
+ upload_file.add_argument("--file", required=True)
+ upload_file.add_argument("--name")
+ upload_file.add_argument("--fields", nargs="*")
+ upload_file.set_defaults(handler=handle_upload_file)
+
+ move_item = subparsers.add_parser(
+ "move-item", help="Move a file or folder to a different parent folder."
+ )
+ add_common_auth_args(move_item)
+ move_item.add_argument("--item-type", required=True, choices=["file", "folder"])
+ move_item.add_argument("--item-id", required=True)
+ move_item.add_argument("--parent-folder-id", required=True)
+ move_item.add_argument("--fields", nargs="*")
+ move_item.set_defaults(handler=handle_move_item)
+
+ create_shared_link = subparsers.add_parser(
+ "create-shared-link", help="Create or update a shared link."
+ )
+ add_common_auth_args(create_shared_link)
+ create_shared_link.add_argument(
+ "--item-type", required=True, choices=["file", "folder"]
+ )
+ create_shared_link.add_argument("--item-id", required=True)
+ create_shared_link.add_argument(
+ "--access", choices=["open", "company", "collaborators"]
+ )
+ create_shared_link.add_argument(
+ "--allow-download",
+ type=parse_bool,
+ default=None,
+ metavar="{true,false}",
+ help="Set to true or false.",
+ )
+ create_shared_link.add_argument(
+ "--unshared-at",
+ help="Optional ISO-8601 expiration timestamp.",
+ )
+ create_shared_link.set_defaults(handler=handle_create_shared_link)
+
+ args = parser.parse_args()
+ args.token = get_token(args.token)
+ args.handler(args)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())