~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-agents

agents and atirc modules for atlibs: launch agents from a composition, chat on atirc; pages and the agents service

0
0
main
159 lines · 8.1 KB Raw History
// node --test web/agents/agents-cubit.test.mjs
// The page cubit against a stubbed OAuth client (the session's PDS minting service auth) and a stubbed
// agents service that checks each token's aud and lxm and scopes launches to their caller.
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import { test } from "node:test";

registerHooks({
	resolve(specifier, context, next) {
		const lib = specifier.startsWith("atlibs/") && new URL(`../../../atlibs/javascript/src/${specifier.slice("atlibs/".length)}`, import.meta.url).href;
		return next(lib || specifier, context);
	},
});
const { AgentsCubit, DEFAULT_SERVICE, NSID, SCOPE } = await import("./agents-cubit.js");

const BOB = "did:plc:ocnuqjlzgohypj3zoznermms";
const SERVICE = "https://did-key-zdnaetest.xrpc.fedproxy.com";
const json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });

function memoryStorage() {
	const m = new Map();
	return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => void m.set(k, String(v)), map: m };
}

// The PDS: getServiceAuth returns a token naming aud and lxm (the stub service reads them back).
function session(did = BOB) {
	const minted = [];
	const agent = {
		did,
		com: { atproto: { server: { getSession: async () => ({ success: true, data: { handle: "bob.test" } }) } } },
		async fetchHandler(path) {
			const [nsid, query] = path.replace("/xrpc/", "").split("?");
			assert.equal(nsid, "com.atproto.server.getServiceAuth");
			const { aud, lxm } = Object.fromEntries(new URLSearchParams(query));
			minted.push({ aud, lxm });
			return json(200, { token: `${did}|${aud}|${lxm}` });
		},
	};
	const client = { init: async () => ({ session: {} }), signIn: async () => undefined, revoke: async () => undefined };
	return { agent, client, minted };
}

// The service: verifies the token's aud/lxm, records caller, answers only the caller's launches.
function service() {
	const launches = new Map();
	const calls = [];
	let n = 0;
	const fetch = async (url, init) => {
		const u = new URL(url);
		const nsid = u.pathname.replace("/xrpc/", "");
		const [caller, aud, lxm] = (init.headers.authorization ?? "").replace("Bearer ", "").split("|");
		calls.push({ nsid, method: init.method, caller, body: init.body && JSON.parse(init.body) });
		if (aud !== `did:web:${u.hostname}${u.port ? `%3A${u.port}` : ""}` || lxm !== nsid) return json(401, { error: "AuthenticationRequired", message: "bad token" });
		const own = (rkey) => {
			const l = launches.get(rkey);
			if (!l) return json(400, { error: "InvalidRequest", message: "reading the launch record: RecordNotFound" });
			if (l.caller !== caller) return json(403, { error: "Forbidden", message: `launch ${rkey} belongs to another caller` });
		};
		if (nsid === NSID.launch) {
			const rkey = `3mzz${++n}`;
			launches.set(rkey, { caller, status: "running", name: JSON.parse(init.body).name });
			return json(200, { rkey, uri: `at://svc/com.fedfork.agents.launch/${rkey}`, status: "launching" });
		}
		if (nsid === NSID.get) {
			const rkey = u.searchParams.get("rkey");
			const denied = own(rkey);
			if (denied) return denied;
			return json(200, { rkey, uri: `at://svc/com.fedfork.agents.launch/${rkey}`, record: { ...launches.get(rkey), subhandle: `x-${rkey}.svc.test` } });
		}
		if (nsid === NSID.shutdown) {
			const { rkey, delete: remove } = JSON.parse(init.body);
			const denied = own(rkey);
			if (denied) return denied;
			if (remove) launches.delete(rkey);
			else launches.get(rkey).status = "stopped";
			return json(200, { rkey, status: "stopped", revoked: true, terminated: true, deleted: remove ? ["a", "b"] : [] });
		}
		return json(404, { error: "MethodNotImplemented" });
	};
	return { fetch, launches, calls };
}

async function page({ storage = memoryStorage(), svc = service(), did = BOB } = {}) {
	const s = session(did);
	const cubit = new AgentsCubit({ client: Promise.resolve(s.client), makeAgent: () => s.agent, fetch: svc.fetch, storage, service: SERVICE });
	await cubit.init();
	return { cubit, svc, storage, minted: s.minted };
}

