ultra-builder-pro-cli 0.25.1 → 0.26.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.
- package/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +40 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- 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)
|