hilos-agent 0.11.5 → 0.11.7
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/package.json +1 -1
- package/src/acp-session.mjs +2 -0
- package/src/child-mcp.mjs +4 -1
- package/src/claude-permissions.mjs +4 -1
- package/src/codex-mcp-session.mjs +2 -0
- package/src/config.mjs +11 -0
- package/src/handler.mjs +15 -0
- package/src/permission-gate.mjs +64 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Run your own coding agent (Claude Code, Codex, Cursor, OpenCode, Hermes, or any command) as a teammate in a hilos room. The checkout and credentials stay local; changes go to your configured Git remote as a PR for human review, and bounded progress and reports go to hilos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/acp-session.mjs
CHANGED
|
@@ -215,6 +215,7 @@ export async function runAcpSession({
|
|
|
215
215
|
onEvent,
|
|
216
216
|
requestPermission,
|
|
217
217
|
getPermissionDecision,
|
|
218
|
+
localAllow = null,
|
|
218
219
|
/** Session to continue (0778); null starts a fresh one. */
|
|
219
220
|
resumeSessionId = null,
|
|
220
221
|
mcpServers = [],
|
|
@@ -333,6 +334,7 @@ export async function runAcpSession({
|
|
|
333
334
|
now,
|
|
334
335
|
log,
|
|
335
336
|
label: "acp permission",
|
|
337
|
+
localAllow,
|
|
336
338
|
});
|
|
337
339
|
const option = pickAcpPermissionOption(msg.params?.options, reply);
|
|
338
340
|
if (option && (reply !== "reject" || option.kind?.startsWith("reject"))) {
|
package/src/child-mcp.mjs
CHANGED
|
@@ -213,7 +213,10 @@ export function codexChildMcpArgs(url) {
|
|
|
213
213
|
*/
|
|
214
214
|
export function claudeChildMcpArgs(configPath) {
|
|
215
215
|
if (!configPath) return [];
|
|
216
|
-
|
|
216
|
+
// `--allowedTools` is variadic: written as two argv entries before the prompt it
|
|
217
|
+
// swallows the prompt as one more tool name (seen live on 1198 step 6). The
|
|
218
|
+
// `=` form binds the value to the flag and leaves the prompt alone.
|
|
219
|
+
return ["--mcp-config", configPath, `--allowedTools=${CLAUDE_CHILD_MCP_TOOL_PATTERN}`];
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
/**
|
|
@@ -179,7 +179,8 @@ export function claudePermissionArgs({
|
|
|
179
179
|
configPath,
|
|
180
180
|
"--permission-prompt-tool",
|
|
181
181
|
toolId,
|
|
182
|
-
|
|
182
|
+
// The `=` form: `--allowedTools` is variadic and would swallow the prompt (1255).
|
|
183
|
+
...(allowedTools ? [`--allowedTools=${allowedTools}`] : []),
|
|
183
184
|
];
|
|
184
185
|
}
|
|
185
186
|
|
|
@@ -227,6 +228,7 @@ export async function startClaudePermissionServer({
|
|
|
227
228
|
// file to delete; a second config would be a second lifetime to get wrong.
|
|
228
229
|
extraServers = null,
|
|
229
230
|
allowedTools = "",
|
|
231
|
+
localAllow = null,
|
|
230
232
|
}) {
|
|
231
233
|
if (typeof requestPermission !== "function" || typeof getPermissionDecision !== "function") {
|
|
232
234
|
throw new Error("claude permission server requires requestPermission and getPermissionDecision");
|
|
@@ -389,6 +391,7 @@ export async function startClaudePermissionServer({
|
|
|
389
391
|
pollIntervalMs,
|
|
390
392
|
log,
|
|
391
393
|
label: "claude permission",
|
|
394
|
+
localAllow,
|
|
392
395
|
});
|
|
393
396
|
decisionReply = resolved.reply;
|
|
394
397
|
outcome = resolved.outcome;
|
|
@@ -186,6 +186,7 @@ export async function runCodexMcpSession({
|
|
|
186
186
|
onEvent,
|
|
187
187
|
requestPermission,
|
|
188
188
|
getPermissionDecision,
|
|
189
|
+
localAllow = null,
|
|
189
190
|
beforeSpawn,
|
|
190
191
|
spawnImpl = spawn,
|
|
191
192
|
sleep,
|
|
@@ -316,6 +317,7 @@ export async function runCodexMcpSession({
|
|
|
316
317
|
now,
|
|
317
318
|
log,
|
|
318
319
|
label: "codex permission",
|
|
320
|
+
localAllow,
|
|
319
321
|
});
|
|
320
322
|
respond(msg.id, { decision: mapCodexDecision(reply, available) });
|
|
321
323
|
}
|
package/src/config.mjs
CHANGED
|
@@ -118,6 +118,11 @@ const DEFAULTS = {
|
|
|
118
118
|
// ON by default. `childMcp: false` here turns it off; HILOS_CHILD_MCP=off (or
|
|
119
119
|
// =on) does the same from the environment and wins over the file.
|
|
120
120
|
childMcp: true,
|
|
121
|
+
// 1256 — edits inside the run's own checkout are allowed on the daemon and
|
|
122
|
+
// never raise a permission card: the pull request is the gate. `true` here
|
|
123
|
+
// (or HILOS_GATE_REPO_EDITS=1) restores a card per file for rooms that
|
|
124
|
+
// want it.
|
|
125
|
+
gateRepoEdits: false,
|
|
121
126
|
// Replies in an exact thread bound by an opted-in local-session hook resume
|
|
122
127
|
// that same provider session (0847). The hook install is the consent gate;
|
|
123
128
|
// this switch lets an operator pause inbound pickup without uninstalling it.
|
|
@@ -234,6 +239,12 @@ export function resolveConfig({ flags = {}, join: joinPayload } = {}) {
|
|
|
234
239
|
: /^(0|off|false|no)$/i.test(process.env.HILOS_CHILD_MCP || "")
|
|
235
240
|
? false
|
|
236
241
|
: undefined,
|
|
242
|
+
gateRepoEdits:
|
|
243
|
+
/^(1|on|true|yes)$/i.test(process.env.HILOS_GATE_REPO_EDITS || "")
|
|
244
|
+
? true
|
|
245
|
+
: /^(0|off|false|no)$/i.test(process.env.HILOS_GATE_REPO_EDITS || "")
|
|
246
|
+
? false
|
|
247
|
+
: undefined,
|
|
237
248
|
};
|
|
238
249
|
const merged = { ...DEFAULTS, ...file };
|
|
239
250
|
for (const [k, v] of Object.entries(env)) if (v !== undefined && v !== "") merged[k] = v;
|
package/src/handler.mjs
CHANGED
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
startChildHilosMcp,
|
|
105
105
|
} from "./child-mcp.mjs";
|
|
106
106
|
import { createUngatedRunNotice } from "./permission-gate.mjs";
|
|
107
|
+
import { repoEditAllowance } from "./permission-gate.mjs";
|
|
107
108
|
import { webMcpAgentPrompt } from "./webmcp-bridge.mjs";
|
|
108
109
|
import {
|
|
109
110
|
conflictCommitTitle,
|
|
@@ -361,8 +362,14 @@ function openCodePermissionCallbacks({
|
|
|
361
362
|
// decides, instead of the gate discovering the decision on its next 1s poll.
|
|
362
363
|
// 0 (older servers) keeps the plain immediate read.
|
|
363
364
|
decisionWaitMs = 0,
|
|
365
|
+
// 1256 — the checkout this run edits; an edit under it is allowed on the
|
|
366
|
+
// daemon without a card (the pull request is the gate). Empty means "no
|
|
367
|
+
// local allowance": every ask still reaches the room.
|
|
368
|
+
repoRoot = "",
|
|
369
|
+
gateRepoEdits = false,
|
|
364
370
|
}) {
|
|
365
371
|
return {
|
|
372
|
+
localAllow: (request) => repoEditAllowance(request, { repoRoot, gateRepoEdits }),
|
|
366
373
|
requestPermission: async (request) => {
|
|
367
374
|
const detail =
|
|
368
375
|
request.metadata && typeof request.metadata === "object"
|
|
@@ -2938,6 +2945,8 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2938
2945
|
threadRoot,
|
|
2939
2946
|
decisionWaitMs: caps.decisionWaitMs,
|
|
2940
2947
|
provider: vendor,
|
|
2948
|
+
repoRoot: folderPath,
|
|
2949
|
+
gateRepoEdits: cfg.gateRepoEdits === true,
|
|
2941
2950
|
}),
|
|
2942
2951
|
});
|
|
2943
2952
|
} else if (gateClaudePermissions) {
|
|
@@ -2959,6 +2968,8 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2959
2968
|
threadRoot,
|
|
2960
2969
|
decisionWaitMs: caps.decisionWaitMs,
|
|
2961
2970
|
provider: vendor,
|
|
2971
|
+
repoRoot: folderPath,
|
|
2972
|
+
gateRepoEdits: cfg.gateRepoEdits === true,
|
|
2962
2973
|
}),
|
|
2963
2974
|
});
|
|
2964
2975
|
} else {
|
|
@@ -4598,6 +4609,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {}, iter
|
|
|
4598
4609
|
decisionWaitMs: caps.decisionWaitMs,
|
|
4599
4610
|
runId,
|
|
4600
4611
|
provider: vendor,
|
|
4612
|
+
repoRoot: repoPath,
|
|
4613
|
+
gateRepoEdits: cfg.gateRepoEdits === true,
|
|
4601
4614
|
}),
|
|
4602
4615
|
});
|
|
4603
4616
|
} else if (gateClaudePermissions) {
|
|
@@ -4624,6 +4637,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {}, iter
|
|
|
4624
4637
|
decisionWaitMs: caps.decisionWaitMs,
|
|
4625
4638
|
runId,
|
|
4626
4639
|
provider: vendor,
|
|
4640
|
+
repoRoot: repoPath,
|
|
4641
|
+
gateRepoEdits: cfg.gateRepoEdits === true,
|
|
4627
4642
|
}),
|
|
4628
4643
|
});
|
|
4629
4644
|
} else {
|
package/src/permission-gate.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the same block, the same timeout" a mechanical fact instead of three
|
|
14
14
|
// hand-copied implementations that can drift apart.
|
|
15
15
|
|
|
16
|
+
import path from "node:path";
|
|
16
17
|
import { mapOpenCodePermissionDecision } from "./opencode-permissions.mjs";
|
|
17
18
|
|
|
18
19
|
export const DEFAULT_PERMISSION_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -136,6 +137,59 @@ function withDeadline(promise, { signal, deadlineAt, now, label }) {
|
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Edits inside the run's own checkout never raise a card (1256).
|
|
142
|
+
*
|
|
143
|
+
* The room asked the agent to change this repository, the daemon commits the
|
|
144
|
+
* result to a branch, and a person decides at the pull request. Asking that
|
|
145
|
+
* same person to approve every file the agent touches on the way is the
|
|
146
|
+
* ten-minute silence seen live on 1198 step 6: a permission card for the very
|
|
147
|
+
* README the ask named, and nobody there to click it. So a file edit whose
|
|
148
|
+
* every path resolves under the repo root (and outside `.git`) is allowed
|
|
149
|
+
* here, on the daemon, before hilos is asked. Everything else keeps the gate:
|
|
150
|
+
* shell commands, network, writes outside the tree, and any request whose
|
|
151
|
+
* paths the daemon cannot see. `gateRepoEdits: true` restores the old asking.
|
|
152
|
+
*/
|
|
153
|
+
export const REPO_EDIT_ACTIONS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "edit"]);
|
|
154
|
+
|
|
155
|
+
function requestPaths(request) {
|
|
156
|
+
const out = [];
|
|
157
|
+
const meta = request?.metadata && typeof request.metadata === "object" ? request.metadata : {};
|
|
158
|
+
if (typeof meta.path === "string" && meta.path) out.push(meta.path);
|
|
159
|
+
for (const r of Array.isArray(request?.resources) ? request.resources : []) {
|
|
160
|
+
if (typeof r === "string" && r.startsWith("/")) out.push(r);
|
|
161
|
+
}
|
|
162
|
+
// The same file rides in both `metadata.path` and `resources`; one file is one file.
|
|
163
|
+
return [...new Set(out)];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function underRoot(candidate, repoRoot) {
|
|
167
|
+
const resolved = path.resolve(repoRoot, candidate);
|
|
168
|
+
const rel = path.relative(repoRoot, resolved);
|
|
169
|
+
if (!rel || rel === "") return false;
|
|
170
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
171
|
+
if (rel === ".git" || rel.startsWith(".git" + path.sep)) return false;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function repoEditAllowance(request, { repoRoot = "", gateRepoEdits = false } = {}) {
|
|
176
|
+
if (gateRepoEdits || !repoRoot || !request) return null;
|
|
177
|
+
if (!REPO_EDIT_ACTIONS.has(String(request.action || ""))) return null;
|
|
178
|
+
const root = path.resolve(repoRoot);
|
|
179
|
+
const meta = request.metadata && typeof request.metadata === "object" ? request.metadata : {};
|
|
180
|
+
const paths = requestPaths(request);
|
|
181
|
+
if (paths.length) {
|
|
182
|
+
if (!paths.every((p) => underRoot(p, root))) return null;
|
|
183
|
+
return { reason: `edit inside the repository (${paths.length === 1 ? path.relative(root, path.resolve(root, paths[0])) : paths.length + " files"})` };
|
|
184
|
+
}
|
|
185
|
+
// Codex patch approvals name no files, only the directory the patch applies
|
|
186
|
+
// in; a patch applied inside the checkout is the same edit.
|
|
187
|
+
if (typeof meta.cwd === "string" && meta.cwd && (path.resolve(meta.cwd) === root || underRoot(meta.cwd, root))) {
|
|
188
|
+
return { reason: "patch applied inside the repository" };
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
139
193
|
/**
|
|
140
194
|
* Raise one permission card and block until a human decides it.
|
|
141
195
|
*
|
|
@@ -153,8 +207,9 @@ function withDeadline(promise, { signal, deadlineAt, now, label }) {
|
|
|
153
207
|
* settlementTimeoutMs?: number,
|
|
154
208
|
* sleep?: (ms: number) => Promise<void>,
|
|
155
209
|
* now?: () => number,
|
|
156
|
-
* log?: { error?: (message: string) => void },
|
|
210
|
+
* log?: { error?: (message: string) => void, info?: (message: string) => void },
|
|
157
211
|
* label?: string,
|
|
212
|
+
* localAllow?: ((request: Record<string, unknown>) => { reason?: string } | null) | null,
|
|
158
213
|
* }} options
|
|
159
214
|
* @returns {Promise<{ reply: "once"|"always"|"reject", outcome: string, handle: unknown }>}
|
|
160
215
|
*/
|
|
@@ -171,10 +226,18 @@ export async function resolveHilosPermissionReply({
|
|
|
171
226
|
now = () => Date.now(),
|
|
172
227
|
log,
|
|
173
228
|
label = "permission",
|
|
229
|
+
localAllow = null,
|
|
174
230
|
}) {
|
|
175
231
|
if (typeof requestPermission !== "function" || typeof getPermissionDecision !== "function") {
|
|
176
232
|
throw new Error("permission gate requires requestPermission and getPermissionDecision");
|
|
177
233
|
}
|
|
234
|
+
// 1256 — a daemon-side allowance answers before hilos is asked: no card, no
|
|
235
|
+
// wait, one log line so the run's exhaust still says what happened.
|
|
236
|
+
const local = typeof localAllow === "function" ? localAllow(request) : null;
|
|
237
|
+
if (local && typeof local === "object") {
|
|
238
|
+
log?.info?.(`${label}: allowed on the daemon, ${local.reason || "local rule"}`);
|
|
239
|
+
return { reply: "once", outcome: "local-allow", handle: null, byPolicy: false, policyReason: null, localReason: local.reason || null };
|
|
240
|
+
}
|
|
178
241
|
const deadline = deadlineAt ?? now() + Math.max(1, timeoutMs || DEFAULT_PERMISSION_TIMEOUT_MS);
|
|
179
242
|
let handle = null;
|
|
180
243
|
let reply = "reject";
|