test("the OAuth scope is only service auth for the three methods", () => {
	assert.equal(SCOPE, "atproto rpc:com.fedfork.agents.launchAgent?aud=* rpc:com.fedfork.agents.getAgent?aud=* rpc:com.fedfork.agents.shutdownAgent?aud=*");
	assert.match(DEFAULT_SERVICE, /^https:\/\/did-key-z[a-z0-9]+\.xrpc\.fedproxy\.com$/);
});

test("launch through the service with a PDS-minted token; the page lists it, reads it, shuts it down", async () => {
	const { cubit, svc, minted } = await page();
	assert.equal(cubit.state.session, "signed-in");
	const launched = await cubit.launch({ bidders: "did:plc:4vcig7s43z3nyqrnepmcfzqt", name: "page", exp_s: "600" });
	assert.equal(launched.status, "launching");
	assert.deepEqual(svc.calls[0], { nsid: NSID.launch, method: "POST", caller: BOB, body: { bidders: "did:plc:4vcig7s43z3nyqrnepmcfzqt", name: "page", exp_s: 600 } });
	assert.deepEqual(minted[0], { aud: "did:web:did-key-zdnaetest.xrpc.fedproxy.com", lxm: NSID.launch });
	assert.deepEqual(cubit.state.agents.map((a) => [a.rkey, a.record.status]), [[launched.rkey, "running"]]);
	assert.equal(cubit.state.busy.launch, null);

	const status = await cubit.status(launched.rkey);
	assert.equal(status.record.subhandle, `x-${launched.rkey}.svc.test`);
	const down = await cubit.shutdown(launched.rkey, true);
	assert.equal(down.status, "stopped");
	assert.deepEqual(cubit.state.agents, [], "deleted on the service: forgotten here");
	assert.ok(minted.every(({ aud }) => aud === "did:web:did-key-zdnaetest.xrpc.fedproxy.com"));
	assert.deepEqual([...new Set(minted.map((m) => m.lxm))].sort(), Object.values(NSID).sort());
});

test("a reload lists what this browser launched for this account; another account's page sees none of it", async () => {
	const storage = memoryStorage();
	const svc = service();
	const first = await page({ storage, svc });
	const { rkey } = await first.cubit.launch({ name: "keep" });
	const again = await page({ storage, svc });
	assert.deepEqual(again.cubit.state.agents.map((a) => a.rkey), [rkey]);
	const other = await page({ storage, svc, did: "did:plc:mallory0000000000000000" });
	assert.deepEqual(other.cubit.state.agents, []);
	const denied = await other.cubit.shutdown(rkey);
	assert.match(denied.error, /^403 Forbidden: launch 3mzz1 belongs to another caller/);
	assert.equal(svc.launches.get(rkey).status, "running");
});

test("service errors keep their status; setService needs https and switches the list", async () => {
	const svc = service();
	const { cubit } = await page({ svc });
	const bad = await cubit.status("nope");
	assert.match(bad.error, /^400 InvalidRequest: reading the launch record/);
	await assert.rejects(() => cubit.setService("http://insecure.example"), /https origin/);
	await cubit.launch({ name: "a" });
	await cubit.setService("https://did-key-zdnaeother.xrpc.fedproxy.com/xrpc/whatever");
	assert.equal(cubit.state.service, "https://did-key-zdnaeother.xrpc.fedproxy.com");
	assert.deepEqual(cubit.state.agents, [], "launches are remembered per service");
});

// review 3a L5: the aud was did:web:<hostname>, the port dropped; serviceAud keeps it (%3A).
test("a service on a port gets tokens for did:web:<host>%3A<port>", async () => {
	const { cubit, minted } = await page();
	await cubit.setService("https://svc.example:8443");
	const launched = await cubit.launch({ name: "p" });
	assert.equal(launched.status, "launching", launched.error);
	assert.deepEqual(minted.find((m) => m.lxm === NSID.launch && m.aud.startsWith("did:web:svc")), { aud: "did:web:svc.example%3A8443", lxm: NSID.launch });
});

test("signed out: no calls; logout keeps the service setting", async () => {
	const svc = service();
	const cubit = new AgentsCubit({ client: Promise.resolve({ init: async () => undefined }), makeAgent: () => null, fetch: svc.fetch, storage: memoryStorage(), service: SERVICE });
	await cubit.init();
	assert.equal(cubit.state.session, "signed-out");
	assert.match((await cubit.launch({ name: "x" })).error, /needs a signed-in session/);
	assert.equal(svc.calls.length, 0);
	const { cubit: signedIn } = await page();
	await signedIn.logout();
	assert.deepEqual([signedIn.state.session, signedIn.state.service], ["signed-out", SERVICE]);
});