@ours.network/fleet 1.2.0-nightly.62 → 1.2.0-nightly.63
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 +3 -0
- package/dist/application/role-removal-service.js +1 -1
- package/dist/build-info.json +4 -4
- package/dist/notifications/fleet-producer.js +4 -4
- package/dist/notifications/target-cleanup.d.ts +2 -0
- package/dist/notifications/target-cleanup.js +24 -2
- package/dist/ops.js +1 -1
- package/dist/subscriptions/login.d.ts +1 -0
- package/dist/subscriptions/login.js +30 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1838,6 +1838,9 @@ See [Room layouts](docs/room-layouts.md) for YAML examples and CLI commands.
|
|
|
1838
1838
|
|
|
1839
1839
|
Configure the web backend with `OURS_NOTIFICATIONS_ORIGIN` and a Fleet-scoped `OURS_NOTIFICATIONS_PRODUCER_TOKEN`. It reads each local supervisor ledger incrementally with persistent checkpoints under web state, correlating owner browser/channel prompts with terminal completion. Monitor/startup turns do not notify. First installation skips existing history; no agent restart or conversation controller is needed. Persist web state across deployment/restarts. Reads are bounded to 256 KiB per role per tick, and oversized model/media records are skipped across chunks. Expose `/notifications` through the generic authenticated prefix gateway with a server-side user token and `stripBrowserContext: true`; producer tokens stay server-side. All browser sessions of this single-owner console map to one stable notification account. See [ours-notifications](https://github.com/adapt-toolkit/ours-notifications) for configuration/API/release details.
|
|
1840
1840
|
|
|
1841
|
+
New agent notifications and confirmed role retirement use `/fleet/chats/agents/:roleId`, with each ID escaped once and matching message anchors retained. Deploy the app, notification service, and producers together: old query-based conversation links are intentionally unsupported. Existing pending payload URLs are replayed unchanged for idempotency; obsolete cleanup targets remain stored without being sent or treated as proof of absence. Task management notification paths are unchanged.
|
|
1842
|
+
|
|
1843
|
+
|
|
1841
1844
|
Existing-host enrollment: `ours-fleet workspace-enroll --file <private-file> --preserve-profile` requires Messenger to advertise preservation support before any enrollment POST. It omits Name/Surname and leaves existing/absent profile untouched. Existing Fleet models and agents are retained; the installer must skip init when its configuration exists. Signed root proof, tunnel setup and device QR issuance remain the normal enrollment flow. Older Messenger fails closed and requires its preservation-capable release.
|
|
1842
1845
|
|
|
1843
1846
|
### Connect an installed host to the App
|
|
@@ -94,7 +94,7 @@ export class RoleRemovalService {
|
|
|
94
94
|
await this.options.ops.backend.uninstall(preview.role);
|
|
95
95
|
if (existsSync(sourceState))
|
|
96
96
|
renameSync(sourceState, join(recoveryPath, 'orphan-state'));
|
|
97
|
-
await retireNotificationTarget(`/fleet/chats
|
|
97
|
+
await retireNotificationTarget(`/fleet/chats/agents/${encodeURIComponent(preview.role)}`).catch(() => { });
|
|
98
98
|
this.options.ops.log(`removed orphaned backend/state '${preview.role}'`);
|
|
99
99
|
}
|
|
100
100
|
return { ...preview, removed: true, recoveryPath };
|
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.63",
|
|
3
|
+
"buildId": "9a1df2251871",
|
|
4
|
+
"commit": "ec4a9231a3a43772d7b7348e3fea407cf83e5bd1",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-14T20:26:32.887Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
9
|
"managed-cli.setup-v1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { roomsDir } from '../rooms-tasks/room-state.js';
|
|
2
|
-
import { drainNotificationTargetCleanup } from './target-cleanup.js';
|
|
2
|
+
import { drainNotificationTargetCleanup, notificationAgentId } from './target-cleanup.js';
|
|
3
3
|
import { closeSync, existsSync, lstatSync, openSync, readSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
@@ -71,7 +71,7 @@ export class FleetNotificationProducer {
|
|
|
71
71
|
}
|
|
72
72
|
poll() {
|
|
73
73
|
this.cleanup ??= drainNotificationTargetCleanup(this.config, undefined, undefined, { beforeDelete: async (url) => {
|
|
74
|
-
const name =
|
|
74
|
+
const name = notificationAgentId(url);
|
|
75
75
|
if (!name || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
76
76
|
return false;
|
|
77
77
|
// A recreated or retained role still has a valid target. Filesystem errors cannot prove removal.
|
|
@@ -126,7 +126,7 @@ export class FleetNotificationProducer {
|
|
|
126
126
|
if (!outbox) {
|
|
127
127
|
const key = createHash('sha256').update(roleDir).digest('hex');
|
|
128
128
|
outbox = new NotificationOutbox(join(this.stateDir, key + '.json'), this.config, async (value, eventId) => ({ eventId, title: `${roleId}: agent completed`, body: 'Your agent finished responding. Open the chat to view the result.',
|
|
129
|
-
url: `/fleet/chats
|
|
129
|
+
url: `/fleet/chats/agents/${encodeURIComponent(roleId)}`, ...value }), this.warn);
|
|
130
130
|
if (!outbox.checkpoint) {
|
|
131
131
|
const segment = this.existing.has(roleDir) ? segments.at(-1) : segments[0];
|
|
132
132
|
outbox.saveCheckpoint({ segment, offset: this.existing.has(roleDir) ? statSync(join(dir, segment)).size : 0, prompts: [] });
|
|
@@ -187,7 +187,7 @@ export class FleetNotificationProducer {
|
|
|
187
187
|
if (['message.chunk', 'message.replace'].includes(event.kind) && event.promptId && event.messageId && current.prompts.includes(prompt))
|
|
188
188
|
current.messages[prompt] = event.messageId;
|
|
189
189
|
if (event.kind === 'turn.completed' && event.promptId && current.prompts.includes(prompt)) {
|
|
190
|
-
if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { ...(taskRoles ? { taskId: taskRoles.get(roleId) ?? null } : {}), url: `/fleet/chats
|
|
190
|
+
if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { ...(taskRoles ? { taskId: taskRoles.get(roleId) ?? null } : {}), url: `/fleet/chats/agents/${encodeURIComponent(roleId)}#fleet-message-${encodeURIComponent(current.messages[prompt] ?? event.promptId)}` }))
|
|
191
191
|
break;
|
|
192
192
|
current.prompts = current.prompts.filter(p => p !== prompt);
|
|
193
193
|
delete current.messages[prompt];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ProducerConfig } from './outbox.js';
|
|
2
|
+
/** Only the canonical agent namespace can authorize role-target cleanup. */
|
|
3
|
+
export declare function notificationAgentId(path: string): string | null;
|
|
2
4
|
/** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
|
|
3
5
|
export declare function retireNotificationTarget(url: string, config?: ProducerConfig | undefined, dir?: string, request?: typeof fetch): Promise<void>;
|
|
4
6
|
/** Persisted retries also run from the ordinary web notification producer after restart. */
|
|
@@ -5,6 +5,24 @@ import { replaceFileAtomically } from '../atomic-file.js';
|
|
|
5
5
|
import { stateRoot } from '../paths.js';
|
|
6
6
|
import { producerConfig } from './outbox.js';
|
|
7
7
|
const cleanupDir = () => join(stateRoot(), 'notifications', 'removed-targets');
|
|
8
|
+
/** Only the canonical agent namespace can authorize role-target cleanup. */
|
|
9
|
+
export function notificationAgentId(path) {
|
|
10
|
+
try {
|
|
11
|
+
if (typeof path !== 'string' || /[\\\x00-\x20]|%(?:2f|5c|00|0[ad])/i.test(path))
|
|
12
|
+
return null;
|
|
13
|
+
const match = /^\/fleet\/chats\/agents\/([^/?#]+)(?:[?#]|$)/.exec(path);
|
|
14
|
+
if (!match)
|
|
15
|
+
return null;
|
|
16
|
+
const url = new URL(path, 'https://ours.invalid');
|
|
17
|
+
if (['source', 'chat', 'task', 'detail'].some(key => url.searchParams.has(key)))
|
|
18
|
+
return null;
|
|
19
|
+
const id = decodeURIComponent(match[1]);
|
|
20
|
+
return /^[A-Za-z0-9_-]+$/.test(id) ? id : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
8
26
|
/** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
|
|
9
27
|
export async function retireNotificationTarget(url, config = producerConfig(), dir = cleanupDir(), request = fetch) {
|
|
10
28
|
if (!config)
|
|
@@ -17,11 +35,15 @@ export async function retireNotificationTarget(url, config = producerConfig(), d
|
|
|
17
35
|
export async function drainNotificationTargetCleanup(config, dir = cleanupDir(), request = fetch, options = {}) {
|
|
18
36
|
if (!existsSync(dir))
|
|
19
37
|
return;
|
|
20
|
-
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
for (const file of readdirSync(dir).filter(file => /^[a-f0-9]{64}\.json$/.test(file))) {
|
|
40
|
+
if (attempts >= 32)
|
|
41
|
+
break;
|
|
21
42
|
try {
|
|
22
43
|
const path = join(dir, file), value = JSON.parse(readFileSync(path, 'utf8'));
|
|
23
|
-
if (
|
|
44
|
+
if (!notificationAgentId(value.url))
|
|
24
45
|
continue;
|
|
46
|
+
attempts++;
|
|
25
47
|
if (options.beforeDelete && !(await options.beforeDelete(value.url))) {
|
|
26
48
|
rmSync(path, { force: true });
|
|
27
49
|
continue;
|
package/dist/ops.js
CHANGED
|
@@ -271,7 +271,7 @@ export async function rmRole(cfg, name, deps) {
|
|
|
271
271
|
rmSync(agentDir(name), { recursive: true, force: true });
|
|
272
272
|
if (generatedSource && existsSync(role.sourceFile)) {
|
|
273
273
|
unlinkSync(role.sourceFile);
|
|
274
|
-
await retireNotificationTarget(`/fleet/chats
|
|
274
|
+
await retireNotificationTarget(`/fleet/chats/agents/${encodeURIComponent(name)}`).catch(() => { });
|
|
275
275
|
deps.log(`removed ${role.sourceFile}`);
|
|
276
276
|
}
|
|
277
277
|
deps.log(`removed '${name}' (its ours identity is left intact)`);
|
|
@@ -56,6 +56,7 @@ export declare class ClaudeLogin extends BaseLogin {
|
|
|
56
56
|
private child?;
|
|
57
57
|
private output;
|
|
58
58
|
private submitted;
|
|
59
|
+
private continued;
|
|
59
60
|
/** Resolves with the CLI exit status once it exits. */
|
|
60
61
|
exited: Promise<number | null>;
|
|
61
62
|
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string);
|
|
@@ -73,13 +73,27 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
73
73
|
child;
|
|
74
74
|
output = '';
|
|
75
75
|
submitted = false;
|
|
76
|
+
continued = false;
|
|
76
77
|
/** Resolves with the CLI exit status once it exits. */
|
|
77
78
|
exited;
|
|
78
79
|
constructor(owner, bins, home, profileId) {
|
|
79
80
|
super('claude', owner, 'paste_code');
|
|
80
|
-
const
|
|
81
|
-
|
|
81
|
+
const darwin = process.platform === 'darwin';
|
|
82
|
+
const args = darwin
|
|
83
|
+
? ['-q', '/dev/null', bins.claude, 'auth', 'login']
|
|
84
|
+
: ['-qefc', `${shellQuote(bins.claude)} auth login`, '/dev/null'];
|
|
85
|
+
// BSD script accepts a real pipe, but rejects Node's socket-backed stdin.
|
|
86
|
+
// Own the pipeline as one process group so cancellation also stops cat.
|
|
87
|
+
const child = spawn(darwin ? '/bin/sh' : bins.script, darwin
|
|
88
|
+
? ['-c', '/bin/cat 3>&- | { "$@" 3>&-; result=$?; printf "%s\\n" "$result" >&3; exit "$result"; }', 'fleet-claude-login', bins.script, ...args] : args, {
|
|
89
|
+
detached: darwin,
|
|
90
|
+
env: profileEnv('CLAUDE_CONFIG_DIR', profileId, home),
|
|
91
|
+
stdio: darwin ? ['pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],
|
|
82
92
|
});
|
|
93
|
+
// The terminal has exited; release cat's input so the shell can finish.
|
|
94
|
+
// Only the shell owns fd 3. Final close remains the authoritative status.
|
|
95
|
+
if (darwin)
|
|
96
|
+
child.stdio[3]?.once('data', () => child.stdin.end());
|
|
83
97
|
this.child = child;
|
|
84
98
|
child.stdin.on('error', () => { });
|
|
85
99
|
const onData = (chunk) => {
|
|
@@ -98,14 +112,16 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
114
|
// Claude asks for Enter after success; the answer carries no secret.
|
|
101
|
-
if (this.submitted && /press enter/i.test(this.output))
|
|
115
|
+
if (this.submitted && !this.continued && /press enter/i.test(this.output)) {
|
|
116
|
+
this.continued = true;
|
|
102
117
|
child.stdin.write('\r');
|
|
118
|
+
}
|
|
103
119
|
};
|
|
104
120
|
child.stdout.on('data', onData);
|
|
105
121
|
child.stderr.on('data', onData);
|
|
106
122
|
this.exited = new Promise(resolve => {
|
|
107
123
|
child.once('error', () => { this.end('failed', 'could not start the sign-in'); resolve(null); });
|
|
108
|
-
child.once('close', code => resolve(code));
|
|
124
|
+
child.once('close', code => { this.child = undefined; resolve(code); });
|
|
109
125
|
});
|
|
110
126
|
}
|
|
111
127
|
submitCode(code) {
|
|
@@ -118,7 +134,16 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
118
134
|
this.output = '';
|
|
119
135
|
this.child?.stdin.write(`${code}\r`);
|
|
120
136
|
}
|
|
121
|
-
stop() {
|
|
137
|
+
stop() {
|
|
138
|
+
if (process.platform === 'darwin' && this.child?.pid) {
|
|
139
|
+
try {
|
|
140
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
141
|
+
}
|
|
142
|
+
catch { /* already exited */ }
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
this.child?.kill('SIGTERM');
|
|
146
|
+
}
|
|
122
147
|
}
|
|
123
148
|
/** Codex device-code login through the app-server's supported account API. */
|
|
124
149
|
export class CodexDeviceLogin extends BaseLogin {
|
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.63",
|
|
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",
|