diff --git a/src/pages/plain/publish.astro b/src/pages/plain/publish.astro
index fda90eb..a001c56 100644
--- a/src/pages/plain/publish.astro
+++ b/src/pages/plain/publish.astro
@@ -18,7 +18,7 @@ import PlainLayout from '../../layouts/PlainLayout.astro';
Email: where we send your submission confirmation and the review decision.
Identity: app id (io.pilot.<name>), version (semver), and a one-line description.
Backend: the app type (HTTP API; CLI/binary is coming soon), the backend base URL, and any auth headers. Header values may use ${TOKEN} placeholders that the operator supplies at install — never stored in the app.
-
Methods: each method an agent can call — name (<ns>.<verb>), HTTP route, latency class (fast under 5s, medium up to 15s, slow up to 1 minute), description, and parameters.
+
Methods: each method an agent can call — name (<ns>.<verb>), HTTP route (verb GET/POST/PUT/PATCH/DELETE and a path template that may contain {name} placeholders), latency class (fast under 5s, medium up to 15s, slow up to 1 minute), description, and parameters. For HTTP methods each parameter also has an "in" that says where the adapter reads it from: query (?name=value), path (fills {name} url-encoded), path_raw (fills {name} unescaped, for URL-in-path APIs), body (a JSON body field), or header. It defaults to the verb's natural location (GET → query, POST/PUT/PATCH → body).
Listing: display name, description, license, and categories for the store page.
Vendor: who you are, how autonomous agents should use the app, and the list of capabilities.
Review and submit: sign the Publisher Release Agreement (type your full legal name and check the box), then the server builds, signs, and verifies the adapter and stores it for review.
diff --git a/src/pages/publish.astro b/src/pages/publish.astro
index dcc833f..7683dcc 100644
--- a/src/pages/publish.astro
+++ b/src/pages/publish.astro
@@ -154,7 +154,13 @@ const canonicalUrl = 'https://pilotprotocol.network/publish';
#publish .params { margin-top:18px; padding-top:18px; border-top:1px solid var(--c-line); }
#publish .phead { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
#publish .ptable .prow { display:grid; grid-template-columns:1.2fr .9fr 56px 1.7fr 40px; gap:10px; align-items:center; margin-bottom:8px; }
+ /* HTTP params carry an extra "In" (request-mapping) column. */
+ #publish .params.http .ptable .prow { grid-template-columns:1.1fr .8fr 50px 1.05fr 1.5fr 40px; align-items:start; }
+ #publish .params.http .ptable .prow.phdr { align-items:center; }
#publish .ptable .prow.phdr { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--c-dim); font-weight:600; margin-bottom:8px; }
+ #publish .p-incell { display:flex; flex-direction:column; gap:4px; }
+ #publish .phint { font-size:12px; line-height:1.35; color:oklch(0.55 0.12 70); }
+ #publish .phint code { font-family:var(--mono,monospace); background:#f4efe2; color:var(--c-ink); padding:1px 5px; border-radius:5px; }
#publish .reqcell { display:flex; justify-content:center; align-items:center; }
#publish .reqcell input { width:auto; }
#publish .ptable .p-del { padding:9px 0; }
@@ -218,9 +224,26 @@ const STEPS = ['Email', 'Identity', 'Backend', 'Methods', 'Listing', 'Vendor', '
let step = 0;
-const blankParam = () => ({ name: '', type: 'string', required: false, description: '' });
+// in === '' means "use the verb's natural default" (GET→query, write verbs→body),
+// resolved in submission(). Kept blank in state so simple forms emit the right
+// default with zero clicks; an explicit choice (path/path_raw/header/…) overrides it.
+const blankParam = () => ({ name: '', type: 'string', required: false, in: '', description: '' });
const blankCLI = () => ({ args: '', params_as_flags: false, passthrough: false });
const blankMethod = () => ({ name: '', http: { verb: 'GET', path: '' }, cli: blankCLI(), latency: '', description: '', params: [blankParam()] });
+
+// Request-mapping resolution for an HTTP param: where the adapter reads it from.
+const PARAM_IN = [
+ ['query', '?name=value, url-encoded. Default for GET.'],
+ ['path', 'fills {name} in the path, url-encoded (REST).'],
+ ['path_raw', 'fills {name} in the path UNescaped — for URL-in-path APIs, e.g. a fetch/proxy that takes GET /.'],
+ ['body', 'JSON body field. Default for POST/PUT/PATCH.'],
+ ['header', 'sent as a request header.'],
+];
+const HTTP_VERBS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
+// The natural default location for a param given the method verb.
+function defaultIn(verb){ return verb === 'GET' || verb === 'DELETE' ? 'query' : 'body'; }
+// Does the path template reference {name} (for path / path_raw to be meaningful)?
+function pathHas(path, name){ return !!name && new RegExp('\\{' + name.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + '\\}').test(path || ''); }
const blankHeader = () => ({ name: '', value: '' });
// Version of the Publisher Release Agreement the publisher signs at submit time.
@@ -243,7 +266,12 @@ if (!state.release) state.release = { signer_name: '', agreed: false };
if (state.app_type == null) state.app_type = 'api';
if (state.backend.command == null) state.backend.command = '';
if (state.backend.env_passthrough == null) state.backend.env_passthrough = '';
-state.methods.forEach(m => { if (!m.cli) m.cli = blankCLI(); if (!m.http) m.http = { verb: 'GET', path: '' }; });
+state.methods.forEach(m => {
+ if (!m.cli) m.cli = blankCLI();
+ if (!m.http) m.http = { verb: 'GET', path: '' };
+ // Drafts saved before per-param request mapping won't carry `in`.
+ (m.params || []).forEach(p => { if (p.in == null) p.in = ''; });
+});
function load() { try { return JSON.parse(localStorage.getItem(LS)); } catch { return null; } }
function save() { localStorage.setItem(LS, JSON.stringify(state)); }
@@ -263,7 +291,13 @@ function submission() {
email: state.email,
backend,
methods: state.methods.filter(m=>m.name.trim()).map(m=>{
- const base = { name: m.name, description: m.description, latency: m.latency, params: m.params.filter(p=>p.name.trim()) };
+ // HTTP params carry an `in` (request-mapping location); CLI params don't.
+ const params = m.params.filter(p=>p.name.trim()).map(p=>{
+ const base = { name: p.name, type: p.type, required: !!p.required, description: p.description };
+ if (!cli) base.in = p.in || defaultIn(m.http.verb);
+ return base;
+ });
+ const base = { name: m.name, description: m.description, latency: m.latency, params };
if (cli) base.cli = { args: m.cli.passthrough ? [] : csv(m.cli.args), params_as_flags: !!m.cli.params_as_flags, passthrough: !!m.cli.passthrough };
else base.http = { verb: m.http.verb, path: m.http.path };
return base;
@@ -436,8 +470,8 @@ function cliBackendHTML(){
// ── method route cells (per app type) ──
function httpRouteCells(m){
- return `
Verb
-
Path ${info('Backend route. GET → params become the query string; POST → JSON body.')}
`;
+ return `
Verb
+
Path ${info('Backend route, e.g. /search or /items/{id}. Use {name} placeholders to insert a param into the path (set that param\'s "in" to path or path_raw below). By default GET/DELETE params become the query string and POST/PUT/PATCH params become the JSON body.')}
`;
}
// The CLI route lives in a full-width row below the name/latency grid.
function cliRouteRow(m){
@@ -465,20 +499,40 @@ function methodCard(m,i){
${cli ? cliRouteRow(m) : ''}
Description ${info('Shown in the help doc agents read. Be specific about what it returns.')}
NameTypeReq${cli?'':`In ${info('Where the adapter reads this param from when it calls your backend. query/path/path_raw fill the URL, body is a JSON field, header is a request header. Defaults to the natural location for the verb (GET → query, POST → body).')}`}Description