ultra-builder-pro-cli 0.25.1 → 0.26.1

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 (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,365 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Perform bounded archive mutations relative to inherited directory fds."""
3
-
4
- import base64
5
- import json
6
- import os
7
- import stat
8
- import sys
9
-
10
-
11
- MAX_REQUEST_BYTES = 16 * 1024 * 1024
12
- MAX_DATA_BYTES = 8 * 1024 * 1024
13
- MAX_BASENAME_BYTES = 255
14
- ARCHIVE_FILES = frozenset((
15
- ".archive-intent.json",
16
- ".archive-rebind.json",
17
- "archive-summary.md",
18
- ))
19
-
20
- OPERATION_FIELDS = {
21
- "mkdir_dir": frozenset((
22
- "operation", "directory_fd", "directory_identity", "name",
23
- )),
24
- "write_atomic": frozenset((
25
- "operation", "directory_fd", "directory_identity", "name",
26
- "temp_name", "replace", "data_base64",
27
- )),
28
- "write_rebind_atomic": frozenset((
29
- "operation", "directory_fd", "directory_identity", "name",
30
- "temp_name", "replace", "data_base64",
31
- )),
32
- "unlink_regular": frozenset((
33
- "operation", "directory_fd", "directory_identity", "name",
34
- "allow_missing",
35
- )),
36
- "rename_dir": frozenset((
37
- "operation", "source_parent_fd", "destination_parent_fd",
38
- "guard_parent_fd", "source_parent_identity",
39
- "destination_parent_identity", "guard_parent_identity",
40
- "source_parent_name", "destination_parent_name", "source_name",
41
- "destination_name", "source_identity",
42
- )),
43
- }
44
-
45
-
46
- class MutationError(Exception):
47
- code = "ARCHIVE_PATH_UNSAFE"
48
-
49
-
50
- class ProtocolError(MutationError):
51
- code = "ARCHIVE_MUTATION_INVALID"
52
-
53
-
54
- class RuntimePrerequisiteError(MutationError):
55
- code = "ARCHIVE_RUNTIME_UNAVAILABLE"
56
-
57
-
58
- def identity(value):
59
- return {
60
- "dev": str(value.st_dev),
61
- "ino": str(value.st_ino),
62
- "mode": str(value.st_mode),
63
- }
64
-
65
-
66
- def require_identity_shape(value, label):
67
- if (
68
- not isinstance(value, dict)
69
- or set(value) != {"dev", "ino", "mode"}
70
- or any(
71
- not isinstance(value[field], str) or not value[field].isdigit()
72
- for field in ("dev", "ino", "mode")
73
- )
74
- ):
75
- raise ProtocolError(f"{label} has an invalid identity")
76
-
77
-
78
- def require_identity(actual, expected, label):
79
- if identity(actual) != expected:
80
- raise MutationError(f"{label} changed identity")
81
-
82
-
83
- def require_fd(value, label):
84
- if isinstance(value, bool) or not isinstance(value, int) or value < 3 or value > 5:
85
- raise ProtocolError(f"{label} is outside the inherited descriptor boundary")
86
-
87
-
88
- def require_basename(value, label):
89
- if (
90
- not isinstance(value, str)
91
- or not value
92
- or value in (".", "..")
93
- or "/" in value
94
- or "\\" in value
95
- or "\x00" in value
96
- or len(os.fsencode(value)) > MAX_BASENAME_BYTES
97
- ):
98
- raise ProtocolError(f"{label} must be one bounded basename")
99
-
100
-
101
- def require_platform_support():
102
- if os.name != "posix":
103
- raise RuntimePrerequisiteError("archive mutations require a POSIX Python runtime")
104
- required_dir_fd = (os.open, os.stat, os.rename, os.unlink, os.mkdir)
105
- if any(operation not in os.supports_dir_fd for operation in required_dir_fd):
106
- raise RuntimePrerequisiteError(
107
- "Python dir_fd operations required by archive mutations are unavailable"
108
- )
109
- if os.stat not in os.supports_follow_symlinks or not hasattr(os, "O_NOFOLLOW"):
110
- raise RuntimePrerequisiteError(
111
- "Python no-follow filesystem operations required by archive mutations are unavailable"
112
- )
113
-
114
-
115
- def validate_request(request):
116
- if not isinstance(request, dict):
117
- raise ProtocolError("archive mutation request must be an object")
118
- operation = request.get("operation")
119
- expected = OPERATION_FIELDS.get(operation)
120
- if expected is None or set(request) != expected:
121
- raise ProtocolError("archive mutation operation or fields are not allowlisted")
122
-
123
- for field in (name for name in expected if name.endswith("_fd")):
124
- require_fd(request[field], field)
125
- for field in (name for name in expected if name.endswith("_identity")):
126
- require_identity_shape(request[field], field)
127
- for field in (
128
- name for name in expected
129
- if name == "name" or name.endswith("_name")
130
- ):
131
- require_basename(request[field], field)
132
-
133
- if operation == "mkdir_dir" and request["name"] != "archive":
134
- raise ProtocolError("archive mkdir can create only the archive root")
135
- if operation in ("write_atomic", "unlink_regular") and request["name"] not in ARCHIVE_FILES:
136
- raise ProtocolError("archive file mutation target is not allowlisted")
137
- if operation in ("write_atomic", "write_rebind_atomic"):
138
- if type(request["replace"]) is not bool:
139
- raise ProtocolError("replace must be a boolean")
140
- if request["temp_name"] == request["name"]:
141
- raise ProtocolError("archive publication temp name must be distinct")
142
- encoded = request["data_base64"]
143
- if not isinstance(encoded, str):
144
- raise ProtocolError("archive publication data must be base64 text")
145
- try:
146
- data = base64.b64decode(encoded, validate=True)
147
- except (ValueError, TypeError) as error:
148
- raise ProtocolError("archive publication data is invalid base64") from error
149
- if len(data) > MAX_DATA_BYTES:
150
- raise ProtocolError("archive publication data exceeds the protocol limit")
151
- request["_data"] = data
152
- if operation == "unlink_regular" and type(request["allow_missing"]) is not bool:
153
- raise ProtocolError("allow_missing must be a boolean")
154
- if operation == "rename_dir":
155
- parent_names = {
156
- request["source_parent_name"],
157
- request["destination_parent_name"],
158
- }
159
- if parent_names != {"active", "archive"}:
160
- raise ProtocolError("archive transition parents must be active and archive")
161
- return operation
162
-
163
-
164
- def require_directory_fd(fd, expected, label):
165
- current = os.fstat(fd)
166
- if not stat.S_ISDIR(current.st_mode):
167
- raise MutationError(f"{label} is not a directory")
168
- require_identity(current, expected, label)
169
-
170
-
171
- def lstat_at(fd, name):
172
- try:
173
- return os.stat(name, dir_fd=fd, follow_symlinks=False)
174
- except FileNotFoundError:
175
- return None
176
-
177
-
178
- def require_canonical_directory(fd, name, expected, label):
179
- current = lstat_at(fd, name)
180
- if current is None or not stat.S_ISDIR(current.st_mode):
181
- raise MutationError(f"{label} is missing or unsafe")
182
- require_identity(current, expected, label)
183
-
184
-
185
- def write_all(fd, data):
186
- offset = 0
187
- while offset < len(data):
188
- written = os.write(fd, data[offset:])
189
- if written <= 0:
190
- raise MutationError("short archive publication write")
191
- offset += written
192
-
193
-
194
- def write_atomic(request):
195
- fd = request["directory_fd"]
196
- expected = request["directory_identity"]
197
- name = request["name"]
198
- temp_name = request["temp_name"]
199
- replace = request["replace"]
200
- require_directory_fd(fd, expected, "archive write directory")
201
- current = lstat_at(fd, name)
202
- if current is not None:
203
- if not replace or not stat.S_ISREG(current.st_mode):
204
- raise MutationError("archive publication target is not safely replaceable")
205
- flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
206
- temp_fd = None
207
- published = False
208
- try:
209
- temp_fd = os.open(temp_name, flags, 0o600, dir_fd=fd)
210
- write_all(temp_fd, request["_data"])
211
- os.fsync(temp_fd)
212
- os.close(temp_fd)
213
- temp_fd = None
214
- require_directory_fd(fd, expected, "archive write directory")
215
- os.rename(temp_name, name, src_dir_fd=fd, dst_dir_fd=fd)
216
- published = True
217
- require_directory_fd(fd, expected, "archive write directory")
218
- final = lstat_at(fd, name)
219
- if final is None or not stat.S_ISREG(final.st_mode):
220
- raise MutationError("archive publication did not produce a regular file")
221
- finally:
222
- if temp_fd is not None:
223
- os.close(temp_fd)
224
- if not published:
225
- try:
226
- os.unlink(temp_name, dir_fd=fd)
227
- except FileNotFoundError:
228
- pass
229
-
230
-
231
- def unlink_regular(request):
232
- fd = request["directory_fd"]
233
- expected = request["directory_identity"]
234
- require_directory_fd(fd, expected, "archive cleanup directory")
235
- current = lstat_at(fd, request["name"])
236
- if current is None:
237
- if request["allow_missing"]:
238
- return
239
- raise MutationError("archive cleanup target is missing")
240
- if not stat.S_ISREG(current.st_mode):
241
- raise MutationError("archive cleanup target is not a regular file")
242
- os.unlink(request["name"], dir_fd=fd)
243
- require_directory_fd(fd, expected, "archive cleanup directory")
244
- if lstat_at(fd, request["name"]) is not None:
245
- raise MutationError("archive cleanup target still exists")
246
-
247
-
248
- def rename_dir(request):
249
- source_parent_fd = request["source_parent_fd"]
250
- destination_parent_fd = request["destination_parent_fd"]
251
- guard_parent_fd = request["guard_parent_fd"]
252
- require_directory_fd(
253
- source_parent_fd,
254
- request["source_parent_identity"],
255
- "archive source parent",
256
- )
257
- require_directory_fd(
258
- destination_parent_fd,
259
- request["destination_parent_identity"],
260
- "archive destination parent",
261
- )
262
- require_directory_fd(
263
- guard_parent_fd,
264
- request["guard_parent_identity"],
265
- "archive guard parent",
266
- )
267
- require_canonical_directory(
268
- guard_parent_fd,
269
- request["source_parent_name"],
270
- request["source_parent_identity"],
271
- "canonical archive source parent",
272
- )
273
- require_canonical_directory(
274
- guard_parent_fd,
275
- request["destination_parent_name"],
276
- request["destination_parent_identity"],
277
- "canonical archive destination parent",
278
- )
279
- source = lstat_at(source_parent_fd, request["source_name"])
280
- if source is None or not stat.S_ISDIR(source.st_mode):
281
- raise MutationError("archive source directory is missing or unsafe")
282
- require_identity(source, request["source_identity"], "archive source directory")
283
- if lstat_at(destination_parent_fd, request["destination_name"]) is not None:
284
- raise MutationError("archive destination already exists")
285
- os.rename(
286
- request["source_name"],
287
- request["destination_name"],
288
- src_dir_fd=source_parent_fd,
289
- dst_dir_fd=destination_parent_fd,
290
- )
291
- require_directory_fd(
292
- source_parent_fd,
293
- request["source_parent_identity"],
294
- "archive source parent",
295
- )
296
- require_directory_fd(
297
- destination_parent_fd,
298
- request["destination_parent_identity"],
299
- "archive destination parent",
300
- )
301
- if lstat_at(source_parent_fd, request["source_name"]) is not None:
302
- raise MutationError("archive source still exists after transition")
303
- destination = lstat_at(destination_parent_fd, request["destination_name"])
304
- if destination is None or not stat.S_ISDIR(destination.st_mode):
305
- raise MutationError("archive destination is missing after transition")
306
- require_identity(destination, request["source_identity"], "archive destination directory")
307
-
308
-
309
- def mkdir_dir(request):
310
- fd = request["directory_fd"]
311
- expected = request["directory_identity"]
312
- require_directory_fd(fd, expected, "archive mkdir parent")
313
- current = lstat_at(fd, request["name"])
314
- if current is None:
315
- os.mkdir(request["name"], mode=0o700, dir_fd=fd)
316
- current = lstat_at(fd, request["name"])
317
- if current is None or not stat.S_ISDIR(current.st_mode):
318
- raise MutationError("archive directory cannot be created safely")
319
- require_directory_fd(fd, expected, "archive mkdir parent")
320
-
321
-
322
- OPERATIONS = {
323
- "write_atomic": write_atomic,
324
- "write_rebind_atomic": write_atomic,
325
- "unlink_regular": unlink_regular,
326
- "rename_dir": rename_dir,
327
- "mkdir_dir": mkdir_dir,
328
- }
329
-
330
-
331
- def read_request():
332
- raw = sys.stdin.buffer.read(MAX_REQUEST_BYTES + 1)
333
- if len(raw) > MAX_REQUEST_BYTES:
334
- raise ProtocolError("archive mutation request exceeds the protocol limit")
335
- try:
336
- return json.loads(raw.decode("utf-8"))
337
- except (UnicodeDecodeError, json.JSONDecodeError) as error:
338
- raise ProtocolError("archive mutation request is not valid UTF-8 JSON") from error
339
-
340
-
341
- def main():
342
- request = read_request()
343
- operation_name = validate_request(request)
344
- require_platform_support()
345
- OPERATIONS[operation_name](request)
346
- json.dump({"ok": True}, sys.stdout)
347
- sys.stdout.write("\n")
348
-
349
-
350
- if __name__ == "__main__":
351
- try:
352
- main()
353
- except Exception as error: # fail closed at the process boundary
354
- code = error.code if isinstance(error, MutationError) else "ARCHIVE_PATH_UNSAFE"
355
- json.dump(
356
- {
357
- "ok": False,
358
- "code": code,
359
- "error": type(error).__name__,
360
- "message": str(error),
361
- },
362
- sys.stderr,
363
- )
364
- sys.stderr.write("\n")
365
- sys.exit(3 if code == "ARCHIVE_RUNTIME_UNAVAILABLE" else 2)