@hivemagnus/ardent-studio 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
+ import { requestContext } from '@hivemagnus/hive-common/hono';
1
2
  import { Hono } from 'hono';
2
3
  import { ARDENT_STUDIO_API_PREFIX } from '../shared/browser.js';
3
4
  import { errorResponse } from './errors.js';
4
5
  import { createRouter } from './routing.js';
6
+ import { auditRoutes } from './routes/audit.js';
5
7
  import { agentStudioRoutes } from './routes/agent-studio.js';
6
8
  import { agentRoutes } from './routes/agents.js';
7
9
  import { fileRoutes } from './routes/files.js';
@@ -17,6 +19,9 @@ import { workRoutes } from './routes/work.js';
17
19
  /** The ArdentStudio HTTP API under /api/ardentstudio/v1. */
18
20
  export function createApp(deps) {
19
21
  const app = new Hono();
22
+ // x-request-id (or x-hive-request-id / x-correlation-id) and W3C traceparent: read, generated
23
+ // when absent, echoed as x-request-id, and attached to log lines written during the request.
24
+ app.use('*', requestContext());
20
25
  app.use('*', async (c, next) => {
21
26
  await next();
22
27
  c.header('x-content-type-options', 'nosniff');
@@ -39,6 +44,7 @@ export function createApp(deps) {
39
44
  roomRoutes(register, deps);
40
45
  workRoutes(register, deps);
41
46
  reviewRoutes(register, deps);
47
+ auditRoutes(register, deps);
42
48
  streamRoutes(register, deps);
43
49
  return app;
44
50
  }
@@ -1,6 +1,6 @@
1
1
  import type { AgentDirectory } from '../agents/directory.js';
2
2
  import type { OperationRelay } from '../agents/operations.js';
3
- import type { Authenticator, Principal } from '../auth/oathweave.js';
3
+ import type { Authenticator, Principal } from '../auth/authenticator.js';
4
4
  import type { BlobStore } from '../blobs/store.js';
5
5
  import type { HiveMemClient } from '../clients/hivemem.js';
6
6
  import type { MagnusClient } from '../clients/magnus.js';
@@ -1,4 +1,4 @@
1
- import type { Principal } from '../auth/oathweave.js';
1
+ import type { Principal } from '../auth/authenticator.js';
2
2
  export interface AppEnv {
3
3
  Variables: {
4
4
  principal: Principal | undefined;
@@ -1,3 +1,4 @@
1
+ import { HiveError } from '@hivemagnus/hive-common/errors';
1
2
  import { ZodError } from 'zod';
2
3
  export class ApiError extends Error {
3
4
  status;
@@ -17,6 +18,10 @@ export function errorResponse(c, error, logger) {
17
18
  if (error instanceof ApiError) {
18
19
  return c.json({ error: { code: error.code, message: error.message, ...(error.details !== undefined ? { details: error.details } : {}) } }, error.status);
19
20
  }
21
+ if (HiveError.is(error) && error.status === 400) {
22
+ // hive-common feed helpers: a malformed cursor or limit.
23
+ return c.json({ error: { code: 'VALIDATION_ERROR', message: error.message } }, 400);
24
+ }
20
25
  if (error instanceof ZodError) {
21
26
  return c.json({ error: { code: 'VALIDATION_ERROR', message: 'The request is invalid', details: error.issues.map(issue => ({ path: issue.path.join('.'), message: issue.message })) } }, 400);
22
27
  }
@@ -0,0 +1,3 @@
1
+ import type { AppDeps } from '../deps.js';
2
+ import type { createRouter } from '../routing.js';
3
+ export declare function auditRoutes(register: ReturnType<typeof createRouter>, deps: AppDeps): void;
@@ -0,0 +1,6 @@
1
+ // The suite audit export (HiveAuditRecord v1). Read by Cindertrace with an Oathweave service
2
+ // token holding ardentstudio:audit:read, or by a linked Studio admin.
3
+ import { exportAuditPage } from '../../audit/export.js';
4
+ export function auditRoutes(register, deps) {
5
+ register('GET', '/audit/export', c => ({ body: exportAuditPage(deps.store.audit, new URL(c.req.url).searchParams) }));
6
+ }
@@ -1,28 +1,52 @@
1
+ // Health runs on the suite's shared readiness helper (@hivemagnus/hive-common/health), but keeps
2
+ // Studio's published body shape: HiveXUI parses it through the browser contract.
3
+ import { readiness } from '@hivemagnus/hive-common/health';
1
4
  const CACHE_MS = 10_000;
2
- const failed = (error) => ({ status: 'unavailable', error: error instanceof Error ? error.message.slice(0, 200) : 'unavailable' });
5
+ /** Above the Magnus client's own 20 s timeout, so the clients' timeouts decide, as before. */
6
+ const CHECK_TIMEOUT_MS = 25_000;
7
+ const toCheck = (report, maxError = 200) => report?.status === 'up'
8
+ ? { status: 'ok', error: null }
9
+ : { status: 'unavailable', error: (report?.error ?? 'unavailable').slice(0, maxError) };
3
10
  export function healthRoutes(register, deps) {
4
11
  let cached;
12
+ // Liveness never touches dependencies. Its body stays Studio's published `{status:'ok'}`.
5
13
  register('GET', '/health/live', () => ({ body: { status: 'ok' } }));
6
14
  register('GET', '/health/ready', async (c) => {
7
15
  if (!cached || Date.now() - cached.at > CACHE_MS) {
8
- let database = { status: 'ok', error: null };
9
- try {
10
- deps.store.db.prepare('SELECT 1').get();
11
- }
12
- catch (error) {
13
- database = failed(error);
14
- }
15
- const [agents, hivemem] = await Promise.all([
16
- deps.agents.list().then(list => list.magnus.state === 'ok'
17
- ? { magnus: { status: 'ok', error: null }, available: list.available_count }
18
- : { magnus: { status: 'unavailable', error: list.magnus.error }, available: 0 }),
19
- deps.hivemem.health().then(() => ({ status: 'ok', error: null }), failed),
20
- ]);
21
- // Ardent Project Studio requires at least one reachable Ardent agent.
22
- const ready = database.status === 'ok' && agents.magnus.status === 'ok' && hivemem.status === 'ok' && agents.available > 0;
16
+ // One Magnus listing serves both the magnus and the agents checks.
17
+ const listing = deps.agents.list();
18
+ listing.catch(() => undefined);
19
+ const report = await readiness({
20
+ timeoutMs: CHECK_TIMEOUT_MS,
21
+ exposeErrors: true,
22
+ checks: [
23
+ { name: 'database', check: () => { deps.store.db.prepare('SELECT 1').get(); } },
24
+ { name: 'magnus', check: async () => {
25
+ const list = await listing;
26
+ if (list.magnus.state !== 'ok')
27
+ throw new Error(list.magnus.error ?? 'unavailable');
28
+ } },
29
+ { name: 'hivemem', check: async () => { await deps.hivemem.health(); } },
30
+ // Ardent Project Studio requires at least one reachable Ardent agent.
31
+ { name: 'agents', check: async () => {
32
+ const list = await listing;
33
+ const available = list.magnus.state === 'ok' ? list.available_count : 0;
34
+ return { ok: available > 0, details: { available } };
35
+ } },
36
+ ],
37
+ });
38
+ const { checks } = report.body;
39
+ const agents = checks['agents'];
40
+ const available = typeof agents?.details?.['available'] === 'number' ? agents.details['available'] : 0;
41
+ const ready = report.status === 200;
23
42
  cached = { at: Date.now(), ready, body: {
24
43
  status: ready ? 'ready' : 'not_ready',
25
- checks: { database, magnus: agents.magnus, hivemem, agents: { status: agents.available > 0 ? 'ok' : 'unavailable', available: agents.available } },
44
+ checks: {
45
+ database: toCheck(checks['database']),
46
+ magnus: toCheck(checks['magnus'], 300),
47
+ hivemem: toCheck(checks['hivemem']),
48
+ agents: { status: agents?.status === 'up' ? 'ok' : 'unavailable', available },
49
+ },
26
50
  } };
27
51
  }
28
52
  c.header('cache-control', 'no-store');
@@ -27,6 +27,11 @@ export function projectRoutes(register, deps) {
27
27
  const needing = store.summaries.needingReview();
28
28
  projects = projects.filter(project => needing.has(project.id) === (filter.needs_review === 'true'));
29
29
  }
30
+ if (filter.agentRef !== undefined) {
31
+ // Narrows the list the caller can already read; it never adds a project.
32
+ const bound = store.team.projectsForAgent(filter.agentRef);
33
+ projects = projects.filter(project => bound.has(project.id));
34
+ }
30
35
  return { body: { projects: projects.map(summary) } };
31
36
  });
32
37
  register('POST', '/projects', (_c, { body, principal, idempotencyKey }) => {
@@ -1,5 +1,5 @@
1
1
  import { streamSSE } from 'hono/streaming';
2
- import { STREAM_RECHECK_MS } from '../../auth/oathweave.js';
2
+ import { STREAM_RECHECK_MS } from '../../auth/authenticator.js';
3
3
  import { requireProject } from '../deps.js';
4
4
  import { bearerToken } from '../routing.js';
5
5
  export const HEARTBEAT_MS = 15_000;
@@ -1,5 +1,5 @@
1
1
  import type { Context, Hono } from 'hono';
2
- import { type Authenticator, type Principal } from '../auth/oathweave.js';
2
+ import { type Authenticator, type Principal } from '../auth/authenticator.js';
3
3
  import type { Store } from '../db/store.js';
4
4
  import { type ArdentStudioBrowserRoute } from '../shared/browser.js';
5
5
  import type { AppEnv } from './env.js';
@@ -22,7 +22,10 @@ export declare function routeDefinition(method: ArdentStudioBrowserRoute['method
22
22
  /** `/projects/{projectId}` → `/projects/:projectId` */
23
23
  export declare function honoPath(path: string): string;
24
24
  export declare function bearerToken(c: Context): string | undefined;
25
- export declare function authenticateRequest(c: Context, authenticate: Authenticator, permissions: readonly string[]): Promise<Principal>;
25
+ export declare function authenticateRequest(c: Context, authenticate: Authenticator, permissions: readonly string[], options?: {
26
+ accept?: 'user' | 'any';
27
+ orPermission?: string | undefined;
28
+ }): Promise<Principal>;
26
29
  export declare function createRouter(app: Hono<AppEnv>, deps: {
27
30
  store: Store;
28
31
  authenticate: Authenticator;
@@ -1,7 +1,7 @@
1
1
  // Every route is registered from its entry in the browser contract, so the permissions,
2
2
  // idempotency rule, query and body schemas the contract publishes are the ones enforced.
3
3
  import { createHash } from 'node:crypto';
4
- import { AccessDenied } from '../auth/oathweave.js';
4
+ import { AccessDenied } from '../auth/authenticator.js';
5
5
  import { ARDENT_STUDIO_API_PREFIX, ardentStudioBrowserRoutes, IDEMPOTENCY_HEADER, idempotencyKeySchema } from '../shared/browser.js';
6
6
  import { ApiError } from './errors.js';
7
7
  export const JSON_BODY_LIMIT = 1024 * 1024;
@@ -20,21 +20,24 @@ export function bearerToken(c) {
20
20
  const match = header ? /^Bearer ([A-Za-z0-9._~+/-]+=*)$/.exec(header) : null;
21
21
  return match?.[1];
22
22
  }
23
- export async function authenticateRequest(c, authenticate, permissions) {
23
+ export async function authenticateRequest(c, authenticate, permissions, options = {}) {
24
24
  const token = bearerToken(c);
25
25
  if (!token)
26
26
  throw new ApiError(401, 'UNAUTHENTICATED', 'A bearer token is required');
27
27
  let principal;
28
28
  try {
29
- principal = await authenticate(token);
29
+ principal = await authenticate(token, options.accept === 'any' ? { accept: 'any' } : {});
30
30
  }
31
31
  catch (error) {
32
32
  if (error instanceof AccessDenied)
33
33
  throw new ApiError(403, 'NO_ACCOUNT_LINK', 'This identity has no Ardent Project Studio account link');
34
34
  throw new ApiError(401, 'UNAUTHENTICATED', 'The bearer token is not valid');
35
35
  }
36
- const missing = permissions.filter(permission => !principal.permissions.includes(permission));
37
- if (missing.length)
36
+ const holds = (permission) => principal.permissions.includes(permission);
37
+ const missing = permissions.filter(permission => !holds(permission));
38
+ // The alternative is for linked people (admins); a service only ever holds SERVICE_PERMISSIONS.
39
+ const admitted = principal.kind !== 'service' && options.orPermission !== undefined && holds(options.orPermission);
40
+ if (missing.length && !admitted)
38
41
  throw new ApiError(403, 'FORBIDDEN', 'Missing permission', { missing });
39
42
  return principal;
40
43
  }
@@ -64,7 +67,7 @@ export function createRouter(app, deps) {
64
67
  return function register(method, path, handler) {
65
68
  const route = routeDefinition(method, path);
66
69
  app.on(method, honoPath(path), async (c) => {
67
- const principal = route.permissions.length ? await authenticateRequest(c, deps.authenticate, route.permissions) : undefined;
70
+ const principal = route.permissions.length ? await authenticateRequest(c, deps.authenticate, route.permissions, { accept: route.accept ?? 'user', orPermission: route.orPermission }) : undefined;
68
71
  c.set('principal', principal);
69
72
  let idempotencyKey;
70
73
  if (route.idempotent) {
package/dist/service.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ServerType } from '@hono/node-server';
2
- import { type Authenticator } from './auth/oathweave.js';
2
+ import { type Authenticator } from './auth/authenticator.js';
3
3
  import { ChatterMostClient } from './clients/chattermost.js';
4
4
  import { HiveMemClient } from './clients/hivemem.js';
5
5
  import { MagnusClient } from './clients/magnus.js';
package/dist/service.js CHANGED
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { serve } from '@hono/node-server';
4
4
  import { AgentDirectory } from './agents/directory.js';
5
5
  import { OperationRelay } from './agents/operations.js';
6
- import { createAuthenticator, createOathweaveVerifier } from './auth/oathweave.js';
6
+ import { createAuthenticator, createOathweaveVerifier } from './auth/authenticator.js';
7
7
  import { BlobStore } from './blobs/store.js';
8
8
  import { ChatterMostClient } from './clients/chattermost.js';
9
9
  import { HiveMemClient } from './clients/hivemem.js';
@@ -44,7 +44,7 @@ export function createService(config, options = {}) {
44
44
  const chattermost = config.chattermost ? new ChatterMostClient(config.chattermost.webhookUrl, options.fetch) : undefined;
45
45
  const knowledge = new KnowledgeSync(store, blobs, hivemem, logger);
46
46
  const agents = new AgentDirectory(magnus, store);
47
- const authenticate = options.authenticate ?? createAuthenticator(createOathweaveVerifier({ issuer: config.oathweave.issuer, jwksUrl: config.oathweave.jwksUrl, userinfoUrl: config.oathweave.userinfoUrl }), config.oathweave.identities);
47
+ const authenticate = options.authenticate ?? createAuthenticator(createOathweaveVerifier({ issuer: config.oathweave.issuer, jwksUrl: config.oathweave.jwksUrl, userinfoUrl: config.oathweave.userinfoUrl, nativeIssuers: config.oathweave.nativeIssuers }), config.oathweave.identities);
48
48
  const live = new LiveText(bus);
49
49
  const conductor = new Conductor({ store, blobs, magnus, agents, live, logger });
50
50
  const importer = attachReview({ store, blobs, magnus, conductor, logger });
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  export declare const ardentStudioBrowserContractVersion = 1;
3
3
  export declare const ARDENT_STUDIO_API_PREFIX = "/api/ardentstudio/v1";
4
4
  export declare const ARDENT_STUDIO_AUDIENCE = "ardentstudio";
5
- export declare const ARDENT_STUDIO_PERMISSIONS: readonly ["ardentstudio:projects:read", "ardentstudio:projects:write", "ardentstudio:templates:read", "ardentstudio:templates:write", "ardentstudio:review:write", "ardentstudio:agents:admin"];
5
+ export declare const ARDENT_STUDIO_PERMISSIONS: readonly ["ardentstudio:projects:read", "ardentstudio:projects:write", "ardentstudio:templates:read", "ardentstudio:templates:write", "ardentstudio:review:write", "ardentstudio:agents:admin", "ardentstudio:audit:read"];
6
6
  export declare const permissionSchema: z.ZodEnum<{
7
7
  "ardentstudio:projects:read": "ardentstudio:projects:read";
8
8
  "ardentstudio:projects:write": "ardentstudio:projects:write";
@@ -10,6 +10,7 @@ export declare const permissionSchema: z.ZodEnum<{
10
10
  "ardentstudio:templates:write": "ardentstudio:templates:write";
11
11
  "ardentstudio:review:write": "ardentstudio:review:write";
12
12
  "ardentstudio:agents:admin": "ardentstudio:agents:admin";
13
+ "ardentstudio:audit:read": "ardentstudio:audit:read";
13
14
  }>;
14
15
  export type ArdentStudioPermission = z.infer<typeof permissionSchema>;
15
16
  /** Every mutation sends this header. A repeated key replays the first answer; a different request with the same key is a 409. */
@@ -337,6 +338,7 @@ export declare const listProjectsQuerySchema: z.ZodObject<{
337
338
  true: "true";
338
339
  false: "false";
339
340
  }>>;
341
+ agentRef: z.ZodOptional<z.ZodString>;
340
342
  }, z.core.$strip>;
341
343
  export declare const listProjectsResponseSchema: z.ZodObject<{
342
344
  projects: z.ZodArray<z.ZodObject<{
@@ -5802,6 +5804,86 @@ export declare const readyResponseSchema: z.ZodObject<{
5802
5804
  }, z.core.$strip>;
5803
5805
  }, z.core.$strip>;
5804
5806
  }, z.core.$strip>;
5807
+ export declare const hiveAuditRecordSchema: z.ZodObject<{
5808
+ id: z.ZodString;
5809
+ product: z.ZodEnum<{
5810
+ ardentstudio: "ardentstudio";
5811
+ hivemem: "hivemem";
5812
+ hiveskills: "hiveskills";
5813
+ embermend: "embermend";
5814
+ }>;
5815
+ occurredAt: z.ZodISODateTime;
5816
+ action: z.ZodString;
5817
+ outcome: z.ZodEnum<{
5818
+ success: "success";
5819
+ failure: "failure";
5820
+ }>;
5821
+ actor: z.ZodObject<{
5822
+ type: z.ZodEnum<{
5823
+ agent: "agent";
5824
+ user: "user";
5825
+ system: "system";
5826
+ service: "service";
5827
+ }>;
5828
+ id: z.ZodString;
5829
+ issuer: z.ZodOptional<z.ZodString>;
5830
+ displayName: z.ZodOptional<z.ZodString>;
5831
+ }, z.core.$strict>;
5832
+ target: z.ZodOptional<z.ZodObject<{
5833
+ type: z.ZodOptional<z.ZodString>;
5834
+ id: z.ZodOptional<z.ZodString>;
5835
+ }, z.core.$strict>>;
5836
+ subject: z.ZodOptional<z.ZodObject<{
5837
+ actorId: z.ZodOptional<z.ZodString>;
5838
+ agentRef: z.ZodOptional<z.ZodString>;
5839
+ nodeId: z.ZodOptional<z.ZodString>;
5840
+ }, z.core.$strict>>;
5841
+ details: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
5842
+ }, z.core.$strict>;
5843
+ export type HiveAuditRecord = z.infer<typeof hiveAuditRecordSchema>;
5844
+ export declare const auditExportQuerySchema: z.ZodObject<{
5845
+ after: z.ZodOptional<z.ZodString>;
5846
+ limit: z.ZodOptional<z.ZodString>;
5847
+ }, z.core.$strip>;
5848
+ export declare const auditExportResponseSchema: z.ZodObject<{
5849
+ data: z.ZodArray<z.ZodObject<{
5850
+ id: z.ZodString;
5851
+ product: z.ZodEnum<{
5852
+ ardentstudio: "ardentstudio";
5853
+ hivemem: "hivemem";
5854
+ hiveskills: "hiveskills";
5855
+ embermend: "embermend";
5856
+ }>;
5857
+ occurredAt: z.ZodISODateTime;
5858
+ action: z.ZodString;
5859
+ outcome: z.ZodEnum<{
5860
+ success: "success";
5861
+ failure: "failure";
5862
+ }>;
5863
+ actor: z.ZodObject<{
5864
+ type: z.ZodEnum<{
5865
+ agent: "agent";
5866
+ user: "user";
5867
+ system: "system";
5868
+ service: "service";
5869
+ }>;
5870
+ id: z.ZodString;
5871
+ issuer: z.ZodOptional<z.ZodString>;
5872
+ displayName: z.ZodOptional<z.ZodString>;
5873
+ }, z.core.$strict>;
5874
+ target: z.ZodOptional<z.ZodObject<{
5875
+ type: z.ZodOptional<z.ZodString>;
5876
+ id: z.ZodOptional<z.ZodString>;
5877
+ }, z.core.$strict>>;
5878
+ subject: z.ZodOptional<z.ZodObject<{
5879
+ actorId: z.ZodOptional<z.ZodString>;
5880
+ agentRef: z.ZodOptional<z.ZodString>;
5881
+ nodeId: z.ZodOptional<z.ZodString>;
5882
+ }, z.core.$strict>>;
5883
+ details: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
5884
+ }, z.core.$strict>>;
5885
+ nextCursor: z.ZodNullable<z.ZodString>;
5886
+ }, z.core.$strip>;
5805
5887
  export interface ArdentStudioBrowserRoute {
5806
5888
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
5807
5889
  /** Relative to ARDENT_STUDIO_API_PREFIX, with `{name}` for each path parameter. */
@@ -5814,6 +5896,10 @@ export interface ArdentStudioBrowserRoute {
5814
5896
  response: z.ZodType | null;
5815
5897
  /** Every listed permission is required. An empty list means no authentication. */
5816
5898
  permissions: readonly ArdentStudioPermission[];
5899
+ /** A linked person holding this permission passes instead of `permissions` (Studio admins). */
5900
+ orPermission?: ArdentStudioPermission;
5901
+ /** `any` also accepts Oathweave service (client-credentials) tokens; the default is people only. */
5902
+ accept?: 'user' | 'any';
5817
5903
  /** The route requires the x-idempotency-key header. */
5818
5904
  idempotent: boolean;
5819
5905
  successStatus?: 201;
@@ -15,6 +15,7 @@ export const ARDENT_STUDIO_PERMISSIONS = [
15
15
  'ardentstudio:templates:write',
16
16
  'ardentstudio:review:write',
17
17
  'ardentstudio:agents:admin',
18
+ 'ardentstudio:audit:read',
18
19
  ];
19
20
  export const permissionSchema = z.enum(ARDENT_STUDIO_PERMISSIONS);
20
21
  /** Every mutation sends this header. A repeated key replays the first answer; a different request with the same key is a 409. */
@@ -149,6 +150,8 @@ export const listProjectsQuerySchema = z.object({
149
150
  q: z.string().trim().max(200).optional(),
150
151
  /** true: only projects with review_pending > 0. */
151
152
  needs_review: z.enum(['true', 'false']).optional(),
153
+ /** Only projects where this agent (<magnusNodeId>~<profile>) is bound to a role in the published or the draft team. */
154
+ agentRef: agentRefSchema.optional(),
152
155
  });
153
156
  export const listProjectsResponseSchema = z.object({ projects: z.array(projectSummarySchema) });
154
157
  export const projectResponseSchema = projectSchema;
@@ -1211,18 +1214,52 @@ export const readyResponseSchema = z.object({
1211
1214
  agents: z.object({ status: z.enum(['ok', 'unavailable']), available: z.number().int() }),
1212
1215
  }),
1213
1216
  });
1217
+ // ── Audit export (HiveAuditRecord v1, shared across the suite) ──
1218
+ /** The suite-wide audit record. Every product serves the same shape so one collector can parse them all. */
1219
+ const auditScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
1220
+ export const hiveAuditRecordSchema = z.object({
1221
+ id: z.string().regex(/^(hivemem|hiveskills|ardentstudio|embermend):audit:[A-Za-z0-9._:-]{1,200}$/),
1222
+ product: z.enum(['hivemem', 'hiveskills', 'ardentstudio', 'embermend']),
1223
+ occurredAt: z.iso.datetime(),
1224
+ action: z.string().regex(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/).max(128),
1225
+ outcome: z.enum(['success', 'failure']),
1226
+ actor: z.object({
1227
+ type: z.enum(['user', 'service', 'agent', 'system']),
1228
+ id: z.string().min(1).max(256),
1229
+ issuer: z.string().max(512).optional(),
1230
+ displayName: z.string().max(256).optional(),
1231
+ }).strict(),
1232
+ /** Omitted when empty. */
1233
+ target: z.object({ type: z.string().max(64).optional(), id: z.string().max(256).optional() }).strict().optional(),
1234
+ /** Omitted when empty. */
1235
+ subject: z.object({ actorId: z.string().max(256).optional(), agentRef: z.string().max(257).optional(), nodeId: z.string().max(128).optional() }).strict().optional(),
1236
+ /** Flat scalars, at most 4 KB serialized, already redacted. Never secrets, memory content, prompts or file contents. */
1237
+ details: z.record(z.string(), auditScalarSchema),
1238
+ }).strict();
1239
+ export const auditExportQuerySchema = z.object({
1240
+ /** The opaque nextCursor of the previous page. Omit for the oldest record. */
1241
+ after: z.string().max(512).optional(),
1242
+ /** 1 to 200; default 100. Larger values are clamped to 200. */
1243
+ limit: z.string().regex(/^\d{1,6}$/).optional(),
1244
+ });
1245
+ export const auditExportResponseSchema = z.object({
1246
+ data: z.array(hiveAuditRecordSchema),
1247
+ /** The last record's cursor whenever `data` is not empty, even on the last page; null only when `data` is empty (caught up). Send it as `after` to read on. */
1248
+ nextCursor: z.string().nullable(),
1249
+ });
1214
1250
  const READ = ['ardentstudio:projects:read'];
1215
1251
  const WRITE = ['ardentstudio:projects:write'];
1216
1252
  const TEMPLATES_READ = ['ardentstudio:templates:read'];
1217
1253
  const TEMPLATES_WRITE = ['ardentstudio:templates:write'];
1218
1254
  const REVIEW = ['ardentstudio:review:write'];
1219
1255
  const AGENTS_ADMIN = ['ardentstudio:agents:admin'];
1256
+ const AUDIT_READ = ['ardentstudio:audit:read'];
1220
1257
  export const ardentStudioBrowserRoutes = [
1221
1258
  // Health
1222
1259
  { method: 'GET', path: '/health/live', response: liveResponseSchema, permissions: [], idempotent: false },
1223
1260
  { method: 'GET', path: '/health/ready', response: readyResponseSchema, permissions: [], idempotent: false, note: 'Answers 503 with the same body when not ready. Checks are cached for 10 seconds.' },
1224
1261
  // Projects
1225
- { method: 'GET', path: '/projects', query: listProjectsQuerySchema, response: listProjectsResponseSchema, permissions: READ, idempotent: false, note: 'q matches name, brief and tags. needs_review=true keeps projects with review_pending > 0.' },
1262
+ { method: 'GET', path: '/projects', query: listProjectsQuerySchema, response: listProjectsResponseSchema, permissions: READ, idempotent: false, note: 'q matches name, brief and tags. needs_review=true keeps projects with review_pending > 0. agentRef keeps projects where that agent is bound to a role in the published or the draft team; an agent with no roles gets an empty list. 400 VALIDATION_ERROR for a malformed agentRef.' },
1226
1263
  { method: 'POST', path: '/projects', requestBody: 'json', request: createProjectRequestSchema, response: projectResponseSchema, permissions: WRITE, idempotent: true, successStatus: 201, note: 'Creates the project HiveMem workspace; when HiveMem is unavailable hivemem.state stays pending and is retried.' },
1227
1264
  { method: 'GET', path: '/projects/{projectId}', response: projectResponseSchema, permissions: READ, idempotent: false },
1228
1265
  { method: 'PATCH', path: '/projects/{projectId}', requestBody: 'json', request: updateProjectRequestSchema, response: projectResponseSchema, permissions: WRITE, idempotent: true, note: 'An archived project is read-only (409) except last_area.' },
@@ -1309,6 +1346,9 @@ export const ardentStudioBrowserRoutes = [
1309
1346
  { method: 'POST', path: '/projects/{projectId}/exports', requestBody: 'json', request: createExportRequestSchema, response: exportResponseSchema, permissions: REVIEW, idempotent: true, successStatus: 201, note: 'A zip of the final revisions with manifest.json (Ardent manifest fields, review status and decisions). Deliverables without a final revision are listed as excluded; none at all answers 409 NOTHING_FINAL.' },
1310
1347
  { method: 'GET', path: '/files/{fileId}/docx', response: docxDocumentResponseSchema, permissions: READ, idempotent: false, note: 'Answers 501 DOCX_CONVERTER_UNAVAILABLE until the canvas Word codec package is installed.' },
1311
1348
  { method: 'GET', path: '/revisions/{revisionId}/docx', response: docxDocumentResponseSchema, permissions: READ, idempotent: false, note: 'Answers 501 DOCX_CONVERTER_UNAVAILABLE until the canvas Word codec package is installed.' },
1349
+ // Audit
1350
+ { method: 'GET', path: '/audit/export', query: auditExportQuerySchema, response: auditExportResponseSchema, permissions: AUDIT_READ, orPermission: 'ardentstudio:agents:admin', accept: 'any', idempotent: false,
1351
+ note: 'Every audit record, oldest first, as HiveAuditRecord v1, keyset-paged on the record id. Accepts an Oathweave service token with ardentstudio:audit:read (no account link needed; native-issuer service tokens too when OATHWEAVE_NATIVE_ISSUERS lists the issuer), or a linked person with ardentstudio:audit:read or ardentstudio:agents:admin. 400 for a malformed cursor or limit. nextCursor is the last record\'s cursor whenever data is not empty, and null only when data is empty (caught up).' },
1312
1352
  // Stream
1313
1353
  {
1314
1354
  method: 'GET', path: '/projects/{projectId}/stream', query: streamQuerySchema, response: null, permissions: READ, idempotent: false,