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
17 changes: 17 additions & 0 deletions .github/workflows/gitoxide-helper-admission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ on:
- 'native/gitoxide-helper/**'
- 'packages/runtime-host/src/server/gitoxide-helper-*.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
- 'packages/runtime-host/src/server/packaged-gitoxide-helper-internal.ts'
- 'packages/runtime-host/src/__tests__/packaged-gitoxide-helper.test.ts'
- 'scripts/prepare-gitoxide-helper*'
- 'scripts/generate-gitoxide-cargo-notices.mjs'
- 'apps/desktop/electron-builder.config.mjs'
- 'docs/architecture/gitoxide-*.md'
push:
branches:
Expand All @@ -33,6 +38,11 @@ on:
- 'native/gitoxide-helper/**'
- 'packages/runtime-host/src/server/gitoxide-helper-*.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
- 'packages/runtime-host/src/server/packaged-gitoxide-helper-internal.ts'
- 'packages/runtime-host/src/__tests__/packaged-gitoxide-helper.test.ts'
- 'scripts/prepare-gitoxide-helper*'
- 'scripts/generate-gitoxide-cargo-notices.mjs'
- 'apps/desktop/electron-builder.config.mjs'
- 'docs/architecture/gitoxide-*.md'

permissions:
Expand Down Expand Up @@ -65,8 +75,15 @@ jobs:
- name: Test the short-lived Gitoxide helper
working-directory: native/gitoxide-helper
run: cargo test --locked
- name: Build the release helper
run: npm run build:gitoxide-helper
- name: Install JavaScript dependencies without packaging hooks
run: npm ci --ignore-scripts
- name: Prepare and license the packaged helper
run: >-
npm run prepare:gitoxide-helper &&
npm run generate:gitoxide-cargo-notices &&
node --test scripts/prepare-gitoxide-helper.test.mjs
- name: Build the helper invocation owner
run: >-
npm --workspace @maka/core run build &&
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/electron-builder.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ export default {
'dist/renderer/computer-use-overlay/**',
],
extraResources: [
{
from: '.generated/gitoxide-helper/gitoxide',
to: 'gitoxide',
},
{
from: '.generated/gitoxide-helper/gitoxide-helper.json',
to: 'gitoxide-helper.json',
},
{
from: '.generated/gitoxide-helper/THIRD_PARTY_NOTICES.txt',
to: 'licenses/gitoxide-helper/THIRD_PARTY_NOTICES.txt',
},
{
from: 'bundled-tools.json',
to: 'bundled-tools.json',
Expand Down
80 changes: 80 additions & 0 deletions docs/architecture/gitoxide-packaged-helper-authority-v1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: Gitoxide packaged helper authority v1
status: Draft
milestone: M1.3
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Gitoxide packaged helper authority v1

## 1. 主要不变量

本切片只证明一件事:

> 只有当前 Maka 发布流程构建、清单绑定并随应用资源一起交付的 exact Gitoxide helper,才能被转换成 Runtime Host 内部的调用 capability;普通 caller 不能用裸路径、PATH 发现或自报摘要获得执行权。

它不负责 source import、candidate、projection 或 Desktop managed task。这些能力消费本切片签发的 opaque capability,不能重新接受 executable path。

## 2. Owner 与权限边界

- release build owner:使用锁定的 `native/gitoxide-helper/Cargo.lock` 构建 release binary;
- preparation owner:复制到 fresh `.generated/gitoxide-helper`,计算 bytes/SHA-256,并写 `maka_gitoxide_helper_release_v1`;
- legal owner:从同一个 Cargo.lock graph 生成随包交付的 crate license/notice;
- packaged-resource owner:Electron 只携带 helper、manifest 和 notice;
- Runtime Host release owner:从平台应用已经授予的 `resourcesRoot` 读取严格 manifest,签发 release claim;
- invocation owner:每次调用前重新验证 canonical path、file identity、bytes 和 digest。

manifest 是发布资源的完整性声明,不是独立密码学签名。v1 的外层 trust root 是操作系统认可的应用发布/签名边界;同一用户权限下能同时改写已安装应用和 manifest 的攻击者不在本切片单独抵抗的威胁模型内。后续产品接线只能传递 Desktop 已持有的 packaged-resource authority,不能把公开 CLI 路径参数当成 authority。

## 3. 原子性与失败状态

preparation 先写 fresh helper 目录,再用临时 manifest rename 发布声明。生成失败时整个 `.generated` 输出不是发布输入,打包必须停止。

Runtime admission 只有两种结果:

- exact manifest 与 artifact 匹配:签发 owner-bound invocation capability;
- manifest、平台、路径、类型、大小或摘要任一不匹配:fail closed,不发现 system Git,也不尝试旧 bundled Git。

它没有 T1,也不写 durable state;rollback 是丢弃 `.generated/gitoxide-helper` 并重新构建。

## 4. 平台能力矩阵

| 平台 | 构建/资源 | 运行时校验 | 当前证据 |
| --- | --- | --- | --- |
| Linux x64 | CI release helper | non-symlink regular file、identity、bytes、SHA-256 | Gitoxide workflow |
| macOS arm64 | release helper 随 app 签名 | 同上;外层 trust root 为已签名 app | Gitoxide workflow;正式 notarized artifact 仍由 release lane 验证 |
| Windows x64 | release helper 随安装包 | 拒绝 symlink/junction path,校验 identity、bytes、SHA-256 | Gitoxide workflow |

开发态不会从 PATH、system Git 或任意 `resourcesPath` 自动启用 managed Git。没有经过明确测试 authority 注入时,Gitoxide managed profile 必须报告 unavailable。

## 5. 许可证与包体

Gitoxide helper 是单个短生命周期 Rust binary,不携带 Rust 工具链。Cargo notices 从 exact lock graph 在发布时生成并放入 `licenses/gitoxide-helper/THIRD_PARTY_NOTICES.txt`。普通 TypeScript 开发和非 Gitoxide 测试不需要安装 Rust;只有修改 helper、运行其三平台 CI 或构建正式安装包时需要锁定 Rust toolchain。

## 6. 后续产品接线

下一切片必须由同一个 Runtime Host composition 生命周期持有:

1. packaged-resource authority;
2. Gitoxide invocation/admission/import/projection capability;
3. bundled npm capability 与 dependency storage authority;
4. 专用 managed task consumer。

Host handshake 必须声明 exact managed profile。CLI 启动的无 packaged-resource Host 不能被 Desktop 静默复用为支持该 profile 的 Host;不匹配只能显式拒绝或安全替换,禁止 fallback。
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
"test:product-release": "node --test scripts/product-release.test.mjs scripts/product-release-artifacts.test.mjs scripts/product-release-authority.test.mjs",
"generate:windows-cargo-notices": "node scripts/generate-windows-cargo-notices.mjs",
"check:windows-cargo-notices": "node scripts/generate-windows-cargo-notices.mjs --check",
"build:gitoxide-helper": "cargo +1.98.0 build --manifest-path native/gitoxide-helper/Cargo.toml --release --locked",
"prepare:gitoxide-helper": "node scripts/prepare-gitoxide-helper.mjs",
"generate:gitoxide-cargo-notices": "node scripts/generate-gitoxide-cargo-notices.mjs",
"check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-release.test.mjs scripts/product-release-artifacts.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs",
"package:macos-arm64": "node scripts/package-macos-arm64.mjs",
"verify:macos-arm64": "node scripts/verify-macos-arm64-dmg.mjs",
Expand Down
134 changes: 134 additions & 0 deletions packages/runtime-host/src/__tests__/packaged-gitoxide-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { verifyGitoxideHelperArtifactForInvocationInternal } from '../server/gitoxide-helper-artifact-authority-internal.js';
import {
PackagedGitoxideHelperError,
resolvePackagedGitoxideHelperInternal,
} from '../server/packaged-gitoxide-helper-internal.js';

test('turns an exact packaged helper manifest into an owner-bound invocation capability', async () => {
const fixture = await createFixture();
try {
const invocationOwnerToken = {};
const capability = await withPackagedResourcesRoot(fixture.root, () =>
resolvePackagedGitoxideHelperInternal({ invocationOwnerToken }),
);
const verified = await verifyGitoxideHelperArtifactForInvocationInternal(
invocationOwnerToken,
capability,
);
assert.equal(verified.executablePath, fixture.executablePath);
assert.equal(verified.protocolVersion, 1);
} finally {
await fixture.cleanup();
}
});

test('fails closed when the manifest and packaged helper no longer agree', async () => {
const fixture = await createFixture();
try {
await writeFile(fixture.executablePath, 'tampered');
await assert.rejects(
withPackagedResourcesRoot(fixture.root, () =>
resolvePackagedGitoxideHelperInternal({ invocationOwnerToken: {} }),
),
(error: unknown) =>
error instanceof PackagedGitoxideHelperError &&
error.code === 'packaged_gitoxide_helper_integrity_mismatch',
);
} finally {
await fixture.cleanup();
}
});

test('rejects an unknown or self-declared manifest shape', async () => {
const fixture = await createFixture();
try {
await writeFile(
join(fixture.root, 'gitoxide-helper.json'),
JSON.stringify({ schemaVersion: 999, executableRelativePath: 'gitoxide/helper' }),
);
await assert.rejects(
withPackagedResourcesRoot(fixture.root, () =>
resolvePackagedGitoxideHelperInternal({ invocationOwnerToken: {} }),
),
(error: unknown) =>
error instanceof PackagedGitoxideHelperError &&
error.code === 'packaged_gitoxide_helper_manifest_invalid',
);
} finally {
await fixture.cleanup();
}
});

async function withPackagedResourcesRoot<T>(root: string, run: () => Promise<T>): Promise<T> {
const descriptor = Object.getOwnPropertyDescriptor(process, 'resourcesPath');
Object.defineProperty(process, 'resourcesPath', {
configurable: true,
value: root,
});
try {
return await run();
} finally {
if (descriptor) Object.defineProperty(process, 'resourcesPath', descriptor);
else delete (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
}
}

async function createFixture(): Promise<{
root: string;
executablePath: string;
cleanup(): Promise<void>;
}> {
const root = await mkdtemp(join(tmpdir(), 'maka-packaged-gitoxide-'));
const runtimeRoot = join(root, 'gitoxide');
await mkdir(runtimeRoot, { recursive: true });
const executableName =
process.platform === 'win32' ? 'maka-gitoxide-helper.exe' : 'maka-gitoxide-helper';
const executablePath = join(runtimeRoot, executableName);
const bytes = Buffer.from('packaged-helper');
await writeFile(executablePath, bytes, { mode: 0o755 });
await writeFile(
join(root, 'gitoxide-helper.json'),
`${JSON.stringify({
schemaVersion: 1,
protocol: 'maka_gitoxide_helper_release_v1',
provider: 'maka/gitoxide-helper',
platform: process.platform,
arch: process.arch,
protocolVersion: 1,
executableRelativePath: `gitoxide/${executableName}`,
bytes: bytes.byteLength,
sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}`,
distributionReady: true,
})}\n`,
);
return {
root,
executablePath,
cleanup: () => rm(root, { recursive: true, force: true }),
};
}
Loading
Loading