From eec57a3888cf02693ebd5010af7621f09c5b5219 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:07:59 +0200 Subject: [PATCH 01/11] scaffold: set_key signup step for plain bring-your-own-key apps A byo app whose users create their key on the provider's site had no way to hand that key to the adapter short of editing $APP/secrets.json by hand and restarting the app. The new set_key step generates a local .set_key method that caches a caller-supplied key (overwriting, so a revoked key can be replaced) and is read per request like any minted key. Calls made before a key exists soft-fail with a hint naming set_key and where to get a key. The activation hint is now step-aware: it previously told agents to call the signup method with no arguments even for register flows that need an email. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/publish/submission.go | 11 +- internal/scaffold/config.go | 58 ++++++++++- internal/scaffold/templates/main.go.tmpl | 15 +-- internal/scaffold/templates/signup.go.tmpl | 49 +++++++++ internal/scaffold/zz_set_key_test.go | 111 +++++++++++++++++++++ 5 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 internal/scaffold/zz_set_key_test.go diff --git a/internal/publish/submission.go b/internal/publish/submission.go index 4df1707..0c03f22 100644 --- a/internal/publish/submission.go +++ b/internal/publish/submission.go @@ -180,7 +180,7 @@ type SubMethod struct { // $APP/secrets.json under SecretKey, from which the byo ${TOKEN} headers resolve // it). One SubSignup describes one leg, selected by Step. type SubSignup struct { - Step string `json:"step"` // "create" | "register" | "verify" | "broker" | "account" + Step string `json:"step"` // "create" | "register" | "verify" | "broker" | "account" | "set_key" URL string `json:"url"` // create/register/verify: the provider endpoint POSTed BrokerURL string `json:"broker_url"` // broker: the Pilot broker /signup endpoint (signed) KeyPath string `json:"key_path"` // create/verify: dotted path to the key (create defaults to data.api_key) @@ -468,12 +468,19 @@ func validateSubSignupMethod(n string, m SubMethod) []string { if strings.TrimSpace(m.Signup.SecretKey) == "" { e = append(e, fmt.Sprintf("Method %q: a broker signup step needs signup.secret_key", n)) } + case "set_key": + if strings.TrimSpace(m.Signup.SecretKey) == "" { + e = append(e, fmt.Sprintf("Method %q: a set_key step needs signup.secret_key", n)) + } + if strings.TrimSpace(m.Signup.URL) != "" { + https("url", m.Signup.URL) + } case "account": if strings.TrimSpace(m.Signup.SecretKey) == "" { e = append(e, fmt.Sprintf("Method %q: an account step needs signup.secret_key", n)) } default: - e = append(e, fmt.Sprintf("Method %q: signup.step must be create|register|verify|broker|account", n)) + e = append(e, fmt.Sprintf("Method %q: signup.step must be create|register|verify|broker|account|set_key", n)) } if m.HasHTTP() || m.HasCLI() || m.HasLocal() { e = append(e, fmt.Sprintf("Method %q: a signup method must not also declare an http/cli/local route", n)) diff --git a/internal/scaffold/config.go b/internal/scaffold/config.go index bf15347..307dd0f 100644 --- a/internal/scaffold/config.go +++ b/internal/scaffold/config.go @@ -324,7 +324,7 @@ func (c *Config) HasSignup() bool { // unauthenticated calls with activation instructions instead of a raw 401. func (c *Config) HasKeyMintSignup() bool { for _, m := range c.Methods { - if m.Signup != nil && (m.Signup.IsCreate() || m.Signup.IsVerify() || m.Signup.IsBroker()) { + if m.Signup != nil && m.Signup.mintsKey() { return true } } @@ -335,7 +335,7 @@ func (c *Config) HasKeyMintSignup() bool { // (the first key-minting signup route's SecretKey), or "". func (c *Config) AuthSecretKey() string { for _, m := range c.Methods { - if m.Signup != nil && (m.Signup.IsCreate() || m.Signup.IsVerify() || m.Signup.IsBroker()) { + if m.Signup != nil && m.Signup.mintsKey() { return m.Signup.SecretKey } } @@ -355,9 +355,41 @@ func (c *Config) SignupMethodName() string { return m.Name } } + for _, m := range c.Methods { + if m.Signup != nil && m.Signup.IsSetKey() { + return m.Name + } + } return "" } +// SignupHint is the one-line activation instruction the adapter returns, in +// place of a doomed 401, when an authenticated call arrives before any key is +// on the host. It names the method SignupMethodName picks and says what to pass. +func (c *Config) SignupHint() string { + name := c.SignupMethodName() + for _, m := range c.Methods { + if m.Name != name || m.Signup == nil { + continue + } + switch { + case m.Signup.IsSetKey(): + where := "from your account with the provider" + if m.Signup.URL != "" { + where = "at " + m.Signup.URL + } + return "No API key on this host yet. Create one " + where + ", then call " + name + + ` once with {"api_key":"..."} — it is stored locally under ~/.pilot and injected on every call automatically.` + case strings.EqualFold(m.Signup.Step, "register"): + return "No API key on this host yet. Call " + name + + " to start signup, then finish with the verify method — the key is then stored locally under ~/.pilot and injected on every call automatically." + } + } + return "No API key on this host yet. Call " + name + + " once (no arguments required) to provision a free account and managed inbox — " + + "the key is then stored locally under ~/.pilot and injected on every call automatically; you never pass it." +} + // HasBrokerSignup reports whether any signup route is the broker step — the // adapter then needs an ed25519 signer + identity (to sign the keyless broker // call) and key.sign + net.dial-broker grants, even though its ops stay byo. @@ -701,7 +733,7 @@ type Method struct { // {email, api_key}. The adapter caches BOTH to secrets.json; ops stay byo. // No user email, no code, one call. type SignupRoute struct { - Step string `yaml:"step"` // "create" | "register" | "verify" | "broker" | "account" + Step string `yaml:"step"` // "create" | "register" | "verify" | "broker" | "account" | "set_key" URL string `yaml:"url"` // create/register/verify: the provider endpoint POSTed BrokerURL string `yaml:"broker_url"` // broker: the Pilot broker /signup endpoint (signed) KeyPath string `yaml:"key_path"` // create/verify: dotted path to the key in the response (default application.api_key; create defaults to data.api_key) @@ -722,6 +754,16 @@ func (s *SignupRoute) IsVerify() bool { return strings.EqualFold(s.Step, "verify // IsBroker is the fully-autonomous leg that signs a call to the Pilot broker. func (s *SignupRoute) IsBroker() bool { return strings.EqualFold(s.Step, "broker") } +// IsSetKey is the plain byo leg: the user obtains a key from the provider +// themselves (URL, when set, says where) and hands it to this local method, +// which caches it under SecretKey. No backend call is made. +func (s *SignupRoute) IsSetKey() bool { return strings.EqualFold(s.Step, "set_key") } + +// mintsKey reports whether this route is what puts the byo auth key on the host. +func (s *SignupRoute) mintsKey() bool { + return s.IsCreate() || s.IsVerify() || s.IsBroker() || s.IsSetKey() +} + // BodyJSON renders the static create body as a compact JSON object literal (or // "{}"), baked into the generated adapter and re-parsed at runtime. func (s *SignupRoute) BodyJSON() string { @@ -1442,13 +1484,21 @@ func (c *Config) validateSignupMethod(i int, m Method) []error { if strings.TrimSpace(m.Signup.SecretKey) == "" { errs = append(errs, fmt.Errorf("methods[%d] (%s): a broker signup step needs signup.secret_key (the key the minted secret is cached under)", i, m.Name)) } + case "set_key": + // A local writer of a user-supplied key — no backend call to validate. + if strings.TrimSpace(m.Signup.SecretKey) == "" { + errs = append(errs, fmt.Errorf("methods[%d] (%s): a set_key step needs signup.secret_key (the key the supplied secret is cached under)", i, m.Name)) + } + if strings.TrimSpace(m.Signup.URL) != "" { + httpsURL("url", m.Signup.URL) + } case "account": // A local reader of the cached account — needs only the secrets keys. if strings.TrimSpace(m.Signup.SecretKey) == "" { errs = append(errs, fmt.Errorf("methods[%d] (%s): an account step needs signup.secret_key", i, m.Name)) } default: - errs = append(errs, fmt.Errorf("methods[%d] (%s): signup.step %q must be create|register|verify|broker|account", i, m.Name, m.Signup.Step)) + errs = append(errs, fmt.Errorf("methods[%d] (%s): signup.step %q must be create|register|verify|broker|account|set_key", i, m.Name, m.Signup.Step)) } if c.Managed() { errs = append(errs, fmt.Errorf("methods[%d] (%s): a signup (no-broker) route cannot be combined with managed/provisioned auth", i, m.Name)) diff --git a/internal/scaffold/templates/main.go.tmpl b/internal/scaffold/templates/main.go.tmpl index b0d53c9..fcaf146 100644 --- a/internal/scaffold/templates/main.go.tmpl +++ b/internal/scaffold/templates/main.go.tmpl @@ -201,7 +201,12 @@ func registerHandlers(d *ipc.Dispatcher, c *backend.Client, version, backendURL rawPathParams: []string{ {{- range $p := .HTTP.RawPathParams}}{{printf "%q" $p}}, {{end -}} }, }, manifestPath, dur("{{.TimeoutFor}}"))) // {{.Duration}} (multipart upload) {{- else if .Signup}} -{{- if eq .Signup.Step "account"}} +{{- if .Signup.IsSetKey}} + d.Register("{{.Name}}", setKeyHandler(setKeyConfig{ + secretKey: {{printf "%q" .Signup.SecretKey}}, + keyURL: {{printf "%q" .Signup.URL}}, + }, manifestPath)) // {{.Duration}} (local: cache a key you obtained from the provider) +{{- else if eq .Signup.Step "account"}} d.Register("{{.Name}}", accountHandler(accountConfig{ secretKey: {{printf "%q" .Signup.SecretKey}}, emailKey: {{printf "%q" .Signup.EmailKey}}, @@ -238,7 +243,7 @@ func registerHandlers(d *ipc.Dispatcher, c *backend.Client, version, backendURL }, manifestPath, dur("{{.TimeoutFor}}"))) // {{.Duration}} (no-broker self-signup: register) {{- end}} {{- else}} - d.Register("{{.Name}}", {{if $.HasKeyMintSignup}}requireKey(manifestPath, {{printf "%q" $.AuthSecretKey}}, {{printf "%q" $.SignupMethodName}}, {{end}}{{if .HTTP.CaptureTo}}captureWrap(expandHome("{{.HTTP.CaptureTo}}"), {{end}}forward(c, route{ + d.Register("{{.Name}}", {{if $.HasKeyMintSignup}}requireKey(manifestPath, {{printf "%q" $.AuthSecretKey}}, {{printf "%q" $.SignupMethodName}}, {{printf "%q" $.SignupHint}}, {{end}}{{if .HTTP.CaptureTo}}captureWrap(expandHome("{{.HTTP.CaptureTo}}"), {{end}}forward(c, route{ method: "{{.HTTP.Verb}}", pathTmpl: "{{.HTTP.Path}}", bodyVerb: {{.HTTP.BodyVerb}}, pathParams: []string{ {{- range $p := .HTTP.PathParams}}{{printf "%q" $p}}, {{end -}} }, rawPathParams: []string{ {{- range $p := .HTTP.RawPathParams}}{{printf "%q" $p}}, {{end -}} }, @@ -288,16 +293,14 @@ func localRead(store string) ipc.Handler { // method mints the key, it is stored locally under $APP/secrets.json (in ~/.pilot) // and the adapter injects it into the Authorization header on every subsequent // call automatically — the agent never sees or passes the key. -func requireKey(manifestPath, secretKey, signupMethod string, h ipc.Handler) ipc.Handler { +func requireKey(manifestPath, secretKey, signupMethod, hint string, h ipc.Handler) ipc.Handler { return func(ctx context.Context, req *ipc.Envelope) (json.RawMessage, error) { if os.Getenv(secretKey) == "" && loadSecrets(manifestPath)[secretKey] == "" { msg := map[string]any{ "ok": false, "needs_signup": true, "activate": signupMethod, - "message": "No API key on this host yet. Call " + signupMethod + - " once (no arguments required) to provision a free account and managed inbox — " + - "the key is then stored locally under ~/.pilot and injected on every call automatically; you never pass it.", + "message": hint, } b, _ := json.Marshal(msg) return json.RawMessage(b), nil diff --git a/internal/scaffold/templates/signup.go.tmpl b/internal/scaffold/templates/signup.go.tmpl index c9e4a78..1fb0b4d 100644 --- a/internal/scaffold/templates/signup.go.tmpl +++ b/internal/scaffold/templates/signup.go.tmpl @@ -285,6 +285,50 @@ func accountHandler(ac accountConfig, manifestPath string) ipc.Handler { } } +// setKeyConfig / setKeyHandler are the plain byo leg: the user creates a key at +// the provider (keyURL says where) and hands it over once. It is cached under +// secretKey in $APP/secrets.json and injected on every later call. Unlike the +// minting steps this OVERWRITES a cached key, so a revoked or mistyped key can be +// replaced without touching files by hand. Local, no backend call. +type setKeyConfig struct { + secretKey string + keyURL string +} + +func setKeyHandler(sc setKeyConfig, manifestPath string) ipc.Handler { + return func(ctx context.Context, req *ipc.Envelope) (json.RawMessage, error) { + var in struct { + APIKey string `json:"api_key"` + } + if len(req.Payload) > 0 { + _ = json.Unmarshal(req.Payload, &in) + } + in.APIKey = strings.TrimSpace(in.APIKey) + if in.APIKey == "" { + where := "" + if sc.keyURL != "" { + where = " (create one at " + sc.keyURL + ")" + } + return nil, errors.New("set_key: api_key is required — pass {\"api_key\":\"...\"}" + where) + } + dir := signupAppDir(manifestPath) + if dir == "" { + return nil, errors.New("set_key: cannot locate the app directory to cache the key") + } + path := filepath.Join(dir, "secrets.json") + m := signupReadSecrets(path) + replaced := m[sc.secretKey] != "" + m[sc.secretKey] = in.APIKey + if err := signupWriteSecrets(path, m); err != nil { + return nil, fmt.Errorf("set_key: cache key: %w", err) + } + return signupResult(map[string]any{ + "ok": true, "key_field": sc.secretKey, "api_key": "saved", "replaced": replaced, + "message": "key cached to secrets.json and used on every subsequent call", + }) + } +} + // ── shared helpers ────────────────────────────────────────────────────────── func signupResult(m map[string]any) (json.RawMessage, error) { @@ -326,6 +370,11 @@ func signupMergeSecrets(path string, add map[string]string) error { m[k] = v } } + return signupWriteSecrets(path, m) +} + +// signupWriteSecrets atomically replaces path (0600) with m. +func signupWriteSecrets(path string, m map[string]string) error { out, err := json.MarshalIndent(m, "", " ") if err != nil { return err diff --git a/internal/scaffold/zz_set_key_test.go b/internal/scaffold/zz_set_key_test.go new file mode 100644 index 0000000..05d804e --- /dev/null +++ b/internal/scaffold/zz_set_key_test.go @@ -0,0 +1,111 @@ +package scaffold + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// setKeySpec is a plain byo app: the user creates a key at the provider and +// hands it to .set_key; every other method resolves it from secrets.json. +const setKeySpec = ` +id: io.pilot.rentahuman +app_version: 0.2.1 +description: "Hire humans for real-world tasks with your own RentAHuman API key." +backend: + type: http + base_url: https://rentahuman.ai + auth: byo + headers: + X-API-Key: "${RENTAHUMAN_API_KEY}" +methods: + - name: rentahuman.set_key + summary: "Save the RentAHuman API key this host uses." + duration: fast + signup: + step: set_key + url: https://rentahuman.ai/account/api-keys + secret_key: RENTAHUMAN_API_KEY + - name: rentahuman.list_bounties + summary: "List open bounties." + duration: fast + http: {verb: GET, path: /api/partner/v1/bounties} +` + +func TestSetKeyStepGeneratesAndCompiles(t *testing.T) { + cfg := parseSpec(t, setKeySpec) + if !cfg.HasSignup() || !cfg.HasKeyMintSignup() { + t.Fatal("HasSignup() and HasKeyMintSignup() should be true for a set_key step") + } + if got := cfg.AuthSecretKey(); got != "RENTAHUMAN_API_KEY" { + t.Errorf("AuthSecretKey()=%q, want RENTAHUMAN_API_KEY", got) + } + if got := cfg.SignupMethodName(); got != "rentahuman.set_key" { + t.Errorf("SignupMethodName()=%q, want rentahuman.set_key", got) + } + hint := cfg.SignupHint() + for _, want := range []string{"rentahuman.set_key", "https://rentahuman.ai/account/api-keys", `"api_key"`} { + if !strings.Contains(hint, want) { + t.Errorf("SignupHint() %q missing %q", hint, want) + } + } + if strings.Contains(hint, "no arguments required") { + t.Errorf("set_key hint must not claim no arguments are needed: %q", hint) + } + + dir := t.TempDir() + if _, err := Generate(cfg, dir); err != nil { + t.Fatalf("generate: %v", err) + } + main, _ := os.ReadFile(filepath.Join(dir, "cmd", cfg.BinaryName, "main.go")) + for _, want := range []string{"setKeyHandler(setKeyConfig{", "requireKey(", "rentahuman.ai/account/api-keys", "HeaderFunc:"} { + if !strings.Contains(string(main), want) { + t.Errorf("generated main.go missing %q", want) + } + } + mf, _ := os.ReadFile(filepath.Join(dir, "manifest.json")) + if !strings.Contains(string(mf), `"cap": "fs.write", "target": "$APP/secrets.json"`) { + t.Errorf("manifest must grant fs.write on secrets.json for set_key") + } + if testing.Short() { + return + } + compileGenerated(t, dir) +} + +func TestSetKeyStepValidation(t *testing.T) { + validate := func(spec string) []error { + t.Helper() + cfg, err := Parse([]byte(spec)) + if err != nil { + t.Fatalf("parse: %v", err) + } + cfg.Resolve() + return cfg.Validate() + } + if errs := validate(setKeySpec); len(errs) != 0 { + t.Fatalf("valid set_key spec rejected: %v", errs) + } + bad := strings.Replace(setKeySpec, " secret_key: RENTAHUMAN_API_KEY\n", "", 1) + errs := validate(bad) + found := false + for _, e := range errs { + found = found || strings.Contains(e.Error(), "set_key step needs signup.secret_key") + } + if !found { + t.Errorf("missing secret_key: errs=%v, want the set_key secret_key error", errs) + } + insecure := strings.Replace(setKeySpec, "https://rentahuman.ai/account/api-keys", "http://rentahuman.ai/account/api-keys", 1) + if errs := validate(insecure); len(errs) == 0 { + t.Error("an http:// set_key url should be rejected") + } +} + +// The register flow's hint must not tell agents to call it with no arguments. +func TestSignupHintRegisterStep(t *testing.T) { + cfg := parseSpec(t, signupSpec) + if h := cfg.SignupHint(); strings.Contains(h, "no arguments required") || !strings.Contains(h, "didit.signup") { + t.Errorf("register hint = %q", h) + } +} From 34a33e23175d3d7e734f9a75d657dcbf59cdcd10 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:10:48 +0200 Subject: [PATCH 02/11] scaffold: public http routes skip the no-key-yet gate Apps whose key comes from a signup route wrap every http method in requireKey, so endpoints the provider serves without credentials (method catalogues, recommendations, the signup handshake itself) were unreachable until a key existed. A route marked public: true is forwarded as-is. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/publish/submission.go | 5 ++++- internal/scaffold/config.go | 6 ++++++ internal/scaffold/templates/main.go.tmpl | 4 ++-- internal/scaffold/zz_set_key_test.go | 13 +++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/internal/publish/submission.go b/internal/publish/submission.go index 0c03f22..f47d61e 100644 --- a/internal/publish/submission.go +++ b/internal/publish/submission.go @@ -220,6 +220,9 @@ type SubRoute struct { // metadata file so a SubLocal method can recall it (e.g. buy_number → // ~/.pilot/.agentphone). Best-effort; never fails the call. CaptureTo string `json:"capture_to"` + // Public marks an endpoint the provider serves without credentials; it is + // exempt from the adapter's no-key-yet gate (see scaffold.HTTPRoute.Public). + Public bool `json:"public"` } // SubCLIRoute is the backend CLI mapping for a method. Enumerated methods bake @@ -724,7 +727,7 @@ func (s Submission) ToConfig() *scaffold.Config { Passthrough: m.CLI.Passthrough, } default: - route := &scaffold.HTTPRoute{Verb: orDefault(m.HTTP.Verb, "GET"), Path: m.HTTP.Path, CaptureTo: m.HTTP.CaptureTo} + route := &scaffold.HTTPRoute{Verb: orDefault(m.HTTP.Verb, "GET"), Path: m.HTTP.Path, CaptureTo: m.HTTP.CaptureTo, Public: m.HTTP.Public} // Carry each param's explicit request location so the generator can // resolve query/path/path_raw/body/header placement. Omitted `in` // keeps the verb/path default (back-compat). diff --git a/internal/scaffold/config.go b/internal/scaffold/config.go index 307dd0f..c1d3f25 100644 --- a/internal/scaffold/config.go +++ b/internal/scaffold/config.go @@ -877,6 +877,12 @@ type HTTPRoute struct { // the provisioned number to ~/.pilot/.agentphone. CaptureTo string `yaml:"capture_to"` + // Public marks an endpoint the provider serves without credentials (a + // catalogue, a recommendation, a signup handshake). An app whose key comes + // from a signup route soft-fails authenticated calls until a key exists; + // a public route is exempt, so it keeps working before signup. + Public bool `yaml:"public"` + // Multipart, when set, sends this method as multipart/form-data built from a // staged blob rather than as a JSON body. See MultipartRoute. Multipart *MultipartRoute `yaml:"multipart"` diff --git a/internal/scaffold/templates/main.go.tmpl b/internal/scaffold/templates/main.go.tmpl index fcaf146..1aace9f 100644 --- a/internal/scaffold/templates/main.go.tmpl +++ b/internal/scaffold/templates/main.go.tmpl @@ -243,7 +243,7 @@ func registerHandlers(d *ipc.Dispatcher, c *backend.Client, version, backendURL }, manifestPath, dur("{{.TimeoutFor}}"))) // {{.Duration}} (no-broker self-signup: register) {{- end}} {{- else}} - d.Register("{{.Name}}", {{if $.HasKeyMintSignup}}requireKey(manifestPath, {{printf "%q" $.AuthSecretKey}}, {{printf "%q" $.SignupMethodName}}, {{printf "%q" $.SignupHint}}, {{end}}{{if .HTTP.CaptureTo}}captureWrap(expandHome("{{.HTTP.CaptureTo}}"), {{end}}forward(c, route{ + d.Register("{{.Name}}", {{if and $.HasKeyMintSignup (not .HTTP.Public)}}requireKey(manifestPath, {{printf "%q" $.AuthSecretKey}}, {{printf "%q" $.SignupMethodName}}, {{printf "%q" $.SignupHint}}, {{end}}{{if .HTTP.CaptureTo}}captureWrap(expandHome("{{.HTTP.CaptureTo}}"), {{end}}forward(c, route{ method: "{{.HTTP.Verb}}", pathTmpl: "{{.HTTP.Path}}", bodyVerb: {{.HTTP.BodyVerb}}, pathParams: []string{ {{- range $p := .HTTP.PathParams}}{{printf "%q" $p}}, {{end -}} }, rawPathParams: []string{ {{- range $p := .HTTP.RawPathParams}}{{printf "%q" $p}}, {{end -}} }, @@ -251,7 +251,7 @@ func registerHandlers(d *ipc.Dispatcher, c *backend.Client, version, backendURL bodyParams: []string{ {{- range $p := .HTTP.BodyParams}}{{printf "%q" $p}}, {{end -}} }, headerParams: []string{ {{- range $p := .HTTP.HeaderParams}}{{printf "%q" $p}}, {{end -}} }, bodyRawParam: "{{.HTTP.BodyRawParam}}", - }, dur("{{.TimeoutFor}}")){{if .HTTP.CaptureTo}}){{end}}{{if $.HasKeyMintSignup}}){{end}}) // {{.Duration}} + }, dur("{{.TimeoutFor}}")){{if .HTTP.CaptureTo}}){{end}}{{if and $.HasKeyMintSignup (not .HTTP.Public)}}){{end}}) // {{.Duration}} {{- end}} {{- end}} d.Register("{{.Namespace}}.help", helpHandler(version, backendURL)) diff --git a/internal/scaffold/zz_set_key_test.go b/internal/scaffold/zz_set_key_test.go index 05d804e..78f2dde 100644 --- a/internal/scaffold/zz_set_key_test.go +++ b/internal/scaffold/zz_set_key_test.go @@ -31,6 +31,10 @@ methods: summary: "List open bounties." duration: fast http: {verb: GET, path: /api/partner/v1/bounties} + - name: rentahuman.list_humans + summary: "Browse humans (public)." + duration: fast + http: {verb: GET, path: /api/humans, public: true} ` func TestSetKeyStepGeneratesAndCompiles(t *testing.T) { @@ -64,6 +68,15 @@ func TestSetKeyStepGeneratesAndCompiles(t *testing.T) { t.Errorf("generated main.go missing %q", want) } } + // The gated route is wrapped; the public one is not. + if !strings.Contains(string(main), `requireKey(manifestPath, "RENTAHUMAN_API_KEY", "rentahuman.set_key", `) { + t.Error("list_bounties should be wrapped in requireKey") + } + for _, line := range strings.Split(string(main), "\n") { + if strings.Contains(line, `d.Register("rentahuman.list_humans"`) && strings.Contains(line, "requireKey(") { + t.Errorf("public route must not be gated on a key: %s", line) + } + } mf, _ := os.ReadFile(filepath.Join(dir, "manifest.json")) if !strings.Contains(string(mf), `"cap": "fs.write", "target": "$APP/secrets.json"`) { t.Errorf("manifest must grant fs.write on secrets.json for set_key") From 0b0c1967642b3b35a30ae4bfede563c659d4c554 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:15:59 +0200 Subject: [PATCH 03/11] io.pilot.kinetic 1.0.1: bring-your-own token instead of an unregistered broker 1.0.0 shipped as a managed app, but the broker never had a Kinetic master key or registration, so every call failed with "unknown app". Kinetic submitted it as a bring-your-own-token app and issues self-serve kp_live_ tokens, so 1.0.1 goes direct to api.kineticpricing.com: - kinetic.set_key saves the account holder's token; calls made before it return a hint naming set_key instead of a 401 - kinetic.signup_start/status/complete drive Kinetic's agent-assisted signup - method_list, method_recommend, offer_list and research stay keyless - method_recommend takes the decision enum the API now requires; the old demo passed free text and failed validation - demo and next steps drop the Pilot $5 budget and kinetic.balance, which only exist for managed apps Co-Authored-By: Claude Opus 5.5 (1M context) --- submissions/io.pilot.kinetic/submission.json | 871 +++++++++++++++++++ 1 file changed, 871 insertions(+) create mode 100644 submissions/io.pilot.kinetic/submission.json diff --git a/submissions/io.pilot.kinetic/submission.json b/submissions/io.pilot.kinetic/submission.json new file mode 100644 index 0000000..bc64a34 --- /dev/null +++ b/submissions/io.pilot.kinetic/submission.json @@ -0,0 +1,871 @@ +{ + "product_demo": { + "skill": "io.pilot.kinetic", + "title": "Full usage demo", + "metered": false, + "when_to_use": "When you need to know what to charge and want it grounded in your own customers' answers: a price range, a specific price point, or which features carry the value.", + "quickstart": { + "goal": "Find out which study answers your question", + "command": "pilotctl appstore call io.pilot.kinetic kinetic.method_recommend '{\"decision\":\"price_range\"}'", + "expect": "{\"decision\":\"price_range\",\"recommended\":{\"study_type\":\"van_westendorp\",\"price_cents\":14900},\"why\":\"...\",\"alternatives\":[...]}", + "note": "Free, no token needed. decision is one of price_range, exact_price, feature_priorities, package_pricing — the methods answer different questions." + }, + "examples": [ + { + "title": "Save your API token", + "goal": "Every study, results and billing call needs it", + "command": "pilotctl appstore call io.pilot.kinetic kinetic.set_key '{\"api_key\":\"kp_live_.\"}'", + "expect": "{\"ok\":true,\"key_field\":\"KINETIC_API_TOKEN\",\"api_key\":\"saved\"}", + "note": "Create the token on kineticpricing.com under Integrations. No account yet? kinetic.signup_start emails the holder a confirmation link." + }, + { + "title": "Draft a study and preview it", + "goal": "Everything up to launch is free", + "command": "pilotctl appstore call io.pilot.kinetic kinetic.study_create '{\"Idempotency-Key\":\"\",\"name\":\"Pro tier\",\"product_desc\":\"SaaS analytics for small teams\",\"segment_desc\":\"Founders at 5-50 person startups\",\"decision_type\":\"first_time\",\"currency\":\"USD\"}'", + "expect": "{\"id\":\"3572c965-...\",\"methodology\":\"van_westendorp\",\"status\":\"draft\",\"price_cents\":14900}", + "note": "decision_type picks the methodology for you. Every mutating call needs an Idempotency-Key." + }, + { + "title": "Check the price before spending", + "command": "pilotctl appstore call io.pilot.kinetic kinetic.offer_list '{}'", + "expect": "{\"offers\":[{\"study_type\":\"van_westendorp\",\"price_cents\":14900}]}", + "note": "Launching costs $149-$199, paid by the account holder on Stripe." + }, + { + "title": "Read computed results, not guesses", + "command": "pilotctl appstore call io.pilot.kinetic kinetic.results_get '{\"id\":\"stu_abc123\"}'", + "expect": "{\"optimal_price_point\":42,\"acceptable_range\":{\"low\":34,\"high\":58},\"n\":31}", + "note": "Call study_quality_get first: speeders make a clean number worthless." + } + ], + "gotchas": [ + "Save your token first: kinetic.set_key {\"api_key\":\"kp_live_...\"}. Only method_list, method_recommend, offer_list and research work without one.", + "Everything except launching is free. The $149-$199 fee is paid by a human on Stripe.", + "Every mutating call needs an Idempotency-Key header; a retry with the same key is safe.", + "study_checkout_create returns a link and does NOT pay. Hand the URL to your human and stop.", + "You supply the respondents; Kinetic has no panel.", + "Check study_quality_get and progress before trusting a result." + ], + "next": [ + "io.pilot.kinetic kinetic.help '{}'" + ] + }, + "next_steps": { + "schema": 1, + "app": "io.pilot.kinetic", + "edges": [ + { + "from": "*", + "on": "ok", + "match": "\"needs_signup\"\\s*:\\s*true", + "why": "this host has no Kinetic token yet, so study, results and billing calls cannot run", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.set_key '{\"api_key\":\"kp_live_.\"}'", + "why": "the account holder creates the token under Integrations on kineticpricing.com", + "kind": "gateway" + }, + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.signup_start '{\"email\":\"\",\"authority_basis\":\"authorized_agent\"}'", + "why": "no account yet: Kinetic emails the holder a confirmation link", + "kind": "recovery" + } + ] + }, + { + "from": "kinetic.method_recommend", + "on": "ok", + "why": "you now know which method answers your question, so create that study as a free draft", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.study_create '{\"study_type\":\"van_westendorp\",\"name\":\"Pro tier pricing\"}'", + "why": "use the study_type the recommendation returned; drafting and editing are free", + "kind": "flow" + } + ] + }, + { + "from": "kinetic.study_create", + "on": "ok", + "why": "a draft costs nothing, so the next free step is reading the survey exactly as a respondent will see it", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.survey_preview '{\"id\":\"\"}'", + "why": "preview stores no respondent evidence, and once the study is live the questions are fixed", + "kind": "flow" + } + ] + }, + { + "from": "kinetic.study_checkout_create", + "on": "ok", + "why": "this returned a payment link and did not complete the purchase, so nothing moves until a human pays", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.study_get '{\"id\":\"\"}'", + "why": "give checkout_url to your human, wait for them to pay on Stripe, then poll here before launching", + "kind": "flow" + } + ] + }, + { + "from": "kinetic.results_get", + "on": "ok", + "why": "a clean-looking number can come from bad responses, so quality is worth checking before acting on it", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.study_quality_get '{\"id\":\"\"}'", + "why": "speeders and straight-liners make results worthless; also confirm progress cleared the minimum sample", + "kind": "flow" + } + ] + }, + { + "from": "kinetic.study_launch", + "on": "err", + "code": 402, + "why": "the study has not been paid for, so it cannot go live yet", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.study_checkout_create '{\"study_id\":\"\"}'", + "why": "returns a Stripe link for your human to pay; launch works once payment clears", + "kind": "recovery" + } + ] + }, + { + "from": "kinetic.signup_start", + "on": "ok", + "why": "nothing exists until the holder clicks the emailed link", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.signup_status '{\"id\":\"\",\"attempt_token\":\"\"}'", + "why": "poll until state is verified, then complete with the holder's consent", + "kind": "flow" + } + ] + }, + { + "from": "kinetic.signup_complete", + "on": "ok", + "why": "the account exists but this host still has no API token", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.kinetic kinetic.set_key '{\"api_key\":\"kp_live_.\"}'", + "why": "the holder creates a token under Integrations; save it here", + "kind": "flow" + } + ] + } + ] + }, + "id": "io.pilot.kinetic", + "version": "1.0.1", + "namespace": "kinetic", + "description": "Run real pricing research with your own customers: pick the method that fits the decision, draft and preview the survey, collect responses, and read deterministic results with a narrative written from those numbers. Van Westendorp, Gabor-Granger, MaxDiff and choice-based conjoint.", + "email": "support@kineticpricing.com", + "backend": { + "base_url": "https://api.kineticpricing.com", + "auth": "byo", + "headers": [ + { + "name": "Authorization", + "value": "Bearer ${KINETIC_API_TOKEN}" + } + ] + }, + "methods": [ + { + "name": "kinetic.set_key", + "latency": "fast", + "signup": { + "step": "set_key", + "url": "https://kineticpricing.com", + "secret_key": "KINETIC_API_TOKEN" + }, + "description": "Save the Kinetic API access token this host uses (format kp_live_.). The account holder creates it on kineticpricing.com under Integrations, choosing its scopes. It is stored in $APP/secrets.json and sent on every call; calling again replaces it. Local, free. Call this before any study, results or billing method.", + "params": [ + { + "name": "api_key", + "type": "string", + "required": true, + "description": "The kp_live_... token from Integrations on kineticpricing.com." + } + ] + }, + { + "name": "kinetic.method_list", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/methods", + "public": true + }, + "description": "List the pricing-research methods available, with each one's server-owned launch price in cents, methodology version, and sample-size guidance. Van Westendorp finds an acceptable price RANGE, Gabor-Granger tests exact price POINTS, MaxDiff ranks feature value, choice-based conjoint models trade-offs. Free, and the right first call when you do not yet know which study to run.", + "params": [] + }, + { + "name": "kinetic.method_recommend", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/methods/recommendation", + "public": true + }, + "description": "Say which kind of pricing decision you face and get back the method that answers it, with the reason and the alternatives. Free, no token needed. Use this instead of guessing between Van Westendorp and Gabor-Granger: they answer different questions, and the wrong one produces a confident, useless number.", + "params": [ + { + "name": "decision", + "type": "string", + "required": true, + "in": "body", + "description": "One of: price_range (what range can I charge), exact_price (which of these specific prices), feature_priorities (which features carry the value), package_pricing (how to package tiers)." + } + ] + }, + { + "name": "kinetic.study_create", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/studies" + }, + "description": "START HERE once you know the decision. Creates a draft study. You do NOT pick the method: Kinetic derives the methodology from decision_type (first_time and new_tier map to Van Westendorp today) and returns it on the study along with price_cents. Free — a draft costs nothing and is not launched. Returns {id, methodology, status:\"draft\", price_cents}.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on every mutating call; a retry with the same key returns the original result instead of creating a second study.", + "in": "header" + }, + { + "name": "name", + "type": "string", + "required": true, + "description": "A name for the study.", + "in": "body" + }, + { + "name": "product_desc", + "type": "string", + "required": true, + "description": "What you sell, in plain language, e.g. 'A SaaS analytics tool for small teams'.", + "in": "body" + }, + { + "name": "segment_desc", + "type": "string", + "required": true, + "description": "Who you are pricing for, e.g. 'Founders at 5-50 person startups'.", + "in": "body" + }, + { + "name": "decision_type", + "type": "string", + "required": true, + "description": "One of underpriced | new_tier | first_time | other. This is what selects the methodology, so it is the most consequential field on the call.", + "in": "body" + }, + { + "name": "currency", + "type": "string", + "required": true, + "description": "ISO currency code, e.g. USD.", + "in": "body" + } + ] + }, + { + "name": "kinetic.study_prefill", + "latency": "slow", + "http": { + "verb": "POST", + "path": "/v1/studies/prefill" + }, + "description": "Generate a draft study filled in from a description of your product and audience, so you edit a sensible starting point rather than an empty form. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "description", + "type": "string", + "required": false, + "description": "What you sell and to whom, in plain language.", + "in": "body" + } + ] + }, + { + "name": "kinetic.study_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}" + }, + "description": "Read one study: its configuration, state, and position in the lifecycle. Free.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_list", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies" + }, + "description": "List your studies with their states. Free.", + "params": [] + }, + { + "name": "kinetic.study_update", + "latency": "med", + "http": { + "verb": "PATCH", + "path": "/v1/studies/{study_id}" + }, + "description": "Edit a draft study: name, prices, features, audience and question configuration. Only the fields you send change. Free, and only valid before launch.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_duplicate", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/duplicate" + }, + "description": "Copy an existing study into a new draft, to run the same design against a different audience or set of prices. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study to copy.", + "in": "path" + } + ] + }, + { + "name": "kinetic.survey_regenerate", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/survey/regenerate" + }, + "description": "Regenerate the respondent-facing survey from the study's current configuration after editing it. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.survey_preview", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/preview" + }, + "description": "Read the survey exactly as a respondent will see it, WITHOUT storing any respondent evidence. Free. Always preview before launching: once a study is live the questions are fixed, and a bad question wastes the whole sample.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_launch", + "latency": "slow", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/launch" + }, + "description": "Take a draft live so it can collect responses. COSTS MONEY: the launch price comes from kinetic.method_list (currently $149-$199 by method) and must be paid first via kinetic.study_checkout_create. Confirm with your human before calling. Once live the questions are fixed.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.respondent_link_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/respondent-link" + }, + "description": "Get the link to send to your customers or prospects. THEY are the respondents: Kinetic supplies no panel, so the quality of the answer depends on who you send this to. Free.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_progress_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/progress" + }, + "description": "How many responses have arrived against the recommended sample size. Free. Poll after launch; results are only meaningful once you clear the method's recommended minimum.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_quality_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/quality" + }, + "description": "Response-quality signals: speeders, straight-liners, and other patterns meaning a response should not be trusted. Free. Check this before believing the results.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.study_close", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/close" + }, + "description": "Stop collecting responses and finalize the study so results can be computed. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.results_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/results" + }, + "description": "The deterministic results: numbers Kinetic's analysis engine computed from the responses, not a model's opinion of them. Free. This is the output you act on.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.evidence_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/evidence" + }, + "description": "The structured respondent evidence behind the results, so a number can be traced to the answers that produced it. Free.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.narrative_generate", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/narrative" + }, + "description": "Generate a written explanation of the results. The narrative is written FROM the deterministic results, so it explains the computed numbers rather than inventing them. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.narrative_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/studies/{study_id}/narrative" + }, + "description": "Read a previously generated narrative. Free.", + "params": [ + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.results_export_csv", + "latency": "slow", + "http": { + "verb": "POST", + "path": "/v1/studies/{study_id}/exports/csv" + }, + "description": "Export results and responses as CSV for analysis elsewhere. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The study id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.offer_list", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/billing/offers", + "public": true + }, + "description": "List what is purchasable and what it costs: one-time study prices and Kinetic Pro plans. Free, and the honest place to check a price before committing to a spend.", + "params": [] + }, + { + "name": "kinetic.study_checkout_create", + "latency": "slow", + "http": { + "verb": "POST", + "path": "/v1/billing/checkouts/study" + }, + "description": "Start payment for a draft study. Returns {checkout_url, study_id, amount_cents} and does NOT complete the purchase. A human must open checkout_url and pay on Stripe's hosted page; hand them the link and stop. Once paid, kinetic.study_launch will work. Confirm the amount with your human first.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "study_id", + "type": "string", + "required": true, + "description": "The draft study to pay for.", + "in": "body" + } + ] + }, + { + "name": "kinetic.teardown_create", + "latency": "slow", + "http": { + "verb": "POST", + "path": "/v1/teardowns" + }, + "description": "Run a teardown of a public pricing page and get a structured read of how it is constructed. Free.", + "params": [ + { + "name": "Idempotency-Key", + "type": "string", + "required": true, + "description": "Any unique string per distinct request, e.g. a UUID. Required on mutating calls; a retry with the same key returns the original result rather than repeating the operation.", + "in": "header" + }, + { + "name": "url", + "type": "string", + "required": false, + "description": "The pricing page URL to tear down.", + "in": "body" + } + ] + }, + { + "name": "kinetic.teardown_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/teardowns/{teardown_id}" + }, + "description": "Read a completed teardown. Free.", + "params": [ + { + "name": "teardown_id", + "type": "string", + "required": true, + "description": "The teardown id.", + "in": "path" + } + ] + }, + { + "name": "kinetic.research_list", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/research", + "public": true + }, + "description": "Browse Kinetic's published pricing research. Free.", + "params": [] + }, + { + "name": "kinetic.research_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/research/{slug}", + "public": true + }, + "description": "Read one published research piece. Free.", + "params": [ + { + "name": "slug", + "type": "string", + "required": true, + "description": "The research slug from kinetic.research_list.", + "in": "path" + } + ] + }, + { + "name": "kinetic.task_get", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/tasks/{task_id}" + }, + "description": "Poll a long-running task (prefill, teardown, export) for completion. Free.", + "params": [ + { + "name": "task_id", + "type": "string", + "required": true, + "description": "The task id returned by the operation that started it.", + "in": "path" + } + ] + }, + { + "name": "kinetic.signup_start", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/signup-attempts", + "public": true + }, + "description": "No Kinetic account yet? Start agent-assisted signup: Kinetic emails the account holder a confirmation link and returns an attempt id plus an attempt_token shown ONCE — keep both. The account only exists after the holder clicks the link and you call kinetic.signup_complete with their consent. Free.", + "params": [ + { + "name": "email", + "type": "string", + "required": true, + "description": "The account holder's email; the confirmation link goes there." + }, + { + "name": "authority_basis", + "type": "string", + "required": true, + "description": "\"self\" if the holder is signing up themselves, \"authorized_agent\" if you act for them." + } + ] + }, + { + "name": "kinetic.signup_status", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/signup-attempts/{id}", + "public": true + }, + "description": "Check a signup attempt: pending_verification until the holder clicks the emailed link, then verified, then completed (or expired). Free.", + "params": [ + { + "name": "id", + "type": "string", + "required": true, + "description": "The attempt id from kinetic.signup_start.", + "in": "path" + }, + { + "name": "attempt_token", + "type": "string", + "required": true, + "description": "The attempt_token from kinetic.signup_start." + } + ] + }, + { + "name": "kinetic.signup_complete", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/signup-attempts/{id}/complete", + "public": true + }, + "description": "Create the account once signup_status is verified. Pass terms_accepted=true ONLY with the account holder's actual consent to Kinetic's terms. The holder then creates an API token under Integrations and you save it with kinetic.set_key. Free.", + "params": [ + { + "name": "id", + "type": "string", + "required": true, + "description": "The attempt id.", + "in": "path" + }, + { + "name": "attempt_token", + "type": "string", + "required": true, + "description": "The attempt_token from kinetic.signup_start." + }, + { + "name": "terms_accepted", + "type": "boolean", + "required": true, + "description": "Must be true, and only with the holder's consent." + } + ] + } + ], + "listing": { + "display_name": "Kinetic Pricing", + "tagline": "Decide what to charge, using answers from your own customers rather than a guess.", + "app_description": "**Bring your own Kinetic token.** Create an API access token under Integrations on kineticpricing.com and save it once with `kinetic.set_key`. No account yet? `kinetic.signup_start` emails the account holder a confirmation link; after they confirm, `kinetic.signup_complete` creates the account. `method_list`, `method_recommend`, `offer_list` and the research library work without a token.\n\nKinetic Pricing runs real pricing research end to end: choose the method that fits the decision, draft and preview the survey, send a link to your own customers, and read results computed by an analysis engine rather than asserted by a model.\n\n**Pick the right method or the answer is worthless.** `method_recommend` takes the decision in plain language and returns the method that answers it. Van Westendorp finds an acceptable price *range*; Gabor-Granger tests specific price *points* and produces a demand curve; MaxDiff ranks which features carry value; choice-based conjoint models trade-offs. Asking the wrong one produces a confident, useless number.\n\n**Everything up to launch is free.** Drafting, editing, regenerating the survey and previewing it cost nothing, and preview stores no respondent evidence. Launching costs $149-$199 depending on method, and that fee is paid by the account holder on Stripe's hosted page. `study_checkout_create` returns the link and explicitly does not complete the purchase.\n\n**You supply the respondents.** Kinetic has no panel. `respondent_link_get` gives a link to send to your own customers or prospects, so the quality of the answer depends on who you send it to.\n\n**The numbers are computed, not narrated.** `results_get` returns what the analysis engine calculated, `evidence_get` traces a number back to the answers behind it, and `narrative_generate` writes an explanation *from* those results. Check `study_quality_get` before believing any of it, since speeders and straight-liners produce clean-looking nonsense, and check `study_progress_get` against the method's recommended minimum sample.\n\nAlso included: pricing-page teardowns, Kinetic's published research, and CSV export.", + "license": "Proprietary", + "homepage": "https://kineticpricing.com", + "source_url": "https://kineticpricing.com", + "categories": [ + "business", + "analytics", + "research" + ], + "keywords": [ + "pricing", + "willingness to pay", + "van westendorp", + "gabor granger", + "maxdiff", + "conjoint", + "market research", + "saas pricing" + ] + }, + "vendor": { + "name": "Kinetic Pricing", + "url": "https://kineticpricing.com", + "contact": "support@kineticpricing.com", + "agent_usage": "An agent starts with kinetic.method_recommend, describing the pricing decision in plain language, because Van Westendorp and Gabor-Granger answer different questions. It creates a free draft with kinetic.study_create, or kinetic.study_prefill from a product description, edits it with kinetic.study_update, and reads it exactly as a respondent will with kinetic.survey_preview, which stores no evidence. Launching costs money: kinetic.offer_list gives the price, kinetic.study_checkout_create returns a Stripe link a human must open and pay, and only then does kinetic.study_launch succeed. The agent hands the link from kinetic.respondent_link_get to its human to distribute, since Kinetic supplies no panel. It polls kinetic.study_progress_get until the recommended minimum sample is cleared, checks kinetic.study_quality_get for speeders and straight-liners, then reads kinetic.results_get for the deterministic numbers, kinetic.evidence_get to trace them, and kinetic.narrative_generate for an explanation written from those results.", + "capabilities": "method_list and method_recommend: choose the study that actually answers the pricing decision.\nstudy_create, study_prefill, study_update, study_duplicate: build and edit a draft, free until launch.\nsurvey_preview and survey_regenerate: read the survey as a respondent will, storing no evidence.\noffer_list and study_checkout_create: see the price and get a Stripe link a human pays; the call does not complete the purchase.\nstudy_launch and study_close: take a paid study live and finalize it.\nrespondent_link_get: the link you send to your own customers, since there is no panel.\nstudy_progress_get and study_quality_get: sample progress, and the speeder and straight-liner signals that decide whether results mean anything.\nresults_get, evidence_get, narrative_generate, results_export_csv: computed results, the answers behind them, a written explanation, and CSV.\nteardown_create and research_list: pricing-page teardowns and Kinetic's published research." + } +} \ No newline at end of file From 641a9f46a639c9ce2a32551f66ff65f9da8def11 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:40:40 +0200 Subject: [PATCH 04/11] io.pilot.docker: macOS bundles that drive the engine you already run Docker shipped Linux bundles only, so every macOS install was refused. macOS can't run dockerd, so the darwin assets carry the official static docker CLI and a dockerctl that finds the running engine (Docker Desktop, OrbStack, colima, Rancher Desktop or /var/run/docker.sock). engine_start reports what it found or how to start one; engine_stop is a no-op. The Linux bundles are unchanged, and the version stays 29.6.1 to match upstream Docker. Co-Authored-By: Claude Opus 5.5 (1M context) --- submissions/io.pilot.docker/submission.json | 28 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/submissions/io.pilot.docker/submission.json b/submissions/io.pilot.docker/submission.json index ade4111..279b60f 100644 --- a/submissions/io.pilot.docker/submission.json +++ b/submissions/io.pilot.docker/submission.json @@ -1,7 +1,7 @@ { "id": "io.pilot.docker", "version": "29.6.1", - "description": "Docker 29.6.1 as a native CLI for agents (Linux): delivers the Docker Engine (dockerd + containerd + runc) and the docker CLI, sha-pinned. Start a local engine (docker.engine_start), then pull images and run containers \u2014 run/ps/images/pull/logs, plus build, exec, networks, volumes, and any docker command via a verbatim-argv passthrough. Real containers on a real Linux host, no Docker Desktop.", + "description": "Docker 29.6.1 as a native CLI for agents. On Linux it delivers the Docker Engine (dockerd + containerd + runc) and the docker CLI, sha-pinned. Start a local engine (docker.engine_start), then pull images and run containers \u2014 run/ps/images/pull/logs, plus build, exec, networks, volumes, and any docker command via a verbatim-argv passthrough. Real containers on a real Linux host, no Docker Desktop. On macOS it ships the docker CLI and drives the engine you already run (Docker Desktop, OrbStack, colima).", "email": "apps@pilotprotocol.network", "backend": { "type": "cli", @@ -21,7 +21,7 @@ "methods": [ { "name": "docker.engine_start", - "description": "Start a local Docker Engine (dockerd) on this Linux host, daemonized, on a private unix socket under `DOCKER_DIR` (default /tmp/pilot-docker); waits until the API is ready. Subsequent docker methods auto-target this socket. Requires root (dockerd manages namespaces/cgroups). This is the bundled `dockerd` with `--data-root/--exec-root` under DOCKER_DIR.", + "description": "Linux: start a local Docker Engine (dockerd) on this host, daemonized, on a private unix socket under `DOCKER_DIR` (default /tmp/pilot-docker); waits until the API is ready. Subsequent docker methods auto-target this socket. Requires root (dockerd manages namespaces/cgroups). This is the bundled `dockerd` with `--data-root/--exec-root` under DOCKER_DIR. On macOS there is no local dockerd: this reports the engine it found (Docker Desktop, OrbStack, colima) or how to start one.", "latency": "slow", "cli": { "args": [ @@ -173,8 +173,8 @@ ], "listing": { "display_name": "Docker", - "tagline": "Run Docker from an agent \u2014 a local Docker Engine + CLI on Linux, real containers", - "app_description": "# Docker (Engine + CLI) \u2014 native CLI for agents (Linux)\n\nThis app installs the official **Docker 29.6.1** static distribution on a Linux host and fronts it as typed\nmethods. The bundle carries the full **Docker Engine** \u2014 `dockerd`, `containerd`, `runc`, `containerd-shim-runc-v2`,\n`docker-proxy`, `docker-init`, `ctr` \u2014 plus the `docker` CLI, each sha-pinned and staged at install. A small\n`dockerctl` wrapper manages the engine lifecycle and fronts the CLI.\n\n## Linux only\n\nDocker containers require Linux kernel features (namespaces, cgroups, overlayfs) \u2014 there is **no native macOS\n`dockerd`** (Docker Desktop runs the engine inside a hidden Linux VM). This app therefore targets **Linux (amd64 +\narm64)**. `docker.engine_start` runs a real daemon and requires **root** (the pilot host must run as root, e.g. in a\ncontainer or VM). To use Docker against an existing daemon instead, set `DOCKER_HOST` and skip `engine_start`.\n\n## The usual flow\n\n1. **Start the engine:** `docker.engine_start` `{}` \u2014 boots `dockerd` on a private socket under `DOCKER_DIR`\n (default `/tmp/pilot-docker`), waits until the API is ready.\n2. **Pull + run:** `docker.pull` `{ \"image\": \"hello-world\" }`, then `docker.run` `{ \"image\": \"hello-world\" }`.\n3. **Inspect:** `docker.ps`, `docker.images`, `docker.logs`, `docker.info`.\n4. **Anything else:** `docker.exec` `{ \"args\": [\"run\",\"-d\",\"-p\",\"8080:80\",\"nginx\"] }` \u2014 any docker command with any flags.\n5. **Stop:** `docker.engine_stop`.\n\n## Methods\n\n- `docker.engine_start` / `docker.engine_stop` \u2014 local Docker Engine lifecycle (root).\n- `docker.version`, `docker.info` \u2014 client/server versions and system info.\n- `docker.ps`, `docker.images`, `docker.logs` \u2014 inspect containers/images/logs.\n- `docker.pull` / `docker.run` \u2014 pull an image / run a container (`--rm`).\n- `docker.exec` \u2014 the docker CLI with a verbatim argv (+ optional stdin): run with any flags, `build`, `exec`,\n networks, volumes, compose plugins, etc.\n- `docker.cli_help` \u2014 the full `docker --help`. `docker.help` \u2014 the self-describing method list.\n\n## Configuration\n\n- **`DOCKER_DIR`** (env) \u2014 where dockerd keeps its socket, data-root, exec-root, pidfile, and log\n (default `/tmp/pilot-docker`).\n- **`DOCKER_HOST`** (env) \u2014 point the CLI at an existing/remote daemon instead of the bundled one\n (`tcp://host:2375` or `unix:///path`); when set, skip `docker.engine_start`.\n- **Root** \u2014 `dockerd` needs root and kernel container support. Works on a Linux host/VM/privileged container where\n the pilot daemon runs as root; not on a restricted, capability-stripped sandbox.\n- **Storage driver** \u2014 defaults to `overlay2`; pass an alternative via `docker.exec`\n (`{\"args\":[\"engine-start\",\"--storage-driver\",\"vfs\"]}`) on filesystems where overlay2 is unavailable.\n\n## Good to know\n\n- Free and open source (Apache-2.0). Binaries are the official Docker static release, repackaged unmodified.\n- Output returns verbatim; on a non-zero exit the reply is `{stdout, stderr, exit}`.\n\n## docker --help\n```\nDocker CLI \u2014 commands and options\n=================================\n\nDocker runs applications in containers. This app delivers the Docker Engine\n(dockerd + containerd + runc) and the docker CLI. Start a local engine with\n'engine-start', then use any docker command.\n\nUsage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers\n\nCommon Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Authenticate to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n\nManagement Commands:\n builder Manage builds\n compose* Docker Compose\n container Manage containers\n context Manage contexts\n image Manage images\n manifest Manage Docker image manifests and manifest lists\n network Manage networks\n plugin Manage plugins\n system Manage Docker\n volume Manage volumes\n\nSwarm Commands:\n swarm Manage Swarm\n\nCommands:\n attach Attach local standard input, output, and error streams to a running container\n commit Create a new image from a container's changes\n cp Copy files/folders between a container and the local filesystem\n create Create a new container\n diff Inspect changes to files or directories on a container's filesystem\n events Get real time events from the server\n export Export a container's filesystem as a tar archive\n history Show the history of an image\n import Import the contents from a tarball to create a filesystem image\n inspect Return low-level information on Docker objects\n kill Kill one or more running containers\n load Load an image from a tar archive or STDIN\n logs Fetch the logs of a container\n pause Pause all processes within one or more containers\n port List port mappings or a specific mapping for the container\n rename Rename a container\n restart Restart one or more containers\n rm Remove one or more containers\n rmi Remove one or more images\n save Save one or more images to a tar archive (streamed to STDOUT by default)\n start Start one or more stopped containers\n stats Display a live stream of container(s) resource usage statistics\n stop Stop one or more running containers\n tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE\n top Display the running processes of a container\n unpause Unpause all processes within one or more containers\n update Update configuration of one or more containers\n wait Block until one or more containers stop, then print their exit codes\n\nGlobal Options:\n --config string Location of client config files (default\n \"/Users/alexgodo/.docker\")\n -c, --context string Name of the context to use to connect to the\n daemon (overrides DOCKER_HOST env var and\n default context set with \"docker context use\")\n -D, --debug Enable debug mode\n -H, --host string Daemon socket to connect to\n -l, --log-level string Set the logging level (\"debug\", \"info\",\n \"warn\", \"error\", \"fatal\") (default \"info\")\n --tls Use TLS; implied by --tlsverify\n --tlscacert string Trust certs signed only by this CA (default\n \"/Users/alexgodo/.docker/ca.pem\")\n --tlscert string Path to TLS certificate file (default\n \"/Users/alexgodo/.docker/cert.pem\")\n --tlskey string Path to TLS key file (default\n \"/Users/alexgodo/.docker/key.pem\")\n --tlsverify Use TLS and verify the remote\n -v, --version Print version information and quit\n\nRun 'docker COMMAND --help' for more information on a command.\n\nFor more help on how to use Docker, head to https://docs.docker.com/go/guides/\n```\n", + "tagline": "Run Docker from an agent \u2014 a local Docker Engine on Linux, or your existing engine on macOS", + "app_description": "# Docker (Engine + CLI) \u2014 native CLI for agents (Linux + macOS)\n\nThis app installs the official **Docker 29.6.1** static distribution and fronts it as typed\nmethods. The bundle carries the full **Docker Engine** \u2014 `dockerd`, `containerd`, `runc`, `containerd-shim-runc-v2`,\n`docker-proxy`, `docker-init`, `ctr` \u2014 plus the `docker` CLI, each sha-pinned and staged at install. A small\n`dockerctl` wrapper manages the engine lifecycle and fronts the CLI.\n\n## Linux and macOS\n\nDocker containers require Linux kernel features (namespaces, cgroups, overlayfs), so the **Linux (amd64 + arm64)**\nbundle carries the full engine: `docker.engine_start` runs a real daemon and requires **root** (the pilot host must run\nas root, e.g. in a container or VM). There is **no native macOS `dockerd`**, so the **macOS (arm64 + amd64)** bundle ships\nonly the official static `docker` CLI and targets the engine this Mac already runs: Docker Desktop, OrbStack, colima,\nRancher Desktop or `/var/run/docker.sock`. On macOS `docker.engine_start` reports which engine it found (or how to\nstart one) and `docker.engine_stop` is a no-op. On either OS, set `DOCKER_HOST` to use a specific or remote daemon.\n\n## The usual flow\n\n1. **Start the engine:** `docker.engine_start` `{}` \u2014 boots `dockerd` on a private socket under `DOCKER_DIR`\n (default `/tmp/pilot-docker`), waits until the API is ready.\n2. **Pull + run:** `docker.pull` `{ \"image\": \"hello-world\" }`, then `docker.run` `{ \"image\": \"hello-world\" }`.\n3. **Inspect:** `docker.ps`, `docker.images`, `docker.logs`, `docker.info`.\n4. **Anything else:** `docker.exec` `{ \"args\": [\"run\",\"-d\",\"-p\",\"8080:80\",\"nginx\"] }` \u2014 any docker command with any flags.\n5. **Stop:** `docker.engine_stop`.\n\n## Methods\n\n- `docker.engine_start` / `docker.engine_stop` \u2014 local Docker Engine lifecycle (root).\n- `docker.version`, `docker.info` \u2014 client/server versions and system info.\n- `docker.ps`, `docker.images`, `docker.logs` \u2014 inspect containers/images/logs.\n- `docker.pull` / `docker.run` \u2014 pull an image / run a container (`--rm`).\n- `docker.exec` \u2014 the docker CLI with a verbatim argv (+ optional stdin): run with any flags, `build`, `exec`,\n networks, volumes, compose plugins, etc.\n- `docker.cli_help` \u2014 the full `docker --help`. `docker.help` \u2014 the self-describing method list.\n\n## Configuration\n\n- **`DOCKER_DIR`** (env) \u2014 where dockerd keeps its socket, data-root, exec-root, pidfile, and log\n (default `/tmp/pilot-docker`).\n- **`DOCKER_HOST`** (env) \u2014 point the CLI at an existing/remote daemon instead of the bundled one\n (`tcp://host:2375` or `unix:///path`); when set, skip `docker.engine_start`.\n- **Root** \u2014 `dockerd` needs root and kernel container support. Works on a Linux host/VM/privileged container where\n the pilot daemon runs as root; not on a restricted, capability-stripped sandbox.\n- **Storage driver** \u2014 defaults to `overlay2`; pass an alternative via `docker.exec`\n (`{\"args\":[\"engine-start\",\"--storage-driver\",\"vfs\"]}`) on filesystems where overlay2 is unavailable.\n\n## Good to know\n\n- Free and open source (Apache-2.0). Binaries are the official Docker static release, repackaged unmodified.\n- Output returns verbatim; on a non-zero exit the reply is `{stdout, stderr, exit}`.\n\n## docker --help\n```\nDocker CLI \u2014 commands and options\n=================================\n\nDocker runs applications in containers. This app delivers the Docker Engine\n(dockerd + containerd + runc) and the docker CLI. Start a local engine with\n'engine-start', then use any docker command.\n\nUsage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers\n\nCommon Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Authenticate to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n\nManagement Commands:\n builder Manage builds\n compose* Docker Compose\n container Manage containers\n context Manage contexts\n image Manage images\n manifest Manage Docker image manifests and manifest lists\n network Manage networks\n plugin Manage plugins\n system Manage Docker\n volume Manage volumes\n\nSwarm Commands:\n swarm Manage Swarm\n\nCommands:\n attach Attach local standard input, output, and error streams to a running container\n commit Create a new image from a container's changes\n cp Copy files/folders between a container and the local filesystem\n create Create a new container\n diff Inspect changes to files or directories on a container's filesystem\n events Get real time events from the server\n export Export a container's filesystem as a tar archive\n history Show the history of an image\n import Import the contents from a tarball to create a filesystem image\n inspect Return low-level information on Docker objects\n kill Kill one or more running containers\n load Load an image from a tar archive or STDIN\n logs Fetch the logs of a container\n pause Pause all processes within one or more containers\n port List port mappings or a specific mapping for the container\n rename Rename a container\n restart Restart one or more containers\n rm Remove one or more containers\n rmi Remove one or more images\n save Save one or more images to a tar archive (streamed to STDOUT by default)\n start Start one or more stopped containers\n stats Display a live stream of container(s) resource usage statistics\n stop Stop one or more running containers\n tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE\n top Display the running processes of a container\n unpause Unpause all processes within one or more containers\n update Update configuration of one or more containers\n wait Block until one or more containers stop, then print their exit codes\n\nGlobal Options:\n --config string Location of client config files (default\n \"/Users/alexgodo/.docker\")\n -c, --context string Name of the context to use to connect to the\n daemon (overrides DOCKER_HOST env var and\n default context set with \"docker context use\")\n -D, --debug Enable debug mode\n -H, --host string Daemon socket to connect to\n -l, --log-level string Set the logging level (\"debug\", \"info\",\n \"warn\", \"error\", \"fatal\") (default \"info\")\n --tls Use TLS; implied by --tlsverify\n --tlscacert string Trust certs signed only by this CA (default\n \"/Users/alexgodo/.docker/ca.pem\")\n --tlscert string Path to TLS certificate file (default\n \"/Users/alexgodo/.docker/cert.pem\")\n --tlskey string Path to TLS key file (default\n \"/Users/alexgodo/.docker/key.pem\")\n --tlsverify Use TLS and verify the remote\n -v, --version Print version information and quit\n\nRun 'docker COMMAND --help' for more information on a command.\n\nFor more help on how to use Docker, head to https://docs.docker.com/go/guides/\n```\n", "license": "Apache-2.0", "homepage": "https://www.docker.com", "source_url": "https://github.com/moby/moby", @@ -224,6 +224,26 @@ "unpack": "tar.gz", "exec_path": "docker-29.6.1-linux-arm64/bin/dockerctl", "order": 1 + }, + { + "role": "binary", + "os": "darwin", + "arch": "arm64", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/darwin-arm64/docker-29.6.1-darwin-arm64.tar.gz", + "sha256": "a935d8c4eece263fda2c561d9f20a8b1ee3e8c222271e3f4a80fc2be0f7d0b77", + "unpack": "tar.gz", + "exec_path": "docker-29.6.1-darwin-arm64/bin/dockerctl", + "order": 1 + }, + { + "role": "binary", + "os": "darwin", + "arch": "amd64", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/darwin-amd64/docker-29.6.1-darwin-amd64.tar.gz", + "sha256": "9c6902523bcbf32e32c8f3f0ca2ce9627f4f571b68f3d695c078848ddbd98709", + "unpack": "tar.gz", + "exec_path": "docker-29.6.1-darwin-amd64/bin/dockerctl", + "order": 1 } ], "product_demo": { From c80cb383912f7ebdcb6f1d845fabc4097c714b5c Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:42:39 +0200 Subject: [PATCH 05/11] io.pilot.deadsimple 0.1.1: return the provisioned inbox from signup deadsimple.signup answered "address": "" because it read the default data.address; agent-signup returns the inbox at data.inbox.email. Agents had to call list_inboxes to learn the address they had just been given. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../io.pilot.deadsimple/submission.json | 796 ++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100644 submissions/io.pilot.deadsimple/submission.json diff --git a/submissions/io.pilot.deadsimple/submission.json b/submissions/io.pilot.deadsimple/submission.json new file mode 100644 index 0000000..28ff9f5 --- /dev/null +++ b/submissions/io.pilot.deadsimple/submission.json @@ -0,0 +1,796 @@ +{ + "product_demo": { + "skill": "io.pilot.deadsimple", + "title": "Full usage demo", + "when_to_use": "When your agent needs an email address of its own: to sign itself up for a third-party service and read the code back, or to send and receive real mail as part of a task.", + "metered": false, + "quickstart": { + "goal": "Get an account, an API key and a live inbox in one call", + "command": "pilotctl appstore call io.pilot.deadsimple deadsimple.signup '{\"label\":\"my-agent\"}'", + "expect": "{\"data\":{\"account_id\":\"...\",\"api_key\":\"dse_...\",\"inbox\":{\"inbox_id\":\"...\",\"email\":\"...@box1.deadsimple.email\"},\"plan\":{\"plan\":\"trial\",\"inbox_limit\":1,\"sends_per_hour\":10}}}", + "note": "Everything is under data. Save data.api_key as DEADSIMPLE_API_KEY \u2014 it is shown once. Already have a key? Skip this and call create_inbox." + }, + "examples": [ + { + "title": "Sign up for a third-party service and read the code back", + "goal": "The whole point: clear an email verification without a human", + "command": "pilotctl appstore call io.pilot.deadsimple deadsimple.get_verification_code '{\"inbox_id\":\"\",\"since\":\"2026-08-06T12:00:00Z\",\"from_contains\":\"stripe.com\"}'", + "expect": "{\"data\":{\"found\":true,\"code\":\"481920\",\"magic_link\":null,\"message_id\":\"...\"}}", + "note": "Note the time BEFORE submitting your address, pass it as `since`, then poll every 2-3s. found=false just means it has not landed yet. Never parse the raw email yourself." + }, + { + "title": "Send a real email", + "command": "pilotctl appstore call io.pilot.deadsimple deadsimple.send_email '{\"inbox_id\":\"\",\"to\":\"someone@example.com\",\"subject\":\"Report ready\",\"text_body\":\"The run finished.\"}'", + "expect": "{\"data\":{\"message_id\":\"...\",\"status\":\"queued\"}}", + "note": "DKIM-signed egress, not a sandbox. Trial caps this at 10/hour, 25/day." + }, + { + "title": "Read a conversation before replying", + "goal": "Get the whole thread, then reply on it with threading handled for you", + "command": "pilotctl appstore call io.pilot.deadsimple deadsimple.get_thread '{\"inbox_id\":\"\",\"thread_id\":\"\"}'", + "expect": "{\"data\":{\"thread_id\":\"...\",\"messages\":[{\"message_id\":\"...\",\"from\":\"...\",\"subject\":\"...\"}]}}", + "note": "Then reply with the message_id; threading headers are set for you." + } + ], + "gotchas": [ + "Every response is wrapped: the payload is under data, with meta.request_id beside it.", + "Call signup only when you have no key. It is idempotent per Idempotency-Key, so a retry returns the same account, not a second one.", + "Trial is 1 inbox, 10 sends/hour, 25/day. A 429 trial_send_limit_exceeded is a QUOTA signal, not transient \u2014 do not retry it.", + "Lift the caps with claim then claim_verify. Your key, inboxes and history survive.", + "get_verification_code needs since from BEFORE you triggered the mail, or you may read a stale code." + ], + "next": [ + "io.pilot.deadsimple deadsimple.help '{}'" + ] + }, + "next_steps": { + "schema": 1, + "app": "io.pilot.deadsimple", + "edges": [ + { + "from": "*", + "on": "err", + "code": 401, + "why": "no API key is configured yet, so the account has to be created before anything else will authenticate", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.deadsimple deadsimple.signup '{\"label\":\"my-agent\"}'", + "why": "one call returns an account, an API key and a live inbox; save the key as DEADSIMPLE_API_KEY", + "kind": "gateway" + } + ] + }, + { + "from": "deadsimple.signup", + "on": "ok", + "why": "you now have a key and an inbox, so the address is ready to hand to whatever needs to mail you", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.deadsimple deadsimple.get_verification_code '{\"inbox_id\":\"\",\"since\":\"\"}'", + "why": "the usual reason to want an inbox: read the one-time code out of the newest inbound mail", + "kind": "flow" + } + ] + }, + { + "from": "deadsimple.get_verification_code", + "on": "ok", + "match": "\"found\"\\s*:\\s*false", + "why": "the mail has not arrived yet, which is expected for the first few seconds after triggering it", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.deadsimple deadsimple.get_verification_code '{\"inbox_id\":\"\",\"since\":\"\"}'", + "why": "poll every 2-3 seconds for up to a minute with the SAME since value; do not widen it or you may read a stale code", + "kind": "recovery" + } + ] + }, + { + "from": "deadsimple.send_email", + "on": "err", + "code": 429, + "why": "the trial send cap is reached; this is a quota signal, so retrying the same call will keep failing", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.deadsimple deadsimple.claim '{\"email\":\"\"}'", + "why": "claim then claim_verify moves the account to the Free plan and keeps your key, inboxes and history", + "kind": "recovery" + } + ] + } + ] + }, + "id": "io.pilot.deadsimple", + "version": "0.1.1", + "namespace": "deadsimple", + "description": "An email identity your agent can provision for itself in one call, then use to send, receive, reply on-thread, and pull one-time signup codes straight out of its own inbox.", + "email": "hello@deadsimple.email", + "backend": { + "base_url": "https://api.deadsimple.email", + "auth": "byo", + "headers": [ + { + "name": "Authorization", + "value": "Bearer ${DEADSIMPLE_API_KEY}" + } + ] + }, + "methods": [ + { + "name": "deadsimple.signup", + "description": "START HERE if you have no key. Provisions a Dead Simple account, an API key and a live inbox in one call, with no human, no dashboard and no verification email. Returns {account_id, api_key, inbox}. Save api_key as the DEADSIMPLE_API_KEY secret \u2014 every other method authenticates with it. Idempotent per Idempotency-Key, so a retry after a dropped connection returns the same account rather than a second one. The account starts on the trial tier: 1 inbox, 10 sends an hour, 25 a day.", + "latency": "med", + "signup": { + "step": "create", + "url": "https://api.deadsimple.email/v1/auth/agent-signup", + "secret_key": "DEADSIMPLE_API_KEY", + "address_path": "data.inbox.email" + }, + "params": [ + { + "name": "label", + "type": "string", + "required": false, + "description": "Optional human-readable label for the account, e.g. the agent or project name.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.claim", + "description": "Start lifting the trial caps by attaching an email address a human controls. Sends a 6-digit code to that address; pass it to deadsimple.claim_verify to finish. Your existing API key, inboxes and message history are untouched by the upgrade.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/auth/claim" + }, + "params": [ + { + "name": "email", + "type": "string", + "required": true, + "description": "An email address a human controls. This is the human owner of record, not one of your own inboxes.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.claim_verify", + "description": "Confirm the code from deadsimple.claim and move the account from trial to the Free plan (5 inboxes, 5,000 emails a month). The same API key keeps working.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/auth/claim/verify" + }, + "params": [ + { + "name": "email", + "type": "string", + "required": true, + "description": "The same address passed to deadsimple.claim.", + "in": "body" + }, + { + "name": "code", + "type": "string", + "required": true, + "description": "The 6-digit code delivered to that address.", + "in": "body" + }, + { + "name": "password", + "type": "string", + "required": false, + "description": "Optional password to set for the human-facing account.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.create_inbox", + "description": "Create a real, deliverable email inbox in one call. Returns an inbox_id and a live address that can send and receive immediately \u2014 no SMTP setup, no DNS, no mailbox provisioning. Use this when you already have a key and want an additional identity.", + "latency": "fast", + "http": { + "verb": "POST", + "path": "/v1/inboxes" + }, + "params": [ + { + "name": "local_part", + "type": "string", + "required": false, + "description": "The part before the @. Omit to have one generated.", + "in": "body" + }, + { + "name": "domain", + "type": "string", + "required": false, + "description": "Custom domain to use instead of deadsimple.email. Paid plans only.", + "in": "body" + }, + { + "name": "display_name", + "type": "string", + "required": false, + "description": "Display name recipients see on outbound mail.", + "in": "body" + }, + { + "name": "tags", + "type": "object", + "required": false, + "description": "Free-form key/value tags for grouping inboxes, e.g. {\"task\":\"signup-flow\"}.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.list_inboxes", + "description": "List the inboxes this key can see, newest first. Cursor-paginated: pass the cursor from the previous response for the next page.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes" + }, + "params": [ + { + "name": "cursor", + "type": "string", + "required": false, + "description": "Cursor from the previous response.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Page size.", + "in": "query" + }, + { + "name": "status", + "type": "string", + "required": false, + "description": "Filter by inbox status.", + "in": "query" + }, + { + "name": "tag", + "type": "string", + "required": false, + "description": "Filter by a tag set at creation.", + "in": "query" + } + ] + }, + { + "name": "deadsimple.get_inbox", + "description": "Fetch one inbox: its address, display name, tags, status, and counters.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox id.", + "in": "path" + } + ] + }, + { + "name": "deadsimple.delete_inbox", + "description": "Permanently delete an inbox and its stored messages. IRREVERSIBLE. Use when a throwaway identity is finished so it stops counting against the inbox quota.", + "latency": "fast", + "http": { + "verb": "DELETE", + "path": "/v1/inboxes/{inbox_id}" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox id to delete.", + "in": "path" + } + ] + }, + { + "name": "deadsimple.send_email", + "description": "Send an email from one of your inboxes. Plain text or HTML, cc/bcc, base64 attachments, scheduled send, and optional open/click tracking. Real DKIM-signed egress, not a test harness. On the trial tier this is capped at 10 an hour and 25 a day; a 429 with code trial_send_limit_exceeded is a quota signal, not a transient failure.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/inboxes/{inbox_id}/messages" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox to send from.", + "in": "path" + }, + { + "name": "to", + "type": "string", + "required": true, + "description": "Recipient address, or a list of them.", + "in": "body" + }, + { + "name": "subject", + "type": "string", + "required": true, + "description": "Subject line.", + "in": "body" + }, + { + "name": "text_body", + "type": "string", + "required": false, + "description": "Plain-text body. Supply this or html_body.", + "in": "body" + }, + { + "name": "html_body", + "type": "string", + "required": false, + "description": "HTML body. Supply this or text_body.", + "in": "body" + }, + { + "name": "cc", + "type": "object", + "required": false, + "description": "CC recipients.", + "in": "body" + }, + { + "name": "bcc", + "type": "object", + "required": false, + "description": "BCC recipients.", + "in": "body" + }, + { + "name": "reply_to", + "type": "string", + "required": false, + "description": "Reply-To address.", + "in": "body" + }, + { + "name": "attachments", + "type": "object", + "required": false, + "description": "Base64-encoded attachments.", + "in": "body" + }, + { + "name": "send_at", + "type": "string", + "required": false, + "description": "ISO timestamp to schedule the send.", + "in": "body" + }, + { + "name": "tracking", + "type": "bool", + "required": false, + "description": "Enable open and click tracking on HTML mail.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.get_verification_code", + "description": "THE method agents reach for. Pulls the one-time code or magic link straight out of the newest inbound message, so a signup or 2FA prompt can be cleared without parsing an email body. Non-blocking: returns found=false if nothing has arrived, so poll every 2-3 seconds for up to a minute after triggering the mail. Set `since` to a timestamp taken BEFORE you triggered it, and `from_contains` to the sender domain, so a stale code is never returned.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/verification" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox to read.", + "in": "path" + }, + { + "name": "since", + "type": "string", + "required": false, + "description": "ISO timestamp taken BEFORE triggering the mail. Anything older is ignored, which is how stale codes are avoided.", + "in": "query" + }, + { + "name": "from_contains", + "type": "string", + "required": false, + "description": "Only consider mail whose sender contains this substring, e.g. 'stripe.com'.", + "in": "query" + }, + { + "name": "window", + "type": "int", + "required": false, + "description": "How many seconds back to look.", + "in": "query" + } + ] + }, + { + "name": "deadsimple.list_messages", + "description": "List messages in an inbox, newest first, with sender, subject, snippet, and labels. Spam is excluded unless include_spam is true. Cursor-paginated.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/messages" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox to list.", + "in": "path" + }, + { + "name": "cursor", + "type": "string", + "required": false, + "description": "Cursor from the previous response.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Page size.", + "in": "query" + }, + { + "name": "labels", + "type": "string", + "required": false, + "description": "Filter by label.", + "in": "query" + }, + { + "name": "include_spam", + "type": "bool", + "required": false, + "description": "Include spam-classified mail.", + "in": "query" + } + ] + }, + { + "name": "deadsimple.get_message", + "description": "Get one message in full: headers, plain-text and HTML bodies, and attachment metadata.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/messages/{message_id}" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the message is in.", + "in": "path" + }, + { + "name": "message_id", + "type": "string", + "required": true, + "description": "The message id.", + "in": "path" + } + ] + }, + { + "name": "deadsimple.reply", + "description": "Reply to the sender of a message. Threading headers are set automatically so the reply lands in the same conversation \u2014 never hand-build In-Reply-To.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/inboxes/{inbox_id}/messages/{message_id}/reply" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the message is in.", + "in": "path" + }, + { + "name": "message_id", + "type": "string", + "required": true, + "description": "The message being replied to.", + "in": "path" + }, + { + "name": "text_body", + "type": "string", + "required": false, + "description": "Plain-text reply body.", + "in": "body" + }, + { + "name": "html_body", + "type": "string", + "required": false, + "description": "HTML reply body.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.reply_all", + "description": "Reply to the sender and every other recipient of a message, with threading headers set automatically.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/inboxes/{inbox_id}/messages/{message_id}/reply-all" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the message is in.", + "in": "path" + }, + { + "name": "message_id", + "type": "string", + "required": true, + "description": "The message being replied to.", + "in": "path" + }, + { + "name": "text_body", + "type": "string", + "required": false, + "description": "Plain-text reply body.", + "in": "body" + }, + { + "name": "html_body", + "type": "string", + "required": false, + "description": "HTML reply body.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.forward", + "description": "Forward a message, with its attachments, to new recipients.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/inboxes/{inbox_id}/messages/{message_id}/forward" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the message is in.", + "in": "path" + }, + { + "name": "message_id", + "type": "string", + "required": true, + "description": "The message to forward.", + "in": "path" + }, + { + "name": "to", + "type": "object", + "required": true, + "description": "Recipients to forward to.", + "in": "body" + }, + { + "name": "text_body", + "type": "string", + "required": false, + "description": "Optional note to prepend.", + "in": "body" + } + ] + }, + { + "name": "deadsimple.list_threads", + "description": "List conversation threads in an inbox, each with its latest message, so an agent can track ongoing exchanges instead of loose messages.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/threads" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox to list threads for.", + "in": "path" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Page size.", + "in": "query" + } + ] + }, + { + "name": "deadsimple.get_thread", + "description": "Get a full conversation thread with every message in order \u2014 the context an agent needs before replying.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/threads/{thread_id}" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the thread is in.", + "in": "path" + }, + { + "name": "thread_id", + "type": "string", + "required": true, + "description": "The thread id.", + "in": "path" + } + ] + }, + { + "name": "deadsimple.get_attachment", + "description": "Get a time-limited signed download URL for an attachment. The link expires in one hour.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/inboxes/{inbox_id}/messages/{message_id}/attachments/{attachment_id}" + }, + "params": [ + { + "name": "inbox_id", + "type": "string", + "required": true, + "description": "The inbox the message is in.", + "in": "path" + }, + { + "name": "message_id", + "type": "string", + "required": true, + "description": "The message the attachment is on.", + "in": "path" + }, + { + "name": "attachment_id", + "type": "string", + "required": true, + "description": "The attachment id from get_message.", + "in": "path" + } + ] + }, + { + "name": "deadsimple.list_all_messages", + "description": "List messages across every inbox this key can see, newest first. For a supervisor agent watching many identities at once \u2014 cheaper than iterating inboxes.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/messages" + }, + "params": [ + { + "name": "cursor", + "type": "string", + "required": false, + "description": "Cursor from the previous response.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Page size.", + "in": "query" + }, + { + "name": "direction", + "type": "string", + "required": false, + "description": "Filter to inbound or outbound mail.", + "in": "query" + } + ] + }, + { + "name": "deadsimple.create_webhook", + "description": "Register an HMAC-SHA256 signed webhook for inbound mail, bounces, and complaints, so long-running work does not have to poll list_messages in a loop. Deliveries retry on exponential backoff and every attempt is logged.", + "latency": "fast", + "http": { + "verb": "POST", + "path": "/v1/webhooks" + }, + "params": [ + { + "name": "url", + "type": "string", + "required": true, + "description": "HTTPS endpoint to deliver to.", + "in": "body" + }, + { + "name": "events", + "type": "object", + "required": true, + "description": "Event types to subscribe to, e.g. [\"message.received\"].", + "in": "body" + }, + { + "name": "description", + "type": "string", + "required": false, + "description": "Free-form note about what this webhook is for.", + "in": "body" + } + ] + } + ], + "listing": { + "display_name": "Dead Simple Email", + "tagline": "An email identity your agent provisions for itself, then uses to send, receive, and read its own signup codes.", + "app_description": "Dead Simple Email gives an agent a real, deliverable email identity \u2014 not a sandbox \u2014 and it can get one entirely on its own. A single call to `deadsimple.signup` returns an account, an API key and a live inbox. No dashboard, no verification email, no human in the loop.\n\n**The method agents reach for most is `deadsimple.get_verification_code`.** It pulls the one-time code or magic link straight out of the newest inbound message, so an agent can sign itself up for a third-party service or clear a 2FA prompt without ever parsing an email body. Filter by sender and by timestamp so a stale code is never returned. This is what turns \"the provider emails you a code\" from a dead end into a step.\n\nFrom there the agent sends, replies on-thread, reply-alls, forwards, reads full messages with headers and attachments, and walks whole conversation threads over one REST API.\n\nUnderneath is real sending infrastructure: DKIM-signed egress through a dedicated MTA, bounce and complaint handling, suppression lists, and optional open and click tracking. Inbound mail can be pushed to HMAC-SHA256 signed webhooks that retry on exponential backoff. A supervisor agent can watch every inbox in a workspace at once with `list_all_messages`, and inboxes can be created in bulk, tagged, and torn down when a throwaway identity is finished.\n\n**Tiers.** A self-provisioned agent account starts on trial: 1 inbox, 10 sends an hour, 25 a day. Calling `deadsimple.claim` with an email a human controls, then `deadsimple.claim_verify`, moves it to the Free plan \u2014 5 inboxes and 5,000 emails a month \u2014 keeping the same API key, inboxes and history. Paid plans scale to 500 inboxes, 25 custom domains, and 1,000 requests a minute.\n\n**Edge cases.** A 429 with code `trial_send_limit_exceeded` is a quota signal, not a transient failure \u2014 do not blindly retry, claim the account instead. Pagination is cursor-based. Spam is excluded from `list_messages` unless `include_spam` is true. Delete throwaway inboxes when finished so they stop counting against the quota.", + "license": "Proprietary", + "homepage": "https://deadsimple.email", + "source_url": "https://deadsimple.email", + "categories": [ + "communications", + "email" + ], + "keywords": [ + "email", + "inbox", + "otp", + "verification code", + "magic link", + "agent onboarding", + "transactional email", + "threads", + "attachments", + "webhooks" + ] + }, + "vendor": { + "name": "Dead Simple Email", + "url": "https://deadsimple.email", + "contact": "hello@deadsimple.email", + "agent_usage": "An agent with no key calls deadsimple.signup, which returns an account, an API key and a live inbox in one call with no human step, and caches the key as DEADSIMPLE_API_KEY. The usual next move is to hand the inbox address to a third-party service, note the current time, and poll deadsimple.get_verification_code with `since` set to that timestamp until found=true, which yields the one-time code or magic link without parsing any email. Beyond onboarding, the agent sends with send_email, reads with list_messages and get_message, walks conversations with get_thread, and answers with reply or reply_all, which set threading headers automatically. Trial caps (1 inbox, 10 sends/hour, 25/day) surface as a 429 with code trial_send_limit_exceeded, which is a quota signal rather than a transient error; deadsimple.claim plus claim_verify lifts them to the Free plan while keeping the same key and history.", + "capabilities": "signup: provision an account, API key and inbox in one call, with no human, no dashboard and no verification email.\nget_verification_code: extract a one-time code or magic link from the newest inbound mail, filtered by sender and timestamp so a stale code is never returned.\nsend_email: DKIM-signed sending with plain text or HTML, cc/bcc, base64 attachments, scheduled send, and open/click tracking.\nInbox lifecycle: create, list, inspect and delete real deliverable inboxes, with tags for grouping.\nReading: list messages with sender, subject, snippet and labels; fetch a full message with headers, bodies and attachment metadata; download attachments via a signed one-hour URL.\nConversations: list and read whole threads, then reply, reply-all or forward with threading handled automatically.\nSupervision: list messages across every inbox at once, for an agent watching many identities.\nWebhooks: HMAC-signed delivery for inbound mail, bounces and complaints, with retry and replay.\nPlan upgrade: claim and claim_verify move a trial account to the Free plan without changing the API key." + } +} From 2132b3605134c7cedce050670f837165c16ea6c4 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:53:05 +0200 Subject: [PATCH 06/11] io.pilot.aegis 0.1.4: track upstream, add Intel Mac bundles The app shipped AEGIS 0.1.3 for darwin/arm64 and linux only, so Intel Macs could not install it, and upstream has since released 0.1.4 (adversarial hardening). 0.1.4's official release binaries cover all four platforms; they are repackaged unmodified (Linux builds are static) on the artifact registry. Co-Authored-By: Claude Opus 5.5 (1M context) --- submissions/io.pilot.aegis/submission.json | 43 ++++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/submissions/io.pilot.aegis/submission.json b/submissions/io.pilot.aegis/submission.json index f7af49d..7548d07 100644 --- a/submissions/io.pilot.aegis/submission.json +++ b/submissions/io.pilot.aegis/submission.json @@ -1,6 +1,6 @@ { "id": "io.pilot.aegis", - "version": "0.1.3", + "version": "0.1.4", "description": "AEGIS is a runtime firewall for AI agents: scan files, commands, and tool results for prompt injection, jailbreaks, homoglyph/leetspeak obfuscation, and impersonation before your agent acts on them. An 880 KB, fully-offline Rust binary — L1 Aho-Corasick pattern matching (microseconds) plus an optional local judge model. 90% recall / 95% precision on a held-out set.", "email": "apps@pilotprotocol.network", "backend": { @@ -133,41 +133,52 @@ "url": "https://aegis.pilotprotocol.network", "contact": "apps@pilotprotocol.network", "agent_usage": "Scan untrusted content before acting on it: aegis.scan for files/dirs; aegis.exec {\"args\":[\"scan-cmd\"],\"stdin\":...} as a PreToolUse gate (exit 0 allow / 2 block) and {\"args\":[\"scan-result\"],\"stdin\":...} as a PostToolUse check. aegis.status tails the audit log; aegis.targets/aegis.config introspect. Fully offline.", - "capabilities": "Delivers the AEGIS 0.1.3 runtime firewall as a sha-pinned, relocatable per-OS/arch bundle. L1 Aho-Corasick patterns (pure Rust, microseconds) plus an optional local Qwen3-1.7B judge (llama.cpp, offline). 90% recall / 95% precision on a held-out set. HMAC-chained audit log. MIT." + "capabilities": "Delivers the AEGIS 0.1.4 runtime firewall as a sha-pinned, relocatable per-OS/arch bundle. L1 Aho-Corasick patterns (pure Rust, microseconds) plus an optional local Qwen3-1.7B judge (llama.cpp, offline). 90% recall / 95% precision on a held-out set. HMAC-chained audit log. MIT." }, "artifacts": [ { "role": "binary", "os": "darwin", "arch": "arm64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.3/darwin-arm64/aegis-0.1.3-darwin-arm64.tar.gz", - "sha256": "dc20fc8c70aa0516d5fd503709a5c3c1e7f91964591bfa378c8bfe946db85b01", - "size": 448175, + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.4/darwin-arm64/aegis-0.1.4-darwin-arm64.tar.gz", + "sha256": "5fc1037ab60e20d26640809d39d012d9d41087ab5faab4e2cc85ede974847bdf", + "size": 468831, "unpack": "tar.gz", - "exec_path": "aegis-0.1.3-darwin-arm64/bin/aegis", + "exec_path": "aegis-0.1.4-darwin-arm64/bin/aegis", + "order": 1 + }, + { + "role": "binary", + "os": "darwin", + "arch": "amd64", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.4/darwin-amd64/aegis-0.1.4-darwin-amd64.tar.gz", + "sha256": "4556f59f2fe6f01ec370a3e26eff6b799c1f364b442db15102e5960483710e3b", + "size": 527693, + "unpack": "tar.gz", + "exec_path": "aegis-0.1.4-darwin-amd64/bin/aegis", "order": 1 }, { "role": "binary", "os": "linux", "arch": "amd64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.3/linux-amd64/aegis-0.1.3-linux-amd64.tar.gz", - "sha256": "5cb700460a4072894429fa0f90038d9d597fac0b9e160d7c1090a04ad3ea3745", - "size": 571791, + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.4/linux-amd64/aegis-0.1.4-linux-amd64.tar.gz", + "sha256": "47642a275ffe35eec5e5150274937c3c5bbb95263d8a61a65a6e9773fd24e47f", + "size": 608346, "unpack": "tar.gz", - "exec_path": "aegis-0.1.3-linux-amd64/bin/aegis", - "order": 2 + "exec_path": "aegis-0.1.4-linux-amd64/bin/aegis", + "order": 1 }, { "role": "binary", "os": "linux", "arch": "arm64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.3/linux-arm64/aegis-0.1.3-linux-arm64.tar.gz", - "sha256": "1fd9156ef23bb755965d9d6511f6ba2345bc409233dd010acb20be0ea956b857", - "size": 505315, + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.aegis/0.1.4/linux-arm64/aegis-0.1.4-linux-arm64.tar.gz", + "sha256": "b3564ada80f6c3a0118c402eabcf1c08e0e9430a4a3ffde7d8464858c0f154b9", + "size": 538648, "unpack": "tar.gz", - "exec_path": "aegis-0.1.3-linux-arm64/bin/aegis", - "order": 3 + "exec_path": "aegis-0.1.4-linux-arm64/bin/aegis", + "order": 1 } ], "product_demo": { From 1cb1b7f7ae7fef8c896a844a5f6a09fd4e775a35 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 16:55:13 +0200 Subject: [PATCH 07/11] io.pilot.docker 29.8.1: use a running engine before starting one Upstream Docker 29.8.1, all four platforms. engine_start used to go straight to the bundled dockerd, which needs root, while the standard installer runs the pilot daemon as a normal user, so it failed on ordinary Linux hosts even when Docker was already running. It now uses a running engine first (system or rootless Docker, DOCKER_HOST, or on macOS Docker Desktop, OrbStack or colima), starts the bundled dockerd only as root, and otherwise says how to get an engine. engine_stop leaves engines it didn't start alone. The help text no longer embeds the builder's home directory, and the tarballs carry no macOS extended attributes. Co-Authored-By: Claude Opus 5.5 (1M context) --- submissions/io.pilot.docker/submission.json | 44 ++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/submissions/io.pilot.docker/submission.json b/submissions/io.pilot.docker/submission.json index 279b60f..32615f9 100644 --- a/submissions/io.pilot.docker/submission.json +++ b/submissions/io.pilot.docker/submission.json @@ -1,7 +1,7 @@ { "id": "io.pilot.docker", - "version": "29.6.1", - "description": "Docker 29.6.1 as a native CLI for agents. On Linux it delivers the Docker Engine (dockerd + containerd + runc) and the docker CLI, sha-pinned. Start a local engine (docker.engine_start), then pull images and run containers \u2014 run/ps/images/pull/logs, plus build, exec, networks, volumes, and any docker command via a verbatim-argv passthrough. Real containers on a real Linux host, no Docker Desktop. On macOS it ships the docker CLI and drives the engine you already run (Docker Desktop, OrbStack, colima).", + "version": "29.8.1", + "description": "Docker 29.8.1 as a native CLI for agents. On Linux it delivers the Docker Engine (dockerd + containerd + runc) and the docker CLI, sha-pinned. Start a local engine (docker.engine_start), then pull images and run containers \u2014 run/ps/images/pull/logs, plus build, exec, networks, volumes, and any docker command via a verbatim-argv passthrough. Real containers on a real Linux host, no Docker Desktop. On macOS it ships the docker CLI and drives the engine you already run (Docker Desktop, OrbStack, colima).", "email": "apps@pilotprotocol.network", "backend": { "type": "cli", @@ -21,7 +21,7 @@ "methods": [ { "name": "docker.engine_start", - "description": "Linux: start a local Docker Engine (dockerd) on this host, daemonized, on a private unix socket under `DOCKER_DIR` (default /tmp/pilot-docker); waits until the API is ready. Subsequent docker methods auto-target this socket. Requires root (dockerd manages namespaces/cgroups). This is the bundled `dockerd` with `--data-root/--exec-root` under DOCKER_DIR. On macOS there is no local dockerd: this reports the engine it found (Docker Desktop, OrbStack, colima) or how to start one.", + "description": "Make a Docker engine available. If one is already running \u2014 a system or rootless Docker on Linux; Docker Desktop, OrbStack or colima on macOS; or DOCKER_HOST \u2014 it is used as-is and reported. Otherwise, on Linux, it starts the bundled dockerd on a private unix socket under `DOCKER_DIR` (default /tmp/pilot-docker) and waits until the API is ready; that needs root, and without root it says how to get an engine. macOS has no local dockerd, so there it only reports the engine it found or how to start one. Subsequent docker methods target the same engine.", "latency": "slow", "cli": { "args": [ @@ -31,7 +31,7 @@ }, { "name": "docker.engine_stop", - "description": "Stop the local dockerd started by docker.engine_start.", + "description": "Stop the local dockerd started by docker.engine_start. Only stops a dockerd this app started; a system, rootless or desktop engine is left running.", "latency": "med", "cli": { "args": [ @@ -174,7 +174,7 @@ "listing": { "display_name": "Docker", "tagline": "Run Docker from an agent \u2014 a local Docker Engine on Linux, or your existing engine on macOS", - "app_description": "# Docker (Engine + CLI) \u2014 native CLI for agents (Linux + macOS)\n\nThis app installs the official **Docker 29.6.1** static distribution and fronts it as typed\nmethods. The bundle carries the full **Docker Engine** \u2014 `dockerd`, `containerd`, `runc`, `containerd-shim-runc-v2`,\n`docker-proxy`, `docker-init`, `ctr` \u2014 plus the `docker` CLI, each sha-pinned and staged at install. A small\n`dockerctl` wrapper manages the engine lifecycle and fronts the CLI.\n\n## Linux and macOS\n\nDocker containers require Linux kernel features (namespaces, cgroups, overlayfs), so the **Linux (amd64 + arm64)**\nbundle carries the full engine: `docker.engine_start` runs a real daemon and requires **root** (the pilot host must run\nas root, e.g. in a container or VM). There is **no native macOS `dockerd`**, so the **macOS (arm64 + amd64)** bundle ships\nonly the official static `docker` CLI and targets the engine this Mac already runs: Docker Desktop, OrbStack, colima,\nRancher Desktop or `/var/run/docker.sock`. On macOS `docker.engine_start` reports which engine it found (or how to\nstart one) and `docker.engine_stop` is a no-op. On either OS, set `DOCKER_HOST` to use a specific or remote daemon.\n\n## The usual flow\n\n1. **Start the engine:** `docker.engine_start` `{}` \u2014 boots `dockerd` on a private socket under `DOCKER_DIR`\n (default `/tmp/pilot-docker`), waits until the API is ready.\n2. **Pull + run:** `docker.pull` `{ \"image\": \"hello-world\" }`, then `docker.run` `{ \"image\": \"hello-world\" }`.\n3. **Inspect:** `docker.ps`, `docker.images`, `docker.logs`, `docker.info`.\n4. **Anything else:** `docker.exec` `{ \"args\": [\"run\",\"-d\",\"-p\",\"8080:80\",\"nginx\"] }` \u2014 any docker command with any flags.\n5. **Stop:** `docker.engine_stop`.\n\n## Methods\n\n- `docker.engine_start` / `docker.engine_stop` \u2014 local Docker Engine lifecycle (root).\n- `docker.version`, `docker.info` \u2014 client/server versions and system info.\n- `docker.ps`, `docker.images`, `docker.logs` \u2014 inspect containers/images/logs.\n- `docker.pull` / `docker.run` \u2014 pull an image / run a container (`--rm`).\n- `docker.exec` \u2014 the docker CLI with a verbatim argv (+ optional stdin): run with any flags, `build`, `exec`,\n networks, volumes, compose plugins, etc.\n- `docker.cli_help` \u2014 the full `docker --help`. `docker.help` \u2014 the self-describing method list.\n\n## Configuration\n\n- **`DOCKER_DIR`** (env) \u2014 where dockerd keeps its socket, data-root, exec-root, pidfile, and log\n (default `/tmp/pilot-docker`).\n- **`DOCKER_HOST`** (env) \u2014 point the CLI at an existing/remote daemon instead of the bundled one\n (`tcp://host:2375` or `unix:///path`); when set, skip `docker.engine_start`.\n- **Root** \u2014 `dockerd` needs root and kernel container support. Works on a Linux host/VM/privileged container where\n the pilot daemon runs as root; not on a restricted, capability-stripped sandbox.\n- **Storage driver** \u2014 defaults to `overlay2`; pass an alternative via `docker.exec`\n (`{\"args\":[\"engine-start\",\"--storage-driver\",\"vfs\"]}`) on filesystems where overlay2 is unavailable.\n\n## Good to know\n\n- Free and open source (Apache-2.0). Binaries are the official Docker static release, repackaged unmodified.\n- Output returns verbatim; on a non-zero exit the reply is `{stdout, stderr, exit}`.\n\n## docker --help\n```\nDocker CLI \u2014 commands and options\n=================================\n\nDocker runs applications in containers. This app delivers the Docker Engine\n(dockerd + containerd + runc) and the docker CLI. Start a local engine with\n'engine-start', then use any docker command.\n\nUsage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers\n\nCommon Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Authenticate to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n\nManagement Commands:\n builder Manage builds\n compose* Docker Compose\n container Manage containers\n context Manage contexts\n image Manage images\n manifest Manage Docker image manifests and manifest lists\n network Manage networks\n plugin Manage plugins\n system Manage Docker\n volume Manage volumes\n\nSwarm Commands:\n swarm Manage Swarm\n\nCommands:\n attach Attach local standard input, output, and error streams to a running container\n commit Create a new image from a container's changes\n cp Copy files/folders between a container and the local filesystem\n create Create a new container\n diff Inspect changes to files or directories on a container's filesystem\n events Get real time events from the server\n export Export a container's filesystem as a tar archive\n history Show the history of an image\n import Import the contents from a tarball to create a filesystem image\n inspect Return low-level information on Docker objects\n kill Kill one or more running containers\n load Load an image from a tar archive or STDIN\n logs Fetch the logs of a container\n pause Pause all processes within one or more containers\n port List port mappings or a specific mapping for the container\n rename Rename a container\n restart Restart one or more containers\n rm Remove one or more containers\n rmi Remove one or more images\n save Save one or more images to a tar archive (streamed to STDOUT by default)\n start Start one or more stopped containers\n stats Display a live stream of container(s) resource usage statistics\n stop Stop one or more running containers\n tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE\n top Display the running processes of a container\n unpause Unpause all processes within one or more containers\n update Update configuration of one or more containers\n wait Block until one or more containers stop, then print their exit codes\n\nGlobal Options:\n --config string Location of client config files (default\n \"/Users/alexgodo/.docker\")\n -c, --context string Name of the context to use to connect to the\n daemon (overrides DOCKER_HOST env var and\n default context set with \"docker context use\")\n -D, --debug Enable debug mode\n -H, --host string Daemon socket to connect to\n -l, --log-level string Set the logging level (\"debug\", \"info\",\n \"warn\", \"error\", \"fatal\") (default \"info\")\n --tls Use TLS; implied by --tlsverify\n --tlscacert string Trust certs signed only by this CA (default\n \"/Users/alexgodo/.docker/ca.pem\")\n --tlscert string Path to TLS certificate file (default\n \"/Users/alexgodo/.docker/cert.pem\")\n --tlskey string Path to TLS key file (default\n \"/Users/alexgodo/.docker/key.pem\")\n --tlsverify Use TLS and verify the remote\n -v, --version Print version information and quit\n\nRun 'docker COMMAND --help' for more information on a command.\n\nFor more help on how to use Docker, head to https://docs.docker.com/go/guides/\n```\n", + "app_description": "# Docker (Engine + CLI) \u2014 native CLI for agents (Linux + macOS)\n\nThis app installs the official **Docker 29.8.1** static distribution and fronts it as typed\nmethods. The bundle carries the full **Docker Engine** \u2014 `dockerd`, `containerd`, `runc`, `containerd-shim-runc-v2`,\n`docker-proxy`, `docker-init`, `ctr` \u2014 plus the `docker` CLI, each sha-pinned and staged at install. A small\n`dockerctl` wrapper manages the engine lifecycle and fronts the CLI.\n\n## Linux and macOS\n\n`docker.engine_start` first looks for an engine that is already running and uses it as-is: a system or rootless\nDocker on Linux; Docker Desktop, OrbStack, colima or Rancher Desktop on macOS; or whatever `DOCKER_HOST` points at.\nOnly if there is none does the **Linux (amd64 + arm64)** bundle start its own engine (`dockerd` + `containerd` + `runc`),\nwhich needs **root**. Docker containers need Linux kernel features, so there is **no native macOS `dockerd`**: the\n**macOS (arm64 + amd64)** bundle ships only the official static `docker` CLI and tells you how to start an engine if none\nis running. `docker.engine_stop` only stops an engine this app started.\n\n## The usual flow\n\n1. **Start the engine:** `docker.engine_start` `{}` \u2014 boots `dockerd` on a private socket under `DOCKER_DIR`\n (default `/tmp/pilot-docker`), waits until the API is ready.\n2. **Pull + run:** `docker.pull` `{ \"image\": \"hello-world\" }`, then `docker.run` `{ \"image\": \"hello-world\" }`.\n3. **Inspect:** `docker.ps`, `docker.images`, `docker.logs`, `docker.info`.\n4. **Anything else:** `docker.exec` `{ \"args\": [\"run\",\"-d\",\"-p\",\"8080:80\",\"nginx\"] }` \u2014 any docker command with any flags.\n5. **Stop:** `docker.engine_stop`.\n\n## Methods\n\n- `docker.engine_start` / `docker.engine_stop` \u2014 local Docker Engine lifecycle (root).\n- `docker.version`, `docker.info` \u2014 client/server versions and system info.\n- `docker.ps`, `docker.images`, `docker.logs` \u2014 inspect containers/images/logs.\n- `docker.pull` / `docker.run` \u2014 pull an image / run a container (`--rm`).\n- `docker.exec` \u2014 the docker CLI with a verbatim argv (+ optional stdin): run with any flags, `build`, `exec`,\n networks, volumes, compose plugins, etc.\n- `docker.cli_help` \u2014 the full `docker --help`. `docker.help` \u2014 the self-describing method list.\n\n## Configuration\n\n- **`DOCKER_DIR`** (env) \u2014 where dockerd keeps its socket, data-root, exec-root, pidfile, and log\n (default `/tmp/pilot-docker`).\n- **`DOCKER_HOST`** (env) \u2014 point the CLI at an existing/remote daemon instead of the bundled one\n (`tcp://host:2375` or `unix:///path`); when set, skip `docker.engine_start`.\n- **Root** \u2014 `dockerd` needs root and kernel container support. Works on a Linux host/VM/privileged container where\n the pilot daemon runs as root; not on a restricted, capability-stripped sandbox.\n- **Storage driver** \u2014 defaults to `overlay2`; pass an alternative via `docker.exec`\n (`{\"args\":[\"engine-start\",\"--storage-driver\",\"vfs\"]}`) on filesystems where overlay2 is unavailable.\n\n## Good to know\n\n- Free and open source (Apache-2.0). Binaries are the official Docker static release, repackaged unmodified.\n- Output returns verbatim; on a non-zero exit the reply is `{stdout, stderr, exit}`.\n\n## docker --help\n```\nDocker CLI \u2014 commands and options\n=================================\n\nDocker runs applications in containers. This app delivers the Docker Engine\n(dockerd + containerd + runc) and the docker CLI. Start a local engine with\n'engine-start', then use any docker command.\n\nUsage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers\n\nCommon Commands:\n run Create and run a new container from an image\n exec Execute a command in a running container\n ps List containers\n build Build an image from a Dockerfile\n pull Download an image from a registry\n push Upload an image to a registry\n images List images\n login Authenticate to a registry\n logout Log out from a registry\n search Search Docker Hub for images\n version Show the Docker version information\n info Display system-wide information\n\nManagement Commands:\n builder Manage builds\n compose* Docker Compose\n container Manage containers\n context Manage contexts\n image Manage images\n manifest Manage Docker image manifests and manifest lists\n network Manage networks\n plugin Manage plugins\n system Manage Docker\n volume Manage volumes\n\nSwarm Commands:\n swarm Manage Swarm\n\nCommands:\n attach Attach local standard input, output, and error streams to a running container\n commit Create a new image from a container's changes\n cp Copy files/folders between a container and the local filesystem\n create Create a new container\n diff Inspect changes to files or directories on a container's filesystem\n events Get real time events from the server\n export Export a container's filesystem as a tar archive\n history Show the history of an image\n import Import the contents from a tarball to create a filesystem image\n inspect Return low-level information on Docker objects\n kill Kill one or more running containers\n load Load an image from a tar archive or STDIN\n logs Fetch the logs of a container\n pause Pause all processes within one or more containers\n port List port mappings or a specific mapping for the container\n rename Rename a container\n restart Restart one or more containers\n rm Remove one or more containers\n rmi Remove one or more images\n save Save one or more images to a tar archive (streamed to STDOUT by default)\n start Start one or more stopped containers\n stats Display a live stream of container(s) resource usage statistics\n stop Stop one or more running containers\n tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE\n top Display the running processes of a container\n unpause Unpause all processes within one or more containers\n update Update configuration of one or more containers\n wait Block until one or more containers stop, then print their exit codes\n\nGlobal Options:\n --config string Location of client config files (default\n \"~/.docker\")\n -c, --context string Name of the context to use to connect to the\n daemon (overrides DOCKER_HOST env var and\n default context set with \"docker context use\")\n -D, --debug Enable debug mode\n -H, --host string Daemon socket to connect to\n -l, --log-level string Set the logging level (\"debug\", \"info\",\n \"warn\", \"error\", \"fatal\") (default \"info\")\n --tls Use TLS; implied by --tlsverify\n --tlscacert string Trust certs signed only by this CA (default\n \"~/.docker/ca.pem\")\n --tlscert string Path to TLS certificate file (default\n \"~/.docker/cert.pem\")\n --tlskey string Path to TLS key file (default\n \"~/.docker/key.pem\")\n --tlsverify Use TLS and verify the remote\n -v, --version Print version information and quit\n\nRun 'docker COMMAND --help' for more information on a command.\n\nFor more help on how to use Docker, head to https://docs.docker.com/go/guides/\n```\n", "license": "Apache-2.0", "homepage": "https://www.docker.com", "source_url": "https://github.com/moby/moby", @@ -202,54 +202,54 @@ "url": "https://pilotprotocol.network", "contact": "apps@pilotprotocol.network", "agent_usage": "On a Linux host with root, agents call docker.engine_start to boot a local dockerd, then docker.pull/docker.run/docker.ps/docker.images/docker.logs and docker.exec (any docker command with a verbatim argv, incl. build/exec/networks/volumes). Set DOCKER_HOST to target an existing daemon instead. Output is text, or {stdout,stderr,exit} on a non-zero exit.", - "capabilities": "Delivers the official Docker 29.6.1 static Engine+CLI (dockerd, containerd, runc, containerd-shim-runc-v2, docker-proxy, docker-init, ctr, docker) as a sha-pinned per-arch Linux bundle with a dockerctl wrapper that manages the daemon lifecycle and fronts the CLI. Run real OCI containers on a Linux host/VM/privileged container. Apache-2.0; repackaged unmodified." + "capabilities": "Delivers the official Docker 29.8.1 static Engine+CLI (dockerd, containerd, runc, containerd-shim-runc-v2, docker-proxy, docker-init, ctr, docker) as a sha-pinned per-arch Linux bundle with a dockerctl wrapper that manages the daemon lifecycle and fronts the CLI. Run real OCI containers on a Linux host/VM/privileged container. Apache-2.0; repackaged unmodified." }, "artifacts": [ { "role": "binary", "os": "linux", "arch": "amd64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/linux-amd64/docker-29.6.1-linux-amd64.tar.gz", - "sha256": "af67328c2d617401f22c108b2ebe5f06e6f59978c50a3f2c07d5f0041216172d", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.8.1/linux-amd64/docker-29.8.1-linux-amd64.tar.gz", + "sha256": "5f283010469955c586ea8b3f2c8a0762d7ded7c2d2611f244eff4e50a303e158", "unpack": "tar.gz", - "exec_path": "docker-29.6.1-linux-amd64/bin/dockerctl", + "exec_path": "docker-29.8.1-linux-amd64/bin/dockerctl", "order": 1 }, { "role": "binary", "os": "linux", "arch": "arm64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/linux-arm64/docker-29.6.1-linux-arm64.tar.gz", - "sha256": "c16db13a77f40fd9af7fe5520cb1bd9c78b5c2ab2d6e1749863683dce957f572", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.8.1/linux-arm64/docker-29.8.1-linux-arm64.tar.gz", + "sha256": "e332d971c224eede2a5f421eb064999fa177c3393e4b29f77388509d663cb1b7", "unpack": "tar.gz", - "exec_path": "docker-29.6.1-linux-arm64/bin/dockerctl", + "exec_path": "docker-29.8.1-linux-arm64/bin/dockerctl", "order": 1 }, { "role": "binary", "os": "darwin", "arch": "arm64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/darwin-arm64/docker-29.6.1-darwin-arm64.tar.gz", - "sha256": "a935d8c4eece263fda2c561d9f20a8b1ee3e8c222271e3f4a80fc2be0f7d0b77", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.8.1/darwin-arm64/docker-29.8.1-darwin-arm64.tar.gz", + "sha256": "085781ff82e76cbbf91202d793a21671f9d0569f66f4fd499b4ba67ad63e6f35", "unpack": "tar.gz", - "exec_path": "docker-29.6.1-darwin-arm64/bin/dockerctl", + "exec_path": "docker-29.8.1-darwin-arm64/bin/dockerctl", "order": 1 }, { "role": "binary", "os": "darwin", "arch": "amd64", - "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.6.1/darwin-amd64/docker-29.6.1-darwin-amd64.tar.gz", - "sha256": "9c6902523bcbf32e32c8f3f0ca2ce9627f4f571b68f3d695c078848ddbd98709", + "url": "https://pub-2328865fa11041b8a5efba00b940ec14.r2.dev/io.pilot.docker/29.8.1/darwin-amd64/docker-29.8.1-darwin-amd64.tar.gz", + "sha256": "54966b446ae2d2fe6a4c9dab2c95bd8443a2f23ffef3a3bcc7f56bb2b1d48277", "unpack": "tar.gz", - "exec_path": "docker-29.6.1-darwin-amd64/bin/dockerctl", + "exec_path": "docker-29.8.1-darwin-amd64/bin/dockerctl", "order": 1 } ], "product_demo": { "skill": "io.pilot.docker", "title": "Full usage demo", - "when_to_use": "When you need to run real OCI containers on a Linux host \u2014 pull images and run/build/exec containers via a local Docker Engine \u2014 without Docker Desktop.", + "when_to_use": "When you need to run real OCI containers from an agent \u2014 pull images and run/build/exec containers \u2014 on a Linux host (bundled engine) or a Mac with Docker Desktop, OrbStack or colima.", "metered": false, "quickstart": { "goal": "Boot a local Docker Engine", @@ -286,7 +286,7 @@ ], "gotchas": [ "LINUX-ONLY: there is no native macOS dockerd (Docker Desktop hides a Linux VM), so this app cannot start an engine on macOS.", - "docker.engine_start needs root \u2014 dockerd manages namespaces/cgroups; run on a Linux host/VM/privileged container.", + "docker.engine_start uses a running engine if there is one; starting the bundled dockerd needs root on Linux, and macOS has no local dockerd.", "Call docker.engine_start once before other methods, or set DOCKER_HOST to target an existing daemon and skip it.", "docker.run uses --rm and the image's default command; for flags, ports, detached, or build/exec use docker.exec.", "On a non-zero exit the reply is {stdout, stderr, exit}." @@ -307,7 +307,7 @@ "then": [ { "cmd": "pilotctl appstore call io.pilot.docker docker.engine_start '{}'", - "why": "boot the local engine once, then retry \u2014 needs root, and Linux only (there is no native macOS dockerd)", + "why": "finds a running engine or, on Linux as root, boots the bundled one; then retry", "kind": "gateway" } ] @@ -320,7 +320,7 @@ "then": [ { "cmd": "pilotctl appstore call io.pilot.docker docker.engine_start '{}'", - "why": "boot the local engine once, then retry \u2014 needs root, and Linux only (there is no native macOS dockerd)", + "why": "finds a running engine or, on Linux as root, boots the bundled one; then retry", "kind": "gateway" } ] From 355ec7af56b06b74aafc05736914771911f705f8 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 17:12:25 +0200 Subject: [PATCH 08/11] io.pilot.rentahuman 0.3.0: bring-your-own key on the public API 0.2.0 was built on Rent A Human's partner API through a managed broker entry that was never registered (no master key; partner access is enterprise-gated), so every call failed with "unknown app". 0.3.0 goes back to the surface the vendor submitted: the public REST API at rentahuman.ai/api with a self-serve X-API-Key. - rentahuman.set_key saves the user's rah_ key; calls made before it return a hint naming set_key instead of a 401 - search_humans, get_human, get_reviews, browse_services, availability and the new list_bounties are keyless - hiring, booking, escrow and QA methods are the vendor's, unchanged - new demo and next-steps graph (the vendor spec had neither) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../io.pilot.rentahuman/submission.json | 733 ++++++++++++++++++ 1 file changed, 733 insertions(+) create mode 100644 submissions/io.pilot.rentahuman/submission.json diff --git a/submissions/io.pilot.rentahuman/submission.json b/submissions/io.pilot.rentahuman/submission.json new file mode 100644 index 0000000..35fd23c --- /dev/null +++ b/submissions/io.pilot.rentahuman/submission.json @@ -0,0 +1,733 @@ +{ + "id": "io.pilot.rentahuman", + "version": "0.3.0", + "namespace": "rentahuman", + "description": "Hire real people for physical-world tasks and recurring human QA with your own Rent A Human API key: search humans, book services, hire with escrow, and run QA of a URL.", + "email": "support@rentahuman.ai", + "backend": { + "type": "http", + "base_url": "https://rentahuman.ai/api", + "auth": "byo", + "headers": [ + { + "name": "X-API-Key", + "value": "${RENTAHUMAN_API_KEY}" + } + ] + }, + "methods": [ + { + "name": "rentahuman.set_key", + "latency": "fast", + "signup": { + "step": "set_key", + "url": "https://rentahuman.ai/account/api-keys", + "secret_key": "RENTAHUMAN_API_KEY" + }, + "description": "Save the Rent A Human API key this host uses (format rah_ followed by 32 hex characters). Create one at https://rentahuman.ai/account/api-keys. It is stored in $APP/secrets.json and sent as X-API-Key on every call; calling again replaces it. Local, free. Needed for booking, hiring, escrow and QA; search and browse work without it.", + "params": [ + { + "name": "api_key", + "type": "string", + "required": true, + "description": "Your rah_... key from rentahuman.ai/account/api-keys." + } + ] + }, + { + "name": "rentahuman.search_humans", + "description": "Find available humans by skill, name, hourly rate, city, or country.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/humans", + "public": true + }, + "params": [ + { + "name": "skill", + "type": "string", + "required": false, + "description": "Skill filter.", + "in": "query" + }, + { + "name": "name", + "type": "string", + "required": false, + "description": "Case-insensitive name filter.", + "in": "query" + }, + { + "name": "minRate", + "type": "number", + "required": false, + "description": "Minimum hourly rate in USD.", + "in": "query" + }, + { + "name": "maxRate", + "type": "number", + "required": false, + "description": "Maximum hourly rate in USD.", + "in": "query" + }, + { + "name": "city", + "type": "string", + "required": false, + "description": "City filter.", + "in": "query" + }, + { + "name": "country", + "type": "string", + "required": false, + "description": "Country name or ISO code.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Maximum results; default 50, max 200.", + "in": "query" + }, + { + "name": "offset", + "type": "int", + "required": false, + "description": "Pagination offset.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.get_human", + "description": "Get a public human profile with skills, availability, rate, services, and identity signals.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/humans/{humanId}", + "public": true + }, + "params": [ + { + "name": "humanId", + "type": "string", + "required": true, + "description": "Human profile ID.", + "in": "path" + } + ] + }, + { + "name": "rentahuman.get_reviews", + "description": "Read a human's reviews before hiring or booking.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/reviews", + "public": true + }, + "params": [ + { + "name": "humanId", + "type": "string", + "required": true, + "description": "Human profile ID.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Maximum results; default 50, max 100.", + "in": "query" + }, + { + "name": "cursor", + "type": "string", + "required": false, + "description": "Pagination cursor.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.browse_services", + "description": "Browse bookable human services with provider details, pricing, and duration.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/services/browse", + "public": true + }, + "params": [ + { + "name": "search", + "type": "string", + "required": false, + "description": "Search services.", + "in": "query" + }, + { + "name": "category", + "type": "string", + "required": false, + "description": "Service category.", + "in": "query" + }, + { + "name": "sort", + "type": "string", + "required": false, + "description": "Sort order.", + "in": "query" + }, + { + "name": "limit", + "type": "int", + "required": false, + "description": "Results per page.", + "in": "query" + }, + { + "name": "page", + "type": "int", + "required": false, + "description": "Page number.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.get_service_availability", + "description": "Get booked slots for a human on a date before choosing a service time.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/services/bookings", + "public": true + }, + "params": [ + { + "name": "humanId", + "type": "string", + "required": true, + "description": "Human profile ID.", + "in": "query" + }, + { + "name": "date", + "type": "string", + "required": true, + "description": "Date in YYYY-MM-DD format.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.list_bounties", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/bounties", + "public": true + }, + "description": "List open bounties that agents have posted for humans, newest first. Free, no key needed.", + "params": [ + { + "name": "limit", + "type": "int", + "required": false, + "description": "Page size.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.book_service", + "description": "Reserve a listed service and return a Stripe Checkout URL for the operator to fund escrow.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/services/book" + }, + "params": [ + { + "name": "humanId", + "type": "string", + "required": true, + "description": "Human profile ID.", + "in": "body" + }, + { + "name": "serviceId", + "type": "string", + "required": true, + "description": "Service ID from browse_services.", + "in": "body" + }, + { + "name": "date", + "type": "string", + "required": true, + "description": "Date in YYYY-MM-DD, within the next 30 days.", + "in": "body" + }, + { + "name": "startTime", + "type": "string", + "required": true, + "description": "Start time in HH:mm.", + "in": "body" + } + ] + }, + { + "name": "rentahuman.rent_human", + "description": "Hire a specific human in one step; creates the task and escrow and returns a Stripe Checkout URL for standard accounts.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/escrow/agent-checkout" + }, + "params": [ + { + "name": "humanId", + "type": "string", + "required": true, + "description": "Human profile ID from search_humans.", + "in": "body" + }, + { + "name": "taskTitle", + "type": "string", + "required": true, + "description": "Task title, 5-200 characters.", + "in": "body" + }, + { + "name": "taskDescription", + "type": "string", + "required": true, + "description": "Detailed task description, at least 10 characters. Do not include private addresses or phone numbers.", + "in": "body" + }, + { + "name": "price", + "type": "number", + "required": true, + "description": "Agreed fixed price in USD, 5-10000.", + "in": "body" + }, + { + "name": "estimatedHours", + "type": "number", + "required": false, + "description": "Estimated duration in hours; minimum 0.083.", + "in": "body" + } + ] + }, + { + "name": "rentahuman.get_my_rentals", + "description": "List active and past rentals with status and the next action needed.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/escrow/agent-rentals" + }, + "params": [ + { + "name": "status", + "type": "string", + "required": false, + "description": "Optional escrow status filter.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.confirm_delivery", + "description": "Confirm that delivered work is satisfactory before payment release. Only call after operator approval.", + "latency": "fast", + "http": { + "verb": "POST", + "path": "/escrow/{escrowId}/complete" + }, + "params": [ + { + "name": "escrowId", + "type": "string", + "required": true, + "description": "Escrow ID from get_my_rentals.", + "in": "path" + } + ] + }, + { + "name": "rentahuman.release_payment", + "description": "Irreversibly release confirmed escrow funds to the worker. Require explicit operator approval immediately before calling.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/escrow/{escrowId}/release" + }, + "params": [ + { + "name": "escrowId", + "type": "string", + "required": true, + "description": "Escrow ID in completed or warranty_hold status.", + "in": "path" + }, + { + "name": "applicationId", + "type": "string", + "required": false, + "description": "Optional application binding check to prevent paying the wrong worker.", + "in": "body" + } + ] + }, + { + "name": "rentahuman.cancel_escrow", + "description": "Cancel a funding or funded escrow and refund held funds. Require operator approval before calling.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/escrow/{escrowId}/cancel" + }, + "params": [ + { + "name": "escrowId", + "type": "string", + "required": true, + "description": "Escrow ID to cancel.", + "in": "path" + } + ] + }, + { + "name": "rentahuman.open_dispute", + "description": "Freeze an eligible escrow for Rent A Human admin review when work or terms are disputed.", + "latency": "fast", + "http": { + "verb": "POST", + "path": "/escrow/{escrowId}/dispute" + }, + "params": [ + { + "name": "escrowId", + "type": "string", + "required": true, + "description": "Escrow ID to dispute.", + "in": "path" + }, + { + "name": "category", + "type": "string", + "required": true, + "description": "One of not_delivered, poor_quality, not_as_described, communication_issue, non_payment, fraud, or other.", + "in": "body" + }, + { + "name": "description", + "type": "string", + "required": true, + "description": "Detailed reason, at least 20 characters.", + "in": "body" + } + ] + }, + { + "name": "rentahuman.create_qa_run_template", + "description": "Schedule one-time or recurring human testing of a target URL and return the active QA template. Runs collect photo, video, or document evidence and produce agent-reviewed reports. Requires wallet funds.", + "latency": "med", + "http": { + "verb": "POST", + "path": "/v1/qa/templates" + }, + "params": [ + { + "name": "name", + "type": "string", + "required": true, + "description": "Template name, 3–120 characters.", + "in": "body" + }, + { + "name": "targetUrl", + "type": "string", + "required": true, + "description": "HTTP(S) product URL or journey to test.", + "in": "body" + }, + { + "name": "instructions", + "type": "string", + "required": true, + "description": "Testing instructions, 20–5,000 characters.", + "in": "body" + }, + { + "name": "testerStartMessage", + "type": "string", + "required": false, + "description": "Optional private test-only setup details sent after tester acceptance.", + "in": "body" + }, + { + "name": "cadence", + "type": "string", + "required": true, + "description": "One of once, daily, every_2_days, or weekly.", + "in": "body" + }, + { + "name": "budgetPerRunCents", + "type": "int", + "required": true, + "description": "Per-run budget ceiling in USD cents.", + "in": "body" + }, + { + "name": "payPerTesterCents", + "type": "int", + "required": true, + "description": "Pay per tester in USD cents.", + "in": "body" + }, + { + "name": "testerCount", + "type": "int", + "required": true, + "description": "Number of testers, 1–20.", + "in": "body" + }, + { + "name": "submissionMode", + "type": "string", + "required": true, + "description": "Required evidence: photo, video, or document.", + "in": "body" + }, + { + "name": "requiredCredentials", + "type": "object", + "required": false, + "description": "Optional JSON array of tester qualification strings.", + "in": "body" + }, + { + "name": "allowedCountries", + "type": "object", + "required": false, + "description": "Optional JSON array of ISO-2 country codes.", + "in": "body" + }, + { + "name": "periodCapCents", + "type": "int", + "required": true, + "description": "Monthly reservation cap; equals budgetPerRunCents when cadence is once.", + "in": "body" + } + ] + }, + { + "name": "rentahuman.list_qa_runs", + "description": "List up to 100 owned autonomous QA runs, optionally filtered by template ID, with status, budget totals, reviewed progress, and report availability.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/qa/runs" + }, + "params": [ + { + "name": "templateId", + "type": "string", + "required": false, + "description": "Optional QA template ID filter.", + "in": "query" + } + ] + }, + { + "name": "rentahuman.get_qa_run", + "description": "Get an owned autonomous QA run with status, budget totals, agent-reviewed report, finding diff, and escalation summaries.", + "latency": "fast", + "http": { + "verb": "GET", + "path": "/v1/qa/runs/{runId}" + }, + "params": [ + { + "name": "runId", + "type": "string", + "required": true, + "description": "QA run ID returned by list_qa_runs.", + "in": "path" + } + ] + } + ], + "vendor": { + "name": "Rent A Human", + "url": "https://rentahuman.ai", + "contact": "alex@rentahuman.ai", + "agent_usage": "Create an API key at https://rentahuman.ai/account/api-keys and supply it as RENTAHUMAN_API_KEY. For human QA, call create_qa_run_template with the target URL, instructions, cadence, tester count, evidence mode, and budget. Runs are wallet-funded; deposit if a low-balance warning is returned. Track them with list_qa_runs and retrieve the agent-reviewed report, finding diff, and escalations with get_qa_run. For individual work, use search_humans, get_human, and get_reviews; or browse_services, get_service_availability, and book_service. Stripe Checkout must be completed by the operator. Never confirm delivery, release payment, or cancel escrow without explicit operator approval immediately before the call. Use open_dispute for contested work.", + "capabilities": "- Schedule one-time or recurring human QA of a target URL\n- Collect photo, video, or document evidence from 1–20 testers\n- Retrieve agent-reviewed QA reports, finding diffs, and escalation summaries\n- Search available humans by skill, rate, name, city, or country\n- Inspect human profiles, identity signals, and reviews\n- Browse bookable services and check availability\n- Book a listed service with escrow-backed Stripe Checkout\n- Hire a specific human for an ad-hoc real-world task\n- Track active and past rentals with next-action guidance\n- Confirm satisfactory delivery\n- Release payment to a worker\n- Cancel an eligible escrow and refund held funds\n- Open a dispute for admin review" + }, + "listing": { + "display_name": "Rent A Human", + "tagline": "Dispatch humans for real-world tasks and recurring QA.", + "app_description": "**Bring your own Rent A Human key.** Create one at https://rentahuman.ai/account/api-keys and save it once with `rentahuman.set_key`. Searching humans, browsing services, reading reviews and listing bounties work without a key.\n\nRent A Human gives autonomous agents a typed path from digital intent to physical-world execution. Schedule one-time or recurring human QA of any URL, collect photo, video, or document evidence from 1–20 testers, and retrieve agent-reviewed reports with finding diffs. Agents can also search human profiles, inspect reviews, book services or hire a specific person, track escrow-backed work, and confirm, release, cancel, or dispute payment. Installers supply a Rent A Human API key. Wallet deposits and Stripe Checkout remain operator-controlled.", + "license": "MIT", + "homepage": "https://rentahuman.ai", + "source_url": "", + "categories": [ + "automation", + "marketplace", + "qa", + "testing", + "hiring", + "payments" + ], + "keywords": [ + "human qa", + "website testing", + "usability testing", + "humans", + "real-world tasks", + "marketplace", + "hiring", + "escrow", + "services", + "human-in-the-loop" + ] + }, + "product_demo": { + "skill": "io.pilot.rentahuman", + "title": "Full usage demo", + "metered": false, + "when_to_use": "When a task needs a real person — a local errand, an on-site check, or human QA of a website — and your agent should find, hire and pay them.", + "quickstart": { + "goal": "Find people who can do the task", + "command": "pilotctl appstore call io.pilot.rentahuman rentahuman.search_humans '{\"skill\":\"Photography\",\"city\":\"San Francisco\",\"limit\":5}'", + "expect": "{\"success\":true,\"humans\":[{\"id\":\"EbvgiotaiUFxmcPjnWtI\",\"name\":\"...\",\"headline\":\"...\"}]}", + "note": "Free, no key needed. Keep the id: every hire and booking takes a humanId." + }, + "examples": [ + { + "title": "Save your API key", + "goal": "Hiring, booking, escrow and QA need it", + "command": "pilotctl appstore call io.pilot.rentahuman rentahuman.set_key '{\"api_key\":\"rah_<32 hex>\"}'", + "expect": "{\"ok\":true,\"key_field\":\"RENTAHUMAN_API_KEY\",\"api_key\":\"saved\"}", + "note": "Create the key at rentahuman.ai/account/api-keys." + }, + { + "title": "Hire a specific person", + "goal": "One call creates the task and its escrow", + "command": "pilotctl appstore call io.pilot.rentahuman rentahuman.rent_human '{\"humanId\":\"EbvgiotaiUFxmcPjnWtI\",\"taskTitle\":\"Photograph a storefront\",\"taskDescription\":\"Take 10 daylight photos of the storefront at the address I send in chat.\",\"price\":60}'", + "expect": "{\"success\":true,\"escrowId\":\"...\",\"checkoutUrl\":\"https://checkout.stripe.com/...\"}", + "note": "Returns a Stripe Checkout URL: a human pays it to fund the escrow. Nothing is charged from the agent." + }, + { + "title": "Pay only after delivery", + "command": "pilotctl appstore call io.pilot.rentahuman rentahuman.confirm_delivery '{\"escrowId\":\"\"}'", + "expect": "{\"success\":true}", + "note": "Review the evidence first, then release_payment. cancel_escrow refunds; open_dispute if the work is wrong." + }, + { + "title": "Recurring human QA of a URL", + "command": "pilotctl appstore call io.pilot.rentahuman rentahuman.create_qa_run_template '{\"name\":\"Checkout smoke test\",\"targetUrl\":\"https://example.com/checkout\",\"instructions\":\"Buy the cheapest item with a test card and record each step.\",\"cadence\":\"weekly\",\"budgetPerRunCents\":3000,\"payPerTesterCents\":1000,\"testerCount\":2}'", + "expect": "{\"success\":true,\"template\":{\"id\":\"...\",\"status\":\"active\"}}", + "note": "Needs wallet funds on your Rent A Human account. Read results with list_qa_runs and get_qa_run." + } + ], + "gotchas": [ + "Search, browse, reviews and bounties are free and keyless; everything that books, hires or pays needs rentahuman.set_key first.", + "rent_human and book_service return a Stripe Checkout URL. A human pays it; the agent never holds card details.", + "Release payment only after reviewing the evidence: release_payment is final, cancel_escrow refunds.", + "Never put private addresses or phone numbers in task text; send them in the work conversation once someone accepts." + ], + "next": [ + "io.pilot.rentahuman rentahuman.help '{}'" + ] + }, + "next_steps": { + "schema": 1, + "app": "io.pilot.rentahuman", + "edges": [ + { + "from": "*", + "on": "ok", + "match": "\"needs_signup\"\\s*:\\s*true", + "why": "this host has no Rent A Human key yet, so hiring, booking, escrow and QA cannot run", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.set_key '{\"api_key\":\"rah_<32 hex>\"}'", + "why": "create the key at rentahuman.ai/account/api-keys, then save it here", + "kind": "gateway" + } + ] + }, + { + "from": "rentahuman.search_humans", + "on": "ok", + "why": "you have candidate humanIds; look closer before hiring anyone", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.get_human '{\"humanId\":\"\"}'", + "why": "check rate, location and skills", + "kind": "flow" + }, + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.get_reviews '{\"humanId\":\"\"}'", + "why": "read what earlier clients said", + "kind": "flow" + } + ] + }, + { + "from": "rentahuman.rent_human", + "on": "ok", + "why": "the escrow exists but is unfunded until a human pays the checkout link", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.get_my_rentals '{}'", + "why": "hand checkoutUrl to your human, then watch the rental's status", + "kind": "flow" + } + ] + }, + { + "from": "rentahuman.confirm_delivery", + "on": "ok", + "why": "delivery is confirmed; the worker is paid only when you release", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.release_payment '{\"escrowId\":\"\"}'", + "why": "release once the evidence checks out; it is final", + "kind": "flow" + } + ] + }, + { + "from": "rentahuman.create_qa_run_template", + "on": "ok", + "why": "runs happen on the cadence you set; results land as QA runs", + "then": [ + { + "cmd": "pilotctl appstore call io.pilot.rentahuman rentahuman.list_qa_runs '{\"templateId\":\"