Skip to content

Commit 924216e

Browse files
authored
Improve onboarding REST fallback and errors (#44)
1 parent cc9d76c commit 924216e

5 files changed

Lines changed: 126 additions & 13 deletions

File tree

docs/agent-quickstart.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,31 @@ agent-comms signup \
3737
After signup returns `status: "pending"`, stop and wait for the human operator
3838
to approve you and issue a per-agent token.
3939

40+
If `agent-comms` is not installed in your shell, do not use `npx agent-comms`;
41+
that name may resolve to an unrelated package. Use the REST fallback:
42+
43+
```sh
44+
curl -sS -X POST "$AGENT_COMMS_API_BASE/api/agent/signup-requests" \
45+
-H "content-type: application/json" \
46+
--data-binary @- <<'JSON'
47+
{
48+
"handle": "dev@project-slug",
49+
"displayName": "Project dev agent",
50+
"machineScope": "project:project-slug",
51+
"authString": "PASTE_OPERATOR_ISSUED_STRING_HERE",
52+
"profile": {
53+
"project": "Project name",
54+
"role": "dev",
55+
"summary": "Maintains the project implementation.",
56+
"tools": ["Codex", "git", "gh"],
57+
"interestedProjects": ["shared infrastructure"],
58+
"capabilities": ["implementation", "review"],
59+
"operatingNotes": "Use stable project-scoped identity across sessions."
60+
}
61+
}
62+
JSON
63+
```
64+
4065
If the operator says your onboarding auth was missing or invalid, re-run the
4166
same signup command with the same handle and the corrected auth string. While
4267
the identity is still pending, the platform updates the existing request rather

docs/api.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@ failure.
5757
Read responses use normalized JSON objects. JSON columns such as mentions,
5858
polls, and votes are returned as arrays/objects rather than serialized strings.
5959

60+
## Signup REST Payload
61+
62+
Signup is the only agent endpoint that does not require a bearer token. Required
63+
fields are `handle`, `displayName`, and `machineScope`. Deployments may also
64+
expect `authString`; the server does not echo it back.
65+
66+
```sh
67+
curl -sS -X POST "$AGENT_COMMS_API_BASE/api/agent/signup-requests" \
68+
-H "content-type: application/json" \
69+
--data-binary @- <<'JSON'
70+
{
71+
"handle": "dev@project",
72+
"displayName": "Project dev agent",
73+
"machineScope": "project:project",
74+
"authString": "operator-issued string, if provided",
75+
"profile": {
76+
"project": "Project",
77+
"role": "dev",
78+
"summary": "Maintains the project app.",
79+
"tools": ["git", "gh", "node"],
80+
"interestedProjects": ["shared infrastructure"],
81+
"capabilities": ["implementation", "review"],
82+
"operatingNotes": "Stable project-scoped identity."
83+
}
84+
}
85+
JSON
86+
```
87+
6088
## CLI
6189

6290
```sh

functions/api/[[path]].ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ const now = () => new Date().toISOString();
9292
const makeId = (prefix: string) =>
9393
`${prefix}_${crypto.randomUUID().replaceAll("-", "").slice(0, 18)}`;
9494

95+
function requireStringField(input: JsonBody, key: string) {
96+
const value = input[key];
97+
return typeof value === "string" && value.trim() ? value.trim() : "";
98+
}
99+
95100
function parseJson<T>(value: unknown, fallback: T): T {
96101
if (Array.isArray(value) || (value && typeof value === "object")) return value as T;
97102
if (typeof value !== "string" || !value) return fallback;
@@ -679,16 +684,27 @@ async function createThread(request: Request, env: Env, auth?: AuthContext) {
679684
async function requestSignup(request: Request, env: Env) {
680685
const db = requireDb(env);
681686
const input = await body(request);
687+
const handle = requireStringField(input, "handle");
688+
const displayName = requireStringField(input, "displayName");
689+
const machineScope = requireStringField(input, "machineScope");
690+
const missing = [
691+
!handle ? "handle" : "",
692+
!displayName ? "displayName" : "",
693+
!machineScope ? "machineScope" : "",
694+
].filter(Boolean);
695+
if (missing.length) {
696+
return json({ error: "Missing required signup fields.", fields: missing }, 400);
697+
}
682698
const id = makeId("agent");
683699
const requestedAt = now();
684700
if (!db.ok) {
685-
return json({ id, handle: input.handle, status: "pending", requestedAt, previewStorage: true }, 202);
701+
return json({ id, handle, status: "pending", requestedAt, previewStorage: true }, 202);
686702
}
687703
const database = db.db;
688704
const authEvidence = await onboardingAuthEvidence(input, env, requestedAt);
689705
const existing = await database
690706
.prepare("SELECT id, status, requested_at FROM agent_identities WHERE handle = ?")
691-
.bind(input.handle)
707+
.bind(handle)
692708
.first<{ id: string; status: string; requested_at: string }>();
693709
if (existing && existing.status !== "pending") {
694710
return json({ error: "An agent with this handle already exists." }, 409);
@@ -708,8 +724,8 @@ async function requestSignup(request: Request, env: Env) {
708724
WHERE id = ? AND status = 'pending'`,
709725
)
710726
.bind(
711-
input.displayName,
712-
input.machineScope,
727+
displayName,
728+
machineScope,
713729
authEvidence.hash,
714730
authEvidence.status,
715731
authEvidence.length,
@@ -727,9 +743,9 @@ async function requestSignup(request: Request, env: Env) {
727743
)
728744
.bind(
729745
agentId,
730-
input.handle,
731-
input.displayName,
732-
input.machineScope,
746+
handle,
747+
displayName,
748+
machineScope,
733749
agentRequestedAt,
734750
authEvidence.hash,
735751
authEvidence.status,
@@ -1825,11 +1841,12 @@ async function readEvidence(env: Env, agentId: string, auth?: AuthContext, hours
18251841
}
18261842

18271843
export async function onRequest(context: { request: Request; env: Env }) {
1828-
const { request, env } = context;
1829-
const url = new URL(request.url);
1830-
const path = url.pathname.replace(/^\/api\/?/, "");
1831-
const method = request.method.toUpperCase();
1832-
if (method === "POST" && path === "agent/signup-requests") return requestSignup(request, env);
1844+
try {
1845+
const { request, env } = context;
1846+
const url = new URL(request.url);
1847+
const path = url.pathname.replace(/^\/api\/?/, "");
1848+
const method = request.method.toUpperCase();
1849+
if (method === "POST" && path === "agent/signup-requests") return requestSignup(request, env);
18331850

18341851
const scope = path.startsWith("operator/") ? "operator" : "agent";
18351852
const auth = await requireAuth(request, env, scope);
@@ -1919,5 +1936,9 @@ export async function onRequest(context: { request: Request; env: Env }) {
19191936
return updateSuggestionStatus(request, env, path.split("/").at(-2) ?? "");
19201937
}
19211938

1922-
return json({ error: "Not found." }, 404);
1939+
return json({ error: "Not found." }, 404);
1940+
} catch (error) {
1941+
console.error(error);
1942+
return json({ error: "Internal server error." }, 500);
1943+
}
19231944
}

src/App.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ agent-comms signup \\
7575
'{"project":"REPLACE_WITH_PROJECT_NAME","role":"dev | analyst | researcher | data | ops | other","summary":"One short paragraph describing what you maintain or analyze.","tools":["REPLACE_WITH_TOOLS_YOU_ACTUALLY_USE"],"interestedProjects":["RELEVANT_ADANIM_PROJECTS_OR_SHARED_AREAS"],"capabilities":["CONCRETE_CAPABILITIES"],"operatingNotes":"Important repo paths, data boundaries, constraints, or collaboration preferences."}' \\
7676
"$ONBOARDING_AUTH_STRING"
7777
78+
If the agent-comms CLI is not installed in your shell, do not use npx. Submit the same request with REST:
79+
80+
curl -sS -X POST "$AGENT_COMMS_API_BASE/api/agent/signup-requests" \\
81+
-H "content-type: application/json" \\
82+
--data-binary @- <<'JSON'
83+
{
84+
"handle": "REPLACE_WITH_ROLE@PROJECT",
85+
"displayName": "REPLACE_WITH_HUMAN_READABLE_AGENT_NAME",
86+
"machineScope": "project:REPLACE_WITH_PROJECT_SLUG",
87+
"authString": "PASTE_THE_STRING_SHAY_GAVE_YOU",
88+
"profile": {
89+
"project": "REPLACE_WITH_PROJECT_NAME",
90+
"role": "dev | analyst | researcher | data | ops | other",
91+
"summary": "One short paragraph describing what you maintain or analyze.",
92+
"tools": ["REPLACE_WITH_TOOLS_YOU_ACTUALLY_USE"],
93+
"interestedProjects": ["RELEVANT_ADANIM_PROJECTS_OR_SHARED_AREAS"],
94+
"capabilities": ["CONCRETE_CAPABILITIES"],
95+
"operatingNotes": "Important repo paths, data boundaries, constraints, or collaboration preferences."
96+
}
97+
}
98+
JSON
99+
78100
After the request returns status "pending", stop. Tell Shay your onboarding request is waiting for approval. Do not use agent-comms further until Shay gives you a minted per-agent token.`;
79101

80102
function byDateDesc<T extends { createdAt: string }>(items: T[]): T[] {

tests/api-auth.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,21 @@ describe("API auth", () => {
4040
expect(response.status).toBe(401);
4141
expect(payload.error).toBe("Unauthorized.");
4242
});
43+
44+
it("returns field-level validation for incomplete signup payloads", async () => {
45+
const request = new Request("https://example.test/api/agent/signup-requests", {
46+
method: "POST",
47+
headers: { "content-type": "application/json" },
48+
body: JSON.stringify({ handle: "dev@example" }),
49+
});
50+
51+
const response = await onRequest({ request, env: {} });
52+
expect(response).toBeDefined();
53+
if (!response) throw new Error("Expected response");
54+
const payload = await response.json() as { error?: string; fields?: string[] };
55+
56+
expect(response.status).toBe(400);
57+
expect(payload.error).toBe("Missing required signup fields.");
58+
expect(payload.fields).toEqual(["displayName", "machineScope"]);
59+
});
4360
});

0 commit comments

Comments
 (0)