@ours.network/fleet 1.0.4 → 1.0.5
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 +13 -9
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/task-room-service.d.ts +37 -2
- package/dist/application/task-room-service.js +49 -7
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +6 -3
- package/dist/config.js +0 -17
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +26 -4
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.js +16 -3
- package/dist/owner-channel/commands.d.ts +18 -1
- package/dist/owner-channel/commands.js +73 -4
- package/dist/rooms-tasks/cli.js +121 -2
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/web/runtime.js +2 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +86 -1
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ export interface TaskMemberRole {
|
|
|
24
24
|
cowork_role: string;
|
|
25
25
|
}
|
|
26
26
|
export interface TaskOrigin {
|
|
27
|
-
type: 'cli' | 'owner_channel';
|
|
27
|
+
type: 'cli' | 'owner_channel' | 'web';
|
|
28
28
|
owner_cid?: string;
|
|
29
29
|
}
|
|
30
30
|
export interface TaskOutcome {
|
|
@@ -46,6 +46,10 @@ export interface TaskTerminalIntent {
|
|
|
46
46
|
}
|
|
47
47
|
export interface TaskRecord {
|
|
48
48
|
task_id: string;
|
|
49
|
+
/** Stable organizational list identifier. Missing legacy values mean `default`. */
|
|
50
|
+
list_id: string;
|
|
51
|
+
/** Derived presentation field; repositories must never persist it. */
|
|
52
|
+
list_name: string;
|
|
49
53
|
title: string;
|
|
50
54
|
brief?: string;
|
|
51
55
|
brief_file?: string;
|
|
@@ -64,6 +68,12 @@ export interface TaskRecord {
|
|
|
64
68
|
outcome?: TaskOutcome;
|
|
65
69
|
terminal_intent?: TaskTerminalIntent;
|
|
66
70
|
}
|
|
71
|
+
export interface TaskListRecord {
|
|
72
|
+
list_id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
built_in: boolean;
|
|
75
|
+
created_at: string;
|
|
76
|
+
}
|
|
67
77
|
export type RoomOrchestrationState = 'provisioning' | 'active' | 'closing' | 'closed';
|
|
68
78
|
export type SagaPhase = 'persist_intent' | 'create_room' | 'attach_owner' | 'create_members' | 'join_role_groups' | 'wait_seats' | 'launch_work' | 'activate' | 'completed' | 'failed';
|
|
69
79
|
export interface SagaCursor {
|
package/dist/web/runtime.js
CHANGED
|
@@ -31,6 +31,7 @@ import { startWebControlServer } from './control.js';
|
|
|
31
31
|
import { WebAccessStore, validatePublicOrigin } from './access.js';
|
|
32
32
|
import { buildWatchdogFindings, cachedWatchdogFindingsProvider, WatchdogQueryService } from '../watchdog/query.js';
|
|
33
33
|
import { latestReport } from '../watchdog/store.js';
|
|
34
|
+
import { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
34
35
|
const CONFIG_CACHE_TTL_MS = 5_000;
|
|
35
36
|
/**
|
|
36
37
|
* loadConfig re-parses YAML from disk on every call; the watchdog list/reports
|
|
@@ -159,6 +160,7 @@ export async function startWebConsole(options) {
|
|
|
159
160
|
try {
|
|
160
161
|
server = await buildWebServer({
|
|
161
162
|
query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
|
|
163
|
+
taskRooms: new TaskRoomApplicationService(options.configPath),
|
|
162
164
|
topology: readTopology, topologyDrafts, topologyPromote,
|
|
163
165
|
terminalUpgrade: terminalAvailable
|
|
164
166
|
? async (socket, _request, roleId, _ticket, hello) => terminals.connect(socket, roleId, hello)
|
package/dist/web/server.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { MergedTopology } from './topology-model.js';
|
|
|
15
15
|
import type { TopologyDraftStore } from './topology-draft-store.js';
|
|
16
16
|
import type { TopologyPromoteService } from './topology-promote.js';
|
|
17
17
|
import type { RoleRemovalService } from '../application/role-removal-service.js';
|
|
18
|
+
import type { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
18
19
|
export interface WebServices {
|
|
19
20
|
query: FleetQueryService;
|
|
20
21
|
repository: RoleRepository;
|
|
@@ -30,6 +31,7 @@ export interface WebServices {
|
|
|
30
31
|
topologyDrafts?: TopologyDraftStore;
|
|
31
32
|
topologyPromote?: TopologyPromoteService;
|
|
32
33
|
removal?: RoleRemovalService;
|
|
34
|
+
taskRooms?: TaskRoomApplicationService;
|
|
33
35
|
terminalUpgrade?: (socket: WebSocket, request: FastifyRequest, roleId: string, ticket: string, hello: Record<string, unknown>) => Promise<void>;
|
|
34
36
|
}
|
|
35
37
|
export interface WebServer {
|
package/dist/web/server.js
CHANGED
|
@@ -11,8 +11,10 @@ import { AuditSink } from './audit.js';
|
|
|
11
11
|
import { WebAuth } from './auth.js';
|
|
12
12
|
import { FleetEventBus } from './events.js';
|
|
13
13
|
import { ROLE_NAME_RE } from '../config.js';
|
|
14
|
+
import { TaskListError } from '../rooms-tasks/task-lists.js';
|
|
15
|
+
import { TaskStateError } from '../rooms-tasks/task-state.js';
|
|
14
16
|
const statusFor = (code) => ({
|
|
15
|
-
role_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
|
|
17
|
+
role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
|
|
16
18
|
idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
|
|
17
19
|
invalid_request: 400, capability_unavailable: 409, prerequisite_unavailable: 503,
|
|
18
20
|
}[code] ?? 500);
|
|
@@ -128,6 +130,89 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
128
130
|
auth.authenticate(request);
|
|
129
131
|
return { roles: await services.query.list() };
|
|
130
132
|
});
|
|
133
|
+
const taskApi = async (fn) => {
|
|
134
|
+
try {
|
|
135
|
+
return await fn();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof TaskListError) {
|
|
139
|
+
const code = error.code === 'list_not_found' ? 'resource_not_found'
|
|
140
|
+
: ['duplicate_name', 'destination_required', 'same_destination', 'default_immutable'].includes(error.code)
|
|
141
|
+
? 'conflict' : 'invalid_request';
|
|
142
|
+
throw new FleetError(code, error.message);
|
|
143
|
+
}
|
|
144
|
+
if (error instanceof TaskStateError) {
|
|
145
|
+
throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const requireTaskRooms = () => {
|
|
151
|
+
if (!services.taskRooms)
|
|
152
|
+
throw new FleetError('capability_unavailable', 'task operations are unavailable');
|
|
153
|
+
return services.taskRooms;
|
|
154
|
+
};
|
|
155
|
+
app.get('/api/v1/task-lists', async (request) => {
|
|
156
|
+
auth.authenticate(request);
|
|
157
|
+
return { lists: requireTaskRooms().listTaskLists() };
|
|
158
|
+
});
|
|
159
|
+
app.post('/api/v1/task-lists', async (request, reply) => {
|
|
160
|
+
auth.authenticate(request, true);
|
|
161
|
+
const name = String(request.body?.name ?? '');
|
|
162
|
+
const list = await taskApi(() => requireTaskRooms().createTaskList({ actor: { kind: 'local_control', surface: 'web' }, name }));
|
|
163
|
+
reply.code(201);
|
|
164
|
+
return { list };
|
|
165
|
+
});
|
|
166
|
+
app.patch('/api/v1/task-lists/:name', async (request) => {
|
|
167
|
+
auth.authenticate(request, true);
|
|
168
|
+
const name = request.params.name;
|
|
169
|
+
const newName = String(request.body?.name ?? '');
|
|
170
|
+
return { list: await taskApi(() => requireTaskRooms().renameTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, newName })) };
|
|
171
|
+
});
|
|
172
|
+
app.delete('/api/v1/task-lists/:name', async (request) => {
|
|
173
|
+
auth.authenticate(request, true);
|
|
174
|
+
const name = request.params.name;
|
|
175
|
+
const destination = request.query.destination;
|
|
176
|
+
return taskApi(() => requireTaskRooms().deleteTaskList({ actor: { kind: 'local_control', surface: 'web' }, name, destination }));
|
|
177
|
+
});
|
|
178
|
+
app.get('/api/v1/tasks', async (request) => {
|
|
179
|
+
auth.authenticate(request);
|
|
180
|
+
const query = request.query;
|
|
181
|
+
const states = ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'];
|
|
182
|
+
if (query.state && query.state !== 'all' && !states.includes(query.state))
|
|
183
|
+
throw new FleetError('invalid_request', 'invalid task state filter');
|
|
184
|
+
if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
|
|
185
|
+
throw new FleetError('invalid_request', 'groupByList must be true or false');
|
|
186
|
+
const state = query.state && query.state !== 'all' ? query.state : undefined;
|
|
187
|
+
const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}) };
|
|
188
|
+
return taskApi(() => query.groupByList === 'true'
|
|
189
|
+
? { groups: requireTaskRooms().groupedTasks(filter) }
|
|
190
|
+
: { tasks: requireTaskRooms().listTasks(filter) });
|
|
191
|
+
});
|
|
192
|
+
app.post('/api/v1/tasks', async (request, reply) => {
|
|
193
|
+
auth.authenticate(request, true);
|
|
194
|
+
const body = request.body;
|
|
195
|
+
if (typeof body?.title !== 'string' || !body.title)
|
|
196
|
+
throw new FleetError('invalid_request', 'title is required');
|
|
197
|
+
const task = await taskApi(() => requireTaskRooms().createTask({
|
|
198
|
+
actor: { kind: 'local_control', surface: 'web' }, title: body.title,
|
|
199
|
+
brief: typeof body.brief === 'string' ? body.brief : undefined,
|
|
200
|
+
template: typeof body.template === 'string' ? body.template : undefined,
|
|
201
|
+
backlog: body.backlog === true, noRoom: body.noRoom === true,
|
|
202
|
+
list: typeof body.list === 'string' ? body.list : undefined,
|
|
203
|
+
idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
|
|
204
|
+
? request.headers['idempotency-key'] : undefined,
|
|
205
|
+
origin: { type: 'web' },
|
|
206
|
+
}));
|
|
207
|
+
reply.code(201);
|
|
208
|
+
return { task };
|
|
209
|
+
});
|
|
210
|
+
app.patch('/api/v1/tasks/:id/list', async (request) => {
|
|
211
|
+
auth.authenticate(request, true);
|
|
212
|
+
const taskId = request.params.id;
|
|
213
|
+
const list = String(request.body?.list ?? '');
|
|
214
|
+
return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
|
|
215
|
+
});
|
|
131
216
|
app.get('/api/v1/configuration', async (request) => {
|
|
132
217
|
auth.authenticate(request);
|
|
133
218
|
if (!services.configuration)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|