@ours.network/fleet 1.2.0-nightly.55 → 1.2.0-nightly.57
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/dist/build-info.json +4 -4
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- package/dist/notifications/task-producer.d.ts +18 -0
- package/dist/notifications/task-producer.js +67 -0
- package/dist/rooms-tasks/task-state.d.ts +8 -1
- package/dist/rooms-tasks/task-state.js +76 -1
- package/dist/rooms-tasks/types.d.ts +9 -0
- package/dist/supervisor/manager.js +12 -0
- package/docs/task-workspaces.md +38 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.57",
|
|
3
|
+
"buildId": "e1efd9a9bfd5",
|
|
4
|
+
"commit": "c94b8b63da1f7f0bab4ba1dec0c43a9e455b2366",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-10T18:15:52.108Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
9
|
"managed-cli.setup-v1",
|
package/dist/creation.d.ts
CHANGED
|
@@ -153,7 +153,7 @@ export interface DaemonIdentityProvisionerOptions {
|
|
|
153
153
|
export declare function daemonIdentityProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>, options?: DaemonIdentityProvisionerOptions): IdentityProvisioner;
|
|
154
154
|
/** Inventory-only variant for session-owned temporary identity lifecycles. */
|
|
155
155
|
export declare function daemonIdentityInventoryProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>): IdentityProvisioner;
|
|
156
|
-
/** Atomically write
|
|
156
|
+
/** Atomically write an owner-private Agent file, journalling it for rollback. */
|
|
157
157
|
export declare function writeRoleFile(tx: CreationTransaction, file: string, contents: string): void;
|
|
158
158
|
/** Where a setting's effective value came from. */
|
|
159
159
|
export type ProvenanceSource = 'cli' | 'agent-template' | 'fleet-default' | 'caller-role' | 'built-in';
|
package/dist/creation.js
CHANGED
|
@@ -294,10 +294,10 @@ export function daemonIdentityInventoryProvisioner(env = process.env, attachClie
|
|
|
294
294
|
acceptTemporaryExisting: true,
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
|
-
/** Atomically write
|
|
297
|
+
/** Atomically write an owner-private Agent file, journalling it for rollback. */
|
|
298
298
|
export function writeRoleFile(tx, file, contents) {
|
|
299
299
|
const existed = existsSync(file);
|
|
300
|
-
replaceFileAtomically(file, contents,
|
|
300
|
+
replaceFileAtomically(file, contents, 0o600);
|
|
301
301
|
tx.record({
|
|
302
302
|
stage: `Agent file ${file}`,
|
|
303
303
|
// Only remove what THIS transaction created; never delete a file the
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProducerConfig } from './outbox.js';
|
|
2
|
+
/** The Fleet manager owns this sender even when the web console is stopped.
|
|
3
|
+
* Events are already durable in their owning task commit, so no second outbox
|
|
4
|
+
* admission/crash window is needed. A failed task does not block other tasks. */
|
|
5
|
+
export declare class TaskNotificationProducer {
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly warn;
|
|
8
|
+
private readonly request;
|
|
9
|
+
private readonly timer;
|
|
10
|
+
private running?;
|
|
11
|
+
private offset;
|
|
12
|
+
private stopped;
|
|
13
|
+
private readonly shutdown;
|
|
14
|
+
constructor(config: ProducerConfig, warn?: (line: string) => void, request?: typeof fetch);
|
|
15
|
+
drain(): Promise<void>;
|
|
16
|
+
private drainOnce;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { acknowledgeTaskNotification, pendingTaskNotifications } from '../rooms-tasks/task-state.js';
|
|
2
|
+
/** The Fleet manager owns this sender even when the web console is stopped.
|
|
3
|
+
* Events are already durable in their owning task commit, so no second outbox
|
|
4
|
+
* admission/crash window is needed. A failed task does not block other tasks. */
|
|
5
|
+
export class TaskNotificationProducer {
|
|
6
|
+
config;
|
|
7
|
+
warn;
|
|
8
|
+
request;
|
|
9
|
+
timer;
|
|
10
|
+
running;
|
|
11
|
+
offset = 0;
|
|
12
|
+
stopped = false;
|
|
13
|
+
shutdown = new AbortController();
|
|
14
|
+
constructor(config, warn = () => { }, request = fetch) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.warn = warn;
|
|
17
|
+
this.request = request;
|
|
18
|
+
this.timer = setInterval(() => { void this.drain(); }, 3000);
|
|
19
|
+
this.timer.unref();
|
|
20
|
+
void this.drain();
|
|
21
|
+
}
|
|
22
|
+
drain() {
|
|
23
|
+
return this.running ??= this.drainOnce().catch(() => {
|
|
24
|
+
this.warn('Task notification snapshot unavailable; committed events retained');
|
|
25
|
+
}).finally(() => { this.running = undefined; });
|
|
26
|
+
}
|
|
27
|
+
async drainOnce() {
|
|
28
|
+
if (this.stopped)
|
|
29
|
+
return;
|
|
30
|
+
const pending = pendingTaskNotifications(this.warn);
|
|
31
|
+
if (!pending.length)
|
|
32
|
+
return;
|
|
33
|
+
const start = this.offset % pending.length;
|
|
34
|
+
const batch = [...pending.slice(start), ...pending.slice(0, start)].slice(0, 32);
|
|
35
|
+
this.offset = start + batch.length;
|
|
36
|
+
for (let index = 0; index < batch.length; index += 4) {
|
|
37
|
+
if (this.stopped)
|
|
38
|
+
break;
|
|
39
|
+
await Promise.all(batch.slice(index, index + 4).map(async (task) => {
|
|
40
|
+
for (const event of task.events.slice(0, 32)) {
|
|
41
|
+
if (this.stopped)
|
|
42
|
+
break;
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.request(this.config.origin + '/api/v1/send', {
|
|
45
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.any([this.shutdown.signal, AbortSignal.timeout(10_000)]),
|
|
46
|
+
headers: { ...(this.config.gatewayCredential
|
|
47
|
+
? { 'X-Ours-Api-Token': this.config.gatewayCredential, 'X-Ours-Notifications-Producer': this.config.token }
|
|
48
|
+
: { Authorization: `Bearer ${this.config.token}` }), 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(event),
|
|
50
|
+
});
|
|
51
|
+
await response.body?.cancel();
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
this.warn(`Task notification delivery pending (HTTP ${response.status}); committed event retained`);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
acknowledgeTaskNotification(task.taskId, event.eventId);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
this.warn('Task notification delivery pending; committed event retained');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async close() { this.stopped = true; clearInterval(this.timer); this.shutdown.abort(); await this.running; }
|
|
67
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { ArchivedMemberAbsence } from './types.js';
|
|
2
|
-
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
|
|
2
|
+
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase, TaskNotificationEvent } from './types.js';
|
|
3
3
|
export declare const tasksDir: () => string;
|
|
4
4
|
export declare class TaskStateError extends Error {
|
|
5
5
|
}
|
|
6
|
+
/** Atomic snapshots contain the events, including intermediate states between polls. */
|
|
7
|
+
export declare function pendingTaskNotifications(warn?: (line: string) => void): Array<{
|
|
8
|
+
taskId: string;
|
|
9
|
+
events: TaskNotificationEvent[];
|
|
10
|
+
}>;
|
|
11
|
+
/** Service event-id dedupe covers acceptance followed by a crash before this acknowledgement. */
|
|
12
|
+
export declare function acknowledgeTaskNotification(id: string, eventId: string): void;
|
|
6
13
|
/**
|
|
7
14
|
* Accepted deletion is the per-task epoch guard: once pending, every lifecycle
|
|
8
15
|
* mutation, terminal settlement, and room publication must fail boundedly.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
@@ -89,8 +89,77 @@ function writeTask(record) {
|
|
|
89
89
|
mkdirSync(tasksDir(), { recursive: true });
|
|
90
90
|
const persisted = { ...record };
|
|
91
91
|
delete persisted.list_name;
|
|
92
|
+
const path = taskPath(record.task_id);
|
|
93
|
+
const previous = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
94
|
+
const status = (task) => task.deletion?.status === 'pending' ? 'deleting' : task.blocked ? `${task.state} (blocked)` : task.state;
|
|
95
|
+
// Read pending events under the same task lock as every mutation. Metadata edits
|
|
96
|
+
// and retries preserve the queue; acknowledgement cannot be resurrected by stale input.
|
|
97
|
+
const pending = [...(previous?.notification_events ?? [])];
|
|
98
|
+
if (!previous || status(previous) !== status(record)) {
|
|
99
|
+
const next = status(record), before = previous ? status(previous) : undefined;
|
|
100
|
+
pending.push({ eventId: `task:${record.task_id}:${randomUUID()}`, taskId: record.task_id,
|
|
101
|
+
title: `Task ${!previous ? 'created' : 'updated'}: ${record.title}`.slice(0, 160),
|
|
102
|
+
body: `Task ${record.task_id}: ${before ? `${before} → ` : 'created in '}${next}.`,
|
|
103
|
+
url: `/fleet/tasks/${record.task_id}` });
|
|
104
|
+
}
|
|
105
|
+
if (pending.length)
|
|
106
|
+
persisted.notification_events = pending;
|
|
107
|
+
else
|
|
108
|
+
delete persisted.notification_events;
|
|
92
109
|
replaceFileAtomically(taskPath(record.task_id), JSON.stringify(persisted, null, 2) + '\n');
|
|
93
110
|
}
|
|
111
|
+
const retiredNotificationsDir = () => join(tasksDir(), '.notification-retired');
|
|
112
|
+
/** Atomic snapshots contain the events, including intermediate states between polls. */
|
|
113
|
+
export function pendingTaskNotifications(warn = () => { }) {
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const dir of [tasksDir(), retiredNotificationsDir()]) {
|
|
116
|
+
if (!existsSync(dir))
|
|
117
|
+
continue;
|
|
118
|
+
for (const file of readdirSync(dir).filter(file => TASK_ID_PATTERN.test(file.slice(0, -5)) && file.endsWith('.json'))) {
|
|
119
|
+
try {
|
|
120
|
+
const path = join(dir, file), stat = lstatSync(path);
|
|
121
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
122
|
+
throw new Error('invalid notification source');
|
|
123
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
124
|
+
const id = file.slice(0, -5);
|
|
125
|
+
// A prepared deletion copy has no authority while the original still exists.
|
|
126
|
+
if (dir === retiredNotificationsDir() && existsSync(taskPath(id)))
|
|
127
|
+
continue;
|
|
128
|
+
if (value.notification_events === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
if (!Array.isArray(value.notification_events) || value.notification_events.some(e => !e
|
|
131
|
+
|| typeof e.eventId !== 'string' || !e.eventId.startsWith(`task:${id}:`) || e.eventId.length > 512
|
|
132
|
+
|| e.taskId !== id || (e.url !== `/fleet/tasks/${id}` && !(e.eventId === `task:${id}:deleted` && e.url === `/fleet/tasks?deletedTask=${id}`)) || typeof e.title !== 'string' || !e.title.trim() || e.title.length > 160
|
|
133
|
+
|| typeof e.body !== 'string' || !e.body.trim() || e.body.length > 512))
|
|
134
|
+
throw new Error('invalid notification events');
|
|
135
|
+
if (value.notification_events.length)
|
|
136
|
+
result.push({ taskId: id, events: value.notification_events });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (!isNotFoundError(error))
|
|
140
|
+
warn('Task notification record unavailable; other task deliveries continue');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
/** Service event-id dedupe covers acceptance followed by a crash before this acknowledgement. */
|
|
147
|
+
export function acknowledgeTaskNotification(id, eventId) {
|
|
148
|
+
assertCanonicalTaskId(id);
|
|
149
|
+
withTaskLock(id, () => {
|
|
150
|
+
for (const path of [taskPath(id), join(retiredNotificationsDir(), `${id}.json`)]) {
|
|
151
|
+
if (!existsSync(path))
|
|
152
|
+
continue;
|
|
153
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
154
|
+
if (!value.notification_events?.some(event => event.eventId === eventId))
|
|
155
|
+
continue;
|
|
156
|
+
value.notification_events = value.notification_events.filter(event => event.eventId !== eventId);
|
|
157
|
+
replaceFileAtomically(path, JSON.stringify(value, null, 2) + '\n');
|
|
158
|
+
if (path.startsWith(retiredNotificationsDir() + '/') && !value.notification_events.length)
|
|
159
|
+
unlinkSync(path);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
94
163
|
function presentTask(record) {
|
|
95
164
|
const listId = record.list_id ?? DEFAULT_TASK_LIST_ID;
|
|
96
165
|
const list = readTaskLists().find(item => item.list_id === listId);
|
|
@@ -812,6 +881,12 @@ export function unlinkDeletedTask(id) {
|
|
|
812
881
|
}
|
|
813
882
|
if (stored.deletion?.status !== 'pending')
|
|
814
883
|
throw new TaskStateError(`task ${id} has no pending deletion`);
|
|
884
|
+
// Preserve committed deliveries outside the record before deletion. A crash
|
|
885
|
+
// before unlink may replay two copies; both carry identical service dedupe IDs.
|
|
886
|
+
const removed = { eventId: `task:${id}:deleted`, taskId: id,
|
|
887
|
+
title: `Task deleted: ${stored.title}`.slice(0, 160), body: `Task ${id}: deleting → deleted.`,
|
|
888
|
+
url: `/fleet/tasks?deletedTask=${id}` };
|
|
889
|
+
replaceFileAtomically(join(retiredNotificationsDir(), `${id}.json`), JSON.stringify({ notification_events: [...(stored.notification_events ?? []), removed] }) + '\n');
|
|
815
890
|
try {
|
|
816
891
|
unlinkSync(taskPath(id));
|
|
817
892
|
}
|
|
@@ -106,7 +106,16 @@ export interface TaskLayoutLink {
|
|
|
106
106
|
/** sha256 of the canonical source layout definition the run must snapshot. */
|
|
107
107
|
definition_hash: string;
|
|
108
108
|
}
|
|
109
|
+
export interface TaskNotificationEvent {
|
|
110
|
+
eventId: string;
|
|
111
|
+
title: string;
|
|
112
|
+
body: string;
|
|
113
|
+
url: string;
|
|
114
|
+
taskId: string;
|
|
115
|
+
}
|
|
109
116
|
export interface TaskRecord {
|
|
117
|
+
/** Pending lifecycle events, committed atomically with their task change. */
|
|
118
|
+
notification_events?: TaskNotificationEvent[];
|
|
110
119
|
workspace?: import('./workspace.js').OwnedWorkspace;
|
|
111
120
|
task_id: string;
|
|
112
121
|
/** Stable organizational list identifier. Missing legacy values mean `default`. */
|
|
@@ -2,6 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { closeSync, existsSync, openSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { producerConfig } from '../notifications/outbox.js';
|
|
6
|
+
import { TaskNotificationProducer } from '../notifications/task-producer.js';
|
|
5
7
|
import { acquireOwnerBinderLease } from '../owner-channel/binder.js';
|
|
6
8
|
import { stateRoot } from '../paths.js';
|
|
7
9
|
import { readRestartLedger, writeRestartLedger, backoffFor, RESTART_FAIL_THRESHOLD } from '../runner.js';
|
|
@@ -34,6 +36,7 @@ export async function runFleetManager(entrypoint, deps = {}) {
|
|
|
34
36
|
closeSync(fd);
|
|
35
37
|
}
|
|
36
38
|
});
|
|
39
|
+
let notifications;
|
|
37
40
|
async function reconcile(key) {
|
|
38
41
|
await withMemberLock(key, async () => {
|
|
39
42
|
const member = readMember(key);
|
|
@@ -155,6 +158,14 @@ export async function runFleetManager(entrypoint, deps = {}) {
|
|
|
155
158
|
});
|
|
156
159
|
}
|
|
157
160
|
try {
|
|
161
|
+
try {
|
|
162
|
+
const config = producerConfig(process.env, undefined, log);
|
|
163
|
+
if (config)
|
|
164
|
+
notifications = new TaskNotificationProducer(config, log);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
log('[fleet] task notifications unavailable; committed events retained');
|
|
168
|
+
}
|
|
158
169
|
try {
|
|
159
170
|
const { resumeFleetTransfers } = await import('./adoption.js');
|
|
160
171
|
await resumeFleetTransfers(entrypoint, exec);
|
|
@@ -178,6 +189,7 @@ export async function runFleetManager(entrypoint, deps = {}) {
|
|
|
178
189
|
}
|
|
179
190
|
finally {
|
|
180
191
|
stopping = true;
|
|
192
|
+
await notifications?.close();
|
|
181
193
|
// Shutdown preserves desired intent; the successor resumes the same catalog.
|
|
182
194
|
const deadline = Date.now() + 17_000;
|
|
183
195
|
while (children.size && Date.now() < deadline) {
|
package/docs/task-workspaces.md
CHANGED
|
@@ -185,3 +185,41 @@ before erasing task/room/workspace resources. Permanent siblings remain intact.
|
|
|
185
185
|
Standalone CLI/web removal refuses `TASK_MEMBER_IS_TASK_OWNED`; use the owning
|
|
186
186
|
task lifecycle. Lost/corrupt task or launch records require restoring original
|
|
187
187
|
backup evidence or explicit ownership reconciliation before deletion can finish.
|
|
188
|
+
|
|
189
|
+
## Owner lifecycle notifications
|
|
190
|
+
|
|
191
|
+
A task mutation commits its notification event in the same atomic task record as
|
|
192
|
+
its effective status. Creation, backlog, provisioning, active, review, done,
|
|
193
|
+
cancelled and failed are covered; block/unblock and deletion are effective changes
|
|
194
|
+
as well. Editing a description, list, block reason or saga checkpoint is not a
|
|
195
|
+
status transition. Each event freezes the task identity, title, old/new status and
|
|
196
|
+
navigation route at commit time, so several transitions between delivery polls
|
|
197
|
+
remain separate. Legacy records begin emitting on their next actual change and
|
|
198
|
+
are not retroactively announced as newly created.
|
|
199
|
+
|
|
200
|
+
The Fleet manager drains committed task events without requiring the web console
|
|
201
|
+
or restarting agents. Its notification producer uses the installation's existing
|
|
202
|
+
owner-scoped credential; it grants no agent arbitrary-send capability. Missing
|
|
203
|
+
configuration or service failure retains the events. Delivery acknowledgement
|
|
204
|
+
runs under the task lock and removes only the accepted event, preserving newer
|
|
205
|
+
changes. A corrupt/unreadable task record is isolated with a safe warning, so
|
|
206
|
+
other tasks continue. Bounded rotating batches prevent one rejected task from
|
|
207
|
+
blocking the entire sender. The built-in `default` task list is unchanged.
|
|
208
|
+
|
|
209
|
+
Deleting is announced only after its intent commits. Before unlink, settlement
|
|
210
|
+
prepares retained pending events plus a stable deleted event outside the task-owned
|
|
211
|
+
workspace. The sender admits that retained copy only when the original task record
|
|
212
|
+
is absent; an interrupted or failed unlink cannot announce deletion. The final
|
|
213
|
+
event opens the task list and includes the removed task's identity. Earlier events
|
|
214
|
+
keep their immutable detail routes, which may be absent after explicit deletion.
|
|
215
|
+
|
|
216
|
+
A service restart or producer crash after acceptance may replay an event; the
|
|
217
|
+
Notifications service must retain its acceptance receipts across read/delete to
|
|
218
|
+
make that replay harmless. Upgrade Notifications alongside Fleet to obtain this
|
|
219
|
+
stronger guarantee. The service's authenticated delivery diagnostics distinguish
|
|
220
|
+
provider acceptance, retry, expired subscription and foreground suppression.
|
|
221
|
+
Provider acceptance is not evidence of actual phone receipt. iPhone Web Push
|
|
222
|
+
requires a supported Home Screen installation and granted device/browser permission;
|
|
223
|
+
reopen the workspace to reconcile an expired subscription and explicitly re-enable
|
|
224
|
+
permission if it was revoked. Every received push must display a notification to
|
|
225
|
+
honor Safari's user-visible push requirement.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.57",
|
|
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",
|