@ossy/platform 1.39.2 → 3.0.1
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 +21 -17
- package/package.json +20 -11
- package/src/Definition.js +2 -1
- package/src/PlatformShell.jsx +10 -10
- package/src/actions/action.service.js +85 -7
- package/src/audit/action-invocation-service.js +143 -0
- package/src/audit/action-invocation.aggregate.js +57 -0
- package/src/audit/audit-helpers.js +61 -0
- package/src/audit/detect-channel.js +19 -0
- package/src/audit/list-task-runs.action.js +5 -0
- package/src/audit/list-task-runs.task.js +17 -0
- package/src/audit/task-run-list.aggregate.js +76 -0
- package/src/audit/task-run-service.js +208 -0
- package/src/audit/task-run.aggregate.js +58 -0
- package/src/auth/action-scopes.js +3 -0
- package/src/capability-schemas/action-capability.schema.js +1 -0
- package/src/capability-schemas/action-meta.schema.js +1 -0
- package/src/capability-schemas/component-capability.schema.js +1 -0
- package/src/capability-schemas/page-capability.schema.js +1 -0
- package/src/capability-schemas/page-meta.schema.js +1 -0
- package/src/capability-schemas/task-capability.schema.js +1 -0
- package/src/capability-schemas/task-meta.schema.js +1 -0
- package/src/capability-schemas/task-output.schema.js +1 -0
- package/src/capability-schemas/task-trigger-authoring.schema.js +1 -0
- package/src/capability-schemas/task-trigger.schema.js +1 -0
- package/src/directory.schema.js +7 -0
- package/src/entitlements/action-entitlement.js +69 -0
- package/src/file.schema.js +11 -0
- package/src/index.js +12 -3
- package/src/mcp/create-ossy-mcp-server.js +97 -0
- package/src/mcp/json-schema-to-zod.js +64 -0
- package/src/mcp/mount-ossy-mcp.js +72 -0
- package/src/mcp/mount-platform-mcp.js +101 -0
- package/src/mcp/upload-file-tool.js +62 -0
- package/src/metering/metering-service.js +91 -0
- package/src/{platform-config.resource.js → platform-config.schema.js} +1 -1
- package/src/proxy-internal.js +13 -16
- package/src/push/mount-push-sse.js +91 -0
- package/src/request-diagnostics.js +144 -0
- package/src/request-diagnostics.spec.js +40 -0
- package/src/resources/index.js +3 -2
- package/src/resources/schema.registry.js +26 -0
- package/src/resources/schema.service.js +54 -0
- package/src/resources/schema.validation.js +90 -0
- package/src/runtime.js +20 -6
- package/src/server.js +281 -62
- package/src/storage/filesystem-storage.client.js +109 -0
- package/src/storage/local-storage.client.js +2 -65
- package/src/storage/resource-read-url.js +36 -0
- package/src/storage/resource-read-url.spec.js +22 -0
- package/src/storage/s3-storage.client.js +102 -0
- package/src/storage/s3.client.js +27 -23
- package/src/storage/storage-keys.js +37 -0
- package/src/storage/storage-keys.spec.js +16 -0
- package/src/storage/storage.client.js +52 -8
- package/src/storage/storage.integration.js +40 -0
- package/src/tasks/change-stream.js +78 -11
- package/src/tasks/task-service.js +211 -34
- package/src/tasks/task-service.spec.js +187 -0
- package/src/test/e2e.util.js +18 -39
- package/src/test/flow-runner.js +476 -0
- package/src/test/test.util.js +30 -29
- package/src/user-app-settings.js +44 -0
- package/src/users.middleware.js +61 -17
- package/src/resources/resource-template.registry.js +0 -29
- package/src/resources/resource-template.validation.js +0 -232
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0008 projection — workspace-scoped task run list for UI and billing queries.
|
|
3
|
+
*/
|
|
4
|
+
export class TaskRunListProjection {
|
|
5
|
+
|
|
6
|
+
static kind = 'projection'
|
|
7
|
+
static ProjectionId = '@ossy/platform/data/task-run-list'
|
|
8
|
+
|
|
9
|
+
static sources = [
|
|
10
|
+
{ type: '@ossy/platform/schema/task-run', event: 'Started' },
|
|
11
|
+
{ type: '@ossy/platform/schema/task-run', event: 'Completed' },
|
|
12
|
+
{ type: '@ossy/platform/schema/task-run', event: 'Failed' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
static scopeFromEvent (event) {
|
|
16
|
+
return event.payload?.belongsTo ?? null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static cacheKeys (scopeId) {
|
|
20
|
+
return [`projection:${TaskRunListProjection.ProjectionId}:${scopeId}`]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static initialState (scopeId) {
|
|
24
|
+
return { workspaceId: scopeId, runs: [] }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static Apply (event, state) {
|
|
28
|
+
const workspaceId = event.payload?.belongsTo ?? state.workspaceId
|
|
29
|
+
const entry = {
|
|
30
|
+
id: event.resourceId,
|
|
31
|
+
taskId: event.payload.taskId,
|
|
32
|
+
moduleId: event.payload.moduleId,
|
|
33
|
+
trigger: event.payload.trigger,
|
|
34
|
+
actionInvocationId: event.payload.actionInvocationId ?? null,
|
|
35
|
+
actorId: event.payload.actorId ?? event.createdBy ?? null,
|
|
36
|
+
executionEnv: event.payload.executionEnv ?? 'ossy_server',
|
|
37
|
+
updatedAt: event.created,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const runs = [...(state.runs ?? [])]
|
|
41
|
+
|
|
42
|
+
if (event.event === 'Started') {
|
|
43
|
+
entry.status = 'in progress'
|
|
44
|
+
entry.startedAt = event.created
|
|
45
|
+
runs.unshift(entry)
|
|
46
|
+
return { workspaceId, runs: runs.slice(0, 200) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const idx = runs.findIndex(r => r.id === event.resourceId)
|
|
50
|
+
if (idx < 0) {
|
|
51
|
+
runs.unshift({
|
|
52
|
+
...entry,
|
|
53
|
+
status: event.event === 'Completed' ? 'success' : 'failed',
|
|
54
|
+
startedAt: event.created,
|
|
55
|
+
completedAt: event.created,
|
|
56
|
+
durationMs: event.payload.durationMs,
|
|
57
|
+
})
|
|
58
|
+
return { workspaceId, runs: runs.slice(0, 200) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runs[idx] = {
|
|
62
|
+
...runs[idx],
|
|
63
|
+
...entry,
|
|
64
|
+
status: event.event === 'Completed' ? 'success' : 'failed',
|
|
65
|
+
completedAt: event.created,
|
|
66
|
+
durationMs: event.payload.durationMs,
|
|
67
|
+
error: event.event === 'Failed' ? event.payload.error : undefined,
|
|
68
|
+
resultSummary: event.event === 'Completed' ? event.payload.resultSummary : undefined,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { workspaceId, runs }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { TaskRunListProjection as Aggregate }
|
|
76
|
+
export const id = 'platform/data/task-run-list'
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { Aggregate } from '@ossy/event-store'
|
|
3
|
+
import { createLogger, metrics } from '@ossy/observability'
|
|
4
|
+
import { TaskRun } from './task-run.aggregate.js'
|
|
5
|
+
import { MeteringService } from '../metering/metering-service.js'
|
|
6
|
+
import { detectChannel } from './detect-channel.js'
|
|
7
|
+
import {
|
|
8
|
+
moduleIdFromTaskId,
|
|
9
|
+
resolveActorId,
|
|
10
|
+
resolveWorkspaceId,
|
|
11
|
+
summarizeError,
|
|
12
|
+
summarizeResult,
|
|
13
|
+
} from './audit-helpers.js'
|
|
14
|
+
import { rejectOnAbort } from '../request-diagnostics.js'
|
|
15
|
+
|
|
16
|
+
const log = createLogger('platform/task-run')
|
|
17
|
+
|
|
18
|
+
function assertNotAborted (context) {
|
|
19
|
+
if (context.signal?.aborted) {
|
|
20
|
+
throw context.signal.reason instanceof Error
|
|
21
|
+
? context.signal.reason
|
|
22
|
+
: new Error('Operation aborted')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function recordTaskMetrics (taskId, durationMs) {
|
|
27
|
+
try { metrics.timing('task.duration', durationMs, { task: taskId }) } catch {}
|
|
28
|
+
try { metrics.increment('task.run', { task: taskId }) } catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Records task execution lifecycle as ADR 0008 entity events.
|
|
33
|
+
*/
|
|
34
|
+
export const TaskRunService = {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run a task handler with audit lifecycle (sync path).
|
|
38
|
+
*
|
|
39
|
+
* @param {{
|
|
40
|
+
* taskId: string,
|
|
41
|
+
* handler: (context: object) => Promise<unknown>,
|
|
42
|
+
* context?: object,
|
|
43
|
+
* trigger: 'on_action' | 'on_event' | 'on_schedule' | 'invoke',
|
|
44
|
+
* triggeredBy?: object,
|
|
45
|
+
* executionEnv?: string,
|
|
46
|
+
* }} opts
|
|
47
|
+
*/
|
|
48
|
+
async execute (opts) {
|
|
49
|
+
const {
|
|
50
|
+
taskId,
|
|
51
|
+
handler,
|
|
52
|
+
context = {},
|
|
53
|
+
trigger,
|
|
54
|
+
triggeredBy = {},
|
|
55
|
+
executionEnv = 'ossy_server',
|
|
56
|
+
audit = true,
|
|
57
|
+
} = opts
|
|
58
|
+
|
|
59
|
+
if (audit === false) {
|
|
60
|
+
assertNotAborted(context)
|
|
61
|
+
if (context.signal) {
|
|
62
|
+
return Promise.race([handler(context), rejectOnAbort(context.signal)])
|
|
63
|
+
}
|
|
64
|
+
return handler(context)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const runId = nanoid()
|
|
68
|
+
const workspaceId = resolveWorkspaceId(context)
|
|
69
|
+
const actorId = resolveActorId(context)
|
|
70
|
+
const startMs = Date.now()
|
|
71
|
+
|
|
72
|
+
context.audit = context.audit ?? {}
|
|
73
|
+
context.audit.taskRunId = runId
|
|
74
|
+
|
|
75
|
+
await TaskRunService._appendStarted({
|
|
76
|
+
runId,
|
|
77
|
+
taskId,
|
|
78
|
+
trigger,
|
|
79
|
+
triggeredBy,
|
|
80
|
+
workspaceId,
|
|
81
|
+
actorId,
|
|
82
|
+
executionEnv,
|
|
83
|
+
actionInvocationId: context.actionInvocationId ?? null,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
assertNotAborted(context)
|
|
88
|
+
const result = context.signal
|
|
89
|
+
? await Promise.race([handler(context), rejectOnAbort(context.signal)])
|
|
90
|
+
: await handler(context)
|
|
91
|
+
const durationMs = Date.now() - startMs
|
|
92
|
+
await TaskRunService._appendCompleted({
|
|
93
|
+
runId,
|
|
94
|
+
actorId,
|
|
95
|
+
durationMs,
|
|
96
|
+
resultSummary: summarizeResult(result),
|
|
97
|
+
})
|
|
98
|
+
recordTaskMetrics(taskId, durationMs)
|
|
99
|
+
await MeteringService.record({
|
|
100
|
+
kind: 'task',
|
|
101
|
+
actionId: context.actionId ?? null,
|
|
102
|
+
taskId,
|
|
103
|
+
channel: detectChannel(context.req),
|
|
104
|
+
workspaceId,
|
|
105
|
+
actorId,
|
|
106
|
+
durationMs,
|
|
107
|
+
success: true,
|
|
108
|
+
actionInvocationId: context.actionInvocationId ?? null,
|
|
109
|
+
taskRunId: runId,
|
|
110
|
+
trigger,
|
|
111
|
+
})
|
|
112
|
+
return result
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const durationMs = Date.now() - startMs
|
|
115
|
+
await TaskRunService._appendFailed({
|
|
116
|
+
runId,
|
|
117
|
+
actorId,
|
|
118
|
+
durationMs,
|
|
119
|
+
error: summarizeError(error),
|
|
120
|
+
})
|
|
121
|
+
recordTaskMetrics(taskId, durationMs)
|
|
122
|
+
await MeteringService.record({
|
|
123
|
+
kind: 'task',
|
|
124
|
+
actionId: context.actionId ?? null,
|
|
125
|
+
taskId,
|
|
126
|
+
channel: detectChannel(context.req),
|
|
127
|
+
workspaceId,
|
|
128
|
+
actorId,
|
|
129
|
+
durationMs,
|
|
130
|
+
success: false,
|
|
131
|
+
actionInvocationId: context.actionInvocationId ?? null,
|
|
132
|
+
taskRunId: runId,
|
|
133
|
+
trigger,
|
|
134
|
+
})
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fire-and-forget task execution with audit lifecycle (async path).
|
|
141
|
+
*
|
|
142
|
+
* @param {Parameters<typeof TaskRunService.execute>[0]} opts
|
|
143
|
+
*/
|
|
144
|
+
executeAsync (opts) {
|
|
145
|
+
TaskRunService.execute(opts).catch(error => {
|
|
146
|
+
log.error(`Async task run failed: "${opts.taskId}"`, { taskId: opts.taskId }, error)
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async _appendStarted ({
|
|
151
|
+
runId,
|
|
152
|
+
taskId,
|
|
153
|
+
trigger,
|
|
154
|
+
triggeredBy,
|
|
155
|
+
workspaceId,
|
|
156
|
+
actorId,
|
|
157
|
+
executionEnv,
|
|
158
|
+
actionInvocationId,
|
|
159
|
+
}) {
|
|
160
|
+
try {
|
|
161
|
+
await Aggregate.Of(TaskRun, {
|
|
162
|
+
resourceId: runId,
|
|
163
|
+
type: '@ossy/platform/schema/task-run',
|
|
164
|
+
event: 'Started',
|
|
165
|
+
createdBy: actorId ?? undefined,
|
|
166
|
+
payload: {
|
|
167
|
+
taskId,
|
|
168
|
+
moduleId: moduleIdFromTaskId(taskId),
|
|
169
|
+
trigger,
|
|
170
|
+
triggeredBy,
|
|
171
|
+
executionEnv,
|
|
172
|
+
actionInvocationId,
|
|
173
|
+
actorId,
|
|
174
|
+
belongsTo: workspaceId,
|
|
175
|
+
location: '/@ossy/tasks/runs/',
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
} catch (err) {
|
|
179
|
+
log.warn('[TaskRunService] Failed to record Started', { taskId, runId }, err)
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async _appendCompleted ({ runId, actorId, durationMs, resultSummary }) {
|
|
184
|
+
try {
|
|
185
|
+
await Aggregate.Of(TaskRun, runId)
|
|
186
|
+
.then(Aggregate.Add({
|
|
187
|
+
event: 'Completed',
|
|
188
|
+
createdBy: actorId ?? undefined,
|
|
189
|
+
payload: { durationMs, resultSummary },
|
|
190
|
+
}))
|
|
191
|
+
} catch (err) {
|
|
192
|
+
log.warn('[TaskRunService] Failed to record Completed', { runId }, err)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async _appendFailed ({ runId, actorId, durationMs, error }) {
|
|
197
|
+
try {
|
|
198
|
+
await Aggregate.Of(TaskRun, runId)
|
|
199
|
+
.then(Aggregate.Add({
|
|
200
|
+
event: 'Failed',
|
|
201
|
+
createdBy: actorId ?? undefined,
|
|
202
|
+
payload: { durationMs, error },
|
|
203
|
+
}))
|
|
204
|
+
} catch (err) {
|
|
205
|
+
log.warn('[TaskRunService] Failed to record Failed', { runId }, err)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0008 entity — one stream per task execution (sync invoke, changestream, cron, on_action).
|
|
3
|
+
*/
|
|
4
|
+
export class TaskRun {
|
|
5
|
+
|
|
6
|
+
static kind = 'entity'
|
|
7
|
+
static SchemaId = '@ossy/platform/schema/task-run'
|
|
8
|
+
static AggregateType = 'TaskRun'
|
|
9
|
+
|
|
10
|
+
static View (events, state = {}) {
|
|
11
|
+
return events.reduce((run, event) => {
|
|
12
|
+
switch (event.event) {
|
|
13
|
+
|
|
14
|
+
case 'Started':
|
|
15
|
+
return {
|
|
16
|
+
...run,
|
|
17
|
+
id: event.resourceId,
|
|
18
|
+
type: event.type,
|
|
19
|
+
taskId: event.payload.taskId,
|
|
20
|
+
moduleId: event.payload.moduleId,
|
|
21
|
+
status: 'in progress',
|
|
22
|
+
trigger: event.payload.trigger,
|
|
23
|
+
triggeredBy: event.payload.triggeredBy ?? {},
|
|
24
|
+
actionInvocationId: event.payload.actionInvocationId ?? null,
|
|
25
|
+
actorId: event.payload.actorId ?? event.createdBy ?? null,
|
|
26
|
+
workspaceId: event.payload.belongsTo ?? null,
|
|
27
|
+
executionEnv: event.payload.executionEnv ?? 'ossy_server',
|
|
28
|
+
startedAt: event.created,
|
|
29
|
+
created: event.created,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case 'Completed':
|
|
33
|
+
return {
|
|
34
|
+
...run,
|
|
35
|
+
status: 'success',
|
|
36
|
+
completedAt: event.created,
|
|
37
|
+
durationMs: event.payload.durationMs,
|
|
38
|
+
resultSummary: event.payload.resultSummary ?? null,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'Failed':
|
|
42
|
+
return {
|
|
43
|
+
...run,
|
|
44
|
+
status: 'failed',
|
|
45
|
+
completedAt: event.created,
|
|
46
|
+
durationMs: event.payload.durationMs,
|
|
47
|
+
error: event.payload.error ?? null,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
default:
|
|
51
|
+
return run
|
|
52
|
+
}
|
|
53
|
+
}, state)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { TaskRun as Aggregate }
|
|
58
|
+
export const id = 'platform/schema/task-run'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/action-capability.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/action-meta.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/component-capability.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/page-capability.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/page-meta.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/task-capability.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/task-meta.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/task-output.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/task-trigger-authoring.schema.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@ossy/schema/capability-schemas/task-trigger.schema.js'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { isServiceEntitled, packageNameToSlug } from '@ossy/workspaces/entitlements'
|
|
3
|
+
|
|
4
|
+
/** @typedef {{ packageName: string, entitlementRequired: boolean }} ActionEntitlementMeta */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ actions?: Array<{ id: string, package?: string }>, definitions?: Record<string, { entitlementRequired?: boolean }> }} manifest
|
|
8
|
+
* @returns {Map<string, ActionEntitlementMeta>}
|
|
9
|
+
*/
|
|
10
|
+
export function buildActionEntitlementIndex (manifest) {
|
|
11
|
+
/** @type {Map<string, ActionEntitlementMeta>} */
|
|
12
|
+
const index = new Map()
|
|
13
|
+
for (const action of manifest.actions || []) {
|
|
14
|
+
if (!action.package) continue
|
|
15
|
+
const slug = packageNameToSlug(action.package)
|
|
16
|
+
const def = manifest.definitions?.[slug]
|
|
17
|
+
const entitlementRequired = def?.entitlementRequired !== false
|
|
18
|
+
index.set(action.id, { packageName: action.package, entitlementRequired })
|
|
19
|
+
}
|
|
20
|
+
return index
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Enforce workspace package entitlements for workspace-scoped actions (ADR 0010 Wave 1).
|
|
25
|
+
*
|
|
26
|
+
* @param {{
|
|
27
|
+
* actionId: string,
|
|
28
|
+
* access: string,
|
|
29
|
+
* workspaceId?: string,
|
|
30
|
+
* entitlementIndex: Map<string, ActionEntitlementMeta>,
|
|
31
|
+
* loadWorkspace: (workspaceId: string) => Promise<{ services?: Record<string, unknown> } | null>,
|
|
32
|
+
* }} opts
|
|
33
|
+
*/
|
|
34
|
+
export async function assertActionEntitled ({
|
|
35
|
+
actionId,
|
|
36
|
+
access,
|
|
37
|
+
workspaceId,
|
|
38
|
+
entitlementIndex,
|
|
39
|
+
loadWorkspace,
|
|
40
|
+
}) {
|
|
41
|
+
if (access !== 'workspace' || !workspaceId) return
|
|
42
|
+
|
|
43
|
+
const meta = entitlementIndex.get(actionId)
|
|
44
|
+
if (!meta?.entitlementRequired) return
|
|
45
|
+
|
|
46
|
+
const workspace = await loadWorkspace(workspaceId)
|
|
47
|
+
if (!workspace) {
|
|
48
|
+
throw Object.assign(new Error('Workspace not found'), { status: 404 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isServiceEntitled(workspace.services, meta.packageName)) {
|
|
52
|
+
throw Object.assign(new Error(`Service ${meta.packageName} is not entitled for this workspace`), {
|
|
53
|
+
status: 403,
|
|
54
|
+
code: 'SERVICE_NOT_ENTITLED',
|
|
55
|
+
package: meta.packageName,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {import('@ossy/workspaces/server').Workspace} WorkspaceAggregate
|
|
62
|
+
* @returns {(workspaceId: string) => Promise<{ services?: Record<string, unknown> } | null>}
|
|
63
|
+
*/
|
|
64
|
+
export function createWorkspaceLoader (WorkspaceAggregate) {
|
|
65
|
+
return async (workspaceId) => {
|
|
66
|
+
const workspace = await Aggregate.Of(WorkspaceAggregate, workspaceId).then(Aggregate.View())
|
|
67
|
+
return workspace?.id ? workspace : null
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { TaskService } from './tasks/task-service.js'
|
|
2
2
|
export { loadAndRegisterTasks } from './tasks/task-registry.js'
|
|
3
3
|
export { ChangeStream } from './tasks/change-stream.js'
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
4
|
+
export { registerSchema, getSystemSchemas } from './resources/schema.registry.js'
|
|
5
|
+
export { getPlatformSchema, initPlatformSchema, createSchemaEngine, schemaForWorkspace } from './resources/schema.service.js'
|
|
6
|
+
export { validateSchemasForImport, ALLOWED_FIELD_TYPES, normalizeFieldType, resolveFieldDef } from './resources/schema.validation.js'
|
|
7
7
|
export { IntegrationService } from './integration.service.js'
|
|
8
8
|
export { ConfigService } from './config.service.js'
|
|
9
9
|
export { ActionService } from './actions/action.service.js'
|
|
@@ -12,3 +12,12 @@ export { UsersMiddleware } from './users.middleware.js'
|
|
|
12
12
|
export { WorkspacesMiddleware } from './workspaces.middleware.js'
|
|
13
13
|
export { matchesCron } from './tasks/cron.js'
|
|
14
14
|
export { matchesGlob, globToRegex, policyToQueryClause } from './tasks/glob.js'
|
|
15
|
+
export {
|
|
16
|
+
USER_SETTINGS_COOKIE,
|
|
17
|
+
AUTH_COOKIE,
|
|
18
|
+
readUserAppSettings,
|
|
19
|
+
mergeUserAppSettingsCookie,
|
|
20
|
+
clearWorkspaceFromUserAppSettings,
|
|
21
|
+
setAuthCookie,
|
|
22
|
+
clearAuthCookie,
|
|
23
|
+
} from './user-app-settings.js'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { jsonSchemaToZodShape } from './json-schema-to-zod.js'
|
|
3
|
+
import {
|
|
4
|
+
TASK_TOPOLOGY_RESOURCE_URI,
|
|
5
|
+
capabilitiesTaskTopologyResource,
|
|
6
|
+
} from '@ossy/app/manifest/build-capabilities'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{
|
|
10
|
+
* capabilities: { tools: Array<object>, tasks?: object[], graph?: object },
|
|
11
|
+
* invokeAction: (actionId: string, payload: object, req?: object) => Promise<unknown>,
|
|
12
|
+
* customTools?: Array<{
|
|
13
|
+
* name: string,
|
|
14
|
+
* description: string,
|
|
15
|
+
* inputSchema?: object,
|
|
16
|
+
* handler: (args: object, req?: object) => Promise<unknown>,
|
|
17
|
+
* }>,
|
|
18
|
+
* }} options
|
|
19
|
+
*/
|
|
20
|
+
export function createOssyMcpServer ({ capabilities, invokeAction, customTools = [] }) {
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: 'ossy',
|
|
23
|
+
version: '0.2.0',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const topology = capabilitiesTaskTopologyResource(capabilities)
|
|
27
|
+
if (topology.tasks.length || topology.graph.edges?.length) {
|
|
28
|
+
server.registerResource(
|
|
29
|
+
'task-topology',
|
|
30
|
+
TASK_TOPOLOGY_RESOURCE_URI,
|
|
31
|
+
{
|
|
32
|
+
title: 'Task topology',
|
|
33
|
+
description: 'Read-only task catalog and trigger graph for multi-step agent planning (ADR 0011).',
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
},
|
|
36
|
+
async () => ({
|
|
37
|
+
contents: [{
|
|
38
|
+
uri: TASK_TOPOLOGY_RESOURCE_URI,
|
|
39
|
+
mimeType: 'application/json',
|
|
40
|
+
text: JSON.stringify(topology, null, 2),
|
|
41
|
+
}],
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const tool of capabilities.tools || []) {
|
|
47
|
+
const inputShape = jsonSchemaToZodShape(tool.inputSchema || { type: 'object' })
|
|
48
|
+
server.registerTool(
|
|
49
|
+
tool.name,
|
|
50
|
+
{
|
|
51
|
+
description: tool.description || tool.title || tool.actionId,
|
|
52
|
+
inputSchema: inputShape,
|
|
53
|
+
},
|
|
54
|
+
async (args, extra) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await invokeAction(tool.actionId, args, extra?.requestInfo)
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
isError: true,
|
|
63
|
+
content: [{ type: 'text', text: err?.message || String(err) }],
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const tool of customTools) {
|
|
71
|
+
const inputShape = tool.inputSchema
|
|
72
|
+
? jsonSchemaToZodShape(tool.inputSchema)
|
|
73
|
+
: undefined
|
|
74
|
+
server.registerTool(
|
|
75
|
+
tool.name,
|
|
76
|
+
{
|
|
77
|
+
description: tool.description,
|
|
78
|
+
...(inputShape ? { inputSchema: inputShape } : {}),
|
|
79
|
+
},
|
|
80
|
+
async (args, extra) => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await tool.handler(args, extra?.requestInfo)
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
isError: true,
|
|
89
|
+
content: [{ type: 'text', text: err?.message || String(err) }],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return server
|
|
97
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal JSON Schema → Zod conversion for MCP tool registration.
|
|
5
|
+
* Supports the subset produced by build-capabilities.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} schema
|
|
8
|
+
* @returns {import('zod').ZodTypeAny}
|
|
9
|
+
*/
|
|
10
|
+
export function jsonSchemaToZod (schema) {
|
|
11
|
+
if (!schema || typeof schema !== 'object') {
|
|
12
|
+
return z.record(z.unknown())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length) {
|
|
16
|
+
return z.union(schema.oneOf.map(jsonSchemaToZod))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (schema.type === 'object' || schema.properties) {
|
|
20
|
+
const shape = {}
|
|
21
|
+
for (const [key, propSchema] of Object.entries(schema.properties || {})) {
|
|
22
|
+
let field = jsonSchemaToZod(propSchema)
|
|
23
|
+
if (!schema.required?.includes(key)) {
|
|
24
|
+
field = field.optional()
|
|
25
|
+
}
|
|
26
|
+
shape[key] = field
|
|
27
|
+
}
|
|
28
|
+
const objectSchema = z.object(shape)
|
|
29
|
+
return schema.additionalProperties ? objectSchema.passthrough() : objectSchema
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (schema.type === 'array') {
|
|
33
|
+
return z.array(jsonSchemaToZod(schema.items || {}))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (schema.type === 'number' || schema.type === 'integer') {
|
|
37
|
+
let field = z.number()
|
|
38
|
+
if (schema.description) field = field.describe(schema.description)
|
|
39
|
+
return field
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (schema.type === 'boolean') {
|
|
43
|
+
return z.boolean()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let field = z.string()
|
|
47
|
+
if (schema.description) field = field.describe(schema.description)
|
|
48
|
+
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
49
|
+
field = z.enum(schema.enum)
|
|
50
|
+
}
|
|
51
|
+
return field
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} inputSchema JSON Schema object
|
|
56
|
+
* @returns {import('zod').ZodRawShape}
|
|
57
|
+
*/
|
|
58
|
+
export function jsonSchemaToZodShape (inputSchema) {
|
|
59
|
+
const zodSchema = jsonSchemaToZod(inputSchema)
|
|
60
|
+
if (zodSchema instanceof z.ZodObject) {
|
|
61
|
+
return zodSchema.shape
|
|
62
|
+
}
|
|
63
|
+
return { payload: zodSchema }
|
|
64
|
+
}
|