@ours.network/fleet 2.1.1 → 2.2.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 +41 -2
- package/dist/application/conversation-history.d.ts +8 -0
- package/dist/application/conversation-history.js +37 -2
- package/dist/application/task-room-service.d.ts +7 -0
- package/dist/application/task-room-service.js +19 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1 -0
- package/dist/doctor.js +17 -4
- package/dist/notifications/fleet-producer.d.ts +0 -1
- package/dist/notifications/fleet-producer.js +65 -47
- package/dist/notifications/mission-ownership.d.ts +31 -0
- package/dist/notifications/mission-ownership.js +148 -0
- package/dist/notifications/outbox.d.ts +10 -0
- package/dist/notifications/outbox.js +28 -2
- package/dist/notifications/task-producer.d.ts +11 -1
- package/dist/notifications/task-producer.js +34 -5
- package/dist/rooms-tasks/layout-service.d.ts +2 -0
- package/dist/rooms-tasks/layout-service.js +7 -2
- package/dist/rooms-tasks/layout.d.ts +5 -1
- package/dist/rooms-tasks/layout.js +7 -1
- package/dist/rooms-tasks/notification-manifest.d.ts +29 -0
- package/dist/rooms-tasks/notification-manifest.js +65 -0
- package/dist/rooms-tasks/task-review.d.ts +2 -0
- package/dist/rooms-tasks/task-review.js +7 -5
- package/dist/rooms-tasks/task-state.d.ts +35 -0
- package/dist/rooms-tasks/task-state.js +151 -17
- package/dist/rooms-tasks/types.d.ts +10 -0
- package/dist/runtime-security.d.ts +1 -0
- package/dist/runtime-security.js +6 -0
- package/dist/security-versions.d.ts +20 -0
- package/dist/security-versions.js +70 -0
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +58 -1
- package/dist/session/codex-app-server.js +22 -1
- package/dist/session/conversation-types.d.ts +8 -2
- package/dist/session/session-ownership.d.ts +18 -0
- package/dist/session/session-ownership.js +74 -0
- package/dist/setup-tunnel-preflight.js +10 -3
- package/dist/supervisor/manager.js +9 -2
- package/dist/web/resource-views.d.ts +2 -0
- package/dist/web/resource-views.js +4 -2
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +29 -8
- package/dist/web/workspace-tunnel.d.ts +2 -1
- package/dist/web/workspace-tunnel.js +29 -3
- package/package.json +6 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cloudflaredUnavailableError, cloudflaredVersionError, CLOUDFLARED_VERSION_TIMEOUT_MS, isSecureCloudflaredVersion, isSecureNodeVersion, nodeVersionError } from './security-versions.js';
|
|
1
2
|
import { existsSync, lstatSync, readFileSync, rmSync } from 'node:fs';
|
|
2
3
|
import { userInfo } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -10,9 +11,8 @@ const UNCHANGED = 'Nothing was changed and this host was not bound.';
|
|
|
10
11
|
* proof is sent: a failure afterwards cannot be retried with a fresh command.
|
|
11
12
|
*/
|
|
12
13
|
export async function setupTunnelPreflight({ exec = realExec, platform = process.platform, node = process.versions.node } = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw Error(`Tunnel setup requires Node.js 22.13 or newer; this is ${node}. Update Node.js, then run a fresh tunnel setup command. ${UNCHANGED}`);
|
|
14
|
+
if (!isSecureNodeVersion(node))
|
|
15
|
+
throw Error(`${nodeVersionError(node)} ${UNCHANGED}`);
|
|
16
16
|
if (platform === 'linux') {
|
|
17
17
|
const manager = await exec('systemctl', ['--user', 'show-environment'], { timeout: 10_000 });
|
|
18
18
|
if (manager.code !== 0)
|
|
@@ -20,6 +20,13 @@ export async function setupTunnelPreflight({ exec = realExec, platform = process
|
|
|
20
20
|
+ `Log in directly as ${userInfo().username} (not through su or sudo), or run: sudo loginctl enable-linger ${userInfo().username} && export XDG_RUNTIME_DIR=/run/user/${process.getuid?.()} `
|
|
21
21
|
+ `Then run a fresh tunnel setup command. ${UNCHANGED}`);
|
|
22
22
|
}
|
|
23
|
+
// Refuse an unsafe connector before anything is redeemed; the service checks its own
|
|
24
|
+
// PATH-selected cloudflared again before every tunnel start.
|
|
25
|
+
const version = await exec('cloudflared', ['--version'], { timeout: CLOUDFLARED_VERSION_TIMEOUT_MS });
|
|
26
|
+
if (version.code !== 0)
|
|
27
|
+
throw Error(`${cloudflaredUnavailableError(version.code === 127 ? 'not found' : 'version check failed')} ${UNCHANGED}`);
|
|
28
|
+
if (!isSecureCloudflaredVersion(version.stdout))
|
|
29
|
+
throw Error(`${cloudflaredVersionError(version.stdout)} ${UNCHANGED}`);
|
|
23
30
|
const help = await exec('cloudflared', ['tunnel', 'run', '--help'], { timeout: 10_000 });
|
|
24
31
|
if (help.code === 127)
|
|
25
32
|
throw Error(`cloudflared is not installed or not on PATH. Install it, then run a fresh tunnel setup command. ${UNCHANGED}`);
|
|
@@ -160,8 +160,15 @@ export async function runFleetManager(entrypoint, deps = {}) {
|
|
|
160
160
|
try {
|
|
161
161
|
try {
|
|
162
162
|
const config = producerConfig(process.env, undefined, log);
|
|
163
|
-
if (config)
|
|
164
|
-
|
|
163
|
+
if (config) {
|
|
164
|
+
const [{ listTasks }, { TaskLayouts }] = await Promise.all([import('../rooms-tasks/task-state.js'), import('../application/task-layouts.js')]);
|
|
165
|
+
const layouts = new TaskLayouts();
|
|
166
|
+
notifications = new TaskNotificationProducer(config, log, fetch, {
|
|
167
|
+
// Unreadable task or layout evidence is not an empty inventory: it throws and the bootstrap run stays incomplete.
|
|
168
|
+
tasks: () => listTasks({ strict: true }),
|
|
169
|
+
layoutRooms: task => (layouts.view(task)?.rooms ?? []).flatMap(room => room.identity_cid ? [room.identity_cid] : []),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
165
172
|
}
|
|
166
173
|
catch {
|
|
167
174
|
log('[fleet] task notifications unavailable; committed events retained');
|
|
@@ -91,6 +91,8 @@ export interface ResourceDescriptor {
|
|
|
91
91
|
list?: string;
|
|
92
92
|
groupByList?: boolean;
|
|
93
93
|
includeDeleting?: boolean;
|
|
94
|
+
/** Previews: each agent's newest assistant message time only, without its text. */
|
|
95
|
+
activityOnly?: boolean;
|
|
94
96
|
}
|
|
95
97
|
export declare function canonicalResourceDescriptor(input: unknown): ResourceDescriptor;
|
|
96
98
|
interface BoundProjection {
|
|
@@ -156,9 +156,11 @@ export function canonicalResourceDescriptor(input) {
|
|
|
156
156
|
throw Error('invalid resource descriptor');
|
|
157
157
|
const value = input;
|
|
158
158
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value.viewId) || !['directory', 'roles', 'tasks', 'task-lists', 'configuration', 'previews', 'task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health', 'subscriptions'].includes(value.resource)
|
|
159
|
-
|| Object.keys(value).some(key => !['viewId', 'resource', 'includeTemporary', 'includeDefinitions', 'ids', 'state', 'list', 'groupByList', 'includeDeleting', 'entityId', 'receiptId', 'provider'].includes(key)))
|
|
159
|
+
|| Object.keys(value).some(key => !['viewId', 'resource', 'includeTemporary', 'includeDefinitions', 'ids', 'state', 'list', 'groupByList', 'includeDeleting', 'entityId', 'receiptId', 'provider', 'activityOnly'].includes(key)))
|
|
160
160
|
throw Error('invalid resource descriptor');
|
|
161
|
-
|
|
161
|
+
if (value.activityOnly !== undefined && value.resource !== 'previews')
|
|
162
|
+
throw Error('invalid resource descriptor');
|
|
163
|
+
for (const key of ['includeTemporary', 'includeDefinitions', 'groupByList', 'includeDeleting', 'activityOnly'])
|
|
162
164
|
if (value[key] !== undefined && typeof value[key] !== 'boolean')
|
|
163
165
|
throw Error('invalid resource descriptor');
|
|
164
166
|
if (value.ids !== undefined && (!Array.isArray(value.ids) || value.ids.length > 500 || value.ids.some(id => typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,120}$/.test(id))))
|
package/dist/web/runtime.js
CHANGED
package/dist/web/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { FleetResourceViews, ResourceBindingError, canonicalResourceDescriptor }
|
|
|
2
2
|
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
3
3
|
import { stateRoot, defaultConfigPath } from '../paths.js';
|
|
4
4
|
import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
|
|
5
|
-
import { conversationPreview } from '../application/conversation-history.js';
|
|
5
|
+
import { conversationActivity, conversationPreview } from '../application/conversation-history.js';
|
|
6
6
|
import { registerConversationStream } from './conversation-stream.js';
|
|
7
7
|
import { conversationHasAttachment } from '../application/conversation-history.js';
|
|
8
8
|
import { readDeliveredFile } from '../file-delivery/store.js';
|
|
@@ -26,7 +26,7 @@ import { WebAuth } from './auth.js';
|
|
|
26
26
|
import { FleetEventBus } from './events.js';
|
|
27
27
|
import { ROLE_NAME_RE } from '../config.js';
|
|
28
28
|
import { TaskListError } from '../rooms-tasks/task-lists.js';
|
|
29
|
-
import { TaskStateError } from '../rooms-tasks/task-state.js';
|
|
29
|
+
import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, TaskStateError } from '../rooms-tasks/task-state.js';
|
|
30
30
|
import { appendTaskProgress, getTaskReview, listTaskProgress, publicProgressEntry, publicReview, readTaskReviewArtifact, setTaskReviewMarkdown, taskContributionSummary, TaskReviewError, } from '../rooms-tasks/task-review.js';
|
|
31
31
|
import { isProvider } from '../subscriptions/store.js';
|
|
32
32
|
const ONBOARDING_WORK = ['coordination', 'development', 'review'];
|
|
@@ -371,9 +371,12 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
371
371
|
const filter = { strict, ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
|
|
372
372
|
...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
|
|
373
373
|
const api = requireTaskRooms();
|
|
374
|
+
// Navigation totals cover every task in scope, not only the filtered or visible cards.
|
|
375
|
+
const unreadTotal = () => api.listTasks({ strict, ...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) })
|
|
376
|
+
.reduce((sum, task) => { const unread = contributionUnread(task); return sum + unread.progress + (unread.review ? 1 : 0); }, 0);
|
|
374
377
|
return taskApi(() => query.groupByList === 'true'
|
|
375
|
-
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => wireTask(api.withLayoutRooms(task))) })) }
|
|
376
|
-
: { tasks: api.listTasks(filter).map(task => wireTask(api.withLayoutRooms(task))) });
|
|
378
|
+
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => wireTask(api.withLayoutRooms(task))) })), contribution_unread_total: unreadTotal() }
|
|
379
|
+
: { tasks: api.listTasks(filter).map(task => wireTask(api.withLayoutRooms(task))), contribution_unread_total: unreadTotal() });
|
|
377
380
|
};
|
|
378
381
|
const project = async (descriptor, signal, owner = '') => {
|
|
379
382
|
signal.throwIfAborted();
|
|
@@ -426,7 +429,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
426
429
|
return { ...services.configuration.read(descriptor.includeDefinitions ?? false), provenance: services.presetProvenance?.read() ?? null };
|
|
427
430
|
case 'directory': {
|
|
428
431
|
const [roles, tasks] = await Promise.all([services.query.list(true, true), projectTasks({})]);
|
|
429
|
-
|
|
432
|
+
const { tasks: rows, contribution_unread_total } = tasks;
|
|
433
|
+
return { roles, tasks: rows, contribution_unread_total, lists: requireTaskRooms().listTaskLists() };
|
|
430
434
|
}
|
|
431
435
|
case 'previews': {
|
|
432
436
|
const roles = await services.repository.list(true), selected = new Map(roles.map(role => [role.id, role]));
|
|
@@ -437,14 +441,15 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
437
441
|
if (!role)
|
|
438
442
|
continue;
|
|
439
443
|
try {
|
|
440
|
-
previews[id] = dir ? await
|
|
444
|
+
previews[id] = descriptor.activityOnly ? (dir ? await conversationActivity(dir, signal) : { at: '' })
|
|
445
|
+
: dir ? await conversationPreview(dir, signal) : { text: 'No recent agent output', at: '' };
|
|
441
446
|
}
|
|
442
447
|
catch {
|
|
443
448
|
signal.throwIfAborted();
|
|
444
449
|
// A damaged/unreadable ledger is unavailable, not empty or removed.
|
|
445
450
|
// Keep this selected row observable so a repair can refresh it,
|
|
446
451
|
// without retiring previews for the other selected roles.
|
|
447
|
-
previews[id] = { text: 'Preview unavailable', at: '', unavailable: true };
|
|
452
|
+
previews[id] = descriptor.activityOnly ? { at: '', unavailable: true } : { text: 'Preview unavailable', at: '', unavailable: true };
|
|
448
453
|
}
|
|
449
454
|
}
|
|
450
455
|
return { previews };
|
|
@@ -505,7 +510,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
505
510
|
for (const resource of ['task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health'])
|
|
506
511
|
app.get('/api/v1/' + resource, async () => { throw new ResourceBindingError(409, 'resource binding required'); });
|
|
507
512
|
app.get('/api/v1/directory', async (request) => { auth.authenticate(request); return project({ viewId: 'read', resource: 'directory' }, new AbortController().signal); });
|
|
508
|
-
app.get('/api/v1/previews', async (request) => { auth.authenticate(request); const ids = String(
|
|
513
|
+
app.get('/api/v1/previews', async (request) => { auth.authenticate(request); const query = request.query, ids = String(query.ids ?? '').split(',').filter(Boolean); return project(canonicalResourceDescriptor({ viewId: 'read', resource: 'previews', ids, ...(query.activityOnly === 'true' ? { activityOnly: true } : {}) }), new AbortController().signal); });
|
|
509
514
|
app.get('/api/v1/task-lists', async (request) => {
|
|
510
515
|
auth.authenticate(request);
|
|
511
516
|
return project({ viewId: 'read', resource: 'task-lists' }, new AbortController().signal);
|
|
@@ -619,6 +624,22 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
619
624
|
reply.code(201);
|
|
620
625
|
return { entry: publicProgressEntry(result.entry), replayed: result.replayed };
|
|
621
626
|
});
|
|
627
|
+
/** Acknowledge Progress entries the browser actually showed; returns the task's remaining unread. */
|
|
628
|
+
app.post('/api/v1/tasks/:id/progress/read', async (request) => {
|
|
629
|
+
auth.authenticate(request, true);
|
|
630
|
+
const ids = request.body?.ids;
|
|
631
|
+
if (!Array.isArray(ids) || ids.length > 200 || ids.some(id => typeof id !== 'string' || id.length > 64))
|
|
632
|
+
throw new FleetError('invalid_request', 'ids must be at most 200 progress entry IDs');
|
|
633
|
+
return { unread: await reviewApi(() => acknowledgeTaskProgress(request.params.id, ids)) };
|
|
634
|
+
});
|
|
635
|
+
/** Acknowledge the To review revision the browser fetched and rendered. */
|
|
636
|
+
app.post('/api/v1/tasks/:id/review/read', async (request) => {
|
|
637
|
+
auth.authenticate(request, true);
|
|
638
|
+
const revision = request.body?.revision;
|
|
639
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
640
|
+
throw new FleetError('invalid_request', 'revision must be a non-negative integer');
|
|
641
|
+
return { unread: await reviewApi(() => acknowledgeTaskReview(request.params.id, revision)) };
|
|
642
|
+
});
|
|
622
643
|
app.get('/api/v1/tasks/:id/review', async (request) => {
|
|
623
644
|
auth.authenticate(request);
|
|
624
645
|
const review = await reviewApi(() => getTaskReview(request.params.id));
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Exec } from '../exec.js';
|
|
2
|
+
export declare function startWorkspaceTunnel(origin: string, exec?: Exec): Promise<() => Promise<void>>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { realExec } from '../exec.js';
|
|
5
|
+
import { assertSecureCloudflaredVersion, cloudflaredUnavailableError, CLOUDFLARED_VERSION_TIMEOUT_MS } from '../security-versions.js';
|
|
4
6
|
import { stateRoot } from '../paths.js';
|
|
5
|
-
export function startWorkspaceTunnel(origin) {
|
|
7
|
+
export async function startWorkspaceTunnel(origin, exec = realExec) {
|
|
6
8
|
const dir = join(stateRoot(), 'workspace'), file = join(dir, 'tunnel.json');
|
|
7
9
|
if (!existsSync(file))
|
|
8
10
|
return async () => { };
|
|
@@ -15,7 +17,31 @@ export function startWorkspaceTunnel(origin) {
|
|
|
15
17
|
const stat = lstatSync(tokenFile);
|
|
16
18
|
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())
|
|
17
19
|
throw Error('Connector credential must be an owned private file');
|
|
18
|
-
|
|
20
|
+
// The connector is the executable named cloudflared on this process's PATH, the
|
|
21
|
+
// same lookup spawn uses. Its version is verified before the first start and
|
|
22
|
+
// before every reconnect, so a downgrade is refused and an upgrade is picked up.
|
|
23
|
+
const verify = async () => {
|
|
24
|
+
const result = await exec('cloudflared', ['--version'], { timeout: CLOUDFLARED_VERSION_TIMEOUT_MS });
|
|
25
|
+
if (result.code !== 0)
|
|
26
|
+
throw Error(cloudflaredUnavailableError(result.code === 127 ? 'not found' : 'version check failed'));
|
|
27
|
+
assertSecureCloudflaredVersion(result.stdout);
|
|
28
|
+
};
|
|
29
|
+
await verify();
|
|
30
|
+
let stopped = false, child, timer, backoff = 1000, reported = '';
|
|
31
|
+
const schedule = () => { if (stopped)
|
|
32
|
+
return; timer = setTimeout(reconnect, backoff); backoff = Math.min(30000, backoff * 2); };
|
|
33
|
+
// A refused or unverifiable connector is never started; retry after the backoff.
|
|
34
|
+
const reconnect = () => {
|
|
35
|
+
timer = undefined;
|
|
36
|
+
void verify().then(() => { reported = ''; launch(); }, (error) => {
|
|
37
|
+
if (stopped)
|
|
38
|
+
return;
|
|
39
|
+
if (error.message !== reported)
|
|
40
|
+
console.error(`Workspace tunnel not restarted: ${error.message}`);
|
|
41
|
+
reported = error.message;
|
|
42
|
+
schedule();
|
|
43
|
+
});
|
|
44
|
+
};
|
|
19
45
|
const launch = () => {
|
|
20
46
|
if (stopped)
|
|
21
47
|
return;
|
|
@@ -23,7 +49,7 @@ export function startWorkspaceTunnel(origin) {
|
|
|
23
49
|
let scheduled = false;
|
|
24
50
|
// Keep an error listener for the child's whole lifetime, including shutdown.
|
|
25
51
|
const retry = () => { if (scheduled || stopped)
|
|
26
|
-
return; scheduled = true;
|
|
52
|
+
return; scheduled = true; schedule(); };
|
|
27
53
|
child.on('error', retry);
|
|
28
54
|
child.once('exit', retry);
|
|
29
55
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"docs/task-progress-review.md"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=22.
|
|
33
|
+
"node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1 <27"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
51
51
|
"@fastify/websocket": "^11.2.0",
|
|
52
|
-
"@ours.network/cli": "4.0.
|
|
53
|
-
"@ours.network/mcp": "2.0.
|
|
54
|
-
"@ours.network/sdk": "5.0.
|
|
52
|
+
"@ours.network/cli": "4.0.1",
|
|
53
|
+
"@ours.network/mcp": "2.0.1",
|
|
54
|
+
"@ours.network/sdk": "5.0.1",
|
|
55
55
|
"commander": "^12.1.0",
|
|
56
56
|
"fastify": "^5.4.0",
|
|
57
57
|
"qrcode": "1.5.4",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@agentclientprotocol/codex-acp": "1.10.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@ours.network/daemon": "5.0.
|
|
68
|
+
"@ours.network/daemon": "5.0.1",
|
|
69
69
|
"@types/node": "^22.20.4",
|
|
70
70
|
"@types/qrcode": "^1.5.6",
|
|
71
71
|
"@types/ws": "^8.18.1",
|