@actiondock/core 2.12.0 → 2.14.0

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.
Files changed (38) hide show
  1. package/dist/catalog/graph.d.ts +2 -4
  2. package/dist/catalog/graph.js +182 -32
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +1 -1
  5. package/dist/input/describe.d.ts +12 -1
  6. package/dist/input/describe.js +55 -1
  7. package/dist/input/flat-materializer.d.ts +24 -0
  8. package/dist/input/flat-materializer.js +51 -14
  9. package/dist/input/index.d.ts +1 -0
  10. package/dist/input/index.js +1 -0
  11. package/dist/input/input-resolver.d.ts +10 -2
  12. package/dist/input/input-resolver.js +123 -4
  13. package/dist/input/output-contract.d.ts +72 -0
  14. package/dist/input/output-contract.js +274 -0
  15. package/dist/input/stdin-input.d.ts +11 -1
  16. package/dist/input/stdin-input.js +15 -3
  17. package/dist/project/index.d.ts +2 -1
  18. package/dist/project/index.js +3 -2
  19. package/dist/project/init.js +2 -2
  20. package/dist/project/skill-dir.d.ts +56 -0
  21. package/dist/project/skill-dir.js +214 -0
  22. package/dist/project/types.d.ts +2 -0
  23. package/dist/registry/registry.js +27 -15
  24. package/dist/runtime/action-collection.js +3 -0
  25. package/dist/runtime/standalone-commands.js +18 -3
  26. package/dist/runtime/standalone-help.js +2 -0
  27. package/dist/runtime/standalone.d.ts +2 -0
  28. package/dist/storage/sqlite-driver.d.ts +0 -1
  29. package/dist/storage/sqlite-driver.js +0 -1
  30. package/dist/utils/index.d.ts +0 -1
  31. package/dist/utils/index.js +0 -1
  32. package/dist/utils/semver.d.ts +2 -1
  33. package/dist/utils/semver.js +11 -5
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +2 -7
  37. package/dist/utils/warning.d.ts +0 -1
  38. package/dist/utils/warning.js +0 -52
@@ -6,10 +6,6 @@ import { parseSemVer, type SemVer } from "../utils/index.js";
6
6
  import { type DiscoveredPackage, type PackageDiscoveryOptions } from "./discovery.js";
7
7
  export type { SemVer };
8
8
  export { parseSemVer };
9
- /**
10
- * 校验两个语义化版本范围是否兼容并能收敛为单一解析版本。
11
- * 遵循设计规范:直接或传递版本范围无法收敛时拒绝并抛出 ACTION_PACKAGE_VERSION_CONFLICT。
12
- */
13
9
  export declare function areVersionsCompatible(v1: string, v2: string): boolean;
14
10
  /**
15
11
  * 版本冲突错误。
@@ -145,6 +141,8 @@ export declare class DefaultPackageGraph implements PackageGraph {
145
141
  export interface PackageGraphBuilderOptions {
146
142
  /** 预先发现的包集合,若未提供则通过 PackageDiscovery 自动发现 */
147
143
  packages?: DiscoveredPackage[];
144
+ /** 完整显式集合的依赖存在性与选定版本校验(Host 增量注册时不启用)。 */
145
+ validateDependencies?: boolean;
148
146
  /** 根工程根目录或根包标识(与 projectRoot 互为别名) */
149
147
  root?: string;
150
148
  /** 当前工程根目录绝对或相对物理路径 */
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
- import { ActionDockError, ACTION_PACKAGE_VERSION_CONFLICT, MANIFEST_DIGEST_MISMATCH, MANIFEST_NOT_FOUND, MANIFEST_READ_FAILED, PACKAGE_NOT_FOUND, UNDECLARED_ACTION_DEPENDENCY, } from "../errors.js";
3
+ import { ActionDockError, ACTION_PACKAGE_VERSION_CONFLICT, MANIFEST_DIGEST_MISMATCH, MANIFEST_NOT_FOUND, MANIFEST_READ_FAILED, PACKAGE_NOT_FOUND, PACKAGE_ID_CONFLICT, INVALID_ARGUMENT, UNDECLARED_ACTION_DEPENDENCY, } from "../errors.js";
4
4
  import { computeManifestDigest, parseJsonWithoutDuplicates } from "../project/digest.js";
5
5
  import { loadLockfile } from "../project/lockfile.js";
6
6
  import { loadPlaybooks, loadProjectConfig } from "../project/loader.js";
@@ -10,30 +10,124 @@ import { createPackageIdentity } from "../runtime/identity.js";
10
10
  import { parseSemVer } from "../utils/index.js";
11
11
  import { PackageDiscovery } from "./discovery.js";
12
12
  export { parseSemVer };
