mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
docs: publish ZPan Agent Skill (#543)
* docs: publish zpan agent skill Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * test: cover openapi auth route metadata Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * fix: align zpan skill restish commands Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * docs: clarify zpan skill release boundaries * docs: align zpan upload profile examples Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 * fix: select upload plugin profile via environment * fix: return zpan upload plugin help Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133 --------- Co-authored-by: Noah Reed <noah-reed@mails.agent-kanban.dev> Co-authored-by: saltbo <saltbo@foxmail.com>
This commit is contained in:
committed by
GitHub
parent
360237d069
commit
e50c19051a
@@ -164,6 +164,7 @@ After startup:
|
||||
## Documentation
|
||||
|
||||
- [v2 Launch Offers](docs/v2-launch-offers.md) — earn ZPan Pro for free
|
||||
- [ZPan Agent Skill](docs/agent-skill.md) — agent workflows for Restish setup, least-privilege profiles, uploads, CI, and MCP
|
||||
- [Restish ZPan upload plugin](docs/restish-zpan.md) — install `restish-zpan` and upload local files through Restish profiles
|
||||
- [Roadmap](V2_ROADMAP.md)
|
||||
- [Contributing](CONTRIBUTING.md)
|
||||
|
||||
@@ -40,6 +40,17 @@ type fileIdentity struct {
|
||||
}
|
||||
|
||||
func Run(startupArgs, args []string, h host) error {
|
||||
if wantsHelp(args) {
|
||||
return h.Response(200, nil, map[string]any{
|
||||
"usage": "restish zpan-upload [flags] SOURCE [DESTINATION]",
|
||||
"examples": []string{
|
||||
"RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager ./photo.jpg",
|
||||
"RSH_PROFILE=ci restish zpan-upload --api zpan --profile ci --parent folder-id ./photo.jpg report.jpg",
|
||||
"RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --resume ./large.bin",
|
||||
"RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --abort ./large.bin",
|
||||
},
|
||||
})
|
||||
}
|
||||
opts, err := parseOptions(args)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -50,6 +61,15 @@ func Run(startupArgs, args []string, h host) error {
|
||||
return runWithStorage(ctx, opts, h, newHTTPStorageClient())
|
||||
}
|
||||
|
||||
func wantsHelp(args []string) bool {
|
||||
for _, arg := range args {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseOptions(args []string) (uploadOptions, error) {
|
||||
opts := uploadOptions{API: "zpan", Conflict: "fail", Concurrency: 4}
|
||||
fs := flag.NewFlagSet("zpan-upload", flag.ContinueOnError)
|
||||
|
||||
@@ -1036,6 +1036,24 @@ func TestRunRejectsInvalidArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunReturnsHelp(t *testing.T) {
|
||||
host := &fakeHost{}
|
||||
if err := Run(nil, []string{"--help"}, host); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, ok := host.body.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected help body: %#v", host.body)
|
||||
}
|
||||
examples := strings.Join(body["examples"].([]string), "\n")
|
||||
if !strings.Contains(examples, "RSH_PROFILE=file-manager") || !strings.Contains(examples, "--profile file-manager") {
|
||||
t.Fatalf("help examples do not select profiles: %q", examples)
|
||||
}
|
||||
if len(host.requests) != 0 {
|
||||
t.Fatalf("help should not make delegated requests: %#v", host.requests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithStorageRejectsMissingSource(t *testing.T) {
|
||||
missing := filepath.Join(t.TempDir(), "missing.bin")
|
||||
err := runWithStorage(context.Background(), uploadOptions{API: "zpan", Source: missing, Name: "missing.bin", Conflict: "fail", Concurrency: 1}, &fakeHost{}, &fakeStorage{})
|
||||
|
||||
@@ -32,10 +32,10 @@ func commands() []plugin.CommandDecl {
|
||||
Short: "Upload a local file to ZPan",
|
||||
Long: "Upload a local file to ZPan using Restish-managed API auth and direct presigned storage PUTs.\n\n" +
|
||||
"Examples:\n" +
|
||||
" restish zpan-upload ./photo.jpg\n" +
|
||||
" restish --rsh-profile ci zpan-upload --parent folder-id ./photo.jpg report.jpg\n" +
|
||||
" restish zpan-upload --resume ./large.bin\n" +
|
||||
" restish zpan-upload --abort ./large.bin",
|
||||
" RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager ./photo.jpg\n" +
|
||||
" RSH_PROFILE=ci restish zpan-upload --api zpan --profile ci --parent folder-id ./photo.jpg report.jpg\n" +
|
||||
" RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --resume ./large.bin\n" +
|
||||
" RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --abort ./large.bin",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rest-sh/restish/v2/plugin"
|
||||
@@ -36,6 +37,10 @@ func TestCommandDiscoveryContract(t *testing.T) {
|
||||
if len(discovery.Commands) != 1 || discovery.Commands[0].Name != "zpan-upload" {
|
||||
t.Fatalf("unexpected commands: %#v", discovery.Commands)
|
||||
}
|
||||
help := discovery.Commands[0].Long
|
||||
if !strings.Contains(help, "RSH_PROFILE=file-manager") || strings.Contains(help, "--rsh-profile") {
|
||||
t.Fatalf("upload help must use the delegated HTTP profile environment: %q", help)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandRejectsUnknownCommand(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# ZPan Agent Skill
|
||||
|
||||
ZPan v2.9 publishes a versioned Agent Skill in [skills/zpan](../skills/zpan).
|
||||
The Skill teaches coding agents to use ZPan through Restish and the
|
||||
`restish-zpan` upload plugin.
|
||||
|
||||
## Install and Connect
|
||||
|
||||
Install Restish v2.3 or later, confirm the ZPan origin, then connect the single
|
||||
unified OpenAPI document:
|
||||
|
||||
```sh
|
||||
restish api connect zpan https://files.example.com/api/openapi.json --replace --yes
|
||||
restish api sync zpan
|
||||
```
|
||||
|
||||
Interactive agents use browser OAuth authorization code + PKCE through Restish.
|
||||
CI and unattended jobs use the `ci` profile with `ZPAN_AGENT_API_KEY` from the
|
||||
environment.
|
||||
|
||||
## Upload Plugin
|
||||
|
||||
Before installing the plugin, tell the user that Restish plugins are trusted
|
||||
local executable code and ask them to approve the source:
|
||||
|
||||
```sh
|
||||
restish plugin install saltbo/zpan zpan
|
||||
```
|
||||
|
||||
Every local upload goes through:
|
||||
|
||||
```sh
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --parent root ./file.bin
|
||||
```
|
||||
|
||||
The Skill does not implement upload transport logic. The plugin owns local file
|
||||
streaming, storage response capture, retry, resume, abort, and checkpoint
|
||||
cleanup.
|
||||
|
||||
## Profiles
|
||||
|
||||
- `reader`: read objects, shares, quota, and storage usage.
|
||||
- `file-manager`: reader plus create, upload, move, copy, rename, and soft
|
||||
delete objects.
|
||||
- `publisher`: reader plus public share creation and revocation.
|
||||
- `ci`: environment-backed Agent API key for unattended file-management jobs.
|
||||
|
||||
The profile names are shortcuts for explicit scopes. They are not server-side
|
||||
roles and routes do not authorize by preset name.
|
||||
|
||||
Interactive Restish login uses browser OAuth authorization code + PKCE.
|
||||
@@ -103,6 +103,11 @@ routes must authorize scopes rather than require a concrete `kind`.
|
||||
Credential-specific fields remain available for diagnostics and revocation;
|
||||
they do not select business behavior.
|
||||
|
||||
The versioned ZPan Agent Skill is published under `skills/zpan` and summarized
|
||||
in [ZPan Agent Skill](../agent-skill.md). It consumes this authorization model
|
||||
through Restish profiles instead of adding a second credential or upload
|
||||
protocol.
|
||||
|
||||
This boundary deliberately avoids two migration traps:
|
||||
|
||||
- File routes must not treat an OAuth bearer as an unrestricted browser user.
|
||||
@@ -381,7 +386,9 @@ restish plugin install saltbo/zpan zpan
|
||||
|
||||
The plugin uses Restish delegated HTTP for ZPan draft, part re-sign, complete,
|
||||
and abort operations, preserving the selected profile, OAuth/API-key
|
||||
authentication, TLS, and normalized output. It streams local file sections
|
||||
authentication, TLS, and normalized output. With Restish v2.3 command plugins,
|
||||
the host profile is selected through `RSH_PROFILE` while the plugin's matching
|
||||
`--profile` selects spec validation and checkpoint identity. It streams local file sections
|
||||
directly to presigned S3 URLs with bounded concurrency, retry, ETag capture,
|
||||
resume checkpoints, and idempotent completion.
|
||||
|
||||
|
||||
+16
-7
@@ -4,8 +4,16 @@
|
||||
profiles for ZPan API calls and streams file bytes directly from disk to
|
||||
presigned storage URLs.
|
||||
|
||||
The companion [ZPan Agent Skill](agent-skill.md) selects when to use generated
|
||||
Restish commands and when to invoke this plugin. The Skill does not implement
|
||||
multipart upload behavior itself.
|
||||
|
||||
## Install
|
||||
|
||||
Restish plugins are trusted local executable code. Agents must explain that
|
||||
trust boundary and get explicit user approval for the `saltbo/zpan` source
|
||||
before installing:
|
||||
|
||||
```bash
|
||||
restish plugin install saltbo/zpan zpan
|
||||
```
|
||||
@@ -13,10 +21,10 @@ restish plugin install saltbo/zpan zpan
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
restish zpan-upload ./photo.jpg
|
||||
restish --rsh-profile file-manager zpan-upload --api zpan --parent albums ./photo.jpg cover.jpg
|
||||
restish zpan-upload --resume ./large.bin
|
||||
restish zpan-upload --abort ./large.bin
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager ./photo.jpg
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --parent albums ./photo.jpg cover.jpg
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --resume ./large.bin
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --abort ./large.bin
|
||||
```
|
||||
|
||||
The plugin validates the connected ZPan OpenAPI operations before uploading:
|
||||
@@ -33,6 +41,7 @@ They contain API/profile identity, source file identity, destination identity,
|
||||
the ZPan object/session IDs, part size/count, and completed part ETags. They do
|
||||
not contain credentials, cookies, presigned URLs, or file bytes.
|
||||
|
||||
Restish v2.3 delegated HTTP uses the host's active profile. Select profiles with
|
||||
Restish's global profile flag or `RSH_PROFILE`; the plugin's `--profile` value is
|
||||
used for spec validation and checkpoint identity.
|
||||
Restish v2.3 command plugins receive the delegated HTTP profile through
|
||||
`RSH_PROFILE`; use it for the host credential selection. The plugin's
|
||||
`--profile` value is separately used for spec validation and checkpoint
|
||||
identity.
|
||||
|
||||
+16
-4
@@ -26,6 +26,8 @@ redesigning the OpenAPI operations, Skill, upload plugin, or file use cases.
|
||||
|
||||
The detailed model is in
|
||||
[Agent Authentication and Authorization](../design/agent-authentication.md).
|
||||
The published workflow package is [ZPan Agent Skill](../agent-skill.md), with
|
||||
the installable Skill source under `skills/zpan`.
|
||||
|
||||
## Product Boundary
|
||||
|
||||
@@ -156,6 +158,10 @@ workflows refer to a selected ZPan Restish profile rather than assuming a
|
||||
specific environment variable or credential type, so a future profile may use
|
||||
an external Agent Auth signer without changing the file-operation instructions.
|
||||
|
||||
The repository ships the Skill as `skills/zpan` with routed references for
|
||||
setup, file operations, uploads, CI, MCP, and acceptance evidence. The
|
||||
user-facing setup guide is [docs/agent-skill.md](../agent-skill.md).
|
||||
|
||||
### Restish Upload Plugin
|
||||
|
||||
Ship `restish-zpan` from this repository and install it with:
|
||||
@@ -166,7 +172,10 @@ restish plugin install saltbo/zpan zpan
|
||||
|
||||
It contributes `restish zpan-upload`. The plugin uses Restish delegated HTTP
|
||||
for ZPan draft, re-sign, complete, and abort operations, preserving the selected
|
||||
profile, OAuth/API-key authentication, TLS, and output behavior. It streams
|
||||
profile, OAuth/API-key authentication, TLS, and output behavior. With Restish
|
||||
v2.3 command plugins, the host profile is selected through `RSH_PROFILE` while
|
||||
the plugin's matching `--profile` selects spec validation and checkpoint
|
||||
identity. It streams
|
||||
local file parts directly to presigned S3 URLs with bounded concurrency, retry,
|
||||
ETag capture, resume checkpoints, and idempotent completion. File bytes and
|
||||
presigned URLs never pass through the Agent context or Restish's plugin CBOR
|
||||
@@ -179,14 +188,17 @@ the plugin; it does not implement the upload state machine itself.
|
||||
|
||||
The official `restish-mcp` plugin is an optional transport for ordinary API
|
||||
operations, not the upload implementation. Its default read-only mode is useful
|
||||
for browsing. Enable write tools only with an explicit operation allowlist:
|
||||
for browsing. Keep the default recipe read-only:
|
||||
|
||||
```sh
|
||||
restish plugin install rest-sh/restish mcp
|
||||
restish mcp serve zpan --allow-write-tools \
|
||||
--operations listObjects,getObject,createObject,completeObjectUpload
|
||||
restish mcp serve zpan --operations listObjects,getObject,listShares,getUserQuota,getStorageUsage
|
||||
```
|
||||
|
||||
Enable write tools only with an explicit reviewed operation allowlist. Do not
|
||||
allow upload draft, part signing, completion, or abort operation IDs through
|
||||
MCP.
|
||||
|
||||
Keep MCP results bounded; the plugin's default result limit is 16 KiB. Object
|
||||
contents continue to move through presigned URLs, never through an MCP result.
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"lint:arch": "depcruise server/ shared/ --config .dependency-cruiser.cjs",
|
||||
"lint:http": "tsx scripts/lint-http-boundary.ts",
|
||||
"lint:spec": "node scripts/lint-spec.mjs",
|
||||
"lint:zpan-skill": "node scripts/lint-zpan-skill.mjs",
|
||||
"prepare": "husky",
|
||||
"format": "biome format --write .",
|
||||
"e2e": "playwright test",
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
|
||||
const ROOT = process.cwd()
|
||||
const SKILL_DIR = join(ROOT, 'skills', 'zpan')
|
||||
|
||||
function walkMarkdown(dir) {
|
||||
const files = []
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name)
|
||||
const stat = statSync(full)
|
||||
if (stat.isDirectory()) files.push(...walkMarkdown(full))
|
||||
else if (name.endsWith('.md')) files.push(full)
|
||||
}
|
||||
return files.sort()
|
||||
}
|
||||
|
||||
const files = walkMarkdown(SKILL_DIR)
|
||||
const documents = files.map((file) => ({
|
||||
file,
|
||||
rel: relative(ROOT, file),
|
||||
text: readFileSync(file, 'utf8'),
|
||||
}))
|
||||
const corpus = documents.map((doc) => doc.text).join('\n\n')
|
||||
const normalizedCorpus = corpus.toLowerCase()
|
||||
const failures = []
|
||||
|
||||
function requireMatch(label, pattern) {
|
||||
if (!pattern.test(corpus)) failures.push(`missing required contract: ${label}`)
|
||||
}
|
||||
|
||||
function requireText(label, text) {
|
||||
if (!normalizedCorpus.includes(text.toLowerCase())) {
|
||||
failures.push(`missing required contract: ${label}`)
|
||||
}
|
||||
}
|
||||
|
||||
function forbidMatch(label, pattern) {
|
||||
for (const doc of documents) {
|
||||
for (const match of doc.text.matchAll(pattern)) {
|
||||
const line = doc.text.slice(0, match.index).split('\n').length
|
||||
failures.push(`forbidden contract text: ${label} (${doc.rel}:${line})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forbidUnsafeLine(label, pattern) {
|
||||
const safePrefix = /\b(do not|don't|never|must not|not|no)\b/i
|
||||
for (const doc of documents) {
|
||||
const lines = doc.text.split('\n')
|
||||
lines.forEach((lineText, index) => {
|
||||
if (pattern.test(lineText) && !safePrefix.test(lineText)) {
|
||||
failures.push(`unsafe contract guidance: ${label} (${doc.rel}:${index + 1})`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function commandLines() {
|
||||
return corpus
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith('restish ') || /^RSH_PROFILE=\S+\s+restish\b/.test(line))
|
||||
}
|
||||
|
||||
function requireCommandLine(label, pattern) {
|
||||
if (!commandLines().some((line) => pattern.test(line))) {
|
||||
failures.push(`missing executable command example: ${label}`)
|
||||
}
|
||||
}
|
||||
|
||||
function forbidCommandLine(label, pattern) {
|
||||
for (const doc of documents) {
|
||||
const lines = doc.text.split('\n')
|
||||
lines.forEach((lineText, index) => {
|
||||
const line = lineText.trim()
|
||||
if (/^(?:RSH_PROFILE=\S+\s+)?restish /.test(line) && pattern.test(line)) {
|
||||
failures.push(`forbidden executable command: ${label} (${doc.rel}:${index + 1})`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function validateSkillFrontmatter() {
|
||||
const skill = documents.find((doc) => doc.rel === 'skills/zpan/SKILL.md')
|
||||
if (!skill) {
|
||||
failures.push('missing skills/zpan/SKILL.md')
|
||||
return
|
||||
}
|
||||
const match = skill.text.match(/^---\n([\s\S]*?)\n---\n/)
|
||||
if (!match) {
|
||||
failures.push('missing Skill YAML frontmatter')
|
||||
return
|
||||
}
|
||||
const keys = [...match[1].matchAll(/^([A-Za-z0-9_-]+):/gm)].map((entry) => entry[1])
|
||||
const extras = keys.filter((key) => key !== 'name' && key !== 'description')
|
||||
if (extras.length > 0) {
|
||||
failures.push(`unsupported Skill frontmatter key(s): ${extras.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
validateSkillFrontmatter()
|
||||
|
||||
requireMatch('Restish v2.3 or later', /Restish v2\.3(?:\+| or later)/i)
|
||||
requireText('connect exactly /api/openapi.json', '/api/openapi.json')
|
||||
requireText('plugin install command', 'restish plugin install saltbo/zpan zpan')
|
||||
requireText('upload command surface', 'restish zpan-upload')
|
||||
|
||||
for (const command of [
|
||||
'list-objects',
|
||||
'get-object',
|
||||
'create-object',
|
||||
'update-object',
|
||||
'copy-object',
|
||||
'transfer-object',
|
||||
'delete-object',
|
||||
'list-shares',
|
||||
'create-share',
|
||||
'revoke-share',
|
||||
'get-user-quota',
|
||||
'get-storage-usage',
|
||||
'list-download-tasks',
|
||||
'get-download-task',
|
||||
'list-download-task-events',
|
||||
]) {
|
||||
requireCommandLine(`restish zpan ${command}`, new RegExp(`\\brestish\\s+(?:--rsh-profile\\s+\\S+\\s+)?zpan\\s+${command}\\b`))
|
||||
}
|
||||
|
||||
for (const operationId of ['createObject', 'presignObjectUploadParts', 'completeObjectUpload', 'abortObjectUpload']) {
|
||||
requireText(`upload plugin validates ${operationId}`, operationId)
|
||||
}
|
||||
|
||||
requireCommandLine('list pagination uses --page-size', /\bzpan\s+list-objects\b.*\s--page-size\s+\d+/)
|
||||
requireCommandLine('share pagination uses --page-size', /\bzpan\s+list-shares\b.*\s--page-size\s+\d+/)
|
||||
requireCommandLine('task pagination uses --page-size', /\bzpan\s+list-download-tasks\b.*\s--page-size\s+\d+/)
|
||||
requireCommandLine('create-object uses positional body input', /\bzpan\s+create-object\s+'[^']*\bname:/)
|
||||
requireCommandLine('update-object uses positional body input', /\bzpan\s+update-object\s+\S+\s+'[^']*\bname:/)
|
||||
requireCommandLine('copy-object uses positional body input', /\bzpan\s+copy-object\s+\S+\s+'[^']*\bparent:/)
|
||||
requireCommandLine('transfer-object uses positional body input', /\bzpan\s+transfer-object\s+\S+\s+'[^']*\btargetOrgId:/)
|
||||
requireCommandLine('create-share uses positional body input', /\bzpan\s+create-share\s+'[^']*\bmatterId:/)
|
||||
requireCommandLine('revoke-share uses positional body input', /\bzpan\s+revoke-share\s+\S+\s+'[^']*\bstatus:\s*revoked/)
|
||||
requireCommandLine('upload passes Restish and plugin profiles', /\bRSH_PROFILE=(\S+)\s+restish\s+zpan-upload\b.*\s--api\s+zpan\b.*\s--profile\s+\1\b/)
|
||||
|
||||
requireText('reader profile', '`reader`')
|
||||
requireText('file-manager profile', '`file-manager`')
|
||||
requireText('publisher profile', '`publisher`')
|
||||
requireText('ci profile', '`ci`')
|
||||
requireMatch('least-privilege profile selection', /(least-privilege|narrowest) profile/i)
|
||||
requireText('objects read scope', 'objects:read')
|
||||
requireText('objects write scopes', 'objects:create')
|
||||
requireText('share publishing scopes', 'shares:create')
|
||||
requireText('environment-backed Agent API key', 'Environment-backed')
|
||||
|
||||
requireMatch('OAuth authorization code with PKCE', /OAuth authorization code \+ PKCE|authorization code\s*\+\s*PKCE/i)
|
||||
requireMatch('CI Agent API key guidance', /CI[\s\S]{0,240}Agent API key|Agent API key[\s\S]{0,240}CI/i)
|
||||
|
||||
requireMatch('confirm target workspace', /confirm[\s\S]{0,120}workspace/i)
|
||||
requireMatch('confirm conflict policy', /confirm[\s\S]{0,160}(conflict|overwrite|replace)/i)
|
||||
requireMatch('confirm destructive delete', /confirm[\s\S]{0,160}(destructive|soft delete|delet)/i)
|
||||
requireMatch('confirm permanent purge', /confirm[\s\S]{0,160}(purge|permanent)/i)
|
||||
requireMatch('confirm public sharing', /confirm[\s\S]{0,160}public share/i)
|
||||
requireMatch(
|
||||
'confirm plugin executable trust',
|
||||
/(?:confirm|ask)[\s\S]{0,200}(trusted local executable|executable Restish plugin|plugin trust)/i,
|
||||
)
|
||||
|
||||
forbidMatch('agent OpenAPI document', /\/api\/openapi\.agent\.json/gi)
|
||||
forbidMatch('standalone zpan file CLI', /standalone\s+`?zpan`?\s+file CLI/gi)
|
||||
|
||||
const openApiDocs = [...corpus.matchAll(/\/api\/openapi(?:\.[a-z0-9-]+)?\.json/gi)].map((match) => match[0])
|
||||
for (const doc of openApiDocs) {
|
||||
if (doc !== '/api/openapi.json') {
|
||||
failures.push(`OpenAPI document must be exactly /api/openapi.json, found ${doc}`)
|
||||
}
|
||||
}
|
||||
|
||||
forbidUnsafeLine('bearer-token paste flow', /\b(paste|copy\/paste|copy paste)\b.*\bbearer token\b/i)
|
||||
forbidUnsafeLine('Agent device login as v2.9 flow', /\b(device authorization|device login|device flow)\b.*\bv2\.9\b/i)
|
||||
forbidUnsafeLine('Skill-handled multipart orchestration', /\b(Skill|agent)\b.*\b(orchestrate|handle|implement)\b.*\bmultipart\b/i)
|
||||
forbidUnsafeLine('Skill-handled ETag retry loop', /\b(Skill|agent)\b.*\b(ETag|ETags)\b.*\b(retry|retries|loop|loops)\b/i)
|
||||
forbidUnsafeLine('presigned URL exposure', /\b(expose|return|print|show)\b.*\bpresigned URLs?\b/i)
|
||||
forbidMatch('silent plugin install approval', /restish\s+plugin\s+install\s+saltbo\/zpan\s+zpan[^\n]*--yes/gi)
|
||||
forbidMatch('old Restish list limit flag', /\brestish\s+(?:--rsh-profile\s+\S+\s+)?zpan\s+(?:list-objects|list-shares|list-download-tasks)\b[^\n]*\s--limit\b/gi)
|
||||
forbidMatch('camelCase Restish command example', /\brestish\s+(?:--rsh-profile\s+\S+\s+)?zpan\s+(?:listObjects|getObject|createObject|updateObject|copyObject|transferObject|deleteObject|purgeTrashObject|listShares|createShare|revokeShare|getUserQuota|getStorageUsage|listDownloadTasks|getDownloadTask|listDownloadTaskEvents)\b/gi)
|
||||
forbidMatch('profile template purge command', /\brestish\s+--rsh-profile\s+(?:reader|file-manager|publisher|ci)\s+zpan\s+purge-trash-object\b/gi)
|
||||
forbidMatch('invented operator profile', /\brestish\s+--rsh-profile\s+operator\b/gi)
|
||||
forbidCommandLine('upload without plugin profile', /\b(?:RSH_PROFILE=\S+\s+)?restish\s+(?:--rsh-profile\s+\S+\s+)?zpan-upload\b(?!.*\s--profile\s+\S+)/i)
|
||||
forbidMatch('upload with ineffective host profile flag', /\brestish\s+--rsh-profile\s+\S+\s+zpan-upload\b/gi)
|
||||
forbidCommandLine('upload without delegated profile environment', /^restish\s+zpan-upload\b/i)
|
||||
forbidMatch(
|
||||
'MCP upload control-plane allowlist',
|
||||
/restish\s+mcp\s+serve[\s\S]*?--operations[^\n]*(createObject|create-object|presignObjectUploadParts|presign-object-upload-parts|completeObjectUpload|complete-object-upload|abortObjectUpload|abort-object-upload)/gi,
|
||||
)
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`ZPan Skill static contract failed with ${failures.length} finding(s):`)
|
||||
for (const failure of failures) console.error(`- ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`ZPan Skill static contract passed (${documents.length} markdown files checked)`)
|
||||
@@ -325,6 +325,40 @@ describe('global OpenAPI document', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('emits CLI metadata for non-agent authorization policies', () => {
|
||||
const protectedWithoutScopes = authRoute(
|
||||
{ access: 'protected' },
|
||||
{
|
||||
operationId: 'genericProtectedProbe',
|
||||
method: 'get',
|
||||
path: '/probe',
|
||||
responses: { 200: { description: 'OK' } },
|
||||
},
|
||||
) as { security?: unknown; 'x-zpan-auth'?: unknown }
|
||||
const internalRoute = authRoute(
|
||||
{ access: 'internal' },
|
||||
{
|
||||
operationId: 'internalProbe',
|
||||
method: 'post',
|
||||
path: '/internal-probe',
|
||||
responses: { 204: { description: 'No Content' } },
|
||||
},
|
||||
) as { security?: unknown; 'x-zpan-auth'?: unknown; 'x-cli-ignore'?: boolean; 'x-mcp-ignore'?: boolean }
|
||||
|
||||
expect(protectedWithoutScopes.security).toEqual([{ bearerAuth: [] }, { cookieAuth: [] }])
|
||||
expect(protectedWithoutScopes['x-zpan-auth']).toEqual({
|
||||
access: 'protected',
|
||||
scopes: [],
|
||||
minTeamRole: null,
|
||||
allowDownloader: false,
|
||||
auditDenied: true,
|
||||
})
|
||||
expect(internalRoute.security).toEqual([])
|
||||
expect(internalRoute['x-zpan-auth']).toEqual({ access: 'internal' })
|
||||
expect(internalRoute['x-cli-ignore']).toBe(true)
|
||||
expect(internalRoute['x-mcp-ignore']).toBe(true)
|
||||
})
|
||||
|
||||
it('detects OpenAPI operations missing explicit authorization declarations without an allowlist', () => {
|
||||
expect(
|
||||
findOperationsMissingAuthContract({
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: zpan
|
||||
description: Manage ZPan files through Restish v2.3+ and the trusted restish-zpan upload plugin.
|
||||
---
|
||||
|
||||
# ZPan Agent Skill
|
||||
|
||||
This Skill targets the ZPan v2.9 Restish integration.
|
||||
|
||||
Use this Skill when an agent needs to browse, inspect, move, copy, delete,
|
||||
upload, download, share, revoke shares, check quota, or inspect background
|
||||
tasks on a ZPan instance.
|
||||
|
||||
## Operating Boundary
|
||||
|
||||
ZPan file management uses two surfaces:
|
||||
|
||||
- Generated Restish OpenAPI commands for ordinary API operations.
|
||||
- `restish zpan-upload` from the `restish-zpan` plugin for every local file
|
||||
upload.
|
||||
|
||||
Do not read local file bytes, orchestrate upload parts, handle storage response
|
||||
tags, loop part retries, or expose storage upload URLs. The upload plugin owns
|
||||
local file streaming, upload state, storage response capture, retry, resume,
|
||||
abort, and checkpoint cleanup.
|
||||
|
||||
## Start Here
|
||||
|
||||
1. Confirm the ZPan origin with the user before connecting or mutating data.
|
||||
2. Confirm the Restish API name. Use `zpan` unless the user already has a
|
||||
different local API name.
|
||||
3. Require Restish v2.3 or later.
|
||||
4. Connect exactly one OpenAPI document: `<origin>/api/openapi.json`.
|
||||
5. Select the least-privilege profile that fits the task:
|
||||
`reader`, `file-manager`, `publisher`, or `ci`.
|
||||
6. Sync the Restish API before use when it was connected previously.
|
||||
|
||||
Use [references/setup.md](references/setup.md) for install, connect, sync, and
|
||||
profile selection.
|
||||
|
||||
## Workflow Routing
|
||||
|
||||
- Browsing, inspecting, folders, move/copy/rename, delete, download links,
|
||||
shares, quota, and tasks: use [references/file-workflows.md](references/file-workflows.md).
|
||||
- Local uploads: use [references/uploads.md](references/uploads.md).
|
||||
- CI or unattended automation with an Agent API key:
|
||||
use [references/ci.md](references/ci.md).
|
||||
- Optional MCP transport for reviewed ordinary operations:
|
||||
use [references/mcp.md](references/mcp.md).
|
||||
- Release or preview acceptance evidence:
|
||||
use [references/acceptance.md](references/acceptance.md).
|
||||
|
||||
## Safety Rules
|
||||
|
||||
Confirm before:
|
||||
|
||||
- choosing a target workspace;
|
||||
- overwriting, replacing, or retrying conflict handling;
|
||||
- soft deleting files or folders;
|
||||
- permanently purging trash;
|
||||
- creating public shares;
|
||||
- installing executable Restish plugins.
|
||||
|
||||
Never ask the user to paste a bearer token. Interactive use goes through
|
||||
browser OAuth authorization code + PKCE. CI use relies on an environment-backed
|
||||
Agent API key profile.
|
||||
|
||||
Keep results bounded. Prefer compact object IDs, names, paths, URLs, quota
|
||||
effects, task state, and upload state over full raw responses.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Acceptance Evidence
|
||||
|
||||
For release or preview verification, record the exact origin, Restish version,
|
||||
plugin source, profile, and commands used. Do not record credentials.
|
||||
|
||||
## Fresh-Machine Interactive Flow
|
||||
|
||||
Verify:
|
||||
|
||||
1. Install Restish v2.3 or later.
|
||||
2. Confirm the ZPan origin and local API name.
|
||||
3. Connect `/api/openapi.json`.
|
||||
4. Sync the API.
|
||||
5. Approve installing `restish-zpan` from `saltbo/zpan`.
|
||||
6. Run a safe reader operation and complete browser OAuth authorization code +
|
||||
PKCE consent.
|
||||
7. List objects, upload a local file with `restish zpan-upload`, interrupt and
|
||||
resume one upload when practical, inspect the uploaded object, create a
|
||||
public share, revoke the share, and check quota.
|
||||
|
||||
For pre-release PR or preview acceptance, a v2.9 GitHub release asset does not
|
||||
exist yet. Build the already-reviewed `cmd/restish-zpan` source at the exact PR
|
||||
head, install that trusted local executable for the acceptance run, and
|
||||
separately verify that the release workflow produces the asset names expected
|
||||
by Restish. Record that this was a source-build acceptance.
|
||||
|
||||
After v2.9 is released, repeat the install step through the user-facing release
|
||||
path:
|
||||
|
||||
```sh
|
||||
restish plugin install saltbo/zpan zpan
|
||||
```
|
||||
|
||||
## CI Flow
|
||||
|
||||
Verify:
|
||||
|
||||
1. Create a workspace-scoped Agent API key in ZPan settings.
|
||||
2. Store it in `ZPAN_AGENT_API_KEY`.
|
||||
3. Use the `ci` Restish profile without token copy/paste.
|
||||
4. Run list and upload operations.
|
||||
5. Attempt an operation outside the key scope and confirm `403`.
|
||||
|
||||
## Static Contract
|
||||
|
||||
Run the repository Skill contract check before release. It verifies the required
|
||||
Restish setup, upload plugin, profile, safety, CI, and MCP guidance while
|
||||
guarding against removed or unsafe v2.9 workflows.
|
||||
@@ -0,0 +1,22 @@
|
||||
# CI and Unattended Automation
|
||||
|
||||
Use the `ci` Restish profile for unattended jobs. The profile reads the Agent
|
||||
API key from the environment and does not require token copy/paste:
|
||||
|
||||
```sh
|
||||
export ZPAN_AGENT_API_KEY="$ZPAN_AGENT_API_KEY"
|
||||
restish --rsh-profile ci zpan list-objects --parent root --page-size 50
|
||||
RSH_PROFILE=ci restish zpan-upload --api zpan --profile ci --parent releases ./dist/app.tar.gz
|
||||
```
|
||||
|
||||
The key must be created by a user in ZPan Agent Access settings, scoped to one
|
||||
workspace, named for the environment, given explicit permissions, and stored in
|
||||
the CI secret store. The plaintext key is shown once by ZPan and should never be
|
||||
posted into chat, logs, issue comments, or PR output.
|
||||
|
||||
Use separate keys for separate environments. Expired or revoked keys are
|
||||
terminal; create a new key when the job needs a different lifetime.
|
||||
|
||||
When a job receives `403`, report the missing operation and expected scope. Do
|
||||
not broaden the requested scopes automatically. The user should decide whether
|
||||
to issue a new key or approve a broader scope set.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Ordinary File Workflows
|
||||
|
||||
Use generated Restish OpenAPI commands for ordinary ZPan operations. Run
|
||||
`restish api sync zpan` before relying on operation names from an older local
|
||||
connection.
|
||||
|
||||
## List and Inspect
|
||||
|
||||
Use a reader-capable profile for browse and inspect operations:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile reader zpan list-objects --parent root --page-size 50
|
||||
restish --rsh-profile reader zpan get-object obj_123
|
||||
restish --rsh-profile reader zpan get-user-quota user_123
|
||||
restish --rsh-profile reader zpan get-storage-usage
|
||||
```
|
||||
|
||||
Keep list limits explicit and summarize IDs, names, paths, sizes, and relevant
|
||||
URLs. Do not dump unbounded trees.
|
||||
|
||||
## Create Folders, Move, Copy, and Rename
|
||||
|
||||
Use `file-manager` for object mutations:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile file-manager zpan create-object 'name: releases, parent: root, type: folder, dirtype: 1'
|
||||
restish --rsh-profile file-manager zpan update-object obj_123 'name: release.zip, onConflict: fail'
|
||||
restish --rsh-profile file-manager zpan transfer-object obj_123 'mode: move, targetOrgId: org_123, targetParent: folder_456'
|
||||
restish --rsh-profile file-manager zpan copy-object obj_123 'parent: folder_456, onConflict: fail'
|
||||
```
|
||||
|
||||
Before writes, confirm the workspace and target folder. Before overwrite or
|
||||
replace behavior, confirm the conflict policy.
|
||||
|
||||
## Delete and Purge
|
||||
|
||||
Soft delete requires `objects:delete`:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile file-manager zpan delete-object obj_123
|
||||
```
|
||||
|
||||
Confirm destructive intent before soft delete. Permanent trash purge is more
|
||||
destructive, must be confirmed separately, and is outside the v2.9 Agent
|
||||
OAuth/API-key profile templates because it requires `objects:purge` on an
|
||||
authorized human/operator surface. Do not attempt purge through this Skill or
|
||||
invent an `operator` Restish profile. Ask the user to complete permanent purge
|
||||
in an authorized operator surface instead.
|
||||
|
||||
Return soft-deleted object IDs and any quota effect reported by the API.
|
||||
|
||||
## Public Sharing
|
||||
|
||||
Use `publisher` for public shares:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile publisher zpan create-share 'matterId: obj_123, kind: landing, private: false'
|
||||
restish --rsh-profile publisher zpan list-shares --page-size 50
|
||||
restish --rsh-profile publisher zpan revoke-share share_token_123 'status: revoked'
|
||||
```
|
||||
|
||||
Confirm before creating public shares. Summaries may include share IDs, public
|
||||
URLs, expiry, and revocation state, but should not include credentials.
|
||||
|
||||
## Tasks
|
||||
|
||||
Use generated task operations for status checks:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile file-manager zpan list-download-tasks --page-size 25
|
||||
restish --rsh-profile file-manager zpan get-download-task task_123
|
||||
restish --rsh-profile file-manager zpan list-download-task-events task_123
|
||||
```
|
||||
|
||||
Summarize state, progress, and errors. Keep event output bounded.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Optional Restish MCP
|
||||
|
||||
Restish MCP is optional and only for reviewed ordinary OpenAPI operations. It is
|
||||
not the upload transport. Local file uploads still use `restish zpan-upload`.
|
||||
|
||||
Default to read-only MCP:
|
||||
|
||||
```sh
|
||||
restish plugin install rest-sh/restish mcp
|
||||
restish mcp serve zpan --operations list-objects,get-object,list-shares,get-user-quota,get-storage-usage
|
||||
```
|
||||
|
||||
Enable write tools only after reviewing the exact operation allowlist. Do not
|
||||
allow upload control-plane operation IDs through MCP.
|
||||
|
||||
Keep results bounded. Do not route file bytes, storage upload URLs, bearer
|
||||
tokens, cookies, API keys, or checkpoint contents through MCP results.
|
||||
|
||||
Do not expose authentication, administration, billing, entitlement, membership,
|
||||
or credential-management operations through MCP.
|
||||
@@ -0,0 +1,77 @@
|
||||
# ZPan Restish Setup
|
||||
|
||||
## Confirm Origin and API Name
|
||||
|
||||
Before connecting, ask the user to confirm:
|
||||
|
||||
- the ZPan origin, for example `https://files.example.com`;
|
||||
- the local Restish API name, normally `zpan`;
|
||||
- the intended workspace if the next operation reads or changes workspace data.
|
||||
|
||||
Use one OpenAPI document only:
|
||||
|
||||
```sh
|
||||
restish api connect zpan https://files.example.com/api/openapi.json --replace --yes
|
||||
```
|
||||
|
||||
The `--yes` here approves replacing the Restish API connection after the user
|
||||
has confirmed the origin. It does not approve plugin installation.
|
||||
|
||||
For an existing connection, sync before use:
|
||||
|
||||
```sh
|
||||
restish api sync zpan
|
||||
```
|
||||
|
||||
## Restish Version
|
||||
|
||||
Require Restish v2.3 or later:
|
||||
|
||||
```sh
|
||||
restish --version
|
||||
```
|
||||
|
||||
Stop and ask the user to upgrade if the version is older than v2.3.
|
||||
|
||||
## Profiles and Scopes
|
||||
|
||||
Reader, File manager, and Publisher are Restish convenience profiles that
|
||||
expand to explicit scopes. They are not server-side roles or route names.
|
||||
|
||||
| Profile | Use for | Scope set |
|
||||
| --- | --- | --- |
|
||||
| `reader` | Browse, inspect, download links, quota | `objects:read`, `shares:read`, `quota:read`, `storage-usage:read` |
|
||||
| `file-manager` | Reader plus create folders, upload, move, copy, rename, soft delete | Reader scopes plus `objects:create`, `objects:update`, `objects:delete` |
|
||||
| `publisher` | Reader plus create and revoke public shares | Reader scopes plus `shares:create`, `shares:delete` |
|
||||
| `ci` | Unattended file-management automation | Environment-backed `agentApiKey` with file-manager scopes |
|
||||
|
||||
Prefer the narrowest profile:
|
||||
|
||||
```sh
|
||||
restish --rsh-profile reader zpan list-objects --page-size 50
|
||||
restish --rsh-profile file-manager zpan list-objects --page-size 50
|
||||
restish --rsh-profile publisher zpan list-shares --page-size 50
|
||||
```
|
||||
|
||||
The first safe OAuth-backed command may open the browser for authorization code
|
||||
+ PKCE consent. Restish owns token storage, refresh, logout, and redacted auth
|
||||
diagnostics.
|
||||
|
||||
Use `--rsh-no-browser` only when the authorization-code callback can still be
|
||||
completed manually.
|
||||
|
||||
## Upload Plugin Trust Gate
|
||||
|
||||
Install `restish-zpan` only after telling the user that Restish plugins are
|
||||
trusted local executable code and asking them to approve this source:
|
||||
|
||||
```sh
|
||||
restish plugin install saltbo/zpan zpan
|
||||
```
|
||||
|
||||
This shorthand is the post-v2.9-release user path. For pre-release preview
|
||||
acceptance, follow [acceptance.md](acceptance.md) and label the trusted local
|
||||
source build explicitly.
|
||||
|
||||
Do not add a silent approval flag to plugin installation. After installation,
|
||||
confirm that `restish zpan-upload` is available before using upload workflows.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Upload Workflows
|
||||
|
||||
Every local file upload must use the Restish command plugin:
|
||||
|
||||
```sh
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --parent root ./artifact.zip
|
||||
```
|
||||
|
||||
The Skill must not implement upload chunking or upload orchestration.
|
||||
`restish-zpan` validates `createObject`, `presignObjectUploadParts`,
|
||||
`completeObjectUpload`, and `abortObjectUpload`, then creates or resumes ZPan
|
||||
upload sessions through Restish delegated HTTP, streams file parts from disk to
|
||||
storage, records storage responses, retries parts, and removes safe checkpoints
|
||||
after completion.
|
||||
|
||||
## Before Uploading
|
||||
|
||||
Confirm:
|
||||
|
||||
- the target workspace;
|
||||
- the target folder or parent object ID;
|
||||
- whether a same-name destination should fail, rename, or replace;
|
||||
- plugin trust if `restish zpan-upload` is not installed yet.
|
||||
|
||||
Install only after explicit source approval:
|
||||
|
||||
```sh
|
||||
restish plugin install saltbo/zpan zpan
|
||||
```
|
||||
|
||||
## Upload
|
||||
|
||||
Use the selected Restish host profile, plugin profile, and API name explicitly.
|
||||
For plugin delegated HTTP on Restish v2.3, set `RSH_PROFILE` to the same value
|
||||
as the plugin `--profile` flag. The environment selects the host credential;
|
||||
the flag separately selects spec validation and checkpoint identity.
|
||||
|
||||
```sh
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --parent folder_456 ./release.tar.gz
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --parent folder_456 ./release.tar.gz release-linux.tar.gz
|
||||
```
|
||||
|
||||
If the plugin supports a conflict flag in the installed version, pass only the
|
||||
user-approved policy.
|
||||
|
||||
## Resume and Abort
|
||||
|
||||
Resume interrupted local uploads through the plugin:
|
||||
|
||||
```sh
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --resume ./release.tar.gz
|
||||
```
|
||||
|
||||
Abort an upload only after confirmation:
|
||||
|
||||
```sh
|
||||
RSH_PROFILE=file-manager restish zpan-upload --api zpan --profile file-manager --abort ./release.tar.gz
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Return a compact summary with object ID, object URL or share URL when relevant,
|
||||
parent ID, upload mode, part count, bytes uploaded, task state, and quota effect
|
||||
when reported. Do not expose storage upload URLs, bearer tokens, API keys,
|
||||
cookies, or checkpoint contents.
|
||||
Reference in New Issue
Block a user