agents-city 0.4.0 → 0.5.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/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- package/plugin/scripts/reception.py +665 -0
|
@@ -5,7 +5,7 @@ export async function roadBusCli(args: string[]): Promise<unknown> {
|
|
|
5
5
|
if (['help', '-h', '--help'].includes(verb)) {
|
|
6
6
|
return {
|
|
7
7
|
usage: 'agents-city bus <roster | inbox | send owner/city text>',
|
|
8
|
-
note: 'Only the seat can cross a road
|
|
8
|
+
note: 'Only the seat can cross a road. Inbox returns a bounded batch so a burst never becomes one model turn per message.',
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
if (verb === 'roster') return busCommand('road.roster');
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
{
|
|
2
|
+
"protocolo": "agents-city-arnes/1",
|
|
3
|
+
"_": [
|
|
4
|
+
"What this product does to somebody else's CLI, declared once.",
|
|
5
|
+
"",
|
|
6
|
+
"The pitch is that we orchestrate the CLIs people already have, with the",
|
|
7
|
+
"plugins, skills, MCP servers and settings they already configured. That is",
|
|
8
|
+
"only true if it can be audited, and it can only be audited if the runtime",
|
|
9
|
+
"and the report read the SAME file. So they do: `codex.ts` takes its policy",
|
|
10
|
+
"values from `trato` below, and `agents-city doctor --config` prints this",
|
|
11
|
+
"same table filled in with what is actually on the machine. Change a value",
|
|
12
|
+
"here and both move together; there is nowhere for a claim to drift from a",
|
|
13
|
+
"behaviour.",
|
|
14
|
+
"",
|
|
15
|
+
" trato what we add or override, and why. This is the deal: without it",
|
|
16
|
+
" the bus is not the only route and the cage does not hold. It is",
|
|
17
|
+
" deliberately short, and every line of it is printable.",
|
|
18
|
+
" hereda what we deliberately do NOT send, so their own CLI reads their",
|
|
19
|
+
" own configuration for it.",
|
|
20
|
+
" respeta what loads untouched."
|
|
21
|
+
],
|
|
22
|
+
"motores": {
|
|
23
|
+
"claude": {
|
|
24
|
+
"binario": "claude",
|
|
25
|
+
"config": [
|
|
26
|
+
"~/.claude/settings.json",
|
|
27
|
+
"~/.claude/settings.local.json"
|
|
28
|
+
],
|
|
29
|
+
"trato": [
|
|
30
|
+
{
|
|
31
|
+
"clave": "crossSessionInbound",
|
|
32
|
+
"valor": "refuse",
|
|
33
|
+
"via": "--settings (an added layer, not a replacement)",
|
|
34
|
+
"porque": "the city bus is the only route between agents; Claude's own cross-session path would be a second one nobody can see",
|
|
35
|
+
"rinde": "settings"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"clave": "disallowedTools",
|
|
39
|
+
"valor": "SendMessage,ListAgents",
|
|
40
|
+
"via": "--disallowed-tools",
|
|
41
|
+
"porque": "same reason, from the other side: an agent must not reach a peer directly",
|
|
42
|
+
"rinde": "flag"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"clave": "skipDangerousModePermissionPrompt",
|
|
46
|
+
"valor": "true",
|
|
47
|
+
"via": "--settings",
|
|
48
|
+
"porque": "suppresses Claude's local yolo notice only; it does not grant anything",
|
|
49
|
+
"rinde": "settings"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"hereda": [
|
|
53
|
+
{
|
|
54
|
+
"suyo": "model",
|
|
55
|
+
"cuando": "no `model.<window>` on the card"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"suyo": "effortLevel",
|
|
59
|
+
"cuando": "no `effort.<window>` on the card"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"suyo": "permissions",
|
|
63
|
+
"cuando": "always, unless this city runs yolo"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"respeta": [
|
|
67
|
+
"plugins",
|
|
68
|
+
"skills",
|
|
69
|
+
"MCP servers",
|
|
70
|
+
"statusline",
|
|
71
|
+
"theme",
|
|
72
|
+
"hooks",
|
|
73
|
+
"output styles"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"codex": {
|
|
77
|
+
"binario": "codex",
|
|
78
|
+
"config": [
|
|
79
|
+
"~/.codex/config.toml"
|
|
80
|
+
],
|
|
81
|
+
"trato": [
|
|
82
|
+
{
|
|
83
|
+
"clave": "developerInstructions",
|
|
84
|
+
"valor": "You are one member of an Agents City committee. Never contact repo peers directly; submit evidence through the agents-city committee CLI named in each assignment.",
|
|
85
|
+
"via": "thread configuration",
|
|
86
|
+
"porque": "the committee is chaired: a member that answers its peers directly is not being moderated by anybody"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"clave": "sandbox",
|
|
90
|
+
"valor": "workspace-write",
|
|
91
|
+
"via": "thread configuration and every turn",
|
|
92
|
+
"porque": "the cage: writes stay inside the workspace. `alterno` instead when an outer cage is already holding, or when CITY_CAGE=0 turns it off deliberately",
|
|
93
|
+
"alterno": "danger-full-access"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"clave": "approvalPolicy",
|
|
97
|
+
"valor": "on-request",
|
|
98
|
+
"via": "thread configuration and every turn",
|
|
99
|
+
"porque": "the default when their config says nothing. `never` rejects anything needing approval, which silently disables app and MCP tools",
|
|
100
|
+
"suyo": "approval_policy"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"hereda": [
|
|
104
|
+
{
|
|
105
|
+
"suyo": "model",
|
|
106
|
+
"cuando": "no `model.<window>` on the card"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"suyo": "model_reasoning_effort",
|
|
110
|
+
"cuando": "no `effort.<window>` on the card"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"suyo": "approval_policy",
|
|
114
|
+
"cuando": "they set one"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"suyo": "personality",
|
|
118
|
+
"cuando": "always"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"suyo": "features",
|
|
122
|
+
"cuando": "always"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"respeta": [
|
|
126
|
+
"MCP servers",
|
|
127
|
+
"profiles",
|
|
128
|
+
"personality",
|
|
129
|
+
"features",
|
|
130
|
+
"notify"
|
|
131
|
+
],
|
|
132
|
+
"avisa": [
|
|
133
|
+
{
|
|
134
|
+
"cuando": "approval_policy = never",
|
|
135
|
+
"dice": "your Codex will refuse anything needing approval, app and MCP tools included"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"opencode": {
|
|
140
|
+
"binario": "opencode",
|
|
141
|
+
"config": [
|
|
142
|
+
"~/.config/opencode/opencode.json"
|
|
143
|
+
],
|
|
144
|
+
"trato": [],
|
|
145
|
+
"hereda": [
|
|
146
|
+
{
|
|
147
|
+
"suyo": "model",
|
|
148
|
+
"cuando": "no `model.<window>` on the card"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"respeta": [
|
|
152
|
+
"agents",
|
|
153
|
+
"providers",
|
|
154
|
+
"MCP servers",
|
|
155
|
+
"keybinds"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
"kimi": {
|
|
159
|
+
"binario": "kimi",
|
|
160
|
+
"config": [
|
|
161
|
+
"~/.kimi/config.json"
|
|
162
|
+
],
|
|
163
|
+
"trato": [
|
|
164
|
+
{
|
|
165
|
+
"clave": "system_prompt",
|
|
166
|
+
"valor": "You are one member of an Agents City committee. Never contact repo peers directly; submit evidence through the agents-city committee CLI named in each assignment.",
|
|
167
|
+
"via": "agent_config, when the session is created",
|
|
168
|
+
"porque": "the committee is chaired: a member that answers its peers directly is not being moderated by anybody"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"clave": "permission_mode",
|
|
172
|
+
"valor": "manual",
|
|
173
|
+
"via": "agent_config, on the session and on every turn",
|
|
174
|
+
"porque": "what a turn gets when nothing asks for more: it stops for approval. `auto` when the command says --auto, `yolo` when this city runs yolo or the command says --yolo"
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
"hereda": [
|
|
178
|
+
{
|
|
179
|
+
"suyo": "model",
|
|
180
|
+
"cuando": "no `model.<window>` on the card"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"respeta": [
|
|
184
|
+
"providers",
|
|
185
|
+
"MCP servers"
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness declaration, in the runtimes' own language.
|
|
3
|
+
*
|
|
4
|
+
* `arnes.json` is the single source of truth for what this product adds to
|
|
5
|
+
* somebody else's CLI. The connectors read their policy values from here rather
|
|
6
|
+
* than spelling them inline, so `agents-city doctor --config` can print the
|
|
7
|
+
* same table and be believed: there is one place to change, and both move.
|
|
8
|
+
*/
|
|
9
|
+
import declaracion from './arnes.json' with { type: 'json' };
|
|
10
|
+
|
|
11
|
+
export interface Trato {
|
|
12
|
+
clave: string;
|
|
13
|
+
valor: string;
|
|
14
|
+
via: string;
|
|
15
|
+
porque: string;
|
|
16
|
+
/** Their own config key, when this one is a default they can outrank. */
|
|
17
|
+
suyo?: string;
|
|
18
|
+
/** The other value this key takes, when the choice is ours to compute. */
|
|
19
|
+
alterno?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Motor {
|
|
23
|
+
binario: string;
|
|
24
|
+
config: string[];
|
|
25
|
+
trato: Trato[];
|
|
26
|
+
hereda: Array<{ suyo: string; cuando: string }>;
|
|
27
|
+
respeta: string[];
|
|
28
|
+
avisa?: Array<{ cuando: string; dice: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MOTORES = (declaracion as { motores: Record<string, Motor> }).motores;
|
|
32
|
+
|
|
33
|
+
/** The same value in the app-server's camelCase spelling: `workspace-write`
|
|
34
|
+
* and `workspaceWrite` are one decision written twice, and only one of them
|
|
35
|
+
* gets to be the source. */
|
|
36
|
+
export function camello(valor: string): string {
|
|
37
|
+
return valor.replace(/-([a-z])/g, (_, letra: string) => letra.toUpperCase());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One declared value, by runtime and key. Throws rather than guess: a missing
|
|
41
|
+
* entry means the declaration and the code have parted ways, which is the one
|
|
42
|
+
* thing this file exists to prevent. */
|
|
43
|
+
export function trato(nombre: string, clave: string): Trato {
|
|
44
|
+
const encontrado = MOTORES[nombre]?.trato.find((t) => t.clave === clave);
|
|
45
|
+
if (!encontrado) throw new Error(`arnes.json declares no ${clave} for ${nombre}`);
|
|
46
|
+
return encontrado;
|
|
47
|
+
}
|
|
@@ -17,6 +17,7 @@ interface ClaudeTurn {
|
|
|
17
17
|
timer: NodeJS.Timeout;
|
|
18
18
|
resolveAcceptance: (at: string) => void;
|
|
19
19
|
rejectAcceptance: (error: Error) => void;
|
|
20
|
+
releaseTurn: () => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -33,6 +34,7 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
33
34
|
private stderrTail = '';
|
|
34
35
|
private sessionId = '';
|
|
35
36
|
private turns: ClaudeTurn[] = [];
|
|
37
|
+
private turnDone: Promise<void> = Promise.resolve();
|
|
36
38
|
private closing = false;
|
|
37
39
|
private ready = false;
|
|
38
40
|
private fatalError: Error | null = null;
|
|
@@ -123,8 +125,20 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
async accept(prompt: string, envelope: BusEnvelope): Promise<NativeAcceptance> {
|
|
128
|
+
// Claude's stream-json transport acknowledges input before the model has
|
|
129
|
+
// finished its turn. Serialize here, as the Codex/OpenCode/Kimi connectors
|
|
130
|
+
// already do, so a busy seat applies durable local backpressure instead of
|
|
131
|
+
// starting an unbounded number of concurrent model turns.
|
|
132
|
+
const previousTurn = this.turnDone;
|
|
133
|
+
let releaseTurn: () => void = () => {};
|
|
134
|
+
const currentTurn = new Promise<void>((resolve) => {
|
|
135
|
+
releaseTurn = resolve;
|
|
136
|
+
});
|
|
137
|
+
this.turnDone = previousTurn.then(() => currentTurn);
|
|
138
|
+
await previousTurn;
|
|
126
139
|
const child = this.child;
|
|
127
140
|
if (!this.ready || !child?.stdin?.writable) {
|
|
141
|
+
releaseTurn();
|
|
128
142
|
throw this.fatalError || new Error('Claude stream connector is not ready');
|
|
129
143
|
}
|
|
130
144
|
const uuid = randomUUID();
|
|
@@ -151,6 +165,7 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
151
165
|
}, acknowledgementTimeoutMs()),
|
|
152
166
|
resolveAcceptance,
|
|
153
167
|
rejectAcceptance,
|
|
168
|
+
releaseTurn,
|
|
154
169
|
};
|
|
155
170
|
this.turns.push(turn);
|
|
156
171
|
const input = {
|
|
@@ -184,6 +199,7 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
184
199
|
|
|
185
200
|
async close(): Promise<void> {
|
|
186
201
|
this.closing = true;
|
|
202
|
+
this.ready = false;
|
|
187
203
|
const child = this.child;
|
|
188
204
|
this.child = null;
|
|
189
205
|
for (const turn of this.turns) {
|
|
@@ -326,6 +342,7 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
326
342
|
turn.completed = true;
|
|
327
343
|
clearTimeout(turn.timer);
|
|
328
344
|
this.turns = this.turns.filter((candidate) => candidate !== turn);
|
|
345
|
+
turn.releaseTurn();
|
|
329
346
|
}
|
|
330
347
|
|
|
331
348
|
private currentTurn(): ClaudeTurn | undefined {
|
|
@@ -337,6 +354,7 @@ export class ClaudeConnector implements RuntimeConnector {
|
|
|
337
354
|
if (!turn.acknowledged) turn.rejectAcceptance(error);
|
|
338
355
|
turn.completed = true;
|
|
339
356
|
this.turns = this.turns.filter((candidate) => candidate !== turn);
|
|
357
|
+
turn.releaseTurn();
|
|
340
358
|
}
|
|
341
359
|
|
|
342
360
|
private errorOutput(chunk: string): void {
|
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import { delimiter, isAbsolute, resolve } from 'path';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One top-level string out of the owner's `~/.codex/config.toml`.
|
|
9
|
+
*
|
|
10
|
+
* Not a TOML parser, and not pretending to be one: it reads `key = "value"`
|
|
11
|
+
* lines before the first `[table]` and nothing else, which is exactly where
|
|
12
|
+
* `model`, `model_reasoning_effort` and `approval_policy` live. Anything it
|
|
13
|
+
* cannot read confidently comes back empty, and empty means "we did not find
|
|
14
|
+
* a preference", never "they have none" — the difference matters, because the
|
|
15
|
+
* caller uses it to decide whether to impose a default.
|
|
16
|
+
*/
|
|
17
|
+
const LEIDO = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
export function ownerCodexSetting(key: string): string {
|
|
20
|
+
const path = process.env.CODEX_HOME
|
|
21
|
+
? join(process.env.CODEX_HOME, 'config.toml')
|
|
22
|
+
: join(homedir(), '.codex', 'config.toml');
|
|
23
|
+
// Remembered for the life of the gateway. This is consulted on every accepted
|
|
24
|
+
// turn, and it is a setting the app-server was already started with: reading
|
|
25
|
+
// and splitting the whole file per turn bought nothing it did not already
|
|
26
|
+
// have.
|
|
27
|
+
const recordado = LEIDO.get(`${path}\u0000${key}`);
|
|
28
|
+
if (recordado !== undefined) return recordado;
|
|
29
|
+
const valor = _leeAjuste(path, key);
|
|
30
|
+
LEIDO.set(`${path}\u0000${key}`, valor);
|
|
31
|
+
return valor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _leeAjuste(path: string, key: string): string {
|
|
35
|
+
let text: string;
|
|
36
|
+
try {
|
|
37
|
+
text = readFileSync(path, 'utf8');
|
|
38
|
+
} catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
for (const raw of text.split('\n')) {
|
|
42
|
+
const line = raw.trim();
|
|
43
|
+
if (line.startsWith('[')) break; // past the top level
|
|
44
|
+
if (!line || line.startsWith('#')) continue;
|
|
45
|
+
const equals = line.indexOf('=');
|
|
46
|
+
if (equals < 0) continue;
|
|
47
|
+
if (line.slice(0, equals).trim() !== key) continue;
|
|
48
|
+
const value = line.slice(equals + 1).trim();
|
|
49
|
+
const quoted = /^"([^"]*)"$/.exec(value) || /^'([^']*)'$/.exec(value);
|
|
50
|
+
return quoted ? quoted[1] : value;
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
4
54
|
|
|
5
55
|
interface McpEntry {
|
|
6
56
|
name?: unknown;
|
|
@@ -4,6 +4,8 @@ import { resolve } from 'path';
|
|
|
4
4
|
import { BusEnvelope } from '../protocol.js';
|
|
5
5
|
import { NativeAcceptance } from '../runtime-metrics.js';
|
|
6
6
|
import { executableFor, hasOption, optionValue } from './command.js';
|
|
7
|
+
import { camello, trato } from './arnes.js';
|
|
8
|
+
import { ownerCodexSetting } from './codex-config.js';
|
|
7
9
|
import { WebSocketJsonRpc } from './json-rpc.js';
|
|
8
10
|
import { freeLoopbackPort, spawnNative, terminate, wait } from './process.js';
|
|
9
11
|
import { ConnectorOptions, NativeUiCommand, RuntimeActivity, RuntimeConnector } from './types.js';
|
|
@@ -189,10 +191,7 @@ export class CodexConnector implements RuntimeConnector {
|
|
|
189
191
|
clientUserMessageId: envelope.id,
|
|
190
192
|
...(model ? { model } : {}),
|
|
191
193
|
...(effort ? { effort } : {}),
|
|
192
|
-
|
|
193
|
-
// yolo mode. `never` means "reject anything needing approval", which
|
|
194
|
-
// silently disabled app/MCP tools even when the owner chose yolo.
|
|
195
|
-
approvalPolicy: 'on-request',
|
|
194
|
+
approvalPolicy: this.approvalPolicy(),
|
|
196
195
|
// Same decoupling as thread/start: yolo keeps its speed (no
|
|
197
196
|
// approvals, network open) while writes stay inside the workspace.
|
|
198
197
|
// CITY_CAGE=0 restores the old fully-open behaviour, deliberately.
|
|
@@ -380,17 +379,37 @@ export class CodexConnector implements RuntimeConnector {
|
|
|
380
379
|
const model = optionValue(this.options.command, ['--model', '-m']);
|
|
381
380
|
return {
|
|
382
381
|
...(model ? { model } : {}),
|
|
383
|
-
approvalPolicy:
|
|
382
|
+
approvalPolicy: this.approvalPolicy(),
|
|
384
383
|
// Approval and confinement are different axes: auto-approve means "do
|
|
385
384
|
// not ask", never "touch everything". Writes stay inside the workspace
|
|
386
385
|
// unless the owner explicitly lowers the cage with CITY_CAGE=0.
|
|
387
|
-
sandbox: this.providerSandboxIsOuterCaged()
|
|
388
|
-
|
|
389
|
-
'
|
|
390
|
-
|
|
386
|
+
sandbox: this.providerSandboxIsOuterCaged()
|
|
387
|
+
? String(trato('codex', 'sandbox').alterno)
|
|
388
|
+
: trato('codex', 'sandbox').valor,
|
|
389
|
+
developerInstructions: trato('codex', 'developerInstructions').valor,
|
|
391
390
|
};
|
|
392
391
|
}
|
|
393
392
|
|
|
393
|
+
/**
|
|
394
|
+
* How this Codex asks for permission.
|
|
395
|
+
*
|
|
396
|
+
* Theirs wins. `on-request` is only what we fall back to when their config
|
|
397
|
+
* says nothing — and it is what the declaration says it is, so the doctor's
|
|
398
|
+
* report cannot claim one thing while this claims another.
|
|
399
|
+
*
|
|
400
|
+
* Their `never` is honoured even though it rejects anything needing approval
|
|
401
|
+
* and so disables app and MCP tools: that is their machine and their choice.
|
|
402
|
+
* `doctor --config` says the consequence out loud rather than this quietly
|
|
403
|
+
* deciding they did not mean it.
|
|
404
|
+
*/
|
|
405
|
+
private approvalPolicy(): string {
|
|
406
|
+
const declarado = trato('codex', 'approvalPolicy');
|
|
407
|
+
// Their key is named by the declaration too — `suyo` is precisely "the
|
|
408
|
+
// setting of theirs this one defers to", and naming it twice is how the
|
|
409
|
+
// two stop meaning the same thing.
|
|
410
|
+
return ownerCodexSetting(String(declarado.suyo)) || declarado.valor;
|
|
411
|
+
}
|
|
412
|
+
|
|
394
413
|
private autoApprove(): boolean {
|
|
395
414
|
return (
|
|
396
415
|
this.options.autoApprove ||
|
|
@@ -403,9 +422,10 @@ export class CodexConnector implements RuntimeConnector {
|
|
|
403
422
|
}
|
|
404
423
|
|
|
405
424
|
private sandboxPolicy(): Record<string, unknown> {
|
|
406
|
-
if (this.providerSandboxIsOuterCaged())
|
|
425
|
+
if (this.providerSandboxIsOuterCaged())
|
|
426
|
+
return { type: camello(String(trato('codex', 'sandbox').alterno)) };
|
|
407
427
|
return {
|
|
408
|
-
type: '
|
|
428
|
+
type: camello(trato('codex', 'sandbox').valor),
|
|
409
429
|
networkAccess: this.autoApprove(),
|
|
410
430
|
writableRoots: [],
|
|
411
431
|
};
|
|
@@ -3,6 +3,7 @@ import WebSocket from 'ws';
|
|
|
3
3
|
import { BusEnvelope, randomId } from '../protocol.js';
|
|
4
4
|
import { NativeAcceptance } from '../runtime-metrics.js';
|
|
5
5
|
import { executableFor, hasOption, optionValue } from './command.js';
|
|
6
|
+
import { trato } from './arnes.js';
|
|
6
7
|
import { freeLoopbackPort, spawnNative, terminate, wait, waitForHttp } from './process.js';
|
|
7
8
|
import { ConnectorOptions, RuntimeConnector } from './types.js';
|
|
8
9
|
|
|
@@ -59,9 +60,7 @@ export class KimiConnector implements RuntimeConnector {
|
|
|
59
60
|
agent_config: {
|
|
60
61
|
...(model ? { model } : {}),
|
|
61
62
|
permission_mode: this.permissionMode(),
|
|
62
|
-
system_prompt:
|
|
63
|
-
'You are one member of an Agents City committee. Never contact repo peers directly; ' +
|
|
64
|
-
'submit evidence through the agents-city committee CLI named in each assignment.',
|
|
63
|
+
system_prompt: trato('kimi', 'system_prompt').valor,
|
|
65
64
|
},
|
|
66
65
|
}),
|
|
67
66
|
}),
|
|
@@ -119,11 +118,14 @@ export class KimiConnector implements RuntimeConnector {
|
|
|
119
118
|
this.child = null;
|
|
120
119
|
}
|
|
121
120
|
|
|
121
|
+
/** The fallback is the declaration's, so `doctor --config` cannot promise one
|
|
122
|
+
* mode while this returns another — which it did: the declaration said `auto`
|
|
123
|
+
* and this said `manual`. */
|
|
122
124
|
private permissionMode(): 'auto' | 'yolo' | 'manual' {
|
|
123
125
|
if (hasOption(this.options.command, ['--auto'])) return 'auto';
|
|
124
126
|
if (this.options.autoApprove || hasOption(this.options.command, ['--yolo', '-y']))
|
|
125
127
|
return 'yolo';
|
|
126
|
-
return 'manual';
|
|
128
|
+
return trato('kimi', 'permission_mode').valor as 'auto' | 'yolo' | 'manual';
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
private auth(): Record<string, string> {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
fsyncSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'fs';
|
|
3
15
|
import { dirname, join } from 'path';
|
|
4
16
|
import { CityContext } from './city-config.js';
|
|
5
17
|
import { ActorCredential, HubEndpoint, safeSegment } from './protocol.js';
|
|
@@ -7,13 +19,35 @@ import { ActorCredential, HubEndpoint, safeSegment } from './protocol.js';
|
|
|
7
19
|
let counter = 0;
|
|
8
20
|
|
|
9
21
|
export function atomicJson(path: string, value: unknown): void {
|
|
10
|
-
|
|
22
|
+
const directory = dirname(path);
|
|
23
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
11
24
|
const tmp = `${path}.tmp-${process.pid}-${counter++}`;
|
|
12
|
-
writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
13
|
-
renameSync(tmp, path);
|
|
14
25
|
try {
|
|
26
|
+
const fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(fd, JSON.stringify(value, null, 2) + '\n');
|
|
29
|
+
fsyncSync(fd);
|
|
30
|
+
} finally {
|
|
31
|
+
closeSync(fd);
|
|
32
|
+
}
|
|
33
|
+
renameSync(tmp, path);
|
|
15
34
|
chmodSync(path, 0o600);
|
|
16
|
-
|
|
35
|
+
try {
|
|
36
|
+
const dirFd = openSync(directory, constants.O_RDONLY);
|
|
37
|
+
try {
|
|
38
|
+
fsyncSync(dirFd);
|
|
39
|
+
} finally {
|
|
40
|
+
closeSync(dirFd);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// The file itself is synced; a few filesystems do not fsync directories.
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
try {
|
|
47
|
+
unlinkSync(tmp);
|
|
48
|
+
} catch {}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
17
51
|
}
|
|
18
52
|
|
|
19
53
|
export function actorCredential(context: CityContext, actor: string): ActorCredential {
|