delegation-routes.mjs
280 lines 10.1 KB
Raw
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b fix(7C-L1b): route hosted delegation proposals through cani… Human minor ⚠ breaking 32 days ago
1 /**
2 * Hosted bridge REST routes for agent delegation (Phase 7C-L1 hosted parity).
3 *
4 * L1b: identity/consent proposals POST to the canister (Hub-visible); approve apply
5 * runs via POST …/delegation/proposals/:id/apply-approved (gateway hook after approve).
6 *
7 * @see docs/AGENT-DELEGATION-V0-SPEC.md §4
8 */
9
10 import {
11 handleAgentIdentityRegisterProposeRequest,
12 handleAgentIdentityListRequest,
13 handleDelegationConsentProposeRequest,
14 handleDelegationConsentRevokeRequest,
15 handleDelegationGrantMintRequest,
16 handleDelegationGrantListRequest,
17 handleDelegationGrantRevokeRequest,
18 handleDelegationAuditAppendRequest,
19 hashPrincipalRef,
20 } from '../../lib/agent/delegation.mjs';
21 import { createDelegationProposalOnCanister, applyApprovedDelegationProposalFromCanister } from '../../lib/agent/delegation-hosted-proposal.mjs';
22
23 /**
24 * @param {import('express').Express} app
25 * @param {{
26 * dataDir: string,
27 * canisterUrl: string,
28 * canisterHeaders: (extra?: Record<string, string>) => Record<string, string>,
29 * requireBridgeAuth: import('express').RequestHandler,
30 * resolveHostedBridgeContext: (req: import('express').Request, actorUid: string) => Promise<{
31 * ok: boolean,
32 * status?: number,
33 * error?: string,
34 * code?: string,
35 * vaultId?: string,
36 * effectiveCanisterUid?: string,
37 * actorUid?: string,
38 * }>,
39 * }} deps
40 */
41 export function registerBridgeDelegationRoutes(app, deps) {
42 const { dataDir, canisterUrl, canisterHeaders, requireBridgeAuth, resolveHostedBridgeContext } = deps;
43
44 /**
45 * @param {import('express').Request} req
46 */
47 async function vaultContext(req) {
48 const hctx = await resolveHostedBridgeContext(req, req.uid);
49 return hctx;
50 }
51
52 /**
53 * @param {{
54 * effectiveCanisterUid: string,
55 * actorUid: string,
56 * vaultId: string,
57 * }} ctx
58 */
59 function hostedCreateProposal(ctx) {
60 return async function createProposal(_dataDir, input) {
61 return createDelegationProposalOnCanister({
62 canisterUrl,
63 headers: canisterHeaders({
64 'X-User-Id': ctx.effectiveCanisterUid,
65 'X-Actor-Id': ctx.actorUid,
66 'X-Vault-Id': ctx.vaultId,
67 }),
68 input,
69 });
70 };
71 }
72
73 /**
74 * @param {import('express').Response} res
75 * @param {unknown} err
76 */
77 function sendRouteError(res, err) {
78 const e = err && typeof err === 'object' ? /** @type {{ status?: number, code?: string, message?: string }} */ (err) : {};
79 const status = typeof e.status === 'number' ? e.status : 500;
80 const code = typeof e.code === 'string' ? e.code : 'RUNTIME_ERROR';
81 const message = typeof e.message === 'string' ? e.message : String(err);
82 return res.status(status).json({ error: message, code });
83 }
84
85 app.post('/api/v1/agents/identities', requireBridgeAuth, async (req, res) => {
86 const hctx = await vaultContext(req);
87 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
88 const body = req.body && typeof req.body === 'object' ? req.body : {};
89 try {
90 const result = await handleAgentIdentityRegisterProposeRequest({
91 dataDir,
92 vaultId: hctx.vaultId,
93 userId: req.uid,
94 kind: body.kind,
95 agentId: body.agent_id,
96 label: body.label,
97 scopeCeiling: body.scope_ceiling,
98 createProposal: hostedCreateProposal({
99 effectiveCanisterUid: hctx.effectiveCanisterUid,
100 actorUid: req.uid,
101 vaultId: hctx.vaultId,
102 }),
103 });
104 if (!result.ok) {
105 return res.status(result.status).json({ error: result.error, code: result.code });
106 }
107 return res.status(201).json(result.payload);
108 } catch (err) {
109 return sendRouteError(res, err);
110 }
111 });
112
113 app.get('/api/v1/agents/identities', requireBridgeAuth, async (req, res) => {
114 const hctx = await vaultContext(req);
115 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
116 const result = handleAgentIdentityListRequest({
117 dataDir,
118 vaultId: hctx.vaultId,
119 kind: typeof req.query.kind === 'string' ? req.query.kind : undefined,
120 status: typeof req.query.status === 'string' ? req.query.status : undefined,
121 });
122 if (!result.ok) {
123 return res.status(result.status).json({ error: result.error, code: result.code });
124 }
125 return res.json(result.payload);
126 });
127
128 app.post('/api/v1/delegation/consents', requireBridgeAuth, async (req, res) => {
129 const hctx = await vaultContext(req);
130 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
131 const body = req.body && typeof req.body === 'object' ? req.body : {};
132 try {
133 const result = await handleDelegationConsentProposeRequest({
134 dataDir,
135 vaultId: hctx.vaultId,
136 userId: req.uid,
137 delegateAgentId: body.delegate_agent_id,
138 scope: body.scope,
139 workspaceId: body.workspace_id,
140 allowedFlowIds: body.allowed_flow_ids,
141 allowedTaskKinds: body.allowed_task_kinds,
142 allowedTaskIds: body.allowed_task_ids,
143 expiresAt: body.expires_at,
144 createProposal: hostedCreateProposal({
145 effectiveCanisterUid: hctx.effectiveCanisterUid,
146 actorUid: req.uid,
147 vaultId: hctx.vaultId,
148 }),
149 });
150 if (!result.ok) {
151 return res.status(result.status).json({ error: result.error, code: result.code });
152 }
153 return res.status(201).json(result.payload);
154 } catch (err) {
155 return sendRouteError(res, err);
156 }
157 });
158
159 app.post('/api/v1/delegation/proposals/:proposal_id/apply-approved', requireBridgeAuth, async (req, res) => {
160 const hctx = await vaultContext(req);
161 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
162 const proposalId =
163 typeof req.params.proposal_id === 'string' ? decodeURIComponent(req.params.proposal_id).trim() : '';
164 if (!proposalId) {
165 return res.status(400).json({ error: 'proposal_id required', code: 'BAD_REQUEST' });
166 }
167 const result = await applyApprovedDelegationProposalFromCanister({
168 dataDir,
169 canisterUrl,
170 headers: canisterHeaders({
171 'X-User-Id': hctx.effectiveCanisterUid,
172 'X-Actor-Id': req.uid,
173 'X-Vault-Id': hctx.vaultId,
174 }),
175 proposalId,
176 requireApproved: true,
177 });
178 if (!result.ok) {
179 return res.status(result.status).json({ error: result.error, code: result.code });
180 }
181 return res.json(result.payload);
182 });
183
184 app.delete('/api/v1/delegation/consents/:consent_id', requireBridgeAuth, async (req, res) => {
185 const hctx = await vaultContext(req);
186 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
187 const consentId =
188 typeof req.params.consent_id === 'string' ? decodeURIComponent(req.params.consent_id).trim() : '';
189 const result = handleDelegationConsentRevokeRequest({
190 dataDir,
191 vaultId: hctx.vaultId,
192 consentId,
193 userId: req.uid,
194 });
195 if (!result.ok) {
196 return res.status(result.status).json({ error: result.error, code: result.code });
197 }
198 return res.json(result.payload);
199 });
200
201 app.post('/api/v1/delegation/grants', requireBridgeAuth, async (req, res) => {
202 const hctx = await vaultContext(req);
203 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
204 const body = req.body && typeof req.body === 'object' ? req.body : {};
205 const result = handleDelegationGrantMintRequest({
206 dataDir,
207 vaultId: hctx.vaultId,
208 consentId: body.consent_id,
209 actorAgentId: body.actor_agent_id,
210 taskRef: body.task_ref,
211 runRef: body.run_ref,
212 flowId: body.flow_id,
213 flowVersion: body.flow_version,
214 ttlSeconds: body.ttl_seconds,
215 });
216 if (!result.ok) {
217 return res.status(result.status).json({ error: result.error, code: result.code });
218 }
219 return res.status(201).json(result.payload);
220 });
221
222 app.get('/api/v1/delegation/grants', requireBridgeAuth, async (req, res) => {
223 const hctx = await vaultContext(req);
224 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
225 const result = handleDelegationGrantListRequest({
226 dataDir,
227 vaultId: hctx.vaultId,
228 actorAgentId: typeof req.query.actor_agent_id === 'string' ? req.query.actor_agent_id : undefined,
229 });
230 if (!result.ok) {
231 return res.status(result.status).json({ error: result.error, code: result.code });
232 }
233 return res.json(result.payload);
234 });
235
236 app.delete('/api/v1/delegation/grants/:grant_id', requireBridgeAuth, async (req, res) => {
237 const hctx = await vaultContext(req);
238 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
239 const grantId =
240 typeof req.params.grant_id === 'string' ? decodeURIComponent(req.params.grant_id).trim() : '';
241 const result = handleDelegationGrantRevokeRequest({
242 dataDir,
243 vaultId: hctx.vaultId,
244 grantId,
245 });
246 if (!result.ok) {
247 return res.status(result.status).json({ error: result.error, code: result.code });
248 }
249 return res.json(result.payload);
250 });
251
252 app.post('/api/v1/delegation/audit', requireBridgeAuth, async (req, res) => {
253 const hctx = await vaultContext(req);
254 if (!hctx.ok) return res.status(hctx.status).json({ error: hctx.error, code: hctx.code });
255 const body = req.body && typeof req.body === 'object' ? req.body : {};
256 const principalRef =
257 typeof body.principal_ref === 'string' && body.principal_ref.trim()
258 ? body.principal_ref.trim()
259 : hashPrincipalRef(req.uid);
260 const result = handleDelegationAuditAppendRequest({
261 dataDir,
262 vaultId: hctx.vaultId,
263 grantId: body.grant_id,
264 actorAgentId: body.actor_agent_id,
265 principalRef,
266 action: body.action,
267 evidenceRefs: body.evidence_refs,
268 taskRef: body.task_ref,
269 runRef: body.run_ref,
270 flowId: body.flow_id,
271 flowVersion: body.flow_version,
272 stepId: body.step_id,
273 executionLocation: body.execution_location,
274 });
275 if (!result.ok) {
276 return res.status(result.status).json({ error: result.error, code: result.code });
277 }
278 return res.status(201).json(result.payload);
279 });
280 }
File History 1 commit
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b fix(7C-L1b): route hosted delegation proposals through cani… Human minor 32 days ago