Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/architecture/gitoxide-managed-product-composition-v1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Gitoxide managed product composition v1

## 主要不变量

只有显式选择 `managed-coding-v1` 的 Session,才能使用 packaged Gitoxide helper 打开 managed epoch,且该
profile 的工具上限只有 Write/Edit。普通 Session 不进入该数据面;helper 缺失或身份不匹配时,managed Session
在 provider dispatch 前失败,禁止从 PATH 发现 Git、回退 attached checkout 或改走普通文件工具。

## Owner 与权限

- product release owner 生成 helper binary、strict manifest 和锁定 Cargo graph 的第三方 notices;
- packaged-resource resolver 只接受 `process.resourcesPath` 内的固定路径,并绑定 platform、arch、bytes、SHA-256、
protocol version 与完整 operation allowlist;
- artifact authority 将 release claim 转为 Runtime Host 私有的 invocation capability;调用者不能提交 executable path;
- managed session owner 从 source root 与 Session ID 派生 durable identity,并向 ToolRuntime 提供 owner-bound
`admitManagedMutation`;
- ToolRuntime 保持 provider result 的唯一 owner,Gitoxide owner 只提交 terminal proof 与 durable successor。

外层已签名应用/安装包是 v1 的发布信任根。本合同防止误打包、manifest/helper 漂移、PATH 注入和普通 Session
误入 managed 数据面;不把本机恶意管理员或可任意改写完整安装目录的进程纳入密码学攻击模型。

## 原子性边界

1. Session header 在任何 T1 以前持久化 `managed-coding-v1`;
2. backend creation 在 provider dispatch 前要求 packaged helper capability,并打开 exact source epoch;
3. Write/Edit tool 在 T1 前取得 managed admission;
4. T1 后只允许 managed mutation state machine 提交 terminal outcome,禁止 generic T2 fallback;
5. Git candidate、SQLite successor 与 accepted ref projection 继续由下层 owner 按既有恢复协议收敛。

package build 的边界是:锁定 Cargo build成功后,复制到 `.generated`、计算 manifest、生成 notices,最后才允许
electron-builder 读取这些资源。最终包验证必须要求 binary、manifest 与 notices 同时存在。

## 失败状态与回滚

| 状态 | 行为 |
| --- | --- |
| dev/CLI Host 没有 packaged helper | 普通 Session 正常;managed Session 明确 `managed_workspace_profile_unavailable` |
| manifest 缺字段、operation list 漂移 | Host 不授予 capability |
| helper bytes/path/platform/arch 不匹配 | fail closed;不从 PATH 替代 |
| source admission/import 失败 | provider dispatch 前失败,不产生 managed T1 |
| T1 后进程退出 | 下层 SQLite/Gitoxide owner 按 durable evidence 恢复,不重跑 Write/Edit |

回滚本切片会删除 `managed-coding-v1` 产品入口和 packaged resource composition;现有普通 Session、SQLite 数据与
attached execution 行为不需要迁移。

## 平台能力矩阵

Linux、macOS、Windows 使用相同 manifest、artifact digest、operation allowlist 与 short-lived helper protocol。
正式 Desktop 当前打包 Windows x64 与 macOS arm64 helper;Linux workflow 继续证明 helper/owner 合同,但本切片
不新增 Linux Desktop 发布物。进程崩溃恢复由三平台 helper workflow 证明;硬件断电与恶意本机管理员不在 v1
新增承诺中。

## 非目标

