mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-10 17:30:37 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19e30fdad | ||
|
|
4298304a98 |
No files matched your search
@@ -367,7 +367,10 @@ registered person can approve (none, or several with no default; name one with
|
||||
`--human`) (`packages/daemon/src/routes/hosts.ts:379-398`).
|
||||
|
||||
The request stays open on the target for as long as `--timeout` waits (600 seconds by default), and the person's
|
||||
approval says how long and until when, in UTC. A target older than this CLI ignores the timeout and keeps the request
|
||||
approval says how long and until when, in UTC. A target that reports who was asked (`person`) and who reads their answer (`owner`), or why
|
||||
nobody does yet (`noReader`), has `rig host pair` name them; an older target's reply keeps the general text. With
|
||||
nobody registered on the target, its refusal (409 `pair_human_required`) says to register a person there:
|
||||
`rig gateway human add <entityId> --display-name <name> --delivery-class B --default`. A target older than this CLI ignores the timeout and keeps the request
|
||||
10 minutes.
|
||||
|
||||
On a 0.7.11 or later target, its person answers the request in the TUI (`rig tui`, then `attention`: open it), on
|
||||
|
||||
@@ -862,7 +862,8 @@ export function hostCommand(doctorDepsOverride?: DoctorDeps): Command {
|
||||
// retry loop forever. Values that work today keep their wait. The target
|
||||
// keeps the request open as long as this side waits, and its approval says so.
|
||||
const timeoutMs = resolvePairTimeoutMs(opts.timeout);
|
||||
let issued: { pairId?: string; code?: string; approvalQitemId?: string; error?: string; message?: string };
|
||||
let issued: { pairId?: string; code?: string; approvalQitemId?: string; error?: string; message?: string;
|
||||
person?: string; owner?: string | null; ownerNote?: string; noReader?: string };
|
||||
try {
|
||||
const res = await httpPost(`${targetBase}/api/hosts/pair-request`, {
|
||||
requester: `${userInfo().username}@${osHostname()}`,
|
||||
@@ -884,7 +885,14 @@ export function hostCommand(doctorDepsOverride?: DoctorDeps): Command {
|
||||
if (!opts.json) {
|
||||
console.log(`Pairing code: ${issued.code}`);
|
||||
console.log(`Waiting for approval on the target (its attention queue carries item ${issued.approvalQitemId ?? "?"}).`);
|
||||
console.log("On the target, its person answers the pairing request in the TUI (rig tui, then attention), on Steering, or in Slack: with Approve or Deny, or in their own words. On a 0.7.11 or later target with a kernel operator, the operator reads the answer and decides (a stopped operator's answer waits for it); on a target with no kernel operator, Approve and Deny decide, and typed words are kept and the person is asked again. On an older target any typed reply approves.");
|
||||
// A target that says who was asked and who reads the answer: name them. An older target keeps the general text.
|
||||
if (issued.person) {
|
||||
console.log(`The request went to ${issued.person} on the target.`);
|
||||
console.log(issued.owner
|
||||
? `${issued.owner} reads their answer and decides${issued.ownerNote ? ` (${issued.ownerNote}, so ${issued.owner} reads it instead)` : ""}.`
|
||||
// The target's own words: none reads answers, or it couldn't tell. Never a categorical "nobody" over an unknown.
|
||||
: `On the target: ${issued.noReader ?? "it didn't say whether an agent reads answers there"}. The request names how to decide directly on the target.`);
|
||||
} else console.log("On the target, its person answers the pairing request in the TUI (rig tui, then attention), on Steering, or in Slack: with Approve or Deny, or in their own words. On a 0.7.11 or later target with a kernel operator, the operator reads the answer and decides (a stopped operator's answer waits for it); on a target with no kernel operator, Approve and Deny decide, and typed words are kept and the person is asked again. On an older target any typed reply approves.");
|
||||
console.log(`On a 0.7.11 or later target, anyone at its terminal with its operator bearer token can also decide: rig host approve ${issued.code} (or rig host deny ${issued.code}), with OPENRIG_AUTH_BEARER_TOKEN set or --bearer <token>.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,26 @@ describe("rig host pair (OPR.0.4.6.MH1 FR-6)", () => {
|
||||
expect(yaml).not.toContain("issued-bearer");
|
||||
}, 15_000);
|
||||
|
||||
it("a target that says who was asked and who reads the answer is named; one where no agent reads says why", async () => {
|
||||
const approved = [{ status: 200, body: { status: "approved", token: "issued-bearer" } }];
|
||||
const owned = fakeDeps({ post: { status: 200, body: { pairId: "p-1", code: "424242", approvalQitemId: "qitem-1", person: "alex@external", owner: "lead@rig" } }, poll: approved });
|
||||
const a = await capture(() => run(owned, ["pair", "vps-a:7433", "--id", "vps-a", "--timeout", "10"]));
|
||||
expect(a.out.join("\n")).toContain("The request went to alex@external on the target.\nlead@rig reads their answer and decides.");
|
||||
expect(a.out.join("\n")).not.toContain("On the target, its person answers the pairing request");
|
||||
fs.rmSync(path.join(dir, "hosts.yaml"), { force: true }); fs.rmSync(path.join(dir, "secrets"), { recursive: true, force: true });
|
||||
const free = fakeDeps({ post: { status: 200, body: { pairId: "p-2", code: "515151", approvalQitemId: "qitem-2", person: "alex@external", owner: null,
|
||||
noReader: "no agent reads answers on this host (no receiving agent is set in agents.operator_session, and it has no operator-agent@kernel)" } }, poll: approved });
|
||||
const b = await capture(() => run(free, ["pair", "vps-b:7433", "--id", "vps-b", "--timeout", "10"]));
|
||||
expect(b.out.join("\n")).toContain("On the target: no agent reads answers on this host (no receiving agent is set in agents.operator_session, and it has no operator-agent@kernel). The request names how to decide directly on the target.");
|
||||
// A target that couldn't tell is printed as unknown, never as "nobody reads".
|
||||
fs.rmSync(path.join(dir, "hosts.yaml"), { force: true }); fs.rmSync(path.join(dir, "secrets"), { recursive: true, force: true });
|
||||
const unknown = fakeDeps({ post: { status: 200, body: { pairId: "p-3", code: "616161", approvalQitemId: "qitem-3", person: "alex@external", owner: null,
|
||||
noReader: "OpenRig couldn't tell whether an agent reads answers on this host" } }, poll: approved });
|
||||
const c = await capture(() => run(unknown, ["pair", "vps-c:7433", "--id", "vps-c", "--timeout", "10"]));
|
||||
expect(c.out.join("\n")).toContain("On the target: OpenRig couldn't tell whether an agent reads answers on this host. The request names how to decide directly on the target.");
|
||||
expect(c.out.join("\n")).not.toContain("No agent reads answers there");
|
||||
}, 15_000);
|
||||
|
||||
it("--timeout reaches the target, which keeps the request open that long", async () => {
|
||||
const deps = fakeDeps({ poll: [{ status: 200, body: { status: "denied" } }] });
|
||||
await capture(() => run(deps, ["pair", "http://vps-a:7433", "--timeout", "3600"]));
|
||||
|
||||
@@ -400,7 +400,11 @@ export function hostsRoutes(opts?: { bearerToken?: string | null; humanRegistry?
|
||||
}
|
||||
const body = (await c.req.json<{ requester?: string; human?: string; timeoutMs?: number }>().catch(() => ({}))) as { requester?: string; human?: string; timeoutMs?: number };
|
||||
const registry = (opts?.humanRegistry ?? loadHumanRegistry)();
|
||||
if (!registry.ok) return c.json({ error: "pair_human_registry_unavailable", message: registry.error }, 409);
|
||||
// Nobody registered on this host (never, or no longer): the fix is here, and the requester's CLI prints this.
|
||||
const noPerson = { error: "pair_human_required", addresses: [] as string[],
|
||||
message: "No person is registered on the target host, so nobody there can be asked to approve pairing. On the target, register one: rig gateway human add <entityId> --display-name <name> --delivery-class B --default (a connector --binding is optional). No approval row was created." };
|
||||
if (!registry.ok) return registry.noHumans ? c.json(noPerson, 409) : c.json({ error: "pair_human_registry_unavailable", message: registry.error }, 409);
|
||||
if (registry.entities.length === 0) return c.json(noPerson, 409);
|
||||
// Named, or else the default person (chosen, or the only one); never the first of several.
|
||||
const fallback = body.human ? null : resolveDefaultPerson(registry.entities).person;
|
||||
const humans = body.human ? registry.entities.filter((human) => human.address === body.human) : fallback ? [fallback] : [];
|
||||
@@ -439,7 +443,9 @@ export function hostsRoutes(opts?: { bearerToken?: string | null; humanRegistry?
|
||||
return c.json({ error: "pair_approval_item_failed", message }, 500);
|
||||
}
|
||||
|
||||
return c.json({ pairId, code, approvalQitemId: qitemId });
|
||||
// Who was asked, and who reads their answer here (or why nobody does yet), for the requester to show.
|
||||
return c.json({ pairId, code, approvalQitemId: qitemId, person: rec.person, owner: rec.owner,
|
||||
...(rec.owner ? (rec.ownerNote ? { ownerNote: rec.ownerNote } : {}) : { noReader: noReader(rec) }) });
|
||||
});
|
||||
|
||||
router.get("/pair-request/:pairId", async (c) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import net from "node:net";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { loadHumanRegistry } from "../src/domain/gateway/human-registry.js";
|
||||
import type Database from "better-sqlite3";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
@@ -460,6 +461,35 @@ describe("pair-request — the target-side issuance handshake (FR-6)", () => {
|
||||
expect(repo.list({ limit: 100 })[0]?.destinationSession).toBe("blair@external");
|
||||
});
|
||||
|
||||
it("a target with nobody registered (never, or no longer) says so from the requester's side; several with no default keep today's text", async () => {
|
||||
const neverHome = mkdtempSync(join(tmpdir(), "pair-never-registered-"));
|
||||
try {
|
||||
expect(loadHumanRegistry(neverHome)).toMatchObject({ ok: false, noHumans: true });
|
||||
for (const registry of [() => loadHumanRegistry(neverHome), () => ({ ok: true as const, entities: [] })]) {
|
||||
const res = await buildApp(repo, BEARER, registry).request("/api/hosts/pair-request", { method: "POST", body: "{}" });
|
||||
expect(res.status).toBe(409);
|
||||
expect(await res.json()).toEqual({ error: "pair_human_required", addresses: [],
|
||||
message: "No person is registered on the target host, so nobody there can be asked to approve pairing. On the target, register one: rig gateway human add <entityId> --display-name <name> --delivery-class B --default (a connector --binding is optional). No approval row was created." });
|
||||
}
|
||||
expect(repo.list({ limit: 100 })).toHaveLength(0);
|
||||
} finally { rmSync(neverHome, { recursive: true, force: true }); }
|
||||
const several = await buildApp(repo, BEARER, () => ({ ok: true, entities: [human, { ...human, entityId: "blair", address: "blair@external" }] }))
|
||||
.request("/api/hosts/pair-request", { method: "POST", body: "{}" });
|
||||
expect(((await several.json()) as { message: string }).message).toContain("Pair approval needs one registered human, or a default when several exist.");
|
||||
});
|
||||
|
||||
it("the reply names who was asked and who reads their answer, or why nobody does yet", async () => {
|
||||
const res = await app.request("/api/hosts/pair-request", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requester: "tester@somewhere" }) });
|
||||
expect(await res.json()).toMatchObject({ person: "alex@external", owner: null,
|
||||
noReader: "no agent reads answers on this host (no receiving agent is set in agents.operator_session, and it has no operator-agent@kernel)" });
|
||||
vi.stubEnv("OPENRIG_AGENTS_OPERATOR_SESSION", "lead@rig");
|
||||
try {
|
||||
const owned = await (await app.request("/api/hosts/pair-request", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requester: "tester@somewhere" }) })).json() as Record<string, unknown>;
|
||||
expect(owned).toMatchObject({ person: "alex@external", owner: "lead@rig" });
|
||||
expect(owned.noReader).toBeUndefined();
|
||||
} finally { vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("issuance mints the ONE human approval moment: a human-routed qitem with code + summary + evidence_ref", async () => {
|
||||
const create = vi.spyOn(repo, "create");
|
||||
const { code, approvalQitemId } = await issue();
|
||||
|
||||
Reference in new issue
Block a user