@ours.network/fleet 0.13.0 → 0.13.2
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/harness/claude-code.js +5 -0
- package/dist/harness/types.d.ts +6 -0
- package/dist/owner-channel/attachments.js +19 -5
- package/dist/runner.js +1 -0
- package/dist/session/acp.d.ts +2 -0
- package/dist/session/acp.js +16 -0
- package/dist/spawn.js +1 -0
- package/package.json +1 -1
|
@@ -207,6 +207,11 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
207
207
|
: bundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp');
|
|
208
208
|
return { argv, env: prep.env };
|
|
209
209
|
},
|
|
210
|
+
// Same source as buildLaunch's --permission-mode flag, so the tmux and ACP
|
|
211
|
+
// backends cannot disagree about what a role's permissions translate to.
|
|
212
|
+
acpPermissionModeId(role) {
|
|
213
|
+
return permissionMode(role);
|
|
214
|
+
},
|
|
210
215
|
isolationPaths(_role, _dirs) {
|
|
211
216
|
const claudeHome = join(home(), '.claude');
|
|
212
217
|
return {
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -98,6 +98,12 @@ export interface HarnessAdapter {
|
|
|
98
98
|
prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
|
|
99
99
|
buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
|
|
100
100
|
buildAcpLaunch?(role: ResolvedRole, prep: SessionPrep): AcpLaunch;
|
|
101
|
+
/**
|
|
102
|
+
* The native permission-mode id an ACP session should run at, from the same
|
|
103
|
+
* translation `buildLaunch` uses for its CLI flag. `undefined` keeps the
|
|
104
|
+
* agent's default. Omit for a harness whose ACP agent has no modes.
|
|
105
|
+
*/
|
|
106
|
+
acpPermissionModeId?(role: ResolvedRole): string | undefined;
|
|
101
107
|
/**
|
|
102
108
|
* REQUIRED. Every adapter must either translate neutral permissions or
|
|
103
109
|
* explicitly declare that it cannot. Enforced at registration.
|
|
@@ -88,6 +88,16 @@ function parseTranscription(value, wireId) {
|
|
|
88
88
|
fileWireId: wireId,
|
|
89
89
|
} };
|
|
90
90
|
}
|
|
91
|
+
// Voice notes ride "<base>; x-ours-kind=voice-message" verbatim end to end
|
|
92
|
+
// (the recorder's real container varies: audio/webm Chrome/Android, audio/mp4
|
|
93
|
+
// iOS Safari, audio/ogg fallback), so policy for voice messages applies to the
|
|
94
|
+
// base container type. Ordinary files keep exact allowlist matching.
|
|
95
|
+
function baseMime(mime) {
|
|
96
|
+
return mime.split(';')[0].trim();
|
|
97
|
+
}
|
|
98
|
+
function policyMime(file) {
|
|
99
|
+
return file.kind === 'voice_message' ? baseMime(file.mime) : file.mime;
|
|
100
|
+
}
|
|
91
101
|
export function validateAttachmentSelection(files, config) {
|
|
92
102
|
if (!config.enabled)
|
|
93
103
|
return 'attachments are disabled for this owner channel';
|
|
@@ -95,7 +105,10 @@ export function validateAttachmentSelection(files, config) {
|
|
|
95
105
|
return `the request exceeds the ${config.max_files_per_request}-file limit`;
|
|
96
106
|
let total = 0;
|
|
97
107
|
for (const file of files) {
|
|
98
|
-
|
|
108
|
+
const mime = policyMime(file);
|
|
109
|
+
if (file.kind === 'voice_message' && !mime.startsWith('audio/'))
|
|
110
|
+
return `voice-message MIME type ${file.mime || '(missing)'} is not an audio container`;
|
|
111
|
+
if (!config.allowed_mime.includes(mime))
|
|
99
112
|
return `MIME type ${file.mime || '(missing)'} is not allowed`;
|
|
100
113
|
if (file.size > config.max_file_bytes)
|
|
101
114
|
return `a file exceeds the ${config.max_file_bytes}-byte limit`;
|
|
@@ -148,9 +161,10 @@ export async function admitAttachments(files, dir, config) {
|
|
|
148
161
|
total += bytes.length;
|
|
149
162
|
if (total > config.max_request_bytes)
|
|
150
163
|
throw new Error('retrieved attachments exceed the request size limit');
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
const declaredMime = policyMime(file);
|
|
165
|
+
const detectedMime = detectMime(bytes, declaredMime);
|
|
166
|
+
if (!mimeCompatible(declaredMime, detectedMime))
|
|
167
|
+
throw new Error(`retrieved attachment content does not match declared MIME ${declaredMime}`);
|
|
154
168
|
const filename = sanitizeFilename(file.filename);
|
|
155
169
|
const finalPath = join(dir, `${index + 1}-${file.wireId.slice(0, 12)}-${filename}`);
|
|
156
170
|
const tmp = join(dir, `.${basename(finalPath)}.${randomUUID()}.tmp`);
|
|
@@ -175,7 +189,7 @@ export async function admitAttachments(files, dir, config) {
|
|
|
175
189
|
await rm(tmp, { force: true });
|
|
176
190
|
await chmod(finalPath, 0o600);
|
|
177
191
|
admitted.push({
|
|
178
|
-
wireId: file.wireId, filename, path: finalPath, declaredMime
|
|
192
|
+
wireId: file.wireId, filename, path: finalPath, declaredMime,
|
|
179
193
|
detectedMime, size: bytes.length, sha256: digest, kind: file.kind,
|
|
180
194
|
...(file.transcription ? { transcription: {
|
|
181
195
|
configured: file.transcription.configured, attempted: file.transcription.attempted,
|
package/dist/runner.js
CHANGED
package/dist/session/acp.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface AcpSessionOptions {
|
|
|
8
8
|
stateDir: string;
|
|
9
9
|
mode: 'fresh' | 'resume';
|
|
10
10
|
permissions: CommonPermissions;
|
|
11
|
+
/** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
|
|
12
|
+
modeId?: string;
|
|
11
13
|
log(line: string): void;
|
|
12
14
|
}
|
|
13
15
|
/**
|
package/dist/session/acp.js
CHANGED
|
@@ -251,6 +251,22 @@ export class AcpSession {
|
|
|
251
251
|
this.sessionId = created.sessionId;
|
|
252
252
|
}
|
|
253
253
|
writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
|
|
254
|
+
// Deliver the configured permission mode whichever way the session came up
|
|
255
|
+
// (new, resume or load) — the launch flag never reaches an ACP agent. A
|
|
256
|
+
// refusal is loud but never fatal: the session then simply runs at the
|
|
257
|
+
// agent's own default.
|
|
258
|
+
if (this.options.modeId) {
|
|
259
|
+
try {
|
|
260
|
+
await this.connection.agent.request(acp.methods.agent.session.setMode, {
|
|
261
|
+
sessionId: this.sessionId,
|
|
262
|
+
modeId: this.options.modeId,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
this.options.log(`[${this.options.name}] acp: session/set_mode "${this.options.modeId}" failed ` +
|
|
267
|
+
`(${e instanceof Error ? e.message : String(e)}) — session runs at the agent default permission mode`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
254
270
|
this.readiness = 'idle';
|
|
255
271
|
this.events.emit('state', { status: 'idle', text: `ACP session ${this.sessionId}` });
|
|
256
272
|
}
|
package/dist/spawn.js
CHANGED
|
@@ -253,6 +253,7 @@ function provenanceSettings(o, defaults) {
|
|
|
253
253
|
? { value: explicitModel, source: 'cli' }
|
|
254
254
|
: { value: inheritedModel, source: inheritedModel ? 'fleet-default' : 'built-in' },
|
|
255
255
|
coordinator: provenanceOf(o.coordinator, undefined, undefined),
|
|
256
|
+
permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
|
|
256
257
|
approval: provenanceOf(o.approval, perms.approval, 'ask'),
|
|
257
258
|
filesystem: provenanceOf(o.filesystem, perms.filesystem, 'workspace'),
|
|
258
259
|
unattended: provenanceOf(o.unattended, perms.unattended, 'deny'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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",
|