@syncended/dsh-automations 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/cordis.patch.yml +5 -0
  4. package/dist/agent-executor.d.ts +15 -0
  5. package/dist/agent-executor.d.ts.map +1 -0
  6. package/dist/agent-executor.js +173 -0
  7. package/dist/agent-executor.js.map +1 -0
  8. package/dist/cron.d.ts +15 -0
  9. package/dist/cron.d.ts.map +1 -0
  10. package/dist/cron.js +74 -0
  11. package/dist/cron.js.map +1 -0
  12. package/dist/http.d.ts +8 -0
  13. package/dist/http.d.ts.map +1 -0
  14. package/dist/http.js +203 -0
  15. package/dist/http.js.map +1 -0
  16. package/dist/index.d.ts +68 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +201 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/project-policy.d.ts +9 -0
  21. package/dist/project-policy.d.ts.map +1 -0
  22. package/dist/project-policy.js +53 -0
  23. package/dist/project-policy.js.map +1 -0
  24. package/dist/scheduler.d.ts +58 -0
  25. package/dist/scheduler.d.ts.map +1 -0
  26. package/dist/scheduler.js +518 -0
  27. package/dist/scheduler.js.map +1 -0
  28. package/dist/state.d.ts +8 -0
  29. package/dist/state.d.ts.map +1 -0
  30. package/dist/state.js +44 -0
  31. package/dist/state.js.map +1 -0
  32. package/dist/store.d.ts +21 -0
  33. package/dist/store.d.ts.map +1 -0
  34. package/dist/store.js +317 -0
  35. package/dist/store.js.map +1 -0
  36. package/dist/types.d.ts +169 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +3 -0
  39. package/dist/types.js.map +1 -0
  40. package/dist/validation.d.ts +13 -0
  41. package/dist/validation.d.ts.map +1 -0
  42. package/dist/validation.js +159 -0
  43. package/dist/validation.js.map +1 -0
  44. package/docs/architecture.md +195 -0
  45. package/docs/http-api.md +40 -0
  46. package/docs/security.md +51 -0
  47. package/lib/client.js +1251 -0
  48. package/package.json +105 -0
