@nowcrew/daemon 0.6.88-beta.27 → 0.6.88-beta.29
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/README.md +7 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/config.js +7 -0
- package/dist/execution-runner.js +2 -0
- package/dist/list-models.js +12 -12
- package/dist/local-executor.js +20 -48
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +24 -16
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/message-file-preview.js +239 -0
- package/dist/provider-env.js +26 -12
- package/dist/runner.js +14 -0
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +2 -2
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness.js +17 -12
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +7 -6
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +58 -105
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/package.json +2 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** 固定可信脚本;无 profile、动态命令、注册表写入或桌面用户切换。 */
|
|
2
|
+
export const WINDOWS_ENVIRONMENT_CAPTURE_SCRIPT = String.raw `
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
5
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
6
|
+
try {
|
|
7
|
+
Add-Type -TypeDefinition @'
|
|
8
|
+
using System;
|
|
9
|
+
using System.Collections.Generic;
|
|
10
|
+
using System.IO;
|
|
11
|
+
using System.Runtime.InteropServices;
|
|
12
|
+
using System.Security.Cryptography;
|
|
13
|
+
using System.Security.Principal;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using Microsoft.Win32;
|
|
16
|
+
|
|
17
|
+
public sealed class NowWorkEnvironmentSnapshot {
|
|
18
|
+
public Dictionary<string, string> env;
|
|
19
|
+
public string[] sourceOwnedKeys;
|
|
20
|
+
public string accountSid;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static class NowWorkNativeEnvironment {
|
|
24
|
+
private const int MaxBytes = 1048576;
|
|
25
|
+
private const int MaxKeys = 4096;
|
|
26
|
+
[DllImport("userenv.dll", SetLastError = true)]
|
|
27
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
28
|
+
private static extern bool CreateEnvironmentBlock(out IntPtr block, IntPtr token, [MarshalAs(UnmanagedType.Bool)] bool inherit);
|
|
29
|
+
[DllImport("userenv.dll", SetLastError = true)]
|
|
30
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
31
|
+
private static extern bool DestroyEnvironmentBlock(IntPtr block);
|
|
32
|
+
|
|
33
|
+
private static void CheckLength(long length) {
|
|
34
|
+
if (length > MaxBytes) throw new InvalidOperationException("capture_too_large");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static void WriteRaw(BinaryWriter writer, object value) {
|
|
38
|
+
if (value == null) { writer.Write((byte)0); return; }
|
|
39
|
+
if (value is string) { writer.Write((byte)1); writer.Write((string)value); }
|
|
40
|
+
else if (value is int) { writer.Write((byte)2); writer.Write((int)value); }
|
|
41
|
+
else if (value is long) { writer.Write((byte)3); writer.Write((long)value); }
|
|
42
|
+
else if (value is byte[]) {
|
|
43
|
+
byte[] bytes = (byte[])value;
|
|
44
|
+
CheckLength(bytes.Length);
|
|
45
|
+
writer.Write((byte)4); writer.Write(bytes.Length); writer.Write(bytes);
|
|
46
|
+
} else if (value is string[]) {
|
|
47
|
+
string[] values = (string[])value;
|
|
48
|
+
writer.Write((byte)5); writer.Write(values.Length);
|
|
49
|
+
foreach (string item in values) { writer.Write(item); CheckLength(writer.BaseStream.Length); }
|
|
50
|
+
} else { throw new InvalidOperationException("capture_unavailable"); }
|
|
51
|
+
CheckLength(writer.BaseStream.Length);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static void ReadRegistry(RegistryKey root, string path, BinaryWriter writer, HashSet<string> owned) {
|
|
55
|
+
using (RegistryKey key = root.OpenSubKey(path, false)) {
|
|
56
|
+
writer.Write(path);
|
|
57
|
+
writer.Write(key != null);
|
|
58
|
+
if (key == null) return;
|
|
59
|
+
string[] names = key.GetValueNames();
|
|
60
|
+
if (names.Length > MaxKeys) throw new InvalidOperationException("capture_too_large");
|
|
61
|
+
Array.Sort(names, StringComparer.Ordinal);
|
|
62
|
+
writer.Write(names.Length);
|
|
63
|
+
foreach (string name in names) {
|
|
64
|
+
writer.Write(name);
|
|
65
|
+
writer.Write((int)key.GetValueKind(name));
|
|
66
|
+
WriteRaw(writer, key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames));
|
|
67
|
+
if (name.Length > 0 && name.IndexOf('=') < 0 && name.IndexOf('\0') < 0) owned.Add(name);
|
|
68
|
+
if (owned.Count > MaxKeys) throw new InvalidOperationException("capture_too_large");
|
|
69
|
+
CheckLength(writer.BaseStream.Length);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 只在本地比较 key/type/raw-value 指纹,不输出原始值或指纹。
|
|
75
|
+
private static string Fingerprint(out string[] sourceOwnedKeys) {
|
|
76
|
+
HashSet<string> owned = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
77
|
+
using (MemoryStream memory = new MemoryStream()) {
|
|
78
|
+
using (BinaryWriter writer = new BinaryWriter(memory, Encoding.UTF8, true)) {
|
|
79
|
+
ReadRegistry(Registry.LocalMachine, @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", writer, owned);
|
|
80
|
+
ReadRegistry(Registry.CurrentUser, "Environment", writer, owned);
|
|
81
|
+
}
|
|
82
|
+
sourceOwnedKeys = new string[owned.Count];
|
|
83
|
+
owned.CopyTo(sourceOwnedKeys);
|
|
84
|
+
Array.Sort(sourceOwnedKeys, StringComparer.OrdinalIgnoreCase);
|
|
85
|
+
using (SHA256 hash = SHA256.Create()) { return Convert.ToBase64String(hash.ComputeHash(memory.ToArray())); }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static Dictionary<string, string> DecodeBlock(IntPtr block) {
|
|
90
|
+
Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
91
|
+
StringBuilder entry = new StringBuilder();
|
|
92
|
+
for (int offset = 0; offset < MaxBytes; offset += 2) {
|
|
93
|
+
char character = (char)Marshal.ReadInt16(block, offset);
|
|
94
|
+
if (character != '\0') { entry.Append(character); continue; }
|
|
95
|
+
if (entry.Length == 0) return values;
|
|
96
|
+
string text = entry.ToString();
|
|
97
|
+
entry.Length = 0;
|
|
98
|
+
int equals = text.IndexOf('=');
|
|
99
|
+
if (equals == 0) continue; // =C: 等驱动器工作目录条目不是普通环境变量。
|
|
100
|
+
if (equals < 1) throw new InvalidOperationException("capture_unavailable");
|
|
101
|
+
string name = text.Substring(0, equals);
|
|
102
|
+
if (values.ContainsKey(name)) throw new InvalidOperationException("capture_unavailable");
|
|
103
|
+
values.Add(name, text.Substring(equals + 1));
|
|
104
|
+
if (values.Count > MaxKeys) throw new InvalidOperationException("capture_too_large");
|
|
105
|
+
}
|
|
106
|
+
throw new InvalidOperationException("capture_too_large");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public static NowWorkEnvironmentSnapshot Capture() {
|
|
110
|
+
using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) {
|
|
111
|
+
if (identity.User == null) throw new InvalidOperationException("capture_unavailable");
|
|
112
|
+
for (int attempt = 0; attempt < 4; attempt++) {
|
|
113
|
+
string[] owned;
|
|
114
|
+
string before = Fingerprint(out owned);
|
|
115
|
+
IntPtr block = IntPtr.Zero;
|
|
116
|
+
Dictionary<string, string> values;
|
|
117
|
+
try {
|
|
118
|
+
// bInherit=false:让 userenv 原生重建最新系统 + 当前账号环境,含 Path / REG_EXPAND_SZ。
|
|
119
|
+
if (!CreateEnvironmentBlock(out block, identity.Token, false)) throw new InvalidOperationException("capture_unavailable");
|
|
120
|
+
values = DecodeBlock(block);
|
|
121
|
+
} finally {
|
|
122
|
+
if (block != IntPtr.Zero && !DestroyEnvironmentBlock(block)) throw new InvalidOperationException("capture_unavailable");
|
|
123
|
+
}
|
|
124
|
+
string[] afterOwned;
|
|
125
|
+
string after = Fingerprint(out afterOwned);
|
|
126
|
+
if (before.Equals(after, StringComparison.Ordinal)) {
|
|
127
|
+
return new NowWorkEnvironmentSnapshot { env = values, sourceOwnedKeys = owned, accountSid = identity.User.Value };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new InvalidOperationException("capture_unavailable");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
'@ -ErrorAction Stop | Out-Null
|
|
135
|
+
$snapshot = [NowWorkNativeEnvironment]::Capture()
|
|
136
|
+
$json = ConvertTo-Json -InputObject $snapshot -Depth 5 -Compress
|
|
137
|
+
if ([Text.Encoding]::UTF8.GetByteCount($json) -gt 1048576) { exit 12 }
|
|
138
|
+
[Console]::Write($json)
|
|
139
|
+
exit 0
|
|
140
|
+
} catch {
|
|
141
|
+
# 不输出异常:它可能携带注册表值、编译器命令或用户路径。
|
|
142
|
+
if ($_.Exception.ToString().Contains('capture_too_large')) { exit 12 }
|
|
143
|
+
exit 11
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
export const WINDOWS_DAEMON_SID_SCRIPT = String.raw `
|
|
147
|
+
$ErrorActionPreference = 'Stop'
|
|
148
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
149
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
150
|
+
$identity = $null
|
|
151
|
+
try {
|
|
152
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
153
|
+
$json = ConvertTo-Json -InputObject @{ accountSid = $identity.User.Value } -Compress
|
|
154
|
+
[Console]::Write($json)
|
|
155
|
+
} catch { exit 11 } finally {
|
|
156
|
+
if ($null -ne $identity) { $identity.Dispose() }
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MarketplaceRequestSchema, marketplaceActive } from "./marketplace-protocol.js";
|
|
2
|
+
import { resolveMarketplaceScopes, runMarketplaceScope } from "./marketplace-runner.js";
|
|
3
|
+
export function createMarketplaceController(deps) {
|
|
4
|
+
const operations = new Map();
|
|
5
|
+
let stopped = false;
|
|
6
|
+
let current = null;
|
|
7
|
+
const now = deps.now ?? Date.now;
|
|
8
|
+
const wait = deps.wait ?? (() => new Promise(resolve => { setTimeout(resolve, 1000); }));
|
|
9
|
+
const execute = async (request) => {
|
|
10
|
+
const id = request.operationId;
|
|
11
|
+
const deadline = now() + 10 * 60_000;
|
|
12
|
+
let lease = null;
|
|
13
|
+
try {
|
|
14
|
+
while (!stopped && now() < deadline && lease === null) {
|
|
15
|
+
lease = await deps.acquire();
|
|
16
|
+
if (!lease)
|
|
17
|
+
await wait();
|
|
18
|
+
}
|
|
19
|
+
if (!lease || stopped) {
|
|
20
|
+
operations.set(id, { operationId: id, status: "failed", results: [], errorCode: "runtime_busy" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
operations.set(id, { operationId: id, status: "running", results: [], errorCode: null });
|
|
24
|
+
const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const scope of scopes) {
|
|
27
|
+
if (stopped || now() >= deadline)
|
|
28
|
+
throw new Error("operation_deadline");
|
|
29
|
+
results.push(await (deps.run ?? runMarketplaceScope)(scope, request.mode));
|
|
30
|
+
operations.set(id, { operationId: id, status: "running", results: [...results], errorCode: null });
|
|
31
|
+
if (now() >= deadline)
|
|
32
|
+
throw new Error("operation_deadline");
|
|
33
|
+
}
|
|
34
|
+
const successes = results.filter(x => !["failed", "missing_cli"].includes(x.status)).length;
|
|
35
|
+
operations.set(id, { operationId: id, results, status: successes === results.length ? "completed" : successes ? "partial" : "failed", errorCode: null });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
const previous = operations.get(id);
|
|
39
|
+
operations.set(id, { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
try {
|
|
43
|
+
await lease?.release();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
const previous = operations.get(id);
|
|
47
|
+
operations.set(id, { ...previous, status: "partial", errorCode: "execution_failed" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
handle(raw) {
|
|
53
|
+
const parsed = MarketplaceRequestSchema.safeParse(raw);
|
|
54
|
+
if (!parsed.success || stopped)
|
|
55
|
+
return { ok: false, error: "invalid_request" };
|
|
56
|
+
const request = parsed.data;
|
|
57
|
+
const existing = operations.get(request.operationId);
|
|
58
|
+
if (existing)
|
|
59
|
+
return { ok: true, data: existing };
|
|
60
|
+
if (request.action === "status")
|
|
61
|
+
return { ok: true, data: { operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
|
|
62
|
+
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
63
|
+
return { ok: false, error: "unsupported_platform" };
|
|
64
|
+
if (current !== null || [...operations.values()].some(x => marketplaceActive(x.status)))
|
|
65
|
+
return { ok: false, error: "runtime_busy" };
|
|
66
|
+
if (operations.size >= 100)
|
|
67
|
+
operations.delete(operations.keys().next().value);
|
|
68
|
+
const accepted = { operationId: request.operationId, status: "queued", results: [], errorCode: null };
|
|
69
|
+
operations.set(request.operationId, accepted);
|
|
70
|
+
current = execute(request).finally(() => { current = null; });
|
|
71
|
+
return { ok: true, data: accepted };
|
|
72
|
+
},
|
|
73
|
+
async stop() { stopped = true; await current; },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// 与另一个进程中的 marketplace-protocol.ts 保持一致;跨进程契约测试校验摘要。
|
|
3
|
+
export const MARKETPLACE_CAPABILITY = "marketplace_update_v1";
|
|
4
|
+
export const MARKETPLACE = "nowwork-pm";
|
|
5
|
+
export const PLUGIN = "AIinterview-plugin";
|
|
6
|
+
export const MarketplaceModeSchema = z.enum(["inspect", "refresh", "update"]);
|
|
7
|
+
export const MarketplaceStatusSchema = z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]);
|
|
8
|
+
export const MarketplaceTargetSchema = z.object({
|
|
9
|
+
handle: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/).optional(),
|
|
10
|
+
runtime: z.enum(["claude", "codex"]),
|
|
11
|
+
provider: z.enum(["default", "custom", "deepseek"]).default("default"),
|
|
12
|
+
hasProviderKey: z.boolean().default(false),
|
|
13
|
+
paths: z.object({ HOME: z.string().min(1).max(4096).optional(), CODEX_HOME: z.string().min(1).max(4096).optional(),
|
|
14
|
+
CLAUDE_CONFIG_DIR: z.string().min(1).max(4096).optional() }).strict().default({}),
|
|
15
|
+
}).strict();
|
|
16
|
+
export const MarketplaceRequestSchema = z.object({
|
|
17
|
+
type: z.literal("marketplace:operation"), action: z.enum(["start", "status"]),
|
|
18
|
+
operationId: z.string().uuid(), mode: MarketplaceModeSchema.optional(),
|
|
19
|
+
targets: z.array(MarketplaceTargetSchema).min(1).max(202).optional(),
|
|
20
|
+
}).strict().refine(x => x.action !== "start" || (x.mode !== undefined && x.targets !== undefined));
|
|
21
|
+
const Version = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/).nullable();
|
|
22
|
+
const Revision = z.string().regex(/^[a-f0-9]{40}$/).nullable();
|
|
23
|
+
export const MarketplaceResultSchema = z.object({
|
|
24
|
+
runtime: z.enum(["claude", "codex"]), scopeId: z.string().regex(/^[a-f0-9]{16}$/),
|
|
25
|
+
agents: z.array(z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/)).max(200),
|
|
26
|
+
shared: z.boolean(), status: z.enum(["checked", "refreshed", "updated", "unchanged", "not_installed", "missing_cli", "failed"]),
|
|
27
|
+
beforeVersion: Version, afterVersion: Version, targetVersion: Version,
|
|
28
|
+
marketRevision: Revision, installedRevision: Revision, enabled: z.boolean().nullable(),
|
|
29
|
+
activation: z.enum(["not_changed", "reload_required", "unverified"]),
|
|
30
|
+
errorCode: z.enum(["scope_unavailable", "market_not_registered", "command_failed", "command_timeout", "invalid_inventory",
|
|
31
|
+
"verification_failed", "unsupported_installation", "cli_missing", "configuration_changed"]).nullable(),
|
|
32
|
+
}).strict();
|
|
33
|
+
export const MarketplaceReportSchema = z.object({
|
|
34
|
+
operationId: z.string().uuid(), status: MarketplaceStatusSchema,
|
|
35
|
+
results: z.array(MarketplaceResultSchema).max(202),
|
|
36
|
+
errorCode: z.enum(["runtime_busy", "execution_failed", "operation_unknown", "unsupported_platform", "too_many_scopes"]).nullable(),
|
|
37
|
+
}).strict();
|
|
38
|
+
export const marketplaceActive = (status) => status === "queued" || status === "running";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** 固定市场操作的短命 CLI 适配;原始命令输出不离开设备。 */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFile, readdir, realpath, lstat, mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { join, resolve, isAbsolute, relative, sep } from "node:path";
|
|
8
|
+
import { applyProviderEnv } from "./provider-env.js";
|
|
9
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
10
|
+
import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
|
|
13
|
+
const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
|
|
14
|
+
const SOURCE_PATH = "plugins/AIinterview-plugin";
|
|
15
|
+
export class MarketplaceFailure extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code) {
|
|
18
|
+
super(code);
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const runMarketplaceCommand = async (bin, args, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await exec(bin, [...args], { ...options, timeout: 120_000, maxBuffer: 4 * 1024 * 1024,
|
|
25
|
+
killSignal: "SIGKILL", encoding: "utf8" });
|
|
26
|
+
return result.stdout;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const value = error;
|
|
30
|
+
throw new MarketplaceFailure(value.code === "ENOENT" ? "cli_missing" : value.killed ? "command_timeout" : "command_failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
34
|
+
const parse = (text) => { try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
39
|
+
} };
|
|
40
|
+
const version = (x) => typeof x === "string" && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/.test(x) ? x : null;
|
|
41
|
+
const sha = (x) => typeof x === "string" && /^[a-f0-9]{40}$/.test(x) ? x : null;
|
|
42
|
+
const within = (root, candidate) => {
|
|
43
|
+
const child = relative(root, candidate);
|
|
44
|
+
return child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child));
|
|
45
|
+
};
|
|
46
|
+
const canonical = async (path) => realpath(path).catch(() => resolve(path));
|
|
47
|
+
export async function resolveMarketplaceScopes(targets, agentsRoot, inherited = process.env) {
|
|
48
|
+
const scopes = new Map();
|
|
49
|
+
const trustedHome = await canonical(inherited.HOME ?? homedir());
|
|
50
|
+
for (const target of targets) {
|
|
51
|
+
const home = target.handle ? join(agentsRoot, target.handle, ".home") : homedir();
|
|
52
|
+
const agentHome = target.handle ? await canonical(home) : null;
|
|
53
|
+
const allowedHomeRoots = [trustedHome, ...(agentHome === null ? [] : [agentHome])];
|
|
54
|
+
const sharedConfigRoot = join(trustedHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
55
|
+
const agentConfigRoot = agentHome === null ? null : join(agentHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
56
|
+
const allowedConfigRoots = [sharedConfigRoot, ...(agentConfigRoot === null ? [] : [agentConfigRoot])];
|
|
57
|
+
const suppliedHome = target.paths.HOME;
|
|
58
|
+
if (suppliedHome !== undefined && !within(allowedHomeRoots[0], await canonical(suppliedHome))
|
|
59
|
+
&& (agentHome === null || !within(agentHome, await canonical(suppliedHome))))
|
|
60
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
61
|
+
const suppliedConfig = target.runtime === "codex" ? target.paths.CODEX_HOME : target.paths.CLAUDE_CONFIG_DIR;
|
|
62
|
+
const suppliedConfigPath = suppliedConfig === undefined ? null : await canonical(suppliedConfig);
|
|
63
|
+
if (suppliedConfigPath !== null && !allowedConfigRoots.some(root => within(root, suppliedConfigPath)))
|
|
64
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
65
|
+
// 与 local-executor 使用同一 provider 解析;凭据存在性只用于选择已配置的 provider,不传递凭据内容。
|
|
66
|
+
const env = applyProviderEnv({ ...inherited, ...target.paths }, target.runtime, target.handle ? {
|
|
67
|
+
provider: target.provider, providerApiKey: target.hasProviderKey ? "configured" : null,
|
|
68
|
+
} : {}, home);
|
|
69
|
+
for (const key of Object.keys(env))
|
|
70
|
+
if (/^(CREW_|ANTHROPIC_|OPENAI_|DEEPSEEK_)/.test(key))
|
|
71
|
+
delete env[key];
|
|
72
|
+
for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"])
|
|
73
|
+
delete env[key];
|
|
74
|
+
env.PATH = augmentedPath(inherited);
|
|
75
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
76
|
+
env.CI = "1";
|
|
77
|
+
const raw = target.runtime === "codex" ? env.CODEX_HOME ?? join(env.HOME ?? homedir(), ".codex")
|
|
78
|
+
: env.CLAUDE_CONFIG_DIR ?? join(env.HOME ?? homedir(), ".claude");
|
|
79
|
+
if (!isAbsolute(raw))
|
|
80
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
81
|
+
const configHome = await realpath(raw).catch(() => resolve(raw));
|
|
82
|
+
if (!allowedConfigRoots.some(root => within(root, configHome)))
|
|
83
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
84
|
+
if (target.runtime === "codex")
|
|
85
|
+
env.CODEX_HOME = configHome;
|
|
86
|
+
else
|
|
87
|
+
env.CLAUDE_CONFIG_DIR = configHome;
|
|
88
|
+
const key = `${target.runtime}:${configHome}`;
|
|
89
|
+
const prior = scopes.get(key);
|
|
90
|
+
if (prior) {
|
|
91
|
+
if (target.handle && !prior.agents.includes(target.handle))
|
|
92
|
+
prior.agents.push(target.handle);
|
|
93
|
+
prior.shared ||= !target.handle;
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
scopes.set(key, { runtime: target.runtime, configHome, env, agents: target.handle ? [target.handle] : [], shared: !target.handle });
|
|
97
|
+
}
|
|
98
|
+
return [...scopes.values()];
|
|
99
|
+
}
|
|
100
|
+
export function parsePluginInventory(runtime, value, configHome) {
|
|
101
|
+
const rows = Array.isArray(value) ? value : obj(value).installed;
|
|
102
|
+
if (!Array.isArray(rows))
|
|
103
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
104
|
+
const found = rows.map(obj).filter(x => (x.id ?? x.pluginId) === SELECTOR);
|
|
105
|
+
if (found.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
if (found.length !== 1)
|
|
108
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
109
|
+
const item = found[0];
|
|
110
|
+
const v = version(item.version);
|
|
111
|
+
if (!v || typeof item.enabled !== "boolean")
|
|
112
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
113
|
+
if (runtime === "claude" && item.scope !== "user")
|
|
114
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
115
|
+
const path = runtime === "claude" ? item.installPath : join(configHome, "plugins/cache", MARKETPLACE, PLUGIN, v);
|
|
116
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
117
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
118
|
+
return { version: v, enabled: item.enabled, path, revision: sha(obj(item.source).sha) };
|
|
119
|
+
}
|
|
120
|
+
/** 逐字节比对固定 Git 源子目录,不能只凭 CLI 退出码或版本字符串证明安装完成。 */
|
|
121
|
+
export async function verifyInstalledContent(path, revision, env, command) {
|
|
122
|
+
const temporary = await mkdtemp(join(tmpdir(), "nowwork-plugin-verify-"));
|
|
123
|
+
const options = { env, cwd: temporary };
|
|
124
|
+
try {
|
|
125
|
+
await command("git", ["init", "--bare", "."], options);
|
|
126
|
+
await command("git", ["fetch", "--depth=1", SOURCE, revision], options);
|
|
127
|
+
const tree = await command("git", ["ls-tree", "-rz", `${revision}:${SOURCE_PATH}`], options);
|
|
128
|
+
const expected = new Map();
|
|
129
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
130
|
+
const match = /^(100644|100755) blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
|
|
131
|
+
if (!match || match[3].split("/").some(x => x === ".." || x === "."))
|
|
132
|
+
throw new MarketplaceFailure("verification_failed");
|
|
133
|
+
expected.set(match[3], match[2]);
|
|
134
|
+
}
|
|
135
|
+
if (!expected.size || expected.size > 2000)
|
|
136
|
+
throw new MarketplaceFailure("verification_failed");
|
|
137
|
+
const actual = new Map();
|
|
138
|
+
let bytes = 0;
|
|
139
|
+
const walk = async (dir, prefix = "") => {
|
|
140
|
+
for (const name of await readdir(dir)) {
|
|
141
|
+
if (name === "__pycache__" || name === ".DS_Store")
|
|
142
|
+
continue;
|
|
143
|
+
const entry = join(dir, name);
|
|
144
|
+
const key = prefix + name;
|
|
145
|
+
const info = await lstat(entry);
|
|
146
|
+
if (info.isSymbolicLink())
|
|
147
|
+
throw new MarketplaceFailure("verification_failed");
|
|
148
|
+
if (info.isDirectory())
|
|
149
|
+
await walk(entry, key + "/");
|
|
150
|
+
else if (info.isFile()) {
|
|
151
|
+
bytes += info.size;
|
|
152
|
+
if (bytes > 100 * 1024 * 1024 || actual.size > 2000)
|
|
153
|
+
throw new MarketplaceFailure("verification_failed");
|
|
154
|
+
const data = await readFile(entry);
|
|
155
|
+
actual.set(key, createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex"));
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
throw new MarketplaceFailure("verification_failed");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
await walk(path);
|
|
162
|
+
if (actual.size !== expected.size || [...expected].some(([name, hash]) => actual.get(name) !== hash))
|
|
163
|
+
throw new MarketplaceFailure("verification_failed");
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await rm(temporary, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function runMarketplaceScope(scope, mode, command = runMarketplaceCommand, verify = verifyInstalledContent) {
|
|
170
|
+
let result = { runtime: scope.runtime, scopeId: createHash("sha256").update(`${scope.runtime}:${scope.configHome}`).digest("hex").slice(0, 16),
|
|
171
|
+
agents: scope.agents, shared: scope.shared, status: "failed", beforeVersion: null, afterVersion: null, targetVersion: null,
|
|
172
|
+
marketRevision: null, installedRevision: null, enabled: null, activation: "unverified", errorCode: null };
|
|
173
|
+
const options = { env: scope.env, cwd: scope.configHome };
|
|
174
|
+
const run = (args) => command(scope.runtime, args, options);
|
|
175
|
+
const inventory = async () => parsePluginInventory(scope.runtime, parse(await run(["plugin", "list", "--json"])), scope.configHome);
|
|
176
|
+
try {
|
|
177
|
+
if (!(await lstat(scope.configHome)).isDirectory())
|
|
178
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
179
|
+
const markets = parse(await run(["plugin", "marketplace", "list", "--json"]));
|
|
180
|
+
const rows = Array.isArray(markets) ? markets : obj(markets).marketplaces;
|
|
181
|
+
if (!Array.isArray(rows))
|
|
182
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
183
|
+
const market = rows.map(obj).filter(x => x.name === MARKETPLACE);
|
|
184
|
+
if (market.length !== 1)
|
|
185
|
+
throw new MarketplaceFailure("market_not_registered");
|
|
186
|
+
const root = market[0].root ?? market[0].installLocation;
|
|
187
|
+
if (typeof root !== "string" || !isAbsolute(root))
|
|
188
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
189
|
+
const before = await inventory();
|
|
190
|
+
result = { ...result, beforeVersion: before?.version ?? null, afterVersion: before?.version ?? null, enabled: before?.enabled ?? null };
|
|
191
|
+
if (mode !== "inspect")
|
|
192
|
+
await run(["plugin", "marketplace", scope.runtime === "claude" ? "update" : "upgrade", MARKETPLACE]);
|
|
193
|
+
const revision = sha((await command("git", ["-C", root, "rev-parse", "HEAD"], options)).trim());
|
|
194
|
+
if (!revision)
|
|
195
|
+
throw new MarketplaceFailure("verification_failed");
|
|
196
|
+
const manifest = obj(parse(await readFile(join(root, scope.runtime === "claude" ? ".claude-plugin" : ".agents/plugins", "marketplace.json"), "utf8")));
|
|
197
|
+
const plugins = manifest.plugins;
|
|
198
|
+
const target = Array.isArray(plugins) ? plugins.map(obj).find(x => x.name === PLUGIN) : undefined;
|
|
199
|
+
const source = obj(target?.source);
|
|
200
|
+
const targetVersion = version(target?.version);
|
|
201
|
+
const targetSha = sha(source.sha);
|
|
202
|
+
result = { ...result, marketRevision: revision, targetVersion };
|
|
203
|
+
if (mode === "refresh")
|
|
204
|
+
return { ...result, status: "refreshed", activation: "not_changed" };
|
|
205
|
+
if (!before)
|
|
206
|
+
return { ...result, status: "not_installed", activation: "not_changed" };
|
|
207
|
+
if (mode === "inspect") {
|
|
208
|
+
const installed = obj(parse(await readFile(join(before.path, scope.runtime === "claude" ? ".claude-plugin" : ".codex-plugin", "plugin.json"), "utf8")));
|
|
209
|
+
const actualVersion = version(installed.version);
|
|
210
|
+
result = { ...result, afterVersion: actualVersion };
|
|
211
|
+
if (installed.name !== PLUGIN || actualVersion !== before.version)
|
|
212
|
+
throw new MarketplaceFailure("verification_failed");
|
|
213
|
+
// 清单可能来自市场元数据;没有文件内容核验就不报告已安装源提交。
|
|
214
|
+
return { ...result, status: "checked", installedRevision: null, activation: "unverified" };
|
|
215
|
+
}
|
|
216
|
+
if (!targetVersion || !targetSha || source.url !== SOURCE || source.path !== SOURCE_PATH || source.source !== "git-subdir")
|
|
217
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
218
|
+
// Codex add 会启用插件;不偷偷改变用户已禁用的状态。
|
|
219
|
+
if (!before.enabled)
|
|
220
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
221
|
+
await run(scope.runtime === "claude" ? ["plugin", "update", SELECTOR, "--scope", "user", "--json"] : ["plugin", "add", SELECTOR, "--json"]);
|
|
222
|
+
const after = await inventory();
|
|
223
|
+
result = { ...result, afterVersion: after?.version ?? null, enabled: after?.enabled ?? null };
|
|
224
|
+
if (!after || after.version !== targetVersion || after.enabled !== before.enabled)
|
|
225
|
+
throw new MarketplaceFailure("verification_failed");
|
|
226
|
+
await verify(after.path, targetSha, scope.env, command);
|
|
227
|
+
return { ...result, status: before.version === after.version && before.revision === targetSha ? "unchanged" : "updated",
|
|
228
|
+
installedRevision: targetSha, activation: "reload_required" };
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const code = error instanceof MarketplaceFailure ? error.code : "scope_unavailable";
|
|
232
|
+
return { ...result, status: code === "cli_missing" ? "missing_cli" : "failed", errorCode: code };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createMarketplaceController } from "./marketplace-controller.js";
|
|
2
|
+
import { prepareDaemonRestart } from "./daemon-updater.js";
|
|
3
|
+
export function createServeMarketplace(config, localSlots, hostCoordinator) {
|
|
4
|
+
const controller = createMarketplaceController({ agentsRoot: config.agentsRoot,
|
|
5
|
+
acquire: async () => { const result = await prepareDaemonRestart({ localSlots, hostCoordinator }); return result.ok ? result : null; } });
|
|
6
|
+
return {
|
|
7
|
+
handle(raw, send) {
|
|
8
|
+
if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "marketplace:operation")
|
|
9
|
+
return false;
|
|
10
|
+
const { reqId, ...request } = raw;
|
|
11
|
+
if (typeof reqId === "string")
|
|
12
|
+
send({ type: "fs:result", reqId, ...controller.handle(request) });
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
stop: controller.stop,
|
|
16
|
+
};
|
|
17
|
+
}
|