@hivemagnus/ardent-studio 0.3.0 → 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.
package/README.md CHANGED
@@ -80,6 +80,7 @@ when a required setting is missing or unsafe.
80
80
  | `OATHWEAVE_ISSUER` | yes | | Oathweave browser issuer (HTTPS). |
81
81
  | `OATHWEAVE_JWKS_URL` | yes | | Oathweave JWKS (HTTPS). |
82
82
  | `OATHWEAVE_USERINFO_URL` | yes | | Oathweave userinfo (HTTPS), for session revocation. |
83
+ | `OATHWEAVE_NATIVE_ISSUERS` | no | | Comma-separated Oathweave native issuer ids (its `JWT_ISSUER`). Service tokens they sign are accepted on the audit export only. |
83
84
  | `ARDENT_STUDIO_OATHWEAVE_IDENTITIES_JSON` | yes | | Explicit account links; see [docs/OATHWEAVE.md](docs/OATHWEAVE.md). |
84
85
  | `ARDENT_STUDIO_OATHWEAVE_TOKEN_URL` | yes | | Oathweave token endpoint for `client_credentials`. |
85
86
  | `ARDENT_STUDIO_MAGNUS_URL` | yes | | Magnus control API origin. |
@@ -104,6 +105,12 @@ permission, except the health routes. Every mutation takes `x-idempotency-key`.
104
105
  [`src/shared/browser.ts`](src/shared/browser.ts) (also exported as
105
106
  `@hivemagnus/ardent-studio/browser`).
106
107
 
