49 lines
2.6 KiB
TypeScript
49 lines
2.6 KiB
TypeScript
|
|
/** Same-origin endpoint descriptors for the future HUX-05 backend. */
|
||
|
|
|
||
|
|
import { ACTIVITY_TIMELINE_FLAG, AUTONOMY_FLAG, FOUNDATION_FLAG } from "./model.ts";
|
||
|
|
import { isOpaqueId, normalizeIdentity, sameIdentity } from "./security.ts";
|
||
|
|
import type { AutonomyEndpoint, AutonomyEndpointContract, HuxIdentity } from "./types.ts";
|
||
|
|
|
||
|
|
interface FoundationClient {
|
||
|
|
apiVersion: string;
|
||
|
|
identity: HuxIdentity;
|
||
|
|
enabled(flag: string): boolean;
|
||
|
|
endpoint(path: string): string;
|
||
|
|
}
|
||
|
|
|
||
|
|
function endpoint(method: AutonomyEndpoint["method"], path: string,
|
||
|
|
responseSchema: string, requiredBody?: readonly string[]): AutonomyEndpoint {
|
||
|
|
return Object.freeze({method, path, responseSchema, ...(requiredBody ? {requiredBody} : {})});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Describe, but never call, the tenant-scoped HUX-05 interface. */
|
||
|
|
export function createAutonomyEndpointContract(client: FoundationClient): AutonomyEndpointContract | null {
|
||
|
|
if (client.apiVersion !== "hux.v1" ||
|
||
|
|
![FOUNDATION_FLAG, ACTIVITY_TIMELINE_FLAG, AUTONOMY_FLAG].every((flag) => client.enabled(flag))) return null;
|
||
|
|
const identity = normalizeIdentity(client.identity);
|
||
|
|
if (!identity || !sameIdentity(identity, client.identity)) throw new TypeError("Scoped HUX identity required");
|
||
|
|
const scoped = (root: string, id: string, prefix: string): string => {
|
||
|
|
if (!isOpaqueId(id, prefix)) throw new TypeError(`Opaque ${prefix} id required`);
|
||
|
|
return client.endpoint(`${root}/${encodeURIComponent(id)}`);
|
||
|
|
};
|
||
|
|
return Object.freeze({
|
||
|
|
identity: Object.freeze({...identity}),
|
||
|
|
policy: endpoint("GET", client.endpoint("/autonomy/policy"), "hux.policy.v1"),
|
||
|
|
updatePolicy: endpoint("PUT", client.endpoint("/autonomy/policy"), "hux.policy.v1",
|
||
|
|
["expected_policy_id", "expected_updated_at", "autonomy", "grants", "budgets", "guardrails"]),
|
||
|
|
approvals: endpoint("GET", client.endpoint("/approvals?status=pending&limit=100"), "hux.approval_page.v1"),
|
||
|
|
decideApproval(approvalId: string) {
|
||
|
|
return endpoint("POST", `${scoped("/approvals", approvalId, "apr")}/decision`, "hux.approval.v1",
|
||
|
|
["expected_run_id", "expected_conversation_id", "expected_capability", "expected_requested_at", "choice"]);
|
||
|
|
},
|
||
|
|
stop(runId: string) {
|
||
|
|
if (!/^run_[A-Za-z0-9._-]{1,116}$/.test(runId)) throw new TypeError("Exact run id required");
|
||
|
|
return endpoint("POST", client.endpoint(`/runs/${encodeURIComponent(runId)}/stop`),
|
||
|
|
"hux.cancel_receipt.v1", ["conversation_id"]);
|
||
|
|
},
|
||
|
|
release: Object.freeze({schema: "hux.release.v1" as const,
|
||
|
|
requiredState: "live_verified" as const,
|
||
|
|
featureFlags: Object.freeze([FOUNDATION_FLAG, ACTIVITY_TIMELINE_FLAG, AUTONOMY_FLAG])}),
|
||
|
|
});
|
||
|
|
}
|