dagnam 0.7.0__py3-none-any.whl
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.
- dagnam/NOTICE +8 -0
- dagnam/__init__.py +220 -0
- dagnam/_agent/__init__.py +11 -0
- dagnam/_agent/claude/agents/dagnam-runner.md +16 -0
- dagnam/_agent/claude/hooks/guard.json +15 -0
- dagnam/_agent/claude/plugin.json +8 -0
- dagnam/_agent/codex/agents/openai.yaml +4 -0
- dagnam/_agent/codex/hooks.json +7 -0
- dagnam/_agent/guardhook.py +74 -0
- dagnam/_agent/install.py +274 -0
- dagnam/_agent/runner.py +137 -0
- dagnam/_agent/skill/SKILL.md +83 -0
- dagnam/_agent/skill/reference/account.md +24 -0
- dagnam/_agent/skill/reference/cache.md +20 -0
- dagnam/_agent/skill/reference/codegen.md +32 -0
- dagnam/_agent/skill/reference/datasets.md +26 -0
- dagnam/_agent/skill/reference/deployments.md +31 -0
- dagnam/_agent/skill/reference/hub.md +29 -0
- dagnam/_agent/skill/reference/inference.md +21 -0
- dagnam/_agent/skill/reference/projects.md +35 -0
- dagnam/_agent/skill/reference/training.md +38 -0
- dagnam/_agent/skill/reference/troubleshooting.md +42 -0
- dagnam/_agent/skill/scripts/plan.py +14 -0
- dagnam/_agent/skill/scripts/watch_training.py +14 -0
- dagnam/_contracts/__init__.py +29 -0
- dagnam/_contracts/_architecture.py +36 -0
- dagnam/_contracts/_interpret.py +342 -0
- dagnam/_contracts/_schema.py +39 -0
- dagnam/_contracts/component-schema.json +2035 -0
- dagnam/_contracts/normalize.py +142 -0
- dagnam/_contracts/param-diagnostics.json +1277 -0
- dagnam/_contracts/validation-corpus.json +37391 -0
- dagnam/_core/__init__.py +27 -0
- dagnam/_core/_logging.py +88 -0
- dagnam/_core/_retry.py +263 -0
- dagnam/_core/aio/__init__.py +32 -0
- dagnam/_core/aio/account.py +310 -0
- dagnam/_core/aio/base.py +256 -0
- dagnam/_core/aio/checkpoints.py +90 -0
- dagnam/_core/aio/codegen.py +142 -0
- dagnam/_core/aio/datasets.py +238 -0
- dagnam/_core/aio/deployments.py +316 -0
- dagnam/_core/aio/hub.py +221 -0
- dagnam/_core/aio/inference.py +127 -0
- dagnam/_core/aio/projects.py +273 -0
- dagnam/_core/aio/training.py +411 -0
- dagnam/_core/auth.py +112 -0
- dagnam/_core/client/__init__.py +32 -0
- dagnam/_core/client/account.py +314 -0
- dagnam/_core/client/base.py +492 -0
- dagnam/_core/client/checkpoints.py +76 -0
- dagnam/_core/client/codegen.py +167 -0
- dagnam/_core/client/common.py +368 -0
- dagnam/_core/client/datasets.py +425 -0
- dagnam/_core/client/deployments.py +311 -0
- dagnam/_core/client/hub.py +234 -0
- dagnam/_core/client/inference.py +125 -0
- dagnam/_core/client/projects.py +297 -0
- dagnam/_core/client/training.py +348 -0
- dagnam/_core/config.py +138 -0
- dagnam/_core/exceptions.py +169 -0
- dagnam/_core/lro.py +243 -0
- dagnam/_core/metrics_uploader.py +184 -0
- dagnam/_core/naming.py +85 -0
- dagnam/_core/resolver.py +23 -0
- dagnam/_core/sse.py +304 -0
- dagnam/_types.py +166 -0
- dagnam/aio.py +11 -0
- dagnam/cli/__init__.py +8 -0
- dagnam/cli/_parser.py +209 -0
- dagnam/cli/account.py +400 -0
- dagnam/cli/account_data.py +85 -0
- dagnam/cli/account_keys.py +145 -0
- dagnam/cli/account_profile.py +206 -0
- dagnam/cli/account_security.py +167 -0
- dagnam/cli/account_settings.py +186 -0
- dagnam/cli/agent.py +85 -0
- dagnam/cli/cache.py +119 -0
- dagnam/cli/checkpoint.py +111 -0
- dagnam/cli/codegen.py +149 -0
- dagnam/cli/common.py +444 -0
- dagnam/cli/dataset.py +438 -0
- dagnam/cli/deployment.py +460 -0
- dagnam/cli/errors.py +418 -0
- dagnam/cli/hub.py +353 -0
- dagnam/cli/inference.py +161 -0
- dagnam/cli/login.py +175 -0
- dagnam/cli/main.py +139 -0
- dagnam/cli/presentation.py +112 -0
- dagnam/cli/project.py +469 -0
- dagnam/cli/register.py +99 -0
- dagnam/cli/training.py +598 -0
- dagnam/data/__init__.py +1 -0
- dagnam/data/_polars_utils.py +53 -0
- dagnam/data/cache.py +370 -0
- dagnam/data/dataset/__init__.py +7 -0
- dagnam/data/dataset/_typing.py +86 -0
- dagnam/data/dataset/base.py +466 -0
- dagnam/data/dataset/hooks.py +186 -0
- dagnam/data/dataset/to_flax.py +499 -0
- dagnam/data/dataset/to_polars.py +64 -0
- dagnam/data/dataset/to_pytorch.py +371 -0
- dagnam/data/dataset/to_tensorflow.py +471 -0
- dagnam/data/load.py +276 -0
- dagnam/data/loaders/__init__.py +49 -0
- dagnam/data/loaders/audio/__init__.py +17 -0
- dagnam/data/loaders/audio/dataset.py +192 -0
- dagnam/data/loaders/audio/io.py +137 -0
- dagnam/data/loaders/audio/transforms.py +323 -0
- dagnam/data/loaders/csv.py +218 -0
- dagnam/data/loaders/flax.py +157 -0
- dagnam/data/loaders/image_folder.py +406 -0
- dagnam/data/loaders/json_array.py +10 -0
- dagnam/data/loaders/media.py +402 -0
- dagnam/data/loaders/system/__init__.py +15 -0
- dagnam/data/loaders/system/bound_dataset.py +89 -0
- dagnam/data/loaders/system/column_store.py +68 -0
- dagnam/data/loaders/system/common.py +7 -0
- dagnam/data/loaders/system/decoders/__init__.py +28 -0
- dagnam/data/loaders/system/decoders/_helpers.py +109 -0
- dagnam/data/loaders/system/decoders/array.py +42 -0
- dagnam/data/loaders/system/decoders/audio_folder.py +89 -0
- dagnam/data/loaders/system/decoders/base.py +23 -0
- dagnam/data/loaders/system/decoders/image_folder.py +47 -0
- dagnam/data/loaders/system/decoders/image_mask_folder.py +72 -0
- dagnam/data/loaders/system/decoders/tabular.py +30 -0
- dagnam/data/loaders/system/decoders/text.py +46 -0
- dagnam/data/loaders/system/dispatch.py +285 -0
- dagnam/data/loaders/system/transform_executor.py +127 -0
- dagnam/data/loaders/text_lm.py +40 -0
- dagnam/data/loaders/tf.py +78 -0
- dagnam/data/loaders/torch_utils.py +31 -0
- dagnam/exceptions.py +66 -0
- dagnam/py.typed +0 -0
- dagnam/resources/__init__.py +32 -0
- dagnam/resources/account.py +369 -0
- dagnam/resources/checkpoints.py +151 -0
- dagnam/resources/codegen.py +176 -0
- dagnam/resources/datasets.py +177 -0
- dagnam/resources/deployments.py +564 -0
- dagnam/resources/hub.py +578 -0
- dagnam/resources/inference.py +120 -0
- dagnam/resources/projects.py +471 -0
- dagnam/resources/studio.py +36 -0
- dagnam/resources/training.py +434 -0
- dagnam/training.py +441 -0
- dagnam/training_attach.py +220 -0
- dagnam-0.7.0.dist-info/METADATA +557 -0
- dagnam-0.7.0.dist-info/RECORD +153 -0
- dagnam-0.7.0.dist-info/WHEEL +4 -0
- dagnam-0.7.0.dist-info/entry_points.txt +2 -0
- dagnam-0.7.0.dist-info/licenses/LICENSE +201 -0
- dagnam-0.7.0.dist-info/licenses/NOTICE +8 -0
dagnam/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dagnam
|
|
2
|
+
Copyright 2026 Dagnam.AI
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Dagnam.AI.
|
|
5
|
+
|
|
6
|
+
Dagnam.AI, Dagnam, and related names are trademarks or service marks of
|
|
7
|
+
Dagnam.AI. The Apache License 2.0 does not grant trademark rights except as
|
|
8
|
+
required for describing the origin of this work.
|
dagnam/__init__.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""dagnam - Python client library for Dagnam.AI datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging as _logging
|
|
6
|
+
|
|
7
|
+
# Attach a NullHandler before any subpackage import so a DEBUG log emitted while
|
|
8
|
+
# dagnam.* wires itself up never falls through to logging.lastResort (the
|
|
9
|
+
# idiomatic PyPI-library logging contract — the app configures logging, not us).
|
|
10
|
+
# This one non-import statement must precede all imports below, so E402 is
|
|
11
|
+
# ignored file-wide in pyproject.toml with a documented reason.
|
|
12
|
+
_logging.getLogger("dagnam").addHandler(_logging.NullHandler())
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from dagnam._core._logging import enable_debug_logging
|
|
17
|
+
from dagnam._core.auth import configure, get_api_key, get_api_url
|
|
18
|
+
from dagnam._core.client import DagnamClient
|
|
19
|
+
from dagnam._core.config import get_config_value
|
|
20
|
+
from dagnam._core.lro import LongRunningOperation
|
|
21
|
+
from dagnam.data.cache import (
|
|
22
|
+
compute_file_checksum,
|
|
23
|
+
evict_lru,
|
|
24
|
+
get_cache_dir,
|
|
25
|
+
is_cached,
|
|
26
|
+
load_metadata,
|
|
27
|
+
save_checksum,
|
|
28
|
+
save_metadata,
|
|
29
|
+
touch_cache,
|
|
30
|
+
)
|
|
31
|
+
from dagnam.exceptions import (
|
|
32
|
+
APIError,
|
|
33
|
+
ArchitectureValidationError,
|
|
34
|
+
ArchitectureVersionNotFoundError,
|
|
35
|
+
AuthError,
|
|
36
|
+
CheckpointError,
|
|
37
|
+
CheckpointNotFoundError,
|
|
38
|
+
ChecksumError,
|
|
39
|
+
CodegenError,
|
|
40
|
+
CodegenValidationError,
|
|
41
|
+
DagnamError,
|
|
42
|
+
DatasetNotFoundError,
|
|
43
|
+
DeploymentNotFoundError,
|
|
44
|
+
DeploymentStateError,
|
|
45
|
+
DeploymentValidationError,
|
|
46
|
+
HubError,
|
|
47
|
+
HubModelNotFoundError,
|
|
48
|
+
LROFailedError,
|
|
49
|
+
LROTimeoutError,
|
|
50
|
+
ProjectNotFoundError,
|
|
51
|
+
QuotaExceededError,
|
|
52
|
+
ResponseError,
|
|
53
|
+
StreamError,
|
|
54
|
+
TaskNotFoundError,
|
|
55
|
+
TrainingJobNotFoundError,
|
|
56
|
+
UploadError,
|
|
57
|
+
)
|
|
58
|
+
from dagnam.resources import account, codegen, datasets, deployments, hub, projects, studio
|
|
59
|
+
from dagnam.resources.checkpoints import download_checkpoint
|
|
60
|
+
from dagnam.resources.datasets import (
|
|
61
|
+
delete_dataset,
|
|
62
|
+
preview_dataset,
|
|
63
|
+
update_dataset,
|
|
64
|
+
update_dataset_roles,
|
|
65
|
+
)
|
|
66
|
+
from dagnam.resources.inference import (
|
|
67
|
+
deployment_health,
|
|
68
|
+
inference,
|
|
69
|
+
inference_batch,
|
|
70
|
+
inference_schema,
|
|
71
|
+
inference_stream,
|
|
72
|
+
)
|
|
73
|
+
from dagnam.resources.projects import (
|
|
74
|
+
download_project_thumbnail,
|
|
75
|
+
upload_project_thumbnail,
|
|
76
|
+
)
|
|
77
|
+
from dagnam.resources.training import (
|
|
78
|
+
TrainingEvent,
|
|
79
|
+
allowed_strategies,
|
|
80
|
+
cancel_training_job,
|
|
81
|
+
create_training_job,
|
|
82
|
+
delete_training_jobs,
|
|
83
|
+
download_code,
|
|
84
|
+
download_dag,
|
|
85
|
+
estimate_resources,
|
|
86
|
+
get_training_job,
|
|
87
|
+
list_training_jobs,
|
|
88
|
+
restart,
|
|
89
|
+
restore_checkpoint,
|
|
90
|
+
stream_training,
|
|
91
|
+
training_logs,
|
|
92
|
+
training_metrics,
|
|
93
|
+
training_metrics_summary,
|
|
94
|
+
)
|
|
95
|
+
from dagnam.training import (
|
|
96
|
+
init,
|
|
97
|
+
report_error,
|
|
98
|
+
report_log,
|
|
99
|
+
report_metric,
|
|
100
|
+
report_progress,
|
|
101
|
+
report_system,
|
|
102
|
+
write_training_state,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
__version__ = "0.7.0"
|
|
106
|
+
|
|
107
|
+
if TYPE_CHECKING:
|
|
108
|
+
# Declared for type checkers and ``__all__``; loaded lazily at runtime via
|
|
109
|
+
# ``__getattr__`` (see ``_LAZY_EXPORTS``) to keep import time low.
|
|
110
|
+
from dagnam._core.aio import AsyncDagnamClient
|
|
111
|
+
from dagnam.data.dataset import DagnamDataset
|
|
112
|
+
from dagnam.data.load import load_dataset
|
|
113
|
+
|
|
114
|
+
_LAZY_EXPORTS = {
|
|
115
|
+
"AsyncDagnamClient": ("dagnam._core.aio", "AsyncDagnamClient"),
|
|
116
|
+
"DagnamDataset": ("dagnam.data.dataset", "DagnamDataset"),
|
|
117
|
+
"load_dataset": ("dagnam.data.load", "load_dataset"),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"APIError",
|
|
122
|
+
"ArchitectureValidationError",
|
|
123
|
+
"ArchitectureVersionNotFoundError",
|
|
124
|
+
"AsyncDagnamClient",
|
|
125
|
+
"AuthError",
|
|
126
|
+
"CheckpointError",
|
|
127
|
+
"CheckpointNotFoundError",
|
|
128
|
+
"ChecksumError",
|
|
129
|
+
"CodegenError",
|
|
130
|
+
"CodegenValidationError",
|
|
131
|
+
"DagnamClient",
|
|
132
|
+
"DagnamDataset",
|
|
133
|
+
"DagnamError",
|
|
134
|
+
"DatasetNotFoundError",
|
|
135
|
+
"DeploymentNotFoundError",
|
|
136
|
+
"DeploymentStateError",
|
|
137
|
+
"DeploymentValidationError",
|
|
138
|
+
"HubError",
|
|
139
|
+
"HubModelNotFoundError",
|
|
140
|
+
"LROFailedError",
|
|
141
|
+
"LROTimeoutError",
|
|
142
|
+
"LongRunningOperation",
|
|
143
|
+
"ProjectNotFoundError",
|
|
144
|
+
"QuotaExceededError",
|
|
145
|
+
"ResponseError",
|
|
146
|
+
"StreamError",
|
|
147
|
+
"TaskNotFoundError",
|
|
148
|
+
"TrainingEvent",
|
|
149
|
+
"TrainingJobNotFoundError",
|
|
150
|
+
"UploadError",
|
|
151
|
+
"__version__",
|
|
152
|
+
"account",
|
|
153
|
+
"allowed_strategies",
|
|
154
|
+
"cancel_training_job",
|
|
155
|
+
"codegen",
|
|
156
|
+
"compute_file_checksum",
|
|
157
|
+
"configure",
|
|
158
|
+
"create_training_job",
|
|
159
|
+
"datasets",
|
|
160
|
+
"delete_dataset",
|
|
161
|
+
"delete_training_jobs",
|
|
162
|
+
"deployment_health",
|
|
163
|
+
"deployments",
|
|
164
|
+
"download_checkpoint",
|
|
165
|
+
"download_code",
|
|
166
|
+
"download_dag",
|
|
167
|
+
"download_project_thumbnail",
|
|
168
|
+
"enable_debug_logging",
|
|
169
|
+
"estimate_resources",
|
|
170
|
+
"evict_lru",
|
|
171
|
+
"get_api_key",
|
|
172
|
+
"get_api_url",
|
|
173
|
+
"get_cache_dir",
|
|
174
|
+
"get_config_value",
|
|
175
|
+
"get_training_job",
|
|
176
|
+
"hub",
|
|
177
|
+
"inference",
|
|
178
|
+
"inference_batch",
|
|
179
|
+
"inference_schema",
|
|
180
|
+
"inference_stream",
|
|
181
|
+
"init",
|
|
182
|
+
"is_cached",
|
|
183
|
+
"list_training_jobs",
|
|
184
|
+
"load_dataset",
|
|
185
|
+
"load_metadata",
|
|
186
|
+
"preview_dataset",
|
|
187
|
+
"projects",
|
|
188
|
+
"report_error",
|
|
189
|
+
"report_log",
|
|
190
|
+
"report_metric",
|
|
191
|
+
"report_progress",
|
|
192
|
+
"report_system",
|
|
193
|
+
"restart",
|
|
194
|
+
"restore_checkpoint",
|
|
195
|
+
"save_checksum",
|
|
196
|
+
"save_metadata",
|
|
197
|
+
"stream_training",
|
|
198
|
+
"studio",
|
|
199
|
+
"touch_cache",
|
|
200
|
+
"training_logs",
|
|
201
|
+
"training_metrics",
|
|
202
|
+
"training_metrics_summary",
|
|
203
|
+
"update_dataset",
|
|
204
|
+
"update_dataset_roles",
|
|
205
|
+
"upload_project_thumbnail",
|
|
206
|
+
"write_training_state",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def __getattr__(name: str) -> Any:
|
|
211
|
+
"""Load optional/heavy public exports only when callers request them."""
|
|
212
|
+
if name not in _LAZY_EXPORTS:
|
|
213
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
214
|
+
|
|
215
|
+
from importlib import import_module
|
|
216
|
+
|
|
217
|
+
module_name, attr_name = _LAZY_EXPORTS[name]
|
|
218
|
+
value = getattr(import_module(module_name), attr_name)
|
|
219
|
+
globals()[name] = value
|
|
220
|
+
return value
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Agent-integration assets and installer.
|
|
2
|
+
|
|
3
|
+
This private package bundles the cross-platform Agent Skill (``skill/``) plus the
|
|
4
|
+
Claude Code and Codex adapter files (``claude/``, ``codex/``), and the importable
|
|
5
|
+
logic that powers them (``install.py``, ``runner.py``, ``guardhook.py``). It ships
|
|
6
|
+
as package data so ``dagnam agent install`` can locate it via ``importlib.resources``
|
|
7
|
+
after a pip install.
|
|
8
|
+
|
|
9
|
+
It must never import ``dagnam.cli`` (the CLI depends on this package, not the
|
|
10
|
+
reverse); the installed version is read via ``importlib.metadata`` instead.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dagnam-runner
|
|
3
|
+
description: Runs long Dagnam train->watch->eval->deploy loops in an isolated context. Use when a training job or deployment must be driven to completion so its SSE metric stream and long-running-operation polling stay off the main conversation.
|
|
4
|
+
tools: Bash, Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You drive long-running Dagnam operations to completion in isolation.
|
|
8
|
+
|
|
9
|
+
- Stream training with `python <skill_dir>/scripts/watch_training.py <job_id>` (or
|
|
10
|
+
`dagnam stream <job_id> --json`) and report only the final outcome
|
|
11
|
+
(`complete` / `failed` / `cancelled`) plus a one-line metric summary — never paste the
|
|
12
|
+
full event stream back.
|
|
13
|
+
- Poll long-running operations with the SDK: `op.wait(timeout=...).result()`.
|
|
14
|
+
- Honor the GUARDRAIL: never start a costly action yourself (create a training job, create or
|
|
15
|
+
delete a deployment, delete a project/job, or publish to the hub). If the loop reaches such a
|
|
16
|
+
step, stop and return control with the proposed plan for the user to confirm.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dagnam",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Drive the Dagnam platform (datasets, projects, codegen, training, deployments, inference, hub) from Claude Code. Pairs with the `dagnam` skill installed at ~/.claude/skills/dagnam.",
|
|
5
|
+
"author": "Dagnam.AI",
|
|
6
|
+
"agents": ["./agents/dagnam-runner.md"],
|
|
7
|
+
"hooks": "./hooks/guard.json"
|
|
8
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Cross-platform PreToolUse hook: deny un-confirmed costly Dagnam commands.
|
|
2
|
+
|
|
3
|
+
Reads the harness hook event (JSON on stdin) and, if the Bash command runs a costly
|
|
4
|
+
Dagnam action (deployments create/delete, training create/delete, projects delete,
|
|
5
|
+
hub publish, or a public visibility flag) WITHOUT an explicit ``DAGNAM_CONFIRM=1``
|
|
6
|
+
prefix, emits a deny decision. Both the CLI verb shape (``dagnam deployments create``)
|
|
7
|
+
and the Python SDK shape (``python -c "import dagnam; dagnam.deployments.create(...)"``,
|
|
8
|
+
which ``SKILL.md`` recommends) are matched.
|
|
9
|
+
|
|
10
|
+
This hook is **non-authoritative, best-effort defense-in-depth ONLY.** It has known
|
|
11
|
+
blind spots -- command obfuscation (base64/eval, string-built attribute names),
|
|
12
|
+
aliasing, imports under a different module name, and any non-Bash tool the agent
|
|
13
|
+
might use -- and it fails OPEN on any error so it can never wedge the agent. The
|
|
14
|
+
authoritative gate is the ``SKILL.md`` behavioral rule plus server-side confirmation
|
|
15
|
+
enforcement; this hook only hardens them. The ``visibility=public`` heuristic is
|
|
16
|
+
deliberately broad and will occasionally deny a benign public create/upload -- an
|
|
17
|
+
accepted false positive, since a denial only asks the operator to re-run with a
|
|
18
|
+
``DAGNAM_CONFIRM=1`` prefix.
|
|
19
|
+
|
|
20
|
+
Prints to stdout by design (the hook protocol), so it is T20-exempt.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
# Costly actions in BOTH the CLI verb shape and the Python SDK shape. re.DOTALL so a
|
|
30
|
+
# newline between ``dagnam`` and the action (a multi-line or line-continuation
|
|
31
|
+
# command) cannot hide it from the deny check.
|
|
32
|
+
_COSTLY = re.compile(
|
|
33
|
+
r"\bdagnam\b.*\b(?:"
|
|
34
|
+
r"deployments?\s+(?:create|delete)" # CLI: dagnam deployments create
|
|
35
|
+
r"|training\s+(?:create|delete)"
|
|
36
|
+
r"|projects?\s+delete"
|
|
37
|
+
r"|deployments?\s*\.\s*(?:create|delete)" # SDK: dagnam.deployments.create(
|
|
38
|
+
r"|training\s*\.\s*(?:create|delete)" # SDK: dagnam.training.create(
|
|
39
|
+
r"|projects?\s*\.\s*delete"
|
|
40
|
+
r"|hub\s*\.\s*create" # SDK: publish to the hub
|
|
41
|
+
r"|create_training_job"
|
|
42
|
+
r"|visibility\s*=\s*[\\'\"]*public" # any public create/upload (quotes/escapes optional)
|
|
43
|
+
r")",
|
|
44
|
+
re.DOTALL,
|
|
45
|
+
)
|
|
46
|
+
# Anchor the confirm token to a command-leading env assignment (start of line,
|
|
47
|
+
# or the first token after a shell separator). Matching it ANYWHERE let a
|
|
48
|
+
# hostile string echoed into an argument -- e.g. a dataset description
|
|
49
|
+
# "... DAGNAM_CONFIRM=1" surfaced via prompt injection -- silently satisfy the
|
|
50
|
+
# gate. It must be a real prefix the operator typed, not incidental text.
|
|
51
|
+
_CONFIRMED = re.compile(r"(?:^|[;&|\n]\s*)DAGNAM_CONFIRM=1\b")
|
|
52
|
+
_DENY_REASON = (
|
|
53
|
+
"Dagnam guardrail: this is a costly/irreversible action. Show the user an execution "
|
|
54
|
+
"plan (scripts/plan.py, or codegen.validate/preview + account.entitlements) and get "
|
|
55
|
+
"explicit confirmation, then re-run the command prefixed with DAGNAM_CONFIRM=1."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
"""Entry point wired by claude/hooks/guard.json and codex/hooks.json."""
|
|
61
|
+
try:
|
|
62
|
+
event = json.load(sys.stdin)
|
|
63
|
+
command = str(event.get("tool_input", {}).get("command", ""))
|
|
64
|
+
except (json.JSONDecodeError, AttributeError, ValueError):
|
|
65
|
+
return 0 # fail-open
|
|
66
|
+
if _COSTLY.search(command) and not _CONFIRMED.search(command):
|
|
67
|
+
json.dump(
|
|
68
|
+
{"permissionDecision": "deny", "permissionDecisionReason": _DENY_REASON}, sys.stdout
|
|
69
|
+
)
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
raise SystemExit(main())
|
dagnam/_agent/install.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Detect installed agent harnesses and wire the Dagnam skill into each.
|
|
2
|
+
|
|
3
|
+
Pure logic: returns dataclasses describing planned/performed actions; it never prints
|
|
4
|
+
or prompts (``dagnam.cli.agent`` owns presentation). It must not import ``dagnam.cli``;
|
|
5
|
+
the installed version is read via ``importlib.metadata``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import shutil
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
Harness = str # "claude" | "codex"
|
|
19
|
+
|
|
20
|
+
# The guard hook must run under the SAME interpreter that has ``dagnam``
|
|
21
|
+
# installed. A bare ``python`` on PATH may resolve to a different interpreter
|
|
22
|
+
# (pipx / uv-tool / venv installs), where ``-m dagnam._agent.guardhook`` raises
|
|
23
|
+
# ModuleNotFoundError and breaks every matched tool call. Pin ``sys.executable``.
|
|
24
|
+
_GUARD_HOOK_MODULE_ARGS = ["-m", "dagnam._agent.guardhook"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _guard_hook_entry() -> dict[str, object]:
|
|
28
|
+
"""The Codex ``hooks.json`` guard entry, pinned to the current interpreter."""
|
|
29
|
+
return {"command": [sys.executable, *_GUARD_HOOK_MODULE_ARGS]}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_guard_hook_entry(entry: object) -> bool:
|
|
33
|
+
"""True if ``entry`` is our guard hook.
|
|
34
|
+
|
|
35
|
+
Matches regardless of which interpreter path it was written with, so
|
|
36
|
+
re-install stays idempotent and uninstall can find it even if the
|
|
37
|
+
interpreter moved.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(entry, dict):
|
|
40
|
+
return False
|
|
41
|
+
command = entry.get("command")
|
|
42
|
+
return isinstance(command, list) and command[-2:] == _GUARD_HOOK_MODULE_ARGS
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class InstallPlan:
|
|
47
|
+
"""What ``install_harness`` will write for one harness (no side effects)."""
|
|
48
|
+
|
|
49
|
+
harness: Harness
|
|
50
|
+
skill_dest: Path
|
|
51
|
+
extra_dests: tuple[Path, ...] = ()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class InstallResult:
|
|
56
|
+
"""What ``install_harness`` actually wrote for one harness."""
|
|
57
|
+
|
|
58
|
+
harness: Harness
|
|
59
|
+
skill_dest: Path
|
|
60
|
+
method: str # "symlink" | "copy"
|
|
61
|
+
wrote: list[Path] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _home() -> Path:
|
|
65
|
+
return Path.home()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _assets_root() -> Path:
|
|
69
|
+
"""Filesystem path to the bundled ``dagnam/_agent`` assets (works post-pip-install)."""
|
|
70
|
+
return Path(str(resources.files("dagnam._agent")))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def package_version() -> str:
|
|
74
|
+
"""Installed dagnam version, falling back to the in-tree value for source checkouts."""
|
|
75
|
+
try:
|
|
76
|
+
return version("dagnam")
|
|
77
|
+
except PackageNotFoundError:
|
|
78
|
+
from dagnam import __version__
|
|
79
|
+
|
|
80
|
+
return __version__
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect_harnesses() -> list[Harness]:
|
|
84
|
+
"""Return installed harnesses in stable order. Claude -> ~/.claude; Codex -> ~/.codex or ~/.agents."""
|
|
85
|
+
home = _home()
|
|
86
|
+
found: list[Harness] = []
|
|
87
|
+
if (home / ".claude").exists():
|
|
88
|
+
found.append("claude")
|
|
89
|
+
if (home / ".codex").exists() or (home / ".agents").exists():
|
|
90
|
+
found.append("codex")
|
|
91
|
+
return found
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _skill_dest(harness: Harness) -> Path:
|
|
95
|
+
home = _home()
|
|
96
|
+
if harness == "claude":
|
|
97
|
+
return home / ".claude" / "skills" / "dagnam"
|
|
98
|
+
return home / ".agents" / "skills" / "dagnam" # codex
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _claude_plugin_dir() -> Path:
|
|
102
|
+
return _home() / ".claude" / "plugins" / "dagnam"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def plan_for(harness: Harness) -> InstallPlan:
|
|
106
|
+
"""Describe the install destinations for a harness without writing anything."""
|
|
107
|
+
extra: tuple[Path, ...]
|
|
108
|
+
if harness == "claude":
|
|
109
|
+
extra = (_claude_plugin_dir(),)
|
|
110
|
+
else:
|
|
111
|
+
extra = (_home() / ".codex" / "hooks.json",)
|
|
112
|
+
return InstallPlan(harness=harness, skill_dest=_skill_dest(harness), extra_dests=extra)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _remove_dest(dest: Path) -> bool:
|
|
116
|
+
"""Remove ``dest`` whether it is a symlink or a real directory. Returns True if removed."""
|
|
117
|
+
if dest.is_symlink():
|
|
118
|
+
dest.unlink()
|
|
119
|
+
return True
|
|
120
|
+
if dest.exists():
|
|
121
|
+
shutil.rmtree(dest)
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _try_symlink(dest: Path, src: Path) -> bool:
|
|
127
|
+
"""Attempt a directory symlink; return False on OSError (e.g. unprivileged Windows)."""
|
|
128
|
+
try:
|
|
129
|
+
dest.symlink_to(src, target_is_directory=True)
|
|
130
|
+
except OSError:
|
|
131
|
+
return False
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _copy_tree(src: Path, dest: Path) -> None:
|
|
136
|
+
if dest.exists():
|
|
137
|
+
shutil.rmtree(dest)
|
|
138
|
+
shutil.copytree(src, dest)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _place_skill(method: str, dest: Path) -> str:
|
|
142
|
+
"""Copy or symlink the bundled ``skill/`` into ``dest``. Returns the method used."""
|
|
143
|
+
src = _assets_root() / "skill"
|
|
144
|
+
_remove_dest(dest)
|
|
145
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
if method == "symlink" and _try_symlink(dest, src):
|
|
147
|
+
return "symlink"
|
|
148
|
+
_copy_tree(src, dest)
|
|
149
|
+
return "copy"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _stamp_version(path: Path, key: str = "version") -> None:
|
|
153
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
154
|
+
data[key] = package_version()
|
|
155
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _merge_codex_hook(wrote: list[Path]) -> None:
|
|
159
|
+
"""Idempotently merge the guard hook into ``~/.codex/hooks.json``, preserving other entries."""
|
|
160
|
+
codex = _home() / ".codex"
|
|
161
|
+
codex.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
hooks_path = codex / "hooks.json"
|
|
163
|
+
data: dict[str, object] = {}
|
|
164
|
+
if hooks_path.exists():
|
|
165
|
+
try:
|
|
166
|
+
loaded = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
167
|
+
data = loaded if isinstance(loaded, dict) else {}
|
|
168
|
+
except json.JSONDecodeError:
|
|
169
|
+
data = {}
|
|
170
|
+
hooks = data.setdefault("hooks", {})
|
|
171
|
+
if not isinstance(hooks, dict):
|
|
172
|
+
hooks = {}
|
|
173
|
+
data["hooks"] = hooks
|
|
174
|
+
pre = hooks.setdefault("PreToolUse", [])
|
|
175
|
+
if not isinstance(pre, list):
|
|
176
|
+
pre = []
|
|
177
|
+
hooks["PreToolUse"] = pre
|
|
178
|
+
if not any(_is_guard_hook_entry(entry) for entry in pre):
|
|
179
|
+
pre.append(_guard_hook_entry())
|
|
180
|
+
hooks_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
181
|
+
wrote.append(hooks_path)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _install_codex_extras(wrote: list[Path]) -> None:
|
|
185
|
+
"""Place + version-stamp the Codex skill metadata, then merge the guard hook."""
|
|
186
|
+
dest_agents = _skill_dest("codex") / "agents"
|
|
187
|
+
dest_agents.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
src_yaml = _assets_root() / "codex" / "agents" / "openai.yaml"
|
|
189
|
+
yaml_text = src_yaml.read_text(encoding="utf-8").replace(
|
|
190
|
+
"version: 0.0.0", f"version: {package_version()}"
|
|
191
|
+
)
|
|
192
|
+
yaml_path = dest_agents / "openai.yaml"
|
|
193
|
+
yaml_path.write_text(yaml_text, encoding="utf-8")
|
|
194
|
+
wrote.append(yaml_path)
|
|
195
|
+
_merge_codex_hook(wrote)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def install_harness(harness: Harness, *, method: str = "copy") -> InstallResult:
|
|
199
|
+
"""Install the skill (and adapter files) for one harness. Idempotent."""
|
|
200
|
+
dest = _skill_dest(harness)
|
|
201
|
+
used = _place_skill(method, dest)
|
|
202
|
+
wrote = [dest]
|
|
203
|
+
if harness == "claude":
|
|
204
|
+
plugin_dir = _claude_plugin_dir()
|
|
205
|
+
_copy_tree(_assets_root() / "claude", plugin_dir)
|
|
206
|
+
_stamp_version(plugin_dir / "plugin.json")
|
|
207
|
+
_stamp_claude_guard_interpreter(plugin_dir / "hooks" / "guard.json")
|
|
208
|
+
wrote.append(plugin_dir)
|
|
209
|
+
else: # codex
|
|
210
|
+
_install_codex_extras(wrote)
|
|
211
|
+
return InstallResult(harness=harness, skill_dest=dest, method=used, wrote=wrote)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _stamp_claude_guard_interpreter(guard_path: Path) -> None:
|
|
215
|
+
"""Rewrite the copied Claude guard hook to invoke the current interpreter.
|
|
216
|
+
|
|
217
|
+
The bundled asset ships a bare ``python`` command; pin it to
|
|
218
|
+
``sys.executable`` (double-quoted so a path with spaces survives the shell)
|
|
219
|
+
for the same reason as the Codex entry — a different PATH ``python`` cannot
|
|
220
|
+
import ``dagnam``.
|
|
221
|
+
"""
|
|
222
|
+
data = json.loads(guard_path.read_text(encoding="utf-8"))
|
|
223
|
+
command = f'"{sys.executable}" ' + " ".join(_GUARD_HOOK_MODULE_ARGS)
|
|
224
|
+
for group in data.get("hooks", {}).get("PreToolUse", []):
|
|
225
|
+
for hook in group.get("hooks", []):
|
|
226
|
+
if isinstance(hook, dict) and "-m dagnam._agent.guardhook" in str(hook.get("command")):
|
|
227
|
+
hook["command"] = command
|
|
228
|
+
guard_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _unmerge_codex_hook() -> list[Path]:
|
|
232
|
+
"""Remove our guard entry from ``~/.codex/hooks.json``, leaving others intact.
|
|
233
|
+
|
|
234
|
+
Mirrors ``_merge_codex_hook`` so a codex uninstall — or a plain
|
|
235
|
+
``pip uninstall dagnam`` follow-up — doesn't leave the hook firing against a
|
|
236
|
+
now-missing module.
|
|
237
|
+
"""
|
|
238
|
+
hooks_path = _home() / ".codex" / "hooks.json"
|
|
239
|
+
if not hooks_path.exists():
|
|
240
|
+
return []
|
|
241
|
+
try:
|
|
242
|
+
data = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
243
|
+
except json.JSONDecodeError:
|
|
244
|
+
return []
|
|
245
|
+
if not isinstance(data, dict):
|
|
246
|
+
return []
|
|
247
|
+
hooks = data.get("hooks")
|
|
248
|
+
if not isinstance(hooks, dict):
|
|
249
|
+
return []
|
|
250
|
+
pre = hooks.get("PreToolUse")
|
|
251
|
+
if not isinstance(pre, list):
|
|
252
|
+
return []
|
|
253
|
+
kept = [entry for entry in pre if not _is_guard_hook_entry(entry)]
|
|
254
|
+
if len(kept) == len(pre):
|
|
255
|
+
return []
|
|
256
|
+
hooks["PreToolUse"] = kept
|
|
257
|
+
hooks_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
258
|
+
return [hooks_path]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def uninstall_harness(harness: Harness) -> list[Path]:
|
|
262
|
+
"""Remove what ``install_harness`` wrote for a harness. Returns removed paths. Idempotent."""
|
|
263
|
+
removed: list[Path] = []
|
|
264
|
+
dest = _skill_dest(harness)
|
|
265
|
+
if _remove_dest(dest):
|
|
266
|
+
removed.append(dest)
|
|
267
|
+
if harness == "claude":
|
|
268
|
+
plugin_dir = _claude_plugin_dir()
|
|
269
|
+
if plugin_dir.exists():
|
|
270
|
+
shutil.rmtree(plugin_dir)
|
|
271
|
+
removed.append(plugin_dir)
|
|
272
|
+
else: # codex
|
|
273
|
+
removed.extend(_unmerge_codex_hook())
|
|
274
|
+
return removed
|