108
+ `GET /api/ardentstudio/v1/audit/export` serves the audit log as suite `HiveAuditRecord` v1
109
+ records for a collector such as Cindertrace. It needs the permission `ardentstudio:audit:read`,
110
+ which an Oathweave service (client_credentials) token for the audience `ardentstudio` may carry,
111
+ or a linked Studio admin (`ardentstudio:agents:admin`). See
112
+ [docs/OATHWEAVE.md](docs/OATHWEAVE.md#audit-export).
113
+
107
114
  ## Container image
108
115
 
109
116
  ```bash
@@ -0,0 +1,22 @@
1
+ import type { AuditEntry, AuditRepository } from '../db/repos/audit.js';
2
+ import { type HiveAuditRecord } from '../shared/browser.js';
3
+ export declare const AUDIT_EXPORT_DEFAULT_LIMIT = 100;
4
+ export declare const AUDIT_EXPORT_MAX_LIMIT = 200;
5
+ export declare const AUDIT_DETAILS_MAX_BYTES = 4096;
6
+ type Scalar = string | number | boolean | null;
7
+ export interface AuditExportPage {
8
+ data: HiveAuditRecord[];
9
+ nextCursor: string | null;
10
+ }
11
+ /** The numeric id after a cursor; 0 for the first page. Throws a 400 HiveError for a malformed cursor. */
12
+ export declare function cursorPosition(after: string | undefined): number;
13
+ /** Flat scalars only: arrays of scalars are joined, other arrays become a count, objects are dropped. */
14
+ export declare function flattenDetails(detail: Record<string, unknown>): Record<string, Scalar>;
15
+ export declare function toAuditRecord(entry: AuditEntry, taskRefs?: Map<string, string>): HiveAuditRecord;
16
+ /**
17
+ * One export page for `?after=&limit=`. Throws a 400 HiveError for a malformed query.
18
+ * `nextCursor` is the last record's cursor whenever the page has records, even when it is the
19
+ * last page, so a collector can store it and poll again; it is null only for an empty page.
20
+ */
21
+ export declare function exportAuditPage(audit: AuditRepository, query: URLSearchParams | Readonly<Record<string, string | undefined>>): AuditExportPage;
22
+ export {};
@@ -0,0 +1,115 @@
1
+ // The suite audit export: Studio's append-only audit table as HiveAuditRecord v1, oldest first,
2
+ // keyset-paged on the table's monotonic id. Details are flattened to scalars, redacted with the
3
+ // suite's log redaction and capped at 4 KB; they never hold secrets or file contents.
4
+ import { decodeCursor, encodeCursor, parseFeedQuery } from '@hivemagnus/hive-common/events';
5
+ import { HiveError } from '@hivemagnus/hive-common/errors';
6
+ import { redact } from '@hivemagnus/hive-common/log';
7
+ import { agentRefSchema } from '../shared/browser.js';
8
+ export const AUDIT_EXPORT_DEFAULT_LIMIT = 100;
9
+ export const AUDIT_EXPORT_MAX_LIMIT = 200;
10
+ export const AUDIT_DETAILS_MAX_BYTES = 4096;
11
+ const MAX_STRING = 512;
12
+ /** Detail keys that could carry content rather than metadata. None are recorded today; this keeps it so. */
13
+ const CONTENT_KEYS = new Set(['body', 'body_md', 'content', 'text', 'prompt', 'brief', 'soul', 'identity', 'instructions', 'answer', 'message']);
14
+ /** The numeric id after a cursor; 0 for the first page. Throws a 400 HiveError for a malformed cursor. */
15
+ export function cursorPosition(after) {
16
+ if (after === undefined || after === '')
17
+ return 0;
18
+ const position = decodeCursor(after);
19
+ const id = position.id;
20
+ if (Object.keys(position).length !== 1 || typeof id !== 'number' || !Number.isSafeInteger(id) || id < 0) {
21
+ throw new HiveError('invalid_cursor', 'The cursor is not valid', { status: 400 });
22
+ }
23
+ return id;
24
+ }
25
+ function scalar(value) {
26
+ if (value === null || typeof value === 'boolean')
27
+ return value;
28
+ if (typeof value === 'number')
29
+ return Number.isFinite(value) ? value : undefined;
30
+ if (typeof value === 'string')
31
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
32
+ return undefined;
33
+ }
34
+ /** Flat scalars only: arrays of scalars are joined, other arrays become a count, objects are dropped. */
35
+ export function flattenDetails(detail) {
36
+ const redacted = redact(detail);
37
+ const flat = {};
38
+ for (const [key, value] of Object.entries(redacted)) {
39
+ if (CONTENT_KEYS.has(key.toLowerCase()))
40
+ continue;
41
+ const direct = scalar(value);
42
+ if (direct !== undefined) {
43
+ flat[key] = direct;
44
+ continue;
45
+ }
46
+ if (Array.isArray(value)) {
47
+ const items = value.map(item => scalar(item));
48
+ if (items.every(item => item !== undefined && item !== null && typeof item !== 'boolean')) {
49
+ const joined = scalar(items.join(','));
50
+ if (joined !== undefined)
51
+ flat[key] = joined;
52
+ }
53
+ else
54
+ flat[`${key}_count`] = value.length;
55
+ }
56
+ }
57
+ // Keep the record bounded: drop the longest values until the details fit.
58
+ while (Buffer.byteLength(JSON.stringify(flat)) > AUDIT_DETAILS_MAX_BYTES) {
59
+ const longest = Object.entries(flat).sort(([, a], [, b]) => JSON.stringify(b).length - JSON.stringify(a).length)[0];
60
+ if (!longest)
61
+ break;
62
+ delete flat[longest[0]];
63
+ flat.truncated = true;
64
+ }
65
+ return flat;
66
+ }
67
+ function actorOf(entry) {
68
+ const userId = entry.actor_user_id;
69
+ if (userId === 'system')
70
+ return { type: 'system', id: 'system' };
71
+ if (userId.startsWith('service:')) {
72
+ return { type: 'service', id: entry.actor_subject ?? userId.slice('service:'.length), ...(entry.actor_issuer ? { issuer: entry.actor_issuer } : {}) };
73
+ }
74
+ return {
75
+ type: 'user',
76
+ id: entry.actor_subject ?? userId,
77
+ ...(entry.actor_issuer ? { issuer: entry.actor_issuer } : {}),
78
+ displayName: userId,
79
+ };
80
+ }
81
+ /** The agent a record is about: an agent target, a task that ran on an agent, or a detail agent_ref. */
82
+ function agentRefOf(entry, taskRefs) {
83
+ const candidates = [
84
+ entry.target_type === 'agent' ? entry.target_id : undefined,
85
+ entry.target_type === 'task' ? taskRefs.get(entry.target_id) : undefined,
86
+ typeof entry.detail.agent_ref === 'string' ? entry.detail.agent_ref : undefined,
87
+ ];
88
+ return candidates.find(candidate => candidate !== undefined && agentRefSchema.safeParse(candidate).success);
89
+ }
90
+ export function toAuditRecord(entry, taskRefs = new Map()) {
91
+ const agentRef = agentRefOf(entry, taskRefs);
92
+ return {
93
+ id: `ardentstudio:audit:${entry.id}`,
94
+ product: 'ardentstudio',
95
+ occurredAt: new Date(entry.at).toISOString(),
96
+ action: entry.action.toLowerCase(),
97
+ outcome: 'success',
98
+ actor: actorOf(entry),
99
+ ...(entry.target_type || entry.target_id ? { target: { ...(entry.target_type ? { type: entry.target_type } : {}), ...(entry.target_id ? { id: entry.target_id } : {}) } } : {}),
100
+ ...(agentRef ? { subject: { agentRef, nodeId: agentRef.slice(0, agentRef.indexOf('~')) } } : {}),
101
+ details: flattenDetails(entry.detail),
102
+ };
103
+ }
104
+ /**
105
+ * One export page for `?after=&limit=`. Throws a 400 HiveError for a malformed query.
106
+ * `nextCursor` is the last record's cursor whenever the page has records, even when it is the
107
+ * last page, so a collector can store it and poll again; it is null only for an empty page.
108
+ */
109
+ export function exportAuditPage(audit, query) {
110
+ const { after, limit } = parseFeedQuery(query, { defaultLimit: AUDIT_EXPORT_DEFAULT_LIMIT, maxLimit: AUDIT_EXPORT_MAX_LIMIT });
111
+ const entries = audit.listAfter(cursorPosition(after), limit);
112
+ const taskRefs = audit.taskAgentRefs(entries.filter(entry => entry.target_type === 'task').map(entry => entry.target_id));
113
+ const last = entries.at(-1);
114
+ return { data: entries.map(entry => toAuditRecord(entry, taskRefs)), nextCursor: last ? encodeCursor({ id: last.id }) : null };
115
+ }
@@ -6,8 +6,14 @@ export interface OathweaveOptions {
6
6
  jwksUrl: string;
7
7
  userinfoUrl: string;
8
8
  audience?: string;
9
+ /**
10
+ * Oathweave native issuer identifiers (its JWT issuer name, for example `hive-iam`). Accepted
11
+ * for service tokens only, so only on routes that accept services (the audit export).
12
+ */
13
+ nativeIssuers?: readonly string[];
9
14
  }
10
15
  export interface OathweaveIdentity {
16
+ kind: 'user' | 'service';
11
17
  subject: string;
12
18
  issuer: string;
13
19
  sessionId: string;
@@ -16,9 +22,18 @@ export interface OathweaveIdentity {
16
22
  issuedAt: number;
17
23
  expiresAt: number;
18
24
  }
19
- export type OathweaveVerifier = (token: string) => Promise<OathweaveIdentity>;
20
- /** A verified, linked caller. */
25
+ export interface VerifyOptions {
26
+ /** `any` also accepts service (client-credentials) tokens. The default is people only. */
27
+ accept?: 'user' | 'any';
28
+ }
29
+ export type OathweaveVerifier = (token: string, options?: VerifyOptions) => Promise<OathweaveIdentity>;
30
+ /** The only permissions a service token can carry into Studio. */
31
+ export declare const SERVICE_PERMISSIONS: readonly ArdentStudioPermission[];
32
+ /** A verified, linked caller, or a verified service on a route that accepts services. */
21
33
  export interface Principal {
34
+ /** Absent means a person, as before services were accepted anywhere. */
35
+ kind?: 'user' | 'service';
36
+ /** The linked Studio user, or `service:<client id>` for a service. */
22
37
  userId: string;
23
38
  subject: string;
24
39
  issuer: string;
@@ -49,6 +64,9 @@ export declare function createOathweaveVerifier(options: OathweaveOptions, overr
49
64
  fetch?: typeof fetch;
50
65
  now?: () => number;
51
66
  }): OathweaveVerifier;
52
- export type Authenticator = (token: string) => Promise<Principal>;
53
- /** Verify a bearer token and map it through its one explicit identity link. */
67
+ export type Authenticator = (token: string, options?: VerifyOptions) => Promise<Principal>;
68
+ /**
69
+ * Verify a bearer token and map a person through their one explicit identity link. A service
70
+ * (only with `accept: 'any'`) needs no link and keeps only SERVICE_PERMISSIONS.
71
+ */
54
72
  export declare function createAuthenticator(verify: OathweaveVerifier, links: readonly IdentityLink[]): Authenticator;
@@ -1,10 +1,13 @@
1
1
  // Oathweave resource-token verification for ArdentStudio, using the suite's shared verifier
2
2
  // (@hivemagnus/hive-common/oathweave): RS256 only, exact issuer and audience, at most fifteen
3
3
  // minutes, USER actor with a session, and an online session check through userinfo.
4
- // Failure is closed. Studio accepts people only; service tokens are refused.
4
+ // Failure is closed. Studio accepts people only; service tokens are refused everywhere except on
5
+ // routes that opt in (the audit export), where a service gets only SERVICE_PERMISSIONS.
5
6
  import { createOathweaveVerifier as createSharedVerifier, OATHWEAVE_SESSION_CACHE_TTL_MS, OATHWEAVE_STREAM_RECHECK_MS, } from '@hivemagnus/hive-common/oathweave';
6
7
  import { ARDENT_STUDIO_AUDIENCE } from '../shared/browser.js';
7
8
  import { effectivePermissions, linkFor } from './links.js';
9
+ /** The only permissions a service token can carry into Studio. */
10
+ export const SERVICE_PERMISSIONS = ['ardentstudio:audit:read'];
8
11
  /** The token is valid but its subject has no explicit link, so the person has no Studio access. */
9
12
  export class AccessDenied extends Error {
10
13
  name = 'AccessDenied';
@@ -28,33 +31,52 @@ export function createOathweaveVerifier(options, overrides = {}) {
28
31
  audience: options.audience ?? ARDENT_STUDIO_AUDIENCE,
29
32
  jwksUrl: options.jwksUrl,
30
33
  userinfoUrl: options.userinfoUrl,
34
+ ...(options.nativeIssuers?.length ? { nativeIssuers: options.nativeIssuers, nativeIssuerAccept: 'service' } : {}),
31
35
  accept: 'user',
32
36
  clockToleranceSec: CLOCK_TOLERANCE_SEC,
33
37
  ...(overrides.keys ? { keys: overrides.keys } : {}),
34
38
  ...(overrides.fetch ? { fetch: overrides.fetch } : {}),
35
39
  ...(overrides.now ? { now: overrides.now } : {}),
36
40
  });
37
- return async (token) => {
38
- const principal = await verifier.verifyUser(token);
41
+ return async (token, verifyOptions = {}) => {
42
+ const principal = verifyOptions.accept === 'any' ? await verifier.verify(token, { accept: 'any' }) : await verifier.verifyUser(token);
39
43
  return {
44
+ kind: principal.kind,
40
45
  subject: principal.subject,
41
46
  issuer: principal.issuer,
42
- sessionId: principal.sessionId,
47
+ sessionId: principal.kind === 'user' ? principal.sessionId : '',
43
48
  permissions: [...principal.permissions],
44
- displayName: principal.displayName,
49
+ displayName: principal.kind === 'user' ? principal.displayName : principal.subject,
45
50
  issuedAt: principal.issuedAt,
46
51
  expiresAt: principal.expiresAt,
47
52
  };
48
53
  };
49
54
  }
50
- /** Verify a bearer token and map it through its one explicit identity link. */
55
+ /**
56
+ * Verify a bearer token and map a person through their one explicit identity link. A service
57
+ * (only with `accept: 'any'`) needs no link and keeps only SERVICE_PERMISSIONS.
58
+ */
51
59
  export function createAuthenticator(verify, links) {
52
- return async (token) => {
53
- const identity = await verify(token);
60
+ return async (token, options = {}) => {
61
+ const identity = await verify(token, options);
62
+ if (identity.kind === 'service') {
63
+ if (options.accept !== 'any')
64
+ throw new Error('Service tokens are not accepted here');
65
+ return {
66
+ kind: 'service',
67
+ userId: `service:${identity.subject}`,
68
+ subject: identity.subject,
69
+ issuer: identity.issuer,
70
+ displayName: identity.subject,
71
+ permissions: SERVICE_PERMISSIONS.filter(permission => identity.permissions.includes(permission)),
72
+ expiresAt: identity.expiresAt,
73
+ };
74
+ }
54
75
  const link = linkFor(links, identity.issuer, identity.subject);
55
76
  if (!link)
56
77
  throw new AccessDenied('An explicit unique account link is required');
57
78
  return {
79
+ kind: 'user',
58
80
  userId: link.localUserId,
59
81
  subject: identity.subject,
60
82
  issuer: identity.issuer,
@@ -15,6 +15,7 @@ declare const linkSchema: z.ZodObject<{
15
15
  "ardentstudio:templates:write": "ardentstudio:templates:write";
16
16
  "ardentstudio:review:write": "ardentstudio:review:write";
17
17
  "ardentstudio:agents:admin": "ardentstudio:agents:admin";
18
+ "ardentstudio:audit:read": "ardentstudio:audit:read";
18
19
  }>>>;
19
20
  }, z.core.$strict>;
20
21
  export type IdentityLink = z.infer<typeof linkSchema>;
package/dist/config.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface Config {
22
22
  userinfoUrl: string;
23
23
  tokenUrl: string;
24
24
  identities: IdentityLink[];
25
+ /** OATHWEAVE_NATIVE_ISSUERS: native issuer ids accepted for service tokens on the audit export. */
26
+ nativeIssuers: string[];
25
27
  };
26
28
  magnus: ServiceClientConfig;
27
29
  hivemem: ServiceClientConfig;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, resolve } from 'node:path';
2
2
  import { z } from 'zod';
3
+ import { OATHWEAVE_NATIVE_ISSUER_PATTERN } from '@hivemagnus/hive-common/oathweave';
3
4
  import { parseIdentityLinks } from './auth/links.js';
4
5
  import { trimTrailingSlashes } from './url.js';
5
6
  export const DEFAULT_PORT = 8097;
@@ -24,6 +25,7 @@ export const ENV_CATALOG = [
24
25
  { name: 'OATHWEAVE_ISSUER', required: true, secret: false, description: 'Oathweave browser issuer (HTTPS).' },
25
26
  { name: 'OATHWEAVE_JWKS_URL', required: true, secret: false, description: 'Oathweave JWKS URL (HTTPS).' },
26
27
  { name: 'OATHWEAVE_USERINFO_URL', required: true, secret: false, description: 'Oathweave userinfo URL (HTTPS), used for online session revocation.' },
28
+ { name: 'OATHWEAVE_NATIVE_ISSUERS', required: false, secret: false, description: 'Optional comma-separated Oathweave native issuer ids (its JWT issuer name). Service tokens they sign are accepted on the audit export only; never for people.' },
27
29
  { name: 'ARDENT_STUDIO_OATHWEAVE_IDENTITIES_JSON', required: true, secret: false, description: 'JSON array of explicit account links: [{issuer, subject, localUserId, displayName?, permissions?}].' },
28
30
  { name: 'ARDENT_STUDIO_OATHWEAVE_TOKEN_URL', required: true, secret: false, description: 'Oathweave OAuth token endpoint for the client_credentials grant.' },
29
31
  { name: 'ARDENT_STUDIO_MAGNUS_URL', required: true, secret: false, description: 'Magnus control API base URL (the origin that serves /api/v1).' },
@@ -105,6 +107,10 @@ export function loadConfig(env = process.env) {
105
107
  const jwksUrl = url('OATHWEAVE_JWKS_URL', true);
106
108
  const userinfoUrl = url('OATHWEAVE_USERINFO_URL', true);
107
109
  const tokenUrl = url('ARDENT_STUDIO_OATHWEAVE_TOKEN_URL', false);
110
+ const nativeIssuers = (value('OATHWEAVE_NATIVE_ISSUERS') ?? '').split(',').map(item => item.trim()).filter(Boolean);
111
+ if (nativeIssuers.some(item => !OATHWEAVE_NATIVE_ISSUER_PATTERN.test(item))) {
112
+ issues.push({ name: 'OATHWEAVE_NATIVE_ISSUERS', message: 'must be comma-separated Oathweave native issuer ids ([a-z][a-z0-9-], at most 64 each)' });
113
+ }
108
114
  let identities = [];
109
115
  const identitiesRaw = required('ARDENT_STUDIO_OATHWEAVE_IDENTITIES_JSON');
110
116
  if (identitiesRaw) {
@@ -152,7 +158,7 @@ export function loadConfig(env = process.env) {
152
158
  maxBlobBytes: numeric('ARDENT_STUDIO_MAX_BLOB_BYTES', DEFAULT_MAX_BLOB_BYTES, 1024, 1024 * 1024 * 1024),
153
159
  eventRetentionDays: numeric('ARDENT_STUDIO_EVENT_RETENTION_DAYS', DEFAULT_EVENT_RETENTION_DAYS, 1, 3650),
154
160
  logLevel: logLevel,
155
- oathweave: { issuer, jwksUrl, userinfoUrl, tokenUrl, identities },
161
+ oathweave: { issuer, jwksUrl, userinfoUrl, tokenUrl, identities, nativeIssuers },
156
162
  magnus,
157
163
  hivemem,
158
164
  chattermost,
@@ -34,4 +34,8 @@ export declare class AuditRepository {
34
34
  targetId?: string;
35
35
  limit?: number;
36
36
  }): AuditEntry[];
37
+ /** Oldest first, strictly after `afterId`. The id is the table's monotonic key, so pages never skip or repeat. */
38
+ listAfter(afterId: number, limit: number): AuditEntry[];
39
+ /** The Ardent agent each task ran on, for the tasks that have one. */
40
+ taskAgentRefs(taskIds: readonly string[]): Map<string, string>;
37
41
  }
@@ -17,4 +17,20 @@ export class AuditRepository {
17
17
  : this.db.prepare('SELECT * FROM audit ORDER BY id DESC LIMIT ?').all(filter.limit ?? 100));
18
18
  return rows.map(({ detail_json, ...row }) => ({ ...row, detail: readJson(detail_json, {}) }));
19
19
  }
20
+ /** Oldest first, strictly after `afterId`. The id is the table's monotonic key, so pages never skip or repeat. */
21
+ listAfter(afterId, limit) {
22
+ const rows = this.db.prepare('SELECT * FROM audit WHERE id > ? ORDER BY id ASC LIMIT ?').all(afterId, limit);
23
+ return rows.map(({ detail_json, ...row }) => ({ ...row, detail: readJson(detail_json, {}) }));
24
+ }
25
+ /** The Ardent agent each task ran on, for the tasks that have one. */
26
+ taskAgentRefs(taskIds) {
27
+ const refs = new Map();
28
+ if (!taskIds.length)
29
+ return refs;
30
+ const rows = this.db.prepare(`SELECT id, ardent_agent_ref FROM tasks WHERE ardent_agent_ref IS NOT NULL AND id IN (${taskIds.map(() => '?').join(', ')})`)
31
+ .all(...taskIds);
32
+ for (const row of rows)
33
+ refs.set(row.id, row.ardent_agent_ref);
34
+ return refs;
35
+ }
20
36
  }
@@ -3,6 +3,7 @@ import { Hono } from 'hono';
3
3
  import { ARDENT_STUDIO_API_PREFIX } from '../shared/browser.js';
4
4
  import { errorResponse } from './errors.js';
5
5
  import { createRouter } from './routing.js';
6
+ import { auditRoutes } from './routes/audit.js';
6
7
  import { agentStudioRoutes } from './routes/agent-studio.js';
7
8
  import { agentRoutes } from './routes/agents.js';
8
9
  import { fileRoutes } from './routes/files.js';
@@ -43,6 +44,7 @@ export function createApp(deps) {
43
44
  roomRoutes(register, deps);
44
45
  workRoutes(register, deps);
45
46
  reviewRoutes(register, deps);
47
+ auditRoutes(register, deps);
46
48
  streamRoutes(register, deps);
47
49
  return app;
48
50
  }
@@ -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
+ }
@@ -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;
@@ -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.js CHANGED
@@ -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. */
@@ -5803,6 +5804,86 @@ export declare const readyResponseSchema: z.ZodObject<{
5803
5804
  }, z.core.$strip>;
5804
5805
  }, z.core.$strip>;
5805
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>;
5806
5887
  export interface ArdentStudioBrowserRoute {
5807
5888
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
5808
5889
  /** Relative to ARDENT_STUDIO_API_PREFIX, with `{name}` for each path parameter. */
@@ -5815,6 +5896,10 @@ export interface ArdentStudioBrowserRoute {
5815
5896
  response: z.ZodType | null;
5816
5897
  /** Every listed permission is required. An empty list means no authentication. */
5817
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';
5818
5903
  /** The route requires the x-idempotency-key header. */
5819
5904
  idempotent: boolean;
5820
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. */
@@ -1213,12 +1214,46 @@ export const readyResponseSchema = z.object({
1213
1214
  agents: z.object({ status: z.enum(['ok', 'unavailable']), available: z.number().int() }),
1214
1215
  }),
1215
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
+ });
1216
1250
  const READ = ['ardentstudio:projects:read'];
1217
1251
  const WRITE = ['ardentstudio:projects:write'];
1218
1252
  const TEMPLATES_READ = ['ardentstudio:templates:read'];
1219
1253
  const TEMPLATES_WRITE = ['ardentstudio:templates:write'];
1220
1254
  const REVIEW = ['ardentstudio:review:write'];
1221
1255
  const AGENTS_ADMIN = ['ardentstudio:agents:admin'];
1256
+ const AUDIT_READ = ['ardentstudio:audit:read'];
1222
1257
  export const ardentStudioBrowserRoutes = [
1223
1258
  // Health
1224
1259
  { method: 'GET', path: '/health/live', response: liveResponseSchema, permissions: [], idempotent: false },
@@ -1311,6 +1346,9 @@ export const ardentStudioBrowserRoutes = [
1311
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.' },
1312
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.' },
1313
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).' },
1314
1352
  // Stream
1315
1353
  {
1316
1354
  method: 'GET', path: '/projects/{projectId}/stream', query: streamQuerySchema, response: null, permissions: READ, idempotent: false,
@@ -5256,6 +5256,70 @@
5256
5256
  ]
5257
5257
  }
5258
5258
  },
5259
+ "/api/ardentstudio/v1/audit/export": {
5260
+ "get": {
5261
+ "operationId": "getAuditExport",
5262
+ "parameters": [
5263
+ {
5264
+ "in": "query",
5265
+ "name": "after",
5266
+ "required": false,
5267
+ "schema": {
5268
+ "type": "string",
5269
+ "maxLength": 512
5270
+ }
5271
+ },
5272
+ {
5273
+ "in": "query",
5274
+ "name": "limit",
5275
+ "required": false,
5276
+ "schema": {
5277
+ "type": "string",
5278
+ "pattern": "^\\d{1,6}$"
5279
+ }
5280
+ }
5281
+ ],
5282
+ "responses": {
5283
+ "200": {
5284
+ "description": "Success",
5285
+ "content": {
5286
+ "application/json": {
5287
+ "schema": {
5288
+ "$ref": "#/components/schemas/AuditExportResponse"
5289
+ }
5290
+ }
5291
+ }
5292
+ },
5293
+ "400": {
5294
+ "$ref": "#/components/responses/Error"
5295
+ },
5296
+ "401": {
5297
+ "$ref": "#/components/responses/Error"
5298
+ },
5299
+ "403": {
5300
+ "$ref": "#/components/responses/Error"
5301
+ },
5302
+ "404": {
5303
+ "$ref": "#/components/responses/Error"
5304
+ },
5305
+ "409": {
5306
+ "$ref": "#/components/responses/Error"
5307
+ }
5308
+ },
5309
+ "x-permissions": [
5310
+ "ardentstudio:audit:read"
5311
+ ],
5312
+ "x-idempotent": false,
5313
+ "x-or-permission": "ardentstudio:agents:admin",
5314
+ "x-accept": "any",
5315
+ "description": "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).",
5316
+ "security": [
5317
+ {
5318
+ "oathweave": []
5319
+ }
5320
+ ]
5321
+ }
5322
+ },
5259
5323
  "/api/ardentstudio/v1/projects/{projectId}/stream": {
5260
5324
  "get": {
5261
5325
  "operationId": "getProjectsProjectIdStream",
@@ -5338,7 +5402,7 @@
5338
5402
  "type": "http",
5339
5403
  "scheme": "bearer",
5340
5404
  "bearerFormat": "JWT",
5341
- "description": "Oathweave resource token with audience ardentstudio. Permissions: ardentstudio:projects:read, ardentstudio:projects:write, ardentstudio:templates:read, ardentstudio:templates:write, ardentstudio:review:write, ardentstudio:agents:admin."
5405
+ "description": "Oathweave resource token with audience ardentstudio. Permissions: ardentstudio:projects:read, ardentstudio:projects:write, ardentstudio:templates:read, ardentstudio:templates:write, ardentstudio:review:write, ardentstudio:agents:admin, ardentstudio:audit:read."
5342
5406
  }
5343
5407
  },
5344
5408
  "responses": {
@@ -6408,6 +6472,27 @@
6408
6472
  "knowledge"
6409
6473
  ]
6410
6474
  },
6475
+ "AuditExportResponse": {
6476
+ "type": "object",
6477
+ "properties": {
6478
+ "data": {
6479
+ "type": "array",
6480
+ "items": {
6481
+ "$ref": "#/components/schemas/HiveAuditRecord"
6482
+ }
6483
+ },
6484
+ "nextCursor": {
6485
+ "type": [
6486
+ "string",
6487
+ "null"
6488
+ ]
6489
+ }
6490
+ },
6491
+ "required": [
6492
+ "data",
6493
+ "nextCursor"
6494
+ ]
6495
+ },
6411
6496
  "AvatarPreset": {
6412
6497
  "type": "object",
6413
6498
  "properties": {
@@ -8109,6 +8194,125 @@
8109
8194
  "at"
8110
8195
  ]
8111
8196
  },
8197
+ "HiveAuditRecord": {
8198
+ "type": "object",
8199
+ "properties": {
8200
+ "id": {
8201
+ "type": "string",
8202
+ "pattern": "^(hivemem|hiveskills|ardentstudio|embermend):audit:[A-Za-z0-9._:-]{1,200}$"
8203
+ },
8204
+ "product": {
8205
+ "type": "string",
8206
+ "enum": [
8207
+ "hivemem",
8208
+ "hiveskills",
8209
+ "ardentstudio",
8210
+ "embermend"
8211
+ ]
8212
+ },
8213
+ "occurredAt": {
8214
+ "type": "string",
8215
+ "format": "date-time",
8216
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
8217
+ },
8218
+ "action": {
8219
+ "type": "string",
8220
+ "maxLength": 128,
8221
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$"
8222
+ },
8223
+ "outcome": {
8224
+ "type": "string",
8225
+ "enum": [
8226
+ "success",
8227
+ "failure"
8228
+ ]
8229
+ },
8230
+ "actor": {
8231
+ "type": "object",
8232
+ "properties": {
8233
+ "type": {
8234
+ "type": "string",
8235
+ "enum": [
8236
+ "user",
8237
+ "service",
8238
+ "agent",
8239
+ "system"
8240
+ ]
8241
+ },
8242
+ "id": {
8243
+ "type": "string",
8244
+ "minLength": 1,
8245
+ "maxLength": 256
8246
+ },
8247
+ "issuer": {
8248
+ "type": "string",
8249
+ "maxLength": 512
8250
+ },
8251
+ "displayName": {
8252
+ "type": "string",
8253
+ "maxLength": 256
8254
+ }
8255
+ },
8256
+ "required": [
8257
+ "type",
8258
+ "id"
8259
+ ]
8260
+ },
8261
+ "target": {
8262
+ "type": "object",
8263
+ "properties": {
8264
+ "type": {
8265
+ "type": "string",
8266
+ "maxLength": 64
8267
+ },
8268
+ "id": {
8269
+ "type": "string",
8270
+ "maxLength": 256
8271
+ }
8272
+ }
8273
+ },
8274
+ "subject": {
8275
+ "type": "object",
8276
+ "properties": {
8277
+ "actorId": {
8278
+ "type": "string",
8279
+ "maxLength": 256
8280
+ },
8281
+ "agentRef": {
8282
+ "type": "string",
8283
+ "maxLength": 257
8284
+ },
8285
+ "nodeId": {
8286
+ "type": "string",
8287
+ "maxLength": 128
8288
+ }
8289
+ }
8290
+ },
8291
+ "details": {
8292
+ "type": "object",
8293
+ "propertyNames": {
8294
+ "type": "string"
8295
+ },
8296
+ "additionalProperties": {
8297
+ "type": [
8298
+ "string",
8299
+ "number",
8300
+ "boolean",
8301
+ "null"
8302
+ ]
8303
+ }
8304
+ }
8305
+ },
8306
+ "required": [
8307
+ "id",
8308
+ "product",
8309
+ "occurredAt",
8310
+ "action",
8311
+ "outcome",
8312
+ "actor",
8313
+ "details"
8314
+ ]
8315
+ },
8112
8316
  "HivememState": {
8113
8317
  "type": "string",
8114
8318
  "enum": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hivemagnus/ardent-studio",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Ardent Project Studio service: projects, team assignment, conversation room and deliverable review for Ardent agents managed by Magnus",
5
5
  "keywords": [
6
6
  "ardent",
@@ -67,7 +67,7 @@
67
67
  "verify": "npm run privacy:test && npm run scan:secrets && npm run check:private && npm run typecheck && npm run lint && npm test && npm run build && npm run check:contracts && npm run pack:check"
68
68
  },
69
69
  "dependencies": {
70
- "@hivemagnus/hive-common": "^0.1.0",
70
+ "@hivemagnus/hive-common": "^0.4.0",
71
71
  "@hivemagnus/hivemem-sdk": "1.1.0",
72
72
  "@hivemagnus/magnus-contracts": "2.2.0",
73
73
  "@hono/node-server": "^2.1.5",