@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 +7 -0
- package/dist/audit/export.d.ts +22 -0
- package/dist/audit/export.js +115 -0
- package/dist/auth/authenticator.d.ts +22 -4
- package/dist/auth/authenticator.js +30 -8
- package/dist/auth/links.d.ts +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -1
- package/dist/db/repos/audit.d.ts +4 -0
- package/dist/db/repos/audit.js +16 -0
- package/dist/server/app.js +2 -0
- package/dist/server/errors.js +5 -0
- package/dist/server/routes/audit.d.ts +3 -0
- package/dist/server/routes/audit.js +6 -0
- package/dist/server/routing.d.ts +4 -1
- package/dist/server/routing.js +8 -5
- package/dist/service.js +1 -1
- package/dist/shared/browser.d.ts +86 -1
- package/dist/shared/browser.js +38 -0
- package/docs/openapi-browser.json +205 -1
- package/package.json +2 -2
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
|
|
20
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,
|
package/dist/auth/links.d.ts
CHANGED
|
@@ -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,
|
package/dist/db/repos/audit.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/db/repos/audit.js
CHANGED
|
@@ -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
|
}
|
package/dist/server/app.js
CHANGED
|
@@ -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
|
}
|
package/dist/server/errors.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/server/routing.d.ts
CHANGED
|
@@ -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[]
|
|
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;
|
package/dist/server/routing.js
CHANGED
|
@@ -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
|
|
37
|
-
|
|
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 });
|
package/dist/shared/browser.d.ts
CHANGED
|
@@ -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;
|
package/dist/shared/browser.js
CHANGED
|
@@ -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
|
+
"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.
|
|
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",
|