13
- /**
14
- * 校验两个语义化版本范围是否兼容并能收敛为单一解析版本。
15
- * 遵循设计规范:直接或传递版本范围无法收敛时拒绝并抛出 ACTION_PACKAGE_VERSION_CONFLICT。
16
- */
17
- export function areVersionsCompatible(v1, v2) {
18
- if (v1 === v2 || v1 === "*" || v2 === "*" || !v1 || !v2) {
19
- return true;
13
+ /** 有限版本契约:严格三段版本(允许 v/= 前缀)、^、~、*;不猜测其他范围。 */
14
+ function parseVersionRange(value) {
15
+ const text = value.trim();
16
+ if (!text || text === "*")
17
+ return {};
18
+ const prefix = text[0];
19
+ const minimum = parseSemVer(prefix === "^" || prefix === "~" ? text.slice(1) : text);
20
+ if (!minimum)
21
+ return undefined;
22
+ if (prefix !== "^" && prefix !== "~")
23
+ return { minimum, exact: true };
24
+ const maximum = prefix === "~"
25
+ ? { major: minimum.major, minor: minimum.minor + 1, patch: 0 }
26
+ : minimum.major > 0 ? { major: minimum.major + 1, minor: 0, patch: 0 }
27
+ : minimum.minor > 0 ? { major: 0, minor: minimum.minor + 1, patch: 0 }
28
+ : { major: 0, minor: 0, patch: minimum.patch + 1 };
29
+ return { minimum, maximum };
30
+ }
31
+ function compareVersions(a, b) {
32
+ const core = a.major - b.major || a.minor - b.minor || a.patch - b.patch;
33
+ if (core)
34
+ return core;
35
+ if (a.prerelease === b.prerelease)
36
+ return 0;
37
+ if (a.prerelease === undefined)
38
+ return 1;
39
+ if (b.prerelease === undefined)
40
+ return -1;
41
+ const left = a.prerelease.split(".");
42
+ const right = b.prerelease.split(".");
43
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
44
+ if (left[i] === undefined)
45
+ return -1;
46
+ if (right[i] === undefined)
47
+ return 1;
48
+ if (left[i] === right[i])
49
+ continue;
50
+ const numericLeft = /^\d+$/.test(left[i]);
51
+ const numericRight = /^\d+$/.test(right[i]);
52
+ if (numericLeft && numericRight)
53
+ return BigInt(left[i]) < BigInt(right[i]) ? -1 : 1;
54
+ if (numericLeft !== numericRight)
55
+ return numericLeft ? -1 : 1;
56
+ return left[i] < right[i] ? -1 : 1;
20
57
  }
21
- const clean1 = v1.trim().replace(/^[\^~=]/, "");
22
- const clean2 = v2.trim().replace(/^[\^~=]/, "");
23
- const sem1 = parseSemVer(clean1);
24
- const sem2 = parseSemVer(clean2);
25
- if (!sem1 || !sem2) {
26
- return v1 === v2;
58
+ return 0;
59
+ }
60
+ function satisfiesRange(version, range) {
61
+ if (range.exact)
62
+ return compareVersions(version, range.minimum) === 0;
63
+ if (range.minimum && compareVersions(version, range.minimum) < 0)
64
+ return false;
65
+ if (range.maximum && compareVersions(version, range.maximum) >= 0)
66
+ return false;
67
+ // 预发布版本仅在范围下界显式点名同一核心版本的预发布时纳入。
68
+ if (version.prerelease !== undefined) {
69
+ const min = range.minimum;
70
+ return Boolean(min?.prerelease && version.major === min.major && version.minor === min.minor && version.patch === min.patch);
27
71
  }
28
- if (sem1.major !== sem2.major) {
72
+ return true;
73
+ }
74
+ export function areVersionsCompatible(v1, v2) {
75
+ const left = parseVersionRange(v1);
76
+ const right = parseVersionRange(v2);
77
+ if (!left || !right)
78
+ return false;
79
+ const candidate = !left.minimum ? right.minimum : !right.minimum ? left.minimum
80
+ : compareVersions(left.minimum, right.minimum) >= 0 ? left.minimum : right.minimum;
81
+ if (!candidate)
82
+ return true;
83
+ if (satisfiesRange(candidate, left) && satisfiesRange(candidate, right))
84
+ return true;
85
+ // 两个范围可以在预发布下界之后的稳定版本交汇,但不能改变精确版本。
86
+ const stable = { major: candidate.major, minor: candidate.minor, patch: candidate.patch };
87
+ return satisfiesRange(stable, left) && satisfiesRange(stable, right);
88
+ }
89
+ /**
90
+ * 判定给定的依赖规格是否为不支持的语义化版本范围形态(而非获取描述)。
91
+ *
92
+ * 规则:
93
+ * - 范围操作符开头:^, ~, <, >, =, *;
94
+ * - 复合范围操作符:|| 或 - 范围;
95
+ * - X 范围与通配:如 1.x, 1.2.x, 1.*, 1.2.*, v1.x 等;
96
+ * - 不完整两段或一段版本:如 1, 1.2, v1, v1.2 等;
97
+ * - 版本三段式开头但未被严格解析器支持的非法形态。
98
+ */
99
+ function isUnsupportedVersionRange(spec) {
100
+ const text = spec.trim();
101
+ if (!text)
29
102
  return false;
103
+ if (/^[~^<>=*]/.test(text))
104
+ return true;
105
+ if (/\|\||\s+-\s+/.test(text))
106
+ return true;
107
+ if (/^v?\d+\.(?:[xX*]|\d+\.[xX*])(?:$|[-+])/.test(text))
108
+ return true;
109
+ if (/^v?\d+(?:\.\d+)?$/.test(text))
110
+ return true;
111
+ if (/^v?\d+\.\d+\.\d+/.test(text))
112
+ return true;
113
+ return false;
114
+ }
115
+ /** 获取描述不是版本约束;仅真实范围及锁定版本参与版本兼容校验。 */
116
+ function dependencyVersionRequirements(spec, locked) {
117
+ const requirements = [];
118
+ const range = spec === undefined ? undefined : parseVersionRange(spec);
119
+ if (spec !== undefined && range)
120
+ requirements.push(spec);
121
+ else if (spec !== undefined && isUnsupportedVersionRange(spec)) {
122
+ throw new ActionDockError(INVALID_ARGUMENT, `Unsupported Action package version range '${spec}'; supported forms are exact versions, ^, ~ and *`);
30
123
  }
31
- if (v1.startsWith("~") || v2.startsWith("~")) {
32
- if (sem1.minor !== sem2.minor) {
33
- return false;
34
- }
124
+ const lockedVersion = locked?.resolved || locked?.version;
125
+ if (lockedVersion && parseSemVer(lockedVersion))
126
+ requirements.push(lockedVersion);
127
+ else if (lockedVersion) {
128
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid locked Action package version '${lockedVersion}'`);
35
129
  }
36
- return true;
130
+ return requirements;
37
131
  }
38
132
  /**
39
133
  * 版本冲突错误。
@@ -280,6 +374,9 @@ export class PackageGraphBuilder {
280
374
  */
281
375
  buildFromPackages(nodes, generation) {
282
376
  for (const pkg of this.options.packages) {
377
+ if (nodes.has(pkg.id)) {
378
+ throw new ActionDockError(PACKAGE_ID_CONFLICT, `Package ID '${pkg.id}' occurs more than once in explicit package graph`);
379
+ }
283
380
  const fields = createPackageNodeFields({
284
381
  id: pkg.id,
285
382
  root: pkg.root,
@@ -322,6 +419,39 @@ export class PackageGraphBuilder {
322
419
  rootNode.isDirect = true;
323
420
  }
324
421
  }
422
+ if (this.options.validateDependencies) {
423
+ for (const node of nodes.values()) {
424
+ const lockfile = node.root ? loadLockfile(node.root) : null;
425
+ for (const [depId, spec] of Object.entries(node.manifest.dependencies || {})) {
426
+ const dep = nodes.get(depId);
427
+ if (!dep) {
428
+ throw new ActionDockError(PACKAGE_NOT_FOUND, `Package '${depId}' required by '${node.packageId}' is missing from explicit graph`);
429
+ }
430
+ for (const requirement of dependencyVersionRequirements(spec)) {
431
+ if (!areVersionsCompatible(requirement, dep.version)) {
432
+ throw new ActionPackageVersionConflictError(depId, [
433
+ { requester: node.packageId, rangeOrVersion: requirement },
434
+ { requester: depId, rangeOrVersion: dep.version },
435
+ ]);
436
+ }
437
+ }
438
+ }
439
+ // 根锁文件也可能锁定传递依赖,不能只检查直接 dependencies。
440
+ for (const [depId, locked] of Object.entries(lockfile?.packages || {})) {
441
+ const dep = nodes.get(depId);
442
+ if (!dep)
443
+ continue;
444
+ for (const requirement of dependencyVersionRequirements(undefined, locked)) {
445
+ if (!areVersionsCompatible(requirement, dep.version)) {
446
+ throw new ActionPackageVersionConflictError(depId, [
447
+ { requester: node.packageId, rangeOrVersion: requirement },
448
+ { requester: depId, rangeOrVersion: dep.version },
449
+ ]);
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
325
455
  linkDirectDependencies(nodes);
326
456
  computeTransitiveDependencies(nodes);
327
457
  const effectiveRootNode = rootIdentity ? nodes.get(rootIdentity.id) : undefined;
@@ -415,6 +545,13 @@ export class PackageGraphBuilder {
415
545
  // 忽略扫描外部链接包异常
416
546
  }
417
547
  }
548
+ const requesterLocks = new Map([[rootPackageId, lockfile]]);
549
+ const getRequesterLock = (requester) => {
550
+ if (!requesterLocks.has(requester)) {
551
+ requesterLocks.set(requester, loadLockfile(nodes.get(requester).root));
552
+ }
553
+ return requesterLocks.get(requester);
554
+ };
418
555
  const versionRequirements = new Map();
419
556
  function recordRequirement(pkgId, requester, rangeOrVersion) {
420
557
  const existing = versionRequirements.get(pkgId) || [];
@@ -429,10 +566,22 @@ export class PackageGraphBuilder {
429
566
  existing.push({ requester, rangeOrVersion });
430
567
  versionRequirements.set(pkgId, existing);
431
568
  }
569
+ const recordDependency = (pkgId, requester, spec) => {
570
+ const requesterLocked = getRequesterLock(requester)?.packages[pkgId];
571
+ for (const requirement of dependencyVersionRequirements(spec, requesterLocked)) {
572
+ recordRequirement(pkgId, requester, requirement);
573
+ }
574
+ const rootLocked = lockfile?.packages[pkgId];
575
+ if (rootLocked && rootLocked !== requesterLocked) {
576
+ for (const requirement of dependencyVersionRequirements(undefined, rootLocked)) {
577
+ recordRequirement(pkgId, rootPackageId, requirement);
578
+ }
579
+ }
580
+ };
432
581
  const queue = [];
433
582
  for (const [depId, spec] of Object.entries(rootManifest.dependencies || {})) {
434
583
  rootNode.directDependencies.add(depId);
435
- recordRequirement(depId, rootPackageId, spec);
584
+ recordDependency(depId, rootPackageId, spec);
436
585
  queue.push({
437
586
  pkgId: depId,
438
587
  requester: rootPackageId,
@@ -460,7 +609,7 @@ export class PackageGraphBuilder {
460
609
  continue;
461
610
  }
462
611
  let pkgDir = null;
463
- const locked = lockfile?.packages?.[item.pkgId];
612
+ const locked = getRequesterLock(item.requester)?.packages[item.pkgId] || lockfile?.packages[item.pkgId];
464
613
  if (packageRootsRecord?.[item.pkgId]) {
465
614
  pkgDir = packageRootsRecord[item.pkgId];
466
615
  }
@@ -474,7 +623,7 @@ export class PackageGraphBuilder {
474
623
  }
475
624
  candidateNames.push(item.rangeOrSpec);
476
625
  candidateNames.push(item.pkgId);
477
- let cur = absProjectRoot;
626
+ let cur = nodes.get(item.requester)?.root || absProjectRoot;
478
627
  while (true) {
479
628
  for (const candName of candidateNames) {
480
629
  const candPath = join(cur, "node_modules", candName);
@@ -506,6 +655,9 @@ export class PackageGraphBuilder {
506
655
  throw new ActionDockError(MANIFEST_READ_FAILED, `Failed to read manifest for '${item.pkgId}': ${err.message}`);
507
656
  }
508
657
  const depManifest = parseJsonWithoutDuplicates(manifestRaw);
658
+ if (depManifest.id !== item.pkgId) {
659
+ throw new ActionDockError(PACKAGE_ID_CONFLICT, `Expected package '${item.pkgId}' at '${pkgDir}', found '${depManifest.id}'`);
660
+ }
509
661
  const actualDigest = computeManifestDigest(depManifest);
510
662
  if (locked?.manifestDigest && locked.manifestDigest !== actualDigest) {
511
663
  throw new ActionDockError(MANIFEST_DIGEST_MISMATCH, `Manifest digest mismatch for package '${item.pkgId}': expected '${locked.manifestDigest}', got '${actualDigest}'. Re-resolution required.`);
@@ -538,15 +690,13 @@ export class PackageGraphBuilder {
538
690
  };
539
691
  nodes.set(item.pkgId, newNode);
540
692
  for (const [subDepId, subSpec] of Object.entries(depManifest.dependencies || {})) {
541
- recordRequirement(subDepId, item.pkgId, subSpec);
542
- if (!nodes.has(subDepId)) {
543
- queue.push({
544
- pkgId: subDepId,
545
- requester: item.pkgId,
546
- rangeOrSpec: subSpec,
547
- isDirect: false,
548
- });
549
- }
693
+ recordDependency(subDepId, item.pkgId, subSpec);
694
+ queue.push({
695
+ pkgId: subDepId,
696
+ requester: item.pkgId,
697
+ rangeOrSpec: subSpec,
698
+ isDirect: false,
699
+ });
550
700
  }
551
701
  }
552
702
  if (scanLinked) {
package/dist/index.d.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * 平台大对象驱动与解析器工具移至特定子路径导出(./server, ./project, ./registry,
6
6
  * ./profile, ./graph, ./package),严格杜绝在根导出泄露内部执行与平台上下文机制。
7
7
  */
8
- export { suppressExperimentalWarnings } from "./utils/warning.js";
9
8
  export { ACTIONDOCK_VERSION } from "./version.js";
10
9
  export { createActionDock, connectActionDock, } from "./service/factory.js";
11
10
  export type { ActionDockService, ActionDockService as ActionDock, DiscoveryPort, ExecutionPort, RunsPort, EventsPort, RunEventSubscriptionOptions, ConfigPort, StatePort, CreateActionDockOptions, ConnectActionDockOptions, RemoteServiceOptions, ConfigValueView, ListRunsOptions, RunReadOptions, StateScopeOptions, } from "./service/types.js";
@@ -23,6 +22,8 @@ export { startActionDockServer, } from "./server/server.js";
23
22
  export type { ServerOptions, ServerOptions as ActionDockServerOptions, ActionDockServerInstance, } from "./server/types.js";
24
23
  export { initProject } from "./project/init.js";
25
24
  export { loadProjectConfig, findProjectRoot, } from "./project/loader.js";
25
+ export { inspectLocalPackageDirectory, validateLocalSkillPackages, isExplicitPath, } from "./project/skill-dir.js";
26
26
  export type { ProjectConfig, } from "./project/types.js";
27
+ export type { LocalSkillDirectory, LocalPackageEntry, } from "./project/skill-dir.js";
27
28
  export type { PackageGraph, } from "./catalog/graph.js";
28
29
  export { ActionDockError, ProcessError, PACKAGE_NOT_FOUND, ACTION_PACKAGE_VERSION_CONFLICT, ACTION_NOT_FOUND, ACTION_LOAD_FAILED, ACTION_FAILED, ACTION_TIMEOUT, ACTION_CANCELLED, INPUT_VALIDATION_FAILED, OUTPUT_VALIDATION_FAILED, INPUT_NOT_JSON, OUTPUT_NOT_JSON, ACTION_SUBRUN_LIMIT, ACTION_CALL_CYCLE, UNDECLARED_ACTION_DEPENDENCY, INVALID_ACTION_REF, IDEMPOTENCY_CONFLICT, EXECUTION_FAILED, UNHANDLED_EXECUTION_ERROR, RUN_REPOSITORY_UNAVAILABLE, RUN_PERSISTENCE_FAILED, STANDALONE_ASYNC_UNSUPPORTED, HOST_PROCESS_EXITED, EXECUTION_ABORTED, TIMEOUT, NETWORK_ERROR, CAPABILITY_UNAVAILABLE, STATE_KEY_NOT_FOUND, STORAGE_BUSY, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, PROCESS_OUTPUT_LIMIT, PROCESS_SPAWN_ERROR, PROCESS_CANCELLED, PROCESS_TIMEOUT, UNAUTHORIZED, NOT_FOUND, SERVER_ERROR, PACKAGE_FORBIDDEN, ACTION_FORBIDDEN, PLAYBOOK_NOT_FOUND, REQUEST_TOO_LARGE, ACTION_START_FAILED, ACTION_EXECUTION_ERROR, DATA_DIR_IN_USE, DATA_DIR_RECOVERY_REQUIRED, PROCESS_FAILED, MCP_TOOL_NAME_COLLISION, ACCESS_DENIED, INVALID_FLAT_ARGUMENT, INVALID_JSON, INVALID_JSON_LITERAL, INPUT_PATH_CONFLICT, FLAT_INPUT_LIMIT_EXCEEDED, INPUT_LIMIT_EXCEEDED, INPUT_POLICY_VIOLATION, INPUT_CONFLICT, INPUT_FILE_NOT_FOUND, INPUT_FILE_READ_FAILED, INVALID_ARGUMENT, PACKAGE_NOT_ALLOWED, INVALID_PACKAGE_ID, PATH_TRAVERSAL, AMBIGUOUS_STATE_KEY, RUN_NOT_FOUND, RUN_ALREADY_FINISHED, PROJECT_ROOT_NOT_FOUND, describeActionLoadFailure, createErrorEnvelope, resolveExecutionHint, } from "./errors.js";
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@
5
5
  * 平台大对象驱动与解析器工具移至特定子路径导出(./server, ./project, ./registry,
6
6
  * ./profile, ./graph, ./package),严格杜绝在根导出泄露内部执行与平台上下文机制。
7
7
  */
8
- export { suppressExperimentalWarnings } from "./utils/warning.js";
9
8
  // 1. 核心版本号单一事实源
10
9
  export { ACTIONDOCK_VERSION } from "./version.js";
11
10
  // 2. 统一服务门面与聚合契约接口
@@ -20,5 +19,6 @@ export { startActionDockServer, } from "./server/server.js";
20
19
  // 6. 核心工程辅助
21
20
  export { initProject } from "./project/init.js";
22
21
  export { loadProjectConfig, findProjectRoot, } from "./project/loader.js";
22
+ export { inspectLocalPackageDirectory, validateLocalSkillPackages, isExplicitPath, } from "./project/skill-dir.js";
23
23
  // 8. 统一错误模型与核心标准错误码常量
24
24
  export { ActionDockError, ProcessError, PACKAGE_NOT_FOUND, ACTION_PACKAGE_VERSION_CONFLICT, ACTION_NOT_FOUND, ACTION_LOAD_FAILED, ACTION_FAILED, ACTION_TIMEOUT, ACTION_CANCELLED, INPUT_VALIDATION_FAILED, OUTPUT_VALIDATION_FAILED, INPUT_NOT_JSON, OUTPUT_NOT_JSON, ACTION_SUBRUN_LIMIT, ACTION_CALL_CYCLE, UNDECLARED_ACTION_DEPENDENCY, INVALID_ACTION_REF, IDEMPOTENCY_CONFLICT, EXECUTION_FAILED, UNHANDLED_EXECUTION_ERROR, RUN_REPOSITORY_UNAVAILABLE, RUN_PERSISTENCE_FAILED, STANDALONE_ASYNC_UNSUPPORTED, HOST_PROCESS_EXITED, EXECUTION_ABORTED, TIMEOUT, NETWORK_ERROR, CAPABILITY_UNAVAILABLE, STATE_KEY_NOT_FOUND, STORAGE_BUSY, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, PROCESS_OUTPUT_LIMIT, PROCESS_SPAWN_ERROR, PROCESS_CANCELLED, PROCESS_TIMEOUT, UNAUTHORIZED, NOT_FOUND, SERVER_ERROR, PACKAGE_FORBIDDEN, ACTION_FORBIDDEN, PLAYBOOK_NOT_FOUND, REQUEST_TOO_LARGE, ACTION_START_FAILED, ACTION_EXECUTION_ERROR, DATA_DIR_IN_USE, DATA_DIR_RECOVERY_REQUIRED, PROCESS_FAILED, MCP_TOOL_NAME_COLLISION, ACCESS_DENIED, INVALID_FLAT_ARGUMENT, INVALID_JSON, INVALID_JSON_LITERAL, INPUT_PATH_CONFLICT, FLAT_INPUT_LIMIT_EXCEEDED, INPUT_LIMIT_EXCEEDED, INPUT_POLICY_VIOLATION, INPUT_CONFLICT, INPUT_FILE_NOT_FOUND, INPUT_FILE_READ_FAILED, INVALID_ARGUMENT, PACKAGE_NOT_ALLOWED, INVALID_PACKAGE_ID, PATH_TRAVERSAL, AMBIGUOUS_STATE_KEY, RUN_NOT_FOUND, RUN_ALREADY_FINISHED, PROJECT_ROOT_NOT_FOUND, describeActionLoadFailure, createErrorEnvelope, resolveExecutionHint, } from "./errors.js";
@@ -88,11 +88,22 @@ export declare function buildActionDescribePayload(spec: {
88
88
  * 扁平输入通用语法速查行列表。
89
89
  */
90
90
  export declare const ACTION_DESCRIBE_SYNTAX_REFERENCE: readonly string[];
91
+ /**
92
+ * 格式化 Action 详情的可选配置项。
93
+ */
94
+ export interface FormatActionDetailOptions {
95
+ /**
96
+ * 是否支持显式与声明式正文选择运行选项(普通 CLI 为 true,目录型 Standalone 默认为 false)。
97
+ * @default true
98
+ */
99
+ supportsTextField?: boolean;
100
+ }
91
101
  /**
92
102
  * 格式化渲染 Action 详情人类可读文本。
93
103
  * 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
94
104
  *
95
105
  * @param input Action 描述载荷或 Action 规范
106
+ * @param options 可选格式化配置项
96
107
  * @returns 格式化后的说明文本
97
108
  */
98
109
  export declare function formatActionDetail(input: ActionDescribePayload | (Partial<ActionSpec> & {
@@ -109,4 +120,4 @@ export declare function formatActionDetail(input: ActionDescribePayload | (Parti
109
120
  uses?: string[];
110
121
  entry?: string;
111
122
  [key: string]: any;
112
- }): string;
123
+ }, options?: FormatActionDetailOptions): string;
@@ -1,5 +1,6 @@
1
1
  import { buildCliInputAdviceV1 } from "./advice.js";
2
2
  import { isForbiddenActionInputPropertyName } from "./flat-predicates.js";
3
+ import { analyzeActionOutputContract } from "./output-contract.js";
3
4
  /**
4
5
  * 计算 Action 输入建议。
5
6
  *
@@ -134,9 +135,10 @@ export const ACTION_DESCRIBE_SYNTAX_REFERENCE = [
134
135
  * 普通 CLI 与 Standalone 统一复用该函数,避免长期排版漂移。
135
136
  *
136
137
  * @param input Action 描述载荷或 Action 规范
138
+ * @param options 可选格式化配置项
137
139
  * @returns 格式化后的说明文本
138
140
  */
139
- export function formatActionDetail(input) {
141
+ export function formatActionDetail(input, options) {
140
142
  const payload = "inputAdvice" in input && input.inputAdvice
141
143
  ? input
142
144
  : buildActionDescribePayload(input);
@@ -197,5 +199,57 @@ export function formatActionDetail(input) {
197
199
  lines.push(` - ${issue.path}: ${issue.code}`);
198
200
  }
199
201
  }
202
+ const supportsTextField = options?.supportsTextField ?? true;
203
+ const outputContract = analyzeActionOutputContract({
204
+ annotations: payload.annotations,
205
+ outputSchema: payload.outputSchema,
206
+ });
207
+ const isAnnotationInvalid = outputContract.diagnostics.some((d) => d.code === "INVALID_ANNOTATION");
208
+ lines.push("");
209
+ lines.push("Output Selection:");
210
+ if (supportsTextField) {
211
+ if (isAnnotationInvalid) {
212
+ lines.push(` Invalid annotation: ${outputContract.errors.join("; ")}`);
213
+ lines.push(" Default run: Synchronous execution will be rejected before invocation due to invalid annotation");
214
+ lines.push(" Full result: Pass '--json' to bypass default annotation and receive the complete structured envelope");
215
+ lines.push(" Explicit text field: Pass '--text-field <field>' to bypass default annotation and select a specific field");
216
+ }
217
+ else if (outputContract.declared && outputContract.textField) {
218
+ lines.push(` Default text field: ${outputContract.textField}`);
219
+ if (outputContract.errors.length > 0) {
220
+ lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
221
+ }
222
+ lines.push(` stdout: Raw text content of '${outputContract.textField}' (synchronous execution only)`);
223
+ lines.push(" stderr: Remaining fields as JSON metadata, diagnostics, and logs");
224
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
225
+ lines.push(" Override: Pass '--text-field <field>' to select another top-level string field");
226
+ }
227
+ else {
228
+ lines.push(" Default: Raw string for string results; formatted JSON for objects and arrays");
229
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
230
+ lines.push(" Custom field: Pass '--text-field <field>' to extract a specific top-level string field");
231
+ }
232
+ }
233
+ else {
234
+ if (isAnnotationInvalid) {
235
+ lines.push(` Invalid annotation: ${outputContract.errors.join("; ")} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
236
+ lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
237
+ lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
238
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
239
+ }
240
+ else if (outputContract.declared && outputContract.textField) {
241
+ lines.push(` Default text field: ${outputContract.textField} (note: ignored in standalone runtime; standalone extracts 'content', 'text', or 'message', falling back to formatted JSON)`);
242
+ if (outputContract.errors.length > 0) {
243
+ lines.push(` Contract conflict: ${outputContract.errors.join("; ")}`);
244
+ }
245
+ lines.push(" stdout: Raw string for scalars; extracts 'content', 'text', or 'message' from objects");
246
+ lines.push(" stderr: Remaining fields as metadata (for extracted fields), diagnostics, and logs");
247
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
248
+ }
249
+ else {
250
+ lines.push(" Default: Raw string for scalars; extracts 'content', 'text', or 'message' from objects (with metadata on stderr), falling back to formatted JSON");
251
+ lines.push(" Full result: Pass '--json' to receive the complete structured envelope");
252
+ }
253
+ }
200
254
  return lines.join("\n");
201
255
  }
@@ -16,6 +16,30 @@ export type NodeState = (typeof NodeState)[keyof typeof NodeState];
16
16
  export interface FlatMaterializerOptions {
17
17
  maxMaterializedSizeBytes?: number;
18
18
  }
19
+ /**
20
+ * 断言顶层字符串属性写入不与既有字段结构冲突。
21
+ *
22
+ * 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
23
+ * - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
24
+ * - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
25
+ * - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
26
+ *
27
+ * @param target 目标对象(仅检测自有属性,不触及原型链)
28
+ * @param field 待写入的顶层属性名
29
+ */
30
+ export declare function assertTopLevelLeafAvailable(target: object, field: string): void;
31
+ /**
32
+ * 校验物化后的最终入参值:结构合法性与实体化总大小上限。
33
+ *
34
+ * 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
35
+ * - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
36
+ * - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
37
+ * FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
38
+ *
39
+ * @param result 待校验的最终入参对象
40
+ * @param options 物化配置选项(可选 maxMaterializedSizeBytes)
41
+ */
42
+ export declare function assertMaterializedInputWithinLimits(result: JsonValue, options?: FlatMaterializerOptions): void;
19
43
  /**
20
44
  * 将扁平赋值表达式列表物化为标准 JSON 值对象。
21
45
  *
@@ -11,6 +11,56 @@ export const NodeState = {
11
11
  ARRAY: "ARRAY",
12
12
  VALUE: "VALUE",
13
13
  };
14
+ /**
15
+ * 断言顶层字符串属性写入不与既有字段结构冲突。
16
+ *
17
+ * 作为扁平赋值与 stdin 字段绑定共享的路径冲突判定单一事实源:
18
+ * - 重复赋值同名属性:DUPLICATE_ASSIGNMENT;
19
+ * - 既有同名对象容器:LEAF_CONTAINER_CONFLICT;
20
+ * - 既有同名数组容器:OBJECT_ARRAY_CONFLICT。
21
+ *
22
+ * @param target 目标对象(仅检测自有属性,不触及原型链)
23
+ * @param field 待写入的顶层属性名
24
+ */
25
+ export function assertTopLevelLeafAvailable(target, field) {
26
+ if (!Object.hasOwn(target, field))
27
+ return;
28
+ const existing = target[field];
29
+ if (Array.isArray(existing)) {
30
+ throw inputPathConflict(`Path conflict at "${field}": expected object but found array`, { path: field, reason: "OBJECT_ARRAY_CONFLICT" });
31
+ }
32
+ if (existing !== null && typeof existing === "object") {
33
+ throw inputPathConflict(`Path conflict at "${field}": cannot assign value to existing container (object)`, { path: field, reason: "LEAF_CONTAINER_CONFLICT" });
34
+ }
35
+ throw inputPathConflict(`Duplicate assignment to leaf path "${field}"`, { path: field, reason: "DUPLICATE_ASSIGNMENT" });
36
+ }
37
+ /**
38
+ * 校验物化后的最终入参值:结构合法性与实体化总大小上限。
39
+ *
40
+ * 作为扁平赋值与 stdin 字段绑定共享的最终校验单一事实源:
41
+ * - 结构非法或超深:FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_JSON_DEPTH / INVALID_JSON_VALUE);
42
+ * - 序列化后字节超出 maxMaterializedSizeBytes(默认 10MiB):
43
+ * FLAT_INPUT_LIMIT_EXCEEDED(MAX_MATERIALIZED_BYTES)。
44
+ *
45
+ * @param result 待校验的最终入参对象
46
+ * @param options 物化配置选项(可选 maxMaterializedSizeBytes)
47
+ */
48
+ export function assertMaterializedInputWithinLimits(result, options) {
49
+ const check = validateJsonValue(result);
50
+ if (!check.valid) {
51
+ throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
52
+ reason: check.code === "MAX_JSON_DEPTH"
53
+ ? "MAX_MATERIALIZED_JSON_DEPTH"
54
+ : "INVALID_JSON_VALUE",
55
+ });
56
+ }
57
+ const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
58
+ const serialized = JSON.stringify(result);
59
+ const byteLength = Buffer.byteLength(serialized, "utf8");
60
+ if (byteLength > maxMaterializedSizeBytes) {
61
+ throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
62
+ }
63
+ }
14
64
  function createNode(pathStr, state = NodeState.UNSET) {
15
65
  return {
16
66
  state,
@@ -159,19 +209,6 @@ export function materializeFlatInput(assignments, options) {
159
209
  }
160
210
  }
161
211
  const result = materializeNode(root);
162
- const check = validateJsonValue(result);
163
- if (!check.valid) {
164
- throw flatInputLimitExceeded(`Materialized input validation failed: ${check.reason}`, {
165
- reason: check.code === "MAX_JSON_DEPTH"
166
- ? "MAX_MATERIALIZED_JSON_DEPTH"
167
- : "INVALID_JSON_VALUE",
168
- });
169
- }
170
- const maxMaterializedSizeBytes = options?.maxMaterializedSizeBytes ?? DEFAULT_MAX_MATERIALIZED_SIZE_BYTES;
171
- const serialized = JSON.stringify(result);
172
- const byteLength = Buffer.byteLength(serialized, "utf8");
173
- if (byteLength > maxMaterializedSizeBytes) {
174
- throw flatInputLimitExceeded(`Materialized input size (${byteLength} bytes) exceeds limit (${maxMaterializedSizeBytes} bytes)`, { byteLength, maxMaterializedSizeBytes, reason: "MAX_MATERIALIZED_BYTES" });
175
- }
212
+ assertMaterializedInputWithinLimits(result, options);
176
213
  return result;
177
214
  }
@@ -10,4 +10,5 @@ export * from "./file-input.js";
10
10
  export * from "./stdin-input.js";
11
11
  export * from "./input-resolver.js";
12
12
  export * from "./advice.js";
13
+ export * from "./output-contract.js";
13
14
  export * from "./describe.js";
@@ -10,4 +10,5 @@ export * from "./file-input.js";
10
10
  export * from "./stdin-input.js";
11
11
  export * from "./input-resolver.js";
12
12
  export * from "./advice.js";
13
+ export * from "./output-contract.js";
13
14
  export * from "./describe.js";
@@ -16,6 +16,8 @@ export interface InputResolutionPolicy {
16
16
  sanitizeInputErrors?: boolean;
17
17
  /** 是否仅允许字节流输入(默认 false) */
18
18
  byteStreamOnly?: boolean;
19
+ /** 原始 stdin 文本模式:保留开头 BOM,UTF-8 编码失败视为读取失败(默认 false,仅作用于 stdinField) */
20
+ stdinRawText?: boolean;
19
21
  }
20
22
  /**
21
23
  * 从标准输入流中完整读取全部数据并转换为 UTF-8 字符串(保持历史接口兼容)。
@@ -30,6 +32,8 @@ export interface ResolveActionInputOptions {
30
32
  input?: string;
31
33
  inputFile?: string;
32
34
  flatArgs?: string[];
35
+ /** 将 stdin 完整原始文本绑定为指定顶层入参字段的字符串值(与 input / inputFile 互斥,可与 flatArgs 组合) */
36
+ stdinField?: string;
33
37
  stdin?: NodeJS.ReadableStream;
34
38
  flatOptions?: FlatParserOptions & FlatMaterializerOptions;
35
39
  policy?: InputResolutionPolicy;
@@ -51,11 +55,15 @@ export declare function parseJson(text: string, source: string, policy?: InputRe
51
55
  * - effectiveFlat: flatArgs 存在且 length > 0
52
56
  * - hasInlineJson: options.input !== undefined(即使为 "")
53
57
  * - hasFileJson: options.inputFile !== undefined(包含 "-")
54
- * - 最多一个为 true;若超过一个,抛出 INPUT_CONFLICT(reason: "MULTIPLE_INPUT_MODES")。
55
- * - 若三个均为 false,返回默认 {}。
58
+ * - hasStdinField: options.stdinField !== undefined
59
+ * - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
60
+ * - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
61
+ * 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
62
+ * - 若四者均为 false,返回默认 {}。
56
63
  * - flatArgs = [] 视为未指定。
57
64
  * - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
58
65
  * - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
66
+ * - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
59
67
  *
60
68
  * @param options 输入解析选项
61
69
  * @returns 解析后的 JSON 对象