mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
feat(agent): make coordination routes opt-in and address review
Gate inspect, stage and submit behind the synced agentCoordinationEnabled setting (default OFF, App Settings > Cross-session features, zh-CN strings). While off, all three routes answer 403 FORBIDDEN naming the setting, checked before session lookup; the CLI verbs print it verbatim. Remember the attributed Codex rollout per session and re-verify only its head on later calls, rescanning only date directories not older than it for a /new conflict. The first walk reads 1 KiB prefixes first, so a store of 2,100+ unrelated rollouts still attributes. Answer unexpected inspection errors with 422 OPERATION_FAILED and log the session id and error code only. Read the guarded composer glyph and footer from the CLI registry: codex workDetect.promptGlyph and a new workDetect.composerFooterLine capability, compiled through compileVersionRegex. Codex footer effort words are one shared list with modelDetect.screenLine. Fix the wiki API reference link, frame the versioning policy entry as a case of the experimental rule, join the CLAUDE.md Agent CLI paragraph, and update the changeset, skill reference and architecture notes.
This commit is contained in:
1 parent
1a2c3be0ea
commit
c78635a3d3
32 files changed
+848
-124
No files matched your search
@@ -697,6 +697,9 @@
|
||||
'New sessions for this case land in this group': '此案例的新会话将放入此分组',
|
||||
'Could not save the default session group.': '无法保存默认会话分组。',
|
||||
'Remote auto-reconnect': '远程自动重连',
|
||||
'Experimental agent coordination': '实验性智能体协作',
|
||||
"Lets agents read another session's output in bounded pages and type a prompt into a local Codex composer, then press Enter once after checking it is unchanged (the inspect, stage and submit commands). Experimental: the commands and their HTTP routes may change in any release. Off by default; while off those routes refuse every request.":
|
||||
'允许智能体分页读取另一个会话的输出(每页有上限),并把提示词输入本地 Codex 输入框,确认未被改动后只按一次 Enter(即 inspect、stage 和 submit 命令)。实验性功能:这些命令及其 HTTP 路由可能在任何版本中变化。默认关闭;关闭时这些路由拒绝所有请求。',
|
||||
'Pin tab names on the CLI': '在 CLI 上固定标签名',
|
||||
"Launch every Claude session with its tab name, so the CLI's prompt label and /resume list match your tabs. Claude then stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. Applies from the next session start or respawn, no server restart needed.":
|
||||
'以标签名启动每个 Claude 会话,让 CLI 的提示框标签和 /resume 列表与你的标签一致。Claude 随后不再为这些会话生成自己的标题,在 Codeman 中的重命名也只保留到下一轮。从下一次会话启动或重新生成时生效,无需重启服务器。',
|
||||
|
||||
@@ -2178,6 +2178,13 @@
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsAgentInboxPersist"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row" data-search="experimental agent coordination inspect stage submit codeman agent guarded input">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Experimental agent coordination</span>
|
||||
<span class="set-row-desc">Lets agents read another session's output in bounded pages and type a prompt into a local Codex composer, then press Enter once after checking it is unchanged (the inspect, stage and submit commands). Experimental: the commands and their HTTP routes may change in any release. Off by default; while off those routes refuse every request.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsAgentCoordination"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -541,6 +541,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsApprovalsInbox').checked = settings.approvalsInboxEnabled === true;
|
||||
// Agent inbox snapshot on disk: synced, default OFF.
|
||||
document.getElementById('appSettingsAgentInboxPersist').checked = settings.agentInboxPersistEnabled === true;
|
||||
// Experimental agent coordination (inspect/stage/submit routes): synced, default OFF.
|
||||
document.getElementById('appSettingsAgentCoordination').checked = settings.agentCoordinationEnabled === true;
|
||||
// Custom Model Endpoint Profiles: synced, default OFF. The toggle governs both
|
||||
// the Run-menu picker's generated entries and this settings panel's visibility;
|
||||
// the endpoint list itself is server state, loaded on demand below.
|
||||
@@ -2929,6 +2931,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
configBackupDir: document.getElementById('appSettingsConfigBackupDir').value.trim(),
|
||||
...this._configBackupRetentionPayload(_prev),
|
||||
agentInboxPersistEnabled: document.getElementById('appSettingsAgentInboxPersist').checked,
|
||||
agentCoordinationEnabled: document.getElementById('appSettingsAgentCoordination').checked,
|
||||
readMyMindEnabled: document.getElementById('appSettingsReadMyMind').checked,
|
||||
ultracodeFloatingWindows: document.getElementById('appSettingsUltracodeFloatingWindows').checked,
|
||||
showMultiMonitorButton: document.getElementById('appSettingsShowMultiMonitorButton').checked,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
* @fileoverview Agent-facing bounded inspection and guarded stage/submit endpoints.
|
||||
* Ownership is checked before any disk or pane read. Native evidence comes only
|
||||
* from strict conversation attribution; all other modes get labeled snapshots.
|
||||
*
|
||||
* Experimental and opt-in: all three routes answer `403 FORBIDDEN`, naming the
|
||||
* setting, until the synced `agentCoordinationEnabled` is on (default OFF), the
|
||||
* same shape as `cliManagementEnabled` and `mcpSyncEnabled`. The gate runs first,
|
||||
* before session lookup, so a disabled install reveals nothing about its sessions.
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Session } from '../../session.js';
|
||||
@@ -10,10 +15,32 @@ import { getCli } from '../../config/cli-registry/registry.js';
|
||||
import { findExactCodexConversation, resolveEffectiveCodexHome } from '../../agent-codex-identity.js';
|
||||
import { InspectionReader, InspectionConflict, boundedUtf8Tail } from '../../agent-inspection.js';
|
||||
import { GuardedStageStore, GuardedInputConflict, type GuardedInputTarget } from '../../agent-staging.js';
|
||||
import { ApiErrorCode, createErrorResponse } from '../../types.js';
|
||||
import { findSessionOrFail, getAuthUser, parseBody } from '../route-helpers.js';
|
||||
import { ApiErrorCode, createErrorResponse, type ApiResponse } from '../../types.js';
|
||||
import { findSessionOrFail, getAuthUser, parseBody, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
|
||||
import { AgentInspectQuerySchema, AgentStageSchema, AgentSubmitSchema } from '../schemas.js';
|
||||
|
||||
/** Default OFF, same shape as `readMcpSyncEnabled`: read fresh so a toggle applies at once. */
|
||||
export async function readAgentCoordinationEnabled(): Promise<boolean> {
|
||||
const settings = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'settings.json', {});
|
||||
return settings.agentCoordinationEnabled === true;
|
||||
}
|
||||
|
||||
/** The refusal every route sends while the setting is off. Exported for the tests. */
|
||||
export const AGENT_COORDINATION_DISABLED_MESSAGE =
|
||||
'Experimental agent coordination (inspect, stage, submit) is disabled. Turn on "Experimental agent coordination" in Settings (agentCoordinationEnabled) and save first.';
|
||||
|
||||
async function gate(): Promise<ApiResponse<never> | null> {
|
||||
if (await readAgentCoordinationEnabled()) return null;
|
||||
return createErrorResponse(ApiErrorCode.FORBIDDEN, AGENT_COORDINATION_DISABLED_MESSAGE);
|
||||
}
|
||||
|
||||
/** A Node error code (`EACCES`, `EIO`, ...) or the error's class name; never its message. */
|
||||
export function errorCode(error: unknown): string {
|
||||
const code = (error as { code?: unknown } | null)?.code;
|
||||
if (typeof code === 'string' && /^[A-Z0-9_]{1,40}$/.test(code)) return code;
|
||||
return error instanceof Error ? error.constructor.name : 'unknown';
|
||||
}
|
||||
|
||||
export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPort & InfraPort): void {
|
||||
const reader = new InspectionReader();
|
||||
const stages = new GuardedStageStore();
|
||||
@@ -33,6 +60,8 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
submit: (expected) => session.submitGuardedInput(expected),
|
||||
});
|
||||
app.get('/api/sessions/:id/inspect', async (req, reply) => {
|
||||
const refused = await gate();
|
||||
if (refused) return reply.code(403).send(refused);
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
const { maxBytes, since } = parseBody(AgentInspectQuerySchema, req.query);
|
||||
@@ -99,19 +128,24 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
} catch (error) {
|
||||
if (error instanceof InspectionConflict)
|
||||
return reply.code(409).send(createErrorResponse(ApiErrorCode.CONFLICT, error.message));
|
||||
// Native paths and arbitrary IO errors must never escape into the response.
|
||||
return reply
|
||||
.code(409)
|
||||
.send(createErrorResponse(ApiErrorCode.CONFLICT, 'Inspection source unavailable; retry without a cursor'));
|
||||
// Anything else (an unreadable rollout, an IO error) is not a retryable cursor
|
||||
// conflict. Log the session and the error code only: the message can carry the
|
||||
// native transcript path, which must never reach the log or the response.
|
||||
console.warn(`[agent-inspect] session ${session.id}: inspection failed (${errorCode(error)})`);
|
||||
return reply.code(422).send(createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Inspection source unavailable'));
|
||||
}
|
||||
});
|
||||
app.post('/api/sessions/:id/stage', async (req, reply) => {
|
||||
const refused = await gate();
|
||||
if (refused) return reply.code(403).send(refused);
|
||||
const session = findSessionOrFail(ctx, (req.params as { id: string }).id, req);
|
||||
const { message } = parseBody(AgentStageSchema, req.body);
|
||||
try {
|
||||
const receipt = await stages.stage(target(session), getAuthUser(req).username, message);
|
||||
return { success: true, data: receipt };
|
||||
} catch (error) {
|
||||
if (!(error instanceof GuardedInputConflict))
|
||||
console.warn(`[agent-stage] session ${session.id}: delivery failed (${errorCode(error)})`);
|
||||
return reply
|
||||
.code(409)
|
||||
.send(
|
||||
@@ -125,6 +159,8 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
}
|
||||
});
|
||||
app.post('/api/sessions/:id/submit', async (req, reply) => {
|
||||
const refused = await gate();
|
||||
if (refused) return reply.code(403).send(refused);
|
||||
const session = findSessionOrFail(ctx, (req.params as { id: string }).id, req);
|
||||
const { stageId, confirmSha } = parseBody(AgentSubmitSchema, req.body);
|
||||
try {
|
||||
@@ -133,6 +169,8 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
data: await stages.submit(target(session), getAuthUser(req).username, stageId, confirmSha),
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof GuardedInputConflict))
|
||||
console.warn(`[agent-submit] session ${session.id}: delivery failed (${errorCode(error)})`);
|
||||
return reply
|
||||
.code(409)
|
||||
.send(
|
||||
|
||||
@@ -1544,6 +1544,13 @@ export const SettingsUpdateSchema = z
|
||||
* inboxes stay). On, pending messages between agents survive a server restart.
|
||||
*/
|
||||
agentInboxPersistEnabled: z.boolean().optional(),
|
||||
/**
|
||||
* Experimental agent coordination (src/web/routes/agent-session-routes.ts): bounded
|
||||
* inspection and the guarded stage/submit routes behind `codeman agent inspect|stage|submit`.
|
||||
* SYNCED, default OFF: the routes are labeled experimental and may change in a minor or
|
||||
* patch release, so automation has to opt in. While OFF, all three routes answer 403.
|
||||
*/
|
||||
agentCoordinationEnabled: z.boolean().optional(),
|
||||
/**
|
||||
* Read My Mind predictor model override. Empty/absent = the AI-checker
|
||||
* default (opus: prediction quality is the product and it runs only on an
|
||||
|
||||
Reference in new issue
Block a user