- 不接入 npm dependency environment;纯 Write/Edit 不依赖 npm。
- 不向 managed profile 暴露 Bash、Read、Glob、Grep 或 attached filesystem worker。
- 不自动迁移普通 Session,也不把 managed capability 当作 resident Host 的隐式 fallback。
6 changes: 5 additions & 1 deletion packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,11 @@ export function isTurnStatus(value: unknown): value is TurnStatus {
// Header (JSONL line 1)
// ============================================================================

export const SESSION_TOOL_PROFILES = ['headless-coding-v1', 'workhub-coordination-v1'] as const;
export const SESSION_TOOL_PROFILES = [
'headless-coding-v1',
'managed-coding-v1',
'workhub-coordination-v1',
] as const;
export type SessionToolProfile = (typeof SESSION_TOOL_PROFILES)[number];

export function isSessionToolProfile(value: unknown): value is SessionToolProfile {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,35 @@ test('hosted execution freezes the headless coding provider wire contract', asyn
);
assert.equal(stableHash(profiled.body.tools), HEADLESS_CODING_V1_TOOLS_HASH);
}

const requestCountBeforeManagedAdmission = provider.requests.length;
const managedOutcome = await composition.handlers['hosted.execution.start'](
{
executionId: '00000000-0000-4000-8000-000000000779',
session: {
workspace: { kind: 'host_path', path: root },
modelTarget: {
kind: 'explicit',
connectionSlug: 'profile-deepseek',
model: 'deepseek-v4-flash',
},
permissionMode: 'bypass',
collaborationMode: 'agent',
orchestrationMode: 'default',
toolProfile: 'managed-coding-v1',
},
content: { text: 'Modify the managed workspace.' },
},
context,
);
assert.equal(managedOutcome.ok, true);
if (!managedOutcome.ok || managedOutcome.result.kind !== 'settled') return;
assert.equal(managedOutcome.result.status, 'failed');
assert.match(
managedOutcome.result.failureReason ?? '',
/managed_workspace_profile_unavailable/u,
);
assert.equal(provider.requests.length, requestCountBeforeManagedAdmission);
} finally {
try {
await composition?.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ test('hosted execution tool profiles are durable Session creation inputs', () =>
content: { text: 'solve' },
});
assert.equal(decoded.session.toolProfile, 'headless-coding-v1');
assert.equal(
decodeHostedExecutionStartInput({
...decoded,
session: { ...decoded.session, toolProfile: 'managed-coding-v1' },
}).session.toolProfile,
'managed-coding-v1',
);
assert.throws(
() =>
decodeHostedExecutionStartInput({
Expand Down Expand Up @@ -134,3 +141,30 @@ test('the WorkHub coordination profile has conversational authority but zero too
};
assert.deepEqual(projectHostedExecutionTools([productTool], 'workhub-coordination-v1'), []);
});

test('the managed coding profile exposes only owner-settled Write and Edit', () => {
const profile = hostedExecutionRunProfile('managed-coding-v1');
assert.ok(profile);
assert.deepEqual(profile.toolNames, ['Write', 'Edit']);
assert.equal(profile.memoryExtraction, false);
assert.match(profile.systemPrompt, /managed Git workspace/u);
assert.match(profile.systemPrompt, /Write and Edit/u);

const tools = ['Read', 'Write', 'Edit', 'Bash'].map(
(name): MakaTool => ({
name,
description: name,
parameters: z.object({}),
impl: async () => 'not used by managed mutation execution',
}),
);
const selected = projectHostedExecutionTools(tools, 'managed-coding-v1');
assert.deepEqual(
selected.map(({ name }) => name),
['Write', 'Edit'],
);
for (const tool of selected) {
assert.equal(tool.recoveryMode, 'reconcile');
assert.equal(tool.durableExecutionProfile, 'managed_mutation_v1');
}
});
46 changes: 43 additions & 3 deletions packages/runtime-host/src/server/execution-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ import { HostNetworkProxyCoordinator } from './network-proxy-coordinator.js';
import { HostOAuthExecutionAuthority } from './oauth-execution-authority.js';
import { HostOAuthCoordinator, type HostOAuthCoordinatorInput } from './oauth-coordinator.js';
import { HostPlanCoordinator } from './plan-coordinator.js';
import { openGitoxideManagedSessionOwnerInternal } from './gitoxide-managed-session-owner-internal.js';
import {
PackagedGitoxideHelperError,
resolvePackagedGitoxideHelperInternal,
} from './packaged-gitoxide-helper-internal.js';
import {
HostProjectDirectoryAuthority,
type PublishedProjectDirectoryRoot,
Expand Down Expand Up @@ -226,6 +231,18 @@ export async function createExecutionRuntimeHostComposition(
},
});
const stores = storage.execution;
const gitoxideInvocationOwnerToken = {};
const gitoxideHelperCapability = await resolvePackagedGitoxideHelperInternal({
invocationOwnerToken: gitoxideInvocationOwnerToken,
}).catch((error: unknown) => {
if (
error instanceof PackagedGitoxideHelperError &&
error.code === 'packaged_gitoxide_helper_unavailable'
) {
return undefined;
}
throw error;
});
let graphControlStore: ReturnType<typeof createAgentGraphControlStore> | undefined;
let graphClient: HostAgentGraphCoordinator | undefined;
let sessionEffects: HostSessionEffectCoordinator | undefined;
Expand Down Expand Up @@ -630,8 +647,27 @@ export async function createExecutionRuntimeHostComposition(
backends.register(
'ai-sdk',
dependencies.primaryBackendFactory ??
((backendContext) =>
createHostAiSdkBackend({
(async (backendContext) => {
const managedSession =
backendContext.header.toolProfile === 'managed-coding-v1'
? await (async () => {
if (!gitoxideHelperCapability) {
throw new Error(
'managed_workspace_profile_unavailable: packaged Gitoxide helper authority is unavailable',
);
}
return openGitoxideManagedSessionOwnerInternal({
storageRootLease: context.owner.lease,
stores,
invocationOwnerToken: gitoxideInvocationOwnerToken,
helperCapability: gitoxideHelperCapability,
sourceRoot: backendContext.header.cwd,
sessionId: backendContext.sessionId,
abortSignal: backendContext.abortSignal,
});
})()
: undefined;
return createHostAiSdkBackend({
context: backendContext,
runtimePolicy: runtimePolicyStores,
oauthCredentials,
Expand Down Expand Up @@ -669,8 +705,12 @@ export async function createExecutionRuntimeHostComposition(
backendContext.sessionId,
),
runtimeCommitSink: stores.runtimeEventStore,
...(managedSession
? { admitManagedMutation: managedSession.writeEdit.admitManagedMutation }
: {}),
requestDrain: context.requestDrain,
})),
});
}),
);
const runtimeAuthority: RuntimeHostedRootAuthority = {
bindRun: (identity) => messages.bindRun(identity),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { stableHash, toolCatalogHash } from '@maka/runtime/request-shape';
import { toolAvailabilityHash } from '@maka/runtime/tool-availability';
import { type BackendFactoryContext } from '@maka/runtime/session-manager';
import { type RuntimeCommitSink } from '@maka/runtime/runtime-commit-sink';
import type { ToolRuntimeInput } from '@maka/runtime/tool-runtime';
import type { SandboxDiagnosticsProvider } from '@maka/runtime/sandbox';
import {
createAttachmentByteReader,
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface HostAiSdkBackendInput {
readonly usage: HostExecutionUsageAuthority;
readonly requestDrain: () => void;
readonly runtimeCommitSink?: RuntimeCommitSink;
readonly admitManagedMutation?: ToolRuntimeInput['admitManagedMutation'];
readonly childAgents?: HostChildAgentBackendCapabilities;
readonly createFetchTransport?: (proxy: ProxiedFetchProxy | null) => ProxiedFetchTransport;
}
Expand Down Expand Up @@ -427,6 +429,7 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
assertModelCallAccountingReady,
recordToolInvocation: (event) => recordToolInvocation({ repo: telemetry }, event),
...(input.runtimeCommitSink ? { runtimeCommitSink: input.runtimeCommitSink } : {}),
...(input.admitManagedMutation ? { admitManagedMutation: input.admitManagedMutation } : {}),
...(providerRequestCapture
? {
recordProviderRequestCapture: providerRequestCapture,
Expand Down
30 changes: 26 additions & 4 deletions packages/runtime-host/src/server/hosted-execution-tool-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ const HEADLESS_CODING_V1_TOOL_NAMES = [
'apply_patch',
] as const;

const MANAGED_CODING_V1_TOOL_NAMES = ['Write', 'Edit'] as const;
const MANAGED_CODING_V1_SYSTEM_PROMPT = [
'Modify the managed Git workspace with Write and Edit.',
'These tools transform immutable accepted Git content and publish an owner-verified successor.',
'No shell, attached-workspace read, or unmanaged filesystem authority is available in this profile.',
'Stop when the requested changes are complete.',
].join('\n');

const HEADLESS_CODING_V1_SYSTEM_PROMPT = [
'Complete the task by acting with the available tools, not by narrating.',
'Prefer Read, Glob, and Grep for inspection, Edit and Write for file changes, and Bash for shell commands and tests.',
Expand Down Expand Up @@ -73,6 +81,13 @@ export function hostedExecutionRunProfile(
memoryExtraction: false,
};
}
if (profile === 'managed-coding-v1') {
return {
toolNames: MANAGED_CODING_V1_TOOL_NAMES,
systemPrompt: MANAGED_CODING_V1_SYSTEM_PROMPT,
memoryExtraction: false,
};
}
if (profile === 'workhub-coordination-v1') {
return {
toolNames: [],
Expand All @@ -96,13 +111,20 @@ export function projectHostedExecutionTools(
if (missing.length > 0) {
throw new Error(`Hosted tool profile is unavailable: ${missing.join(', ')}`);
}
return (selected as MakaTool[]).map((tool) =>
tool.name === 'Bash'
return (selected as MakaTool[]).map((tool) => {
if (profile === 'managed-coding-v1') {
return {
...tool,
recoveryMode: 'reconcile',
durableExecutionProfile: 'managed_mutation_v1',
};
}
return tool.name === 'Bash'
? {
...tool,
description: HEADLESS_CODING_V1_BASH_DESCRIPTION,
parameters: HEADLESS_CODING_V1_BASH_PARAMETERS,
}
: tool,
);
: tool;
});
}
3 changes: 3 additions & 0 deletions packages/runtime/src/ai-sdk-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,8 @@ export interface AiSdkBackendInput extends AiSdkCompactionCapabilities {
recordToolInvocation?: ToolTelemetryRecorder;
/** Optional Phase 2 SQLite T1/T2 boundary for real tool execution. */
runtimeCommitSink?: RuntimeCommitSink;
/** Owner-issued admission for managed Write/Edit execution. */
admitManagedMutation?: ToolRuntimeInput['admitManagedMutation'];
/** Durable session-lifetime cumulative usage checkpoint after each completed provider step. */
recordUsageCheckpoint?: (
usage: NormalizedAiSdkUsage & { costUsd?: number },
Expand Down Expand Up @@ -1377,6 +1379,7 @@ export class AiSdkBackend implements AgentBackend {
getRunTrace: () => identity.scope().runTrace,
recordToolInvocation: input.recordToolInvocation,
runtimeCommitSink: input.runtimeCommitSink,
admitManagedMutation: input.admitManagedMutation,
recordToolArtifacts: input.recordToolArtifacts,
});
}
Expand Down
Loading