@@ -0,0 +1,159 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { validateCron } from './cron.js';
3
+ const JOB_ID = /^[a-z0-9](?:[a-z0-9-]{0,62})$/;
4
+ const OVERLAP_POLICIES = new Set(['skip', 'queue', 'allow']);
5
+ const MISFIRE_POLICIES = new Set(['skip', 'run-once']);
6
+ const MAX_PROMPT_CHARS = 131_072;
7
+ const MAX_NAME_CHARS = 120;
8
+ const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
9
+ const MIN_TIMEOUT_MS = 1_000;
10
+ export class AutomationInputError extends Error {
11
+ code;
12
+ status;
13
+ constructor(message, code = 'INVALID_INPUT', status = 400, options) {
14
+ super(message, options);
15
+ this.name = 'AutomationInputError';
16
+ this.code = code;
17
+ this.status = status;
18
+ }
19
+ }
20
+ function object(value, path) {
21
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
22
+ throw new AutomationInputError(`${path} must be an object`);
23
+ }
24
+ return value;
25
+ }
26
+ function rejectUnknown(value, allowed, path) {
27
+ const allowedSet = new Set(allowed);
28
+ const unknown = Object.keys(value).filter((key) => !allowedSet.has(key));
29
+ if (unknown.length > 0)
30
+ throw new AutomationInputError(`${path} contains unknown field(s): ${unknown.join(', ')}`);
31
+ }
32
+ function string(value, path, maxChars = 4_096) {
33
+ if (typeof value !== 'string')
34
+ throw new AutomationInputError(`${path} must be a string`);
35
+ const normalized = value.trim();
36
+ if (normalized === '')
37
+ throw new AutomationInputError(`${path} must not be empty`);
38
+ if (normalized.length > maxChars)
39
+ throw new AutomationInputError(`${path} must be at most ${maxChars} characters`);
40
+ return normalized;
41
+ }
42
+ function optionalString(value, path, maxChars = 512) {
43
+ if (value === undefined || value === null || value === '')
44
+ return undefined;
45
+ return string(value, path, maxChars);
46
+ }
47
+ function boolean(value, path, fallback) {
48
+ if (value === undefined)
49
+ return fallback;
50
+ if (typeof value !== 'boolean')
51
+ throw new AutomationInputError(`${path} must be a boolean`);
52
+ return value;
53
+ }
54
+ function integer(value, path, fallback, min, max) {
55
+ if (value === undefined)
56
+ return fallback;
57
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
58
+ throw new AutomationInputError(`${path} must be a safe integer between ${min} and ${max}`);
59
+ }
60
+ return value;
61
+ }
62
+ export function normalizeJobId(value) {
63
+ const id = value.trim().toLowerCase();
64
+ if (!JOB_ID.test(id)) {
65
+ throw new AutomationInputError('job id must match [a-z0-9][a-z0-9-]{0,62}', 'INVALID_JOB_ID');
66
+ }
67
+ return id;
68
+ }
69
+ export function slugifyJobId(name) {
70
+ const slug = name
71
+ .normalize('NFKD')
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, '-')
74
+ .replace(/^-+|-+$/g, '')
75
+ .slice(0, 48);
76
+ return slug === '' ? 'automation' : slug;
77
+ }
78
+ function normalizeTask(value) {
79
+ const task = object(value, 'spec.task');
80
+ rejectUnknown(task, ['kind', 'prompt'], 'spec.task');
81
+ if (task.kind !== undefined && task.kind !== 'agent') {
82
+ throw new AutomationInputError('spec.task.kind must be "agent"');
83
+ }
84
+ return {
85
+ kind: 'agent',
86
+ prompt: string(task.prompt, 'spec.task.prompt', MAX_PROMPT_CHARS),
87
+ };
88
+ }
89
+ function normalizeExecution(value) {
90
+ const execution = object(value, 'spec.execution');
91
+ rejectUnknown(execution, ['cwd', 'provider', 'model', 'reasoningEffort', 'agentPreset', 'permissionPreset', 'timeoutMs'], 'spec.execution');
92
+ const cwd = string(execution.cwd, 'spec.execution.cwd', 8_192);
93
+ if (!isAbsolute(cwd))
94
+ throw new AutomationInputError('spec.execution.cwd must be an absolute path');
95
+ const provider = optionalString(execution.provider, 'spec.execution.provider');
96
+ const model = optionalString(execution.model, 'spec.execution.model');
97
+ if ((provider === undefined) !== (model === undefined)) {
98
+ throw new AutomationInputError('spec.execution.provider and spec.execution.model must be set together or both omitted');
99
+ }
100
+ const reasoningEffort = optionalString(execution.reasoningEffort, 'spec.execution.reasoningEffort');
101
+ const agentPreset = optionalString(execution.agentPreset, 'spec.execution.agentPreset');
102
+ const permissionPreset = optionalString(execution.permissionPreset, 'spec.execution.permissionPreset') ?? 'workspace-write';
103
+ const timeoutMs = integer(execution.timeoutMs, 'spec.execution.timeoutMs', 3_600_000, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
104
+ return {
105
+ cwd,
106
+ ...(provider === undefined ? {} : { provider }),
107
+ ...(model === undefined ? {} : { model }),
108
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
109
+ ...(agentPreset === undefined ? {} : { agentPreset }),
110
+ permissionPreset,
111
+ timeoutMs,
112
+ };
113
+ }
114
+ function normalizePolicies(value) {
115
+ const policies = value === undefined ? {} : object(value, 'spec.policies');
116
+ rejectUnknown(policies, ['overlap', 'misfire'], 'spec.policies');
117
+ const overlap = policies.overlap ?? 'skip';
118
+ if (typeof overlap !== 'string' || !OVERLAP_POLICIES.has(overlap)) {
119
+ throw new AutomationInputError('spec.policies.overlap must be skip, queue, or allow');
120
+ }
121
+ const misfire = policies.misfire ?? 'run-once';
122
+ if (typeof misfire !== 'string' || !MISFIRE_POLICIES.has(misfire)) {
123
+ throw new AutomationInputError('spec.policies.misfire must be skip or run-once');
124
+ }
125
+ return {
126
+ overlap: overlap,
127
+ misfire: misfire,
128
+ };
129
+ }
130
+ export function normalizeJobSpec(value) {
131
+ const spec = object(value, 'spec');
132
+ rejectUnknown(spec, ['name', 'enabled', 'schedule', 'task', 'execution', 'policies'], 'spec');
133
+ const schedule = object(spec.schedule, 'spec.schedule');
134
+ rejectUnknown(schedule, ['cron', 'timezone'], 'spec.schedule');
135
+ const normalizedSchedule = validateCron(string(schedule.cron, 'spec.schedule.cron', 256), optionalString(schedule.timezone, 'spec.schedule.timezone', 256) ?? 'UTC');
136
+ return {
137
+ name: string(spec.name, 'spec.name', MAX_NAME_CHARS),
138
+ enabled: boolean(spec.enabled, 'spec.enabled', true),
139
+ schedule: normalizedSchedule,
140
+ task: normalizeTask(spec.task),
141
+ execution: normalizeExecution(spec.execution),
142
+ policies: normalizePolicies(spec.policies),
143
+ };
144
+ }
145
+ export function assertExpectedVersion(value) {
146
+ if (value === undefined)
147
+ return undefined;
148
+ if (!Number.isSafeInteger(value) || value < 1) {
149
+ throw new AutomationInputError('expectedVersion must be a positive safe integer');
150
+ }
151
+ return value;
152
+ }
153
+ export function assertJsonObject(value, path = 'request body') {
154
+ return object(value, path);
155
+ }
156
+ export function assertOnlyKeys(value, allowed, path = 'request body') {
157
+ rejectUnknown(value, allowed, path);
158
+ }
159
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,195 @@
1
+ # Architecture
2
+
3
+ ## Goals
4
+
5
+ The MVP must solve deployment-level scheduling without coupling future automation semantics to cron. A schedule decides **when to admit a run**; an executor decides **how that run progresses**.
6
+
7
+ Non-goals for the first release:
8
+
9
+ - distributed/multi-host workers;
10
+ - exactly-once external effects;
11
+ - calendar syntax beyond standard five-field cron;
12
+ - arbitrary untrusted code execution;
13
+ - automatic retry of a run that may already have produced side effects.
14
+
15
+ ## Components
16
+
17
+ ```text
18
+ Settings UI / HTTP API
19
+
20
+
21
+ AutomationService ─── ProjectPolicy
22
+
23
+ ├── AutomationStateStore (atomic JSON, occurrence index)
24
+
25
+ └── AutomationScheduler
26
+
27
+ └── ExecutorRegistry
28
+ └── HarnessAgentExecutor (MVP)
29
+ ```
30
+
31
+ ### AutomationService
32
+
33
+ Cordis service `ctx.automations`. It owns input validation, canonical project authorization, metadata discovery, the Web route, and the public executor-registration seam.
34
+
35
+ ### AutomationStateStore
36
+
37
+ The store owns one versioned document:
38
+
39
+ - `jobs`: versioned user definitions plus the durable `nextRunAt` watermark;
40
+ - `runs`: orchestration state and immutable execution snapshots;
41
+ - `runOrder`: stable history ordering;
42
+ - `occurrences`: idempotency key → run id.
43
+
44
+ Every writer mutation:
45
+
46
+ 1. acquires the DSH cross-process file lock;
47
+ 2. re-reads and validates the latest complete document;
48
+ 3. mutates one detached state;
49
+ 4. atomically renames a private `0600` replacement;
50
+ 5. publishes the committed state in memory.
51
+
52
+ The current startup recovery assumes one active scheduler host. The short mutation lock prevents duplicate admission, but a second live host would incorrectly treat the first host's running records as crash orphans. Multi-host support therefore requires leases before it can be advertised.
53
+
54
+ ### AutomationScheduler
55
+
56
+ The scheduler has four responsibilities only:
57
+
58
+ 1. derive and arm the earliest wall-clock wakeup;
59
+ 2. atomically claim due cron/manual occurrences;
60
+ 3. apply misfire and overlap policy;
61
+ 4. dispatch queued runs through the executor registry under a global concurrency bound.
62
+
63
+ It never knows how an Agent, workflow, or code task works.
64
+
65
+ ### HarnessAgentExecutor
66
+
67
+ The `agent` executor:
68
+
69
+ 1. re-authorizes the canonical project;
70
+ 2. resolves the job's agent and permission presets;
71
+ 3. resolves the current default model or the pinned provider/model route;
72
+ 4. creates a fresh Session and Agent through `ctx.agents.create`;
73
+ 5. composes the preset in the unpublished setup transaction;
74
+ 6. installs model selection and the permission preset;
75
+ 7. submits the prompt as a plugin-origin user message;
76
+ 8. waits for quiescence, flushes Session persistence, and folds the terminal result;
77
+ 9. disposes the live handle while retaining persisted Session history.
78
+
79
+ No nested `dsh` process is required, and the normal DSH tool, sandbox, approval, persistence, and model routing layers remain authoritative.
80
+
81
+ ## State machine
82
+
83
+ ```text
84
+ host crash
85
+ ┌────────────────► interrupted
86
+
87
+ queued ───────────► running ──────────► succeeded
88
+ │ │ │
89
+ │ │ ├────────────► failed
90
+ │ │ ├────────────► timed-out
91
+ │ │ └────────────► cancelled
92
+ ├──────────────────────────────────► cancelled
93
+ └──────────────────────────────────► skipped
94
+ ```
95
+
96
+ Terminal states never transition. `running` is committed before executor work begins. A crash in the narrow interval after that commit but before the first effect may lose work; retrying automatically would create the more dangerous inverse window—duplicating an effect that completed before the crash.
97
+
98
+ ## Admission and idempotency
99
+
100
+ A cron key is:
101
+
102
+ ```text
103
+ <job-id>:cron:<scheduled-for-utc>
104
+ ```
105
+
106
+ A manual key uses a random UUID. The occurrence index and the scheduler watermark are written together. Missed cron history is latest-only:
107
+
108
+ - `skip`: record the overdue occurrence as skipped and advance past `now`;
109
+ - `run-once`: select the latest due occurrence, admit one run, and advance past `now`.
110
+
111
+ There is no backlog explosion after a long shutdown.
112
+
113
+ ## Why runs pin a full job snapshot
114
+
115
+ A queued run must not change when a user edits or deletes its source job. The snapshot pins:
116
+
117
+ - job id/version and name;
118
+ - schedule/policy values used at admission;
119
+ - prompt and execution settings;
120
+ - project/model/preset/permission/timeout values.
121
+
122
+ Deleting a job marks its still-queued runs skipped; already-running work drains against its snapshot.
123
+
124
+ ## Path to durable multi-agent automations
125
+
126
+ Cron remains one trigger feeding the same `Run` aggregate. The next layers are additive.
127
+
128
+ ### Stage 2: workflow executor
129
+
130
+ Add `task.kind = "workflow"` and an executor backed by `ctx.workflowEngine`:
131
+
132
+ - code/meta/args are pinned in the run snapshot;
133
+ - workflow progress is mirrored into durable run events;
134
+ - child Session ids are retained as run artifacts;
135
+ - host cancellation terminates the workflow worker and children.
136
+
137
+ The existing Harness workflow worker is execution isolation from the event loop, **not** a security sandbox.
138
+
139
+ ### Stage 3: durable task graph
140
+
141
+ Introduce a graph below one run:
142
+
143
+ ```text
144
+ Run
145
+ ├── Task A (code/action)
146
+ ├── Task B (agent) dependsOn A
147
+ ├── Task C (agent) dependsOn A
148
+ └── Task D (reduce) dependsOn B,C
149
+ ```
150
+
151
+ Each task needs:
152
+
153
+ - stable `taskId` and definition version;
154
+ - dependency edges;
155
+ - `pending | ready | leased | running | retry-wait | terminal` state;
156
+ - attempt number and idempotency key;
157
+ - lease owner, expiry, and heartbeat;
158
+ - retry policy with deterministic next-at time;
159
+ - input/output references, not unbounded inline blobs;
160
+ - child Session/workflow references;
161
+ - append-only transition events.
162
+
163
+ A projector derives runnable tasks. Workers atomically acquire leases. Expired leases become recoverable according to each task's declared effect semantics:
164
+
165
+ - `pure` / idempotent tasks may retry automatically;
166
+ - `effectful` tasks require an external idempotency key or operator decision;
167
+ - Agent tasks default to effectful because tools may change files or remote systems.
168
+
169
+ ### Stage 4: multi-host storage
170
+
171
+ Replace the JSON document with SQLite (single machine) or a transactional database (multi-host), while keeping the service and executor contracts:
172
+
173
+ - unique occurrence constraint;
174
+ - compare-and-swap job versions;
175
+ - transactional task lease acquisition;
176
+ - append-only run/task events plus projections;
177
+ - artifact storage and retention;
178
+ - metrics, dead-letter/operator recovery, and audit export.
179
+
180
+ ## Extension contract
181
+
182
+ `ctx.automations.registerExecutor(executor)` is intentionally small:
183
+
184
+ ```ts
185
+ interface AutomationExecutor {
186
+ readonly kind: string
187
+ execute(context: {
188
+ run: AutomationRun
189
+ signal: AbortSignal
190
+ attachSession(sessionId: string): Promise<void>
191
+ }): Promise<{ sessionId?: string; output?: string }>
192
+ }
193
+ ```
194
+
195
+ Future executor-specific config should become a discriminated `task` schema. Cron admission, overlap/misfire behavior, run ordering, and the Web run-history surface remain shared.
@@ -0,0 +1,40 @@
1
+ # HTTP API
2
+
3
+ The browser plugin uses a small package-owned API under `/api/automations`. Responses are JSON with `Cache-Control: no-store`.
4
+
5
+ Mutations require:
6
+
7
+ ```http
8
+ Content-Type: application/json
9
+ X-DSH-Automation-Client: 1
10
+ ```
11
+
12
+ ## Read
13
+
14
+ - `GET /api/automations?limit=100` — jobs and newest-first run history.
15
+ - `GET /api/automations/meta` — current model directory, permission presets, and agent presets.
16
+
17
+ ## Jobs
18
+
19
+ - `POST /api/automations/jobs` — `{ "id"?: string, "spec": JobSpec }`.
20
+ - `PUT /api/automations/jobs/:id` — `{ "expectedVersion"?: number, "spec": JobSpec }`.
21
+ - `DELETE /api/automations/jobs/:id` — `{}`.
22
+ - `POST /api/automations/jobs/:id/run` — `{}`.
23
+ - `POST /api/automations/jobs/:id/enabled` — `{ "enabled": boolean, "expectedVersion"?: number }`.
24
+
25
+ ## Runs
26
+
27
+ - `POST /api/automations/runs/:id/cancel` — `{ "reason"?: string }`.
28
+
29
+ Updates use optimistic job versions. A stale `expectedVersion` returns HTTP 409 with error code `VERSION_CONFLICT`.
30
+
31
+ Error shape:
32
+
33
+ ```json
34
+ {
35
+ "error": {
36
+ "code": "INVALID_INPUT",
37
+ "message": "..."
38
+ }
39
+ }
40
+ ```
@@ -0,0 +1,51 @@
1
+ # Security model
2
+
3
+ ## Trust boundary
4
+
5
+ This plugin is intended for the same trusted operator boundary as a local DeepSeek Harness Web deployment. It does not add authentication to `dsh-host-webserver`. If the Harness binds to `0.0.0.0`, anyone who can reach that surface may also be able to manage automations; place the deployment behind an appropriate authenticated boundary before exposing it.
6
+
7
+ ## Management API
8
+
9
+ Mutation requests must:
10
+
11
+ - use `application/json`;
12
+ - send `X-DSH-Automation-Client: 1`;
13
+ - originate from the same browser origin when Fetch Metadata / Origin headers are present.
14
+
15
+ The custom header forces a cross-origin browser request through CORS preflight, which the route does not grant. This is a CSRF fence, not user authentication.
16
+
17
+ ## Filesystem authority
18
+
19
+ A job's project is canonicalized with `realpath` before storage. `workspace-write` uses that Session cwd as its DSH sandbox root. `allowedProjectRoots`, when configured, are canonicalized and checked with path-segment containment rather than string prefixes.
20
+
21
+ The executor repeats authorization at dispatch, catching deleted paths and symlink retargeting after configuration.
22
+
23
+ ## Permission presets
24
+
25
+ The job selects an existing DSH permission preset rather than independently inventing sandbox and approval values. The standard presets are:
26
+
27
+ - `read-only`: no file mutation; approval remains fail-closed;
28
+ - `workspace-write`: writes only under the Session workspace; widening requires approval;
29
+ - `danger-full-access`: unrestricted file effects and no approval prompts.
30
+
31
+ Scheduled Agents are not attached to an interactive browser ownership chain. A policy that needs a human prompt must therefore be expected to reject/unavailable when nobody can answer. Jobs should be designed for deterministic unattended behavior and should normally use `read-only` or `workspace-write`.
32
+
33
+ ## Secrets and persisted data
34
+
35
+ `state.json` contains job prompts, project paths, model/preset selections, run metadata, and bounded final assistant text. It is atomically replaced with mode `0600`; parent directories created by the plugin use `0700`. Session transcripts are persisted by the configured Harness Session backend and may contain substantially more data.
36
+
37
+ Do not put API keys directly in prompts. Use normal Harness credential providers and environment policy.
38
+
39
+ ## Crash semantics
40
+
41
+ The store guarantees complete-file replacement, not fsync durability. After a host restart:
42
+
43
+ - `queued` runs are eligible to dispatch;
44
+ - `running` runs become `interrupted`;
45
+ - interrupted runs are not retried automatically.
46
+
47
+ This avoids silently repeating side effects but does not provide exactly-once execution. Tasks that call external systems should carry their own stable idempotency key (for example, the automation occurrence key) whenever the target supports one.
48
+
49
+ ## Code and future workflows
50
+
51
+ The MVP accepts prompt text, not arbitrary executable automation code. A future workflow executor may use the Harness workflow worker, whose worker thread protects host event-loop liveness but is explicitly not a security sandbox. Hostile code requires a separate process/container/VM boundary with a narrow capability protocol.