@dev.sail.money/sailor 1.2.0-75 → 1.2.0-76
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/README.md +4 -1
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +296 -206
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/scripts/check-docs.mjs +1 -1
- package/scripts/check-init.mjs +16 -13
- package/scripts/check-update.mjs +177 -0
- package/templates/default/.agents/skills/sail-automation/SKILL.md +50 -0
- package/templates/default/.agents/skills/sail-automation/references/docker-vm.md +113 -0
- package/templates/default/.agents/skills/sail-automation/references/github-actions.md +50 -0
- package/templates/default/.agents/skills/sail-automation/references/local-daemon.md +34 -0
- package/templates/default/.agents/skills/sail-automation/references/self-hosted-runner.md +72 -0
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +2 -0
- package/templates/default/AGENTS.md +1 -1
- package/templates/default/Dockerfile +18 -0
- package/templates/default/_dockerignore +15 -0
- package/templates/default/.agents/skills/sail-ci/SKILL.md +0 -63
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.0
|
|
8
|
-
* Generated at : 2026-06-
|
|
8
|
+
* Generated at : 2026-06-18T11:23:35.421Z
|
|
9
9
|
*/
|
|
10
10
|
export declare const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export declare const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.0
|
|
8
|
-
* Generated at : 2026-06-
|
|
8
|
+
* Generated at : 2026-06-18T11:23:35.421Z
|
|
9
9
|
*/
|
|
10
10
|
export const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -217,7 +217,7 @@ function checkSkills(errors) {
|
|
|
217
217
|
errors.push(`templates/default/.agents/skills/${d}: missing SKILL.md`);
|
|
218
218
|
continue;
|
|
219
219
|
}
|
|
220
|
-
const fm = readFileSync(skillFile, "utf-8").match(/^---\n([\s\S]*?)\n---/);
|
|
220
|
+
const fm = readFileSync(skillFile, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
221
221
|
if (!fm) {
|
|
222
222
|
errors.push(`${rel(skillFile)}: missing YAML frontmatter`);
|
|
223
223
|
continue;
|
package/scripts/check-init.mjs
CHANGED
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `sailor init` smoke test.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* asserts the scaffold succeeded. This exists to catch the class of regression
|
|
7
|
-
* the doc-drift gate structurally cannot — e.g. `packageRoot()` resolving to a
|
|
8
|
-
* `bin.sailor` package that ships no `templates/`, which made `init` fail from a
|
|
9
|
-
* monorepo checkout with "Template ... not found. Available: none".
|
|
5
|
+
* PASS 1 — fresh init: scaffolds a new project and asserts expected files exist.
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* dependency is the CLI bundle (`pnpm --filter sailor build`).
|
|
7
|
+
* Template files are read live from disk (not bundled), so no rebuild is needed
|
|
8
|
+
* between runs — the in-tree templates/default/ IS the "latest version".
|
|
14
9
|
*
|
|
15
10
|
* Run: node scripts/check-init.mjs (CI builds the CLI first)
|
|
16
|
-
* Exit: 0 =
|
|
11
|
+
* Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
|
|
17
12
|
*/
|
|
18
13
|
|
|
19
14
|
import { execFileSync } from "node:child_process";
|
|
@@ -65,6 +60,9 @@ try {
|
|
|
65
60
|
"foundry.toml",
|
|
66
61
|
"mandates",
|
|
67
62
|
"AGENTS.md",
|
|
63
|
+
"CLAUDE.md",
|
|
64
|
+
"Dockerfile",
|
|
65
|
+
".dockerignore",
|
|
68
66
|
".sail/contracts/interfaces/IPermission.sol",
|
|
69
67
|
".sail/contracts/interfaces/IBatchPermission.sol",
|
|
70
68
|
"test/BoundedCallPermission.t.sol",
|
|
@@ -75,7 +73,11 @@ try {
|
|
|
75
73
|
".agents/skills/sail-transactions/SKILL.md",
|
|
76
74
|
".agents/skills/sail-mandates/SKILL.md",
|
|
77
75
|
".agents/skills/sail-mandates/references/approvals.md",
|
|
78
|
-
".agents/skills/sail-
|
|
76
|
+
".agents/skills/sail-automation/SKILL.md",
|
|
77
|
+
".agents/skills/sail-automation/references/docker-vm.md",
|
|
78
|
+
".agents/skills/sail-automation/references/github-actions.md",
|
|
79
|
+
".agents/skills/sail-automation/references/local-daemon.md",
|
|
80
|
+
".agents/skills/sail-automation/references/self-hosted-runner.md",
|
|
79
81
|
".agents/skills/sail-extend/SKILL.md",
|
|
80
82
|
];
|
|
81
83
|
for (const rel of mustExist) {
|
|
@@ -94,9 +96,10 @@ try {
|
|
|
94
96
|
fail(`package.json still has "@sail/sdk": "workspace:*" — init did not resolve it`);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
// Regression guard:
|
|
98
|
-
//
|
|
99
|
-
//
|
|
99
|
+
// ── Regression guard: absolute path outside cwd ───────────────────────────
|
|
100
|
+
// An absolute path outside the cwd must be REJECTED, not silently nested into
|
|
101
|
+
// `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the leading slash and
|
|
102
|
+
// scaffolded a bogus nested tree while printing success.)
|
|
100
103
|
const outside = path.join(os.tmpdir(), "sailor-init-outside", "agent");
|
|
101
104
|
let rejected = false;
|
|
102
105
|
try {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `sailor update` smoke tests.
|
|
4
|
+
*
|
|
5
|
+
* PASS 1 — standard update:
|
|
6
|
+
* Deletes a template-owned file and a user skill, runs `sailor update`,
|
|
7
|
+
* asserts the template file is restored and the user skill is untouched.
|
|
8
|
+
*
|
|
9
|
+
* PASS 2 — seeds missing user-space files:
|
|
10
|
+
* Deletes a user-space file (package.json) and dir (src/), runs update,
|
|
11
|
+
* asserts they are re-added. Asserts AGENTS.md / CLAUDE.md / Dockerfile
|
|
12
|
+
* are NOT overwritten when they already exist.
|
|
13
|
+
*
|
|
14
|
+
* PASS 3 — stale path pruning:
|
|
15
|
+
* Manually creates .agents/skills/sail-ci/ (orphan from old template version),
|
|
16
|
+
* runs update, asserts it is deleted and sail-automation is present.
|
|
17
|
+
*
|
|
18
|
+
* PASS 4 — init-on-existing errors:
|
|
19
|
+
* Runs `sailor init` inside the already-initialized project;
|
|
20
|
+
* asserts it exits non-zero with an "already initialized" message.
|
|
21
|
+
*
|
|
22
|
+
* Run: node scripts/check-update.mjs (CI builds the CLI first)
|
|
23
|
+
* Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
|
|
34
|
+
const PROJECT = "smoke-update";
|
|
35
|
+
|
|
36
|
+
function fail(msg) {
|
|
37
|
+
console.error(`✗ update smoke test FAILED: ${msg}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(BUNDLE)) {
|
|
42
|
+
fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-update-smoke-"));
|
|
46
|
+
const dest = path.join(tmpRoot, PROJECT);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Bootstrap: fresh init so we have a valid project to update.
|
|
50
|
+
try {
|
|
51
|
+
execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
|
|
52
|
+
cwd: tmpRoot,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
|
|
58
|
+
fail(`bootstrap \`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── PASS 1 — standard update ───────────────────────────────────────────────
|
|
62
|
+
// Delete a template-owned file and add a user skill that must survive update.
|
|
63
|
+
const templateOwned = path.join(dest, ".agents/skills/sail-automation/SKILL.md");
|
|
64
|
+
const cursorRules = path.join(dest, ".cursor/rules");
|
|
65
|
+
const userSkill = path.join(dest, ".agents/skills/my-custom-skill/SKILL.md");
|
|
66
|
+
|
|
67
|
+
fs.rmSync(templateOwned);
|
|
68
|
+
fs.rmSync(cursorRules);
|
|
69
|
+
fs.mkdirSync(path.dirname(userSkill), { recursive: true });
|
|
70
|
+
fs.writeFileSync(userSkill, "# custom skill\n", "utf-8");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
execFileSync(process.execPath, [BUNDLE, "update"], {
|
|
74
|
+
cwd: dest,
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
|
|
80
|
+
fail(`Pass 1: \`sailor update\` exited non-zero.\n ${out || err.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(templateOwned))
|
|
84
|
+
fail(`Pass 1: update did not restore "${path.relative(dest, templateOwned)}"`);
|
|
85
|
+
if (!fs.existsSync(cursorRules))
|
|
86
|
+
fail(`Pass 1: update did not restore "${path.relative(dest, cursorRules)}"`);
|
|
87
|
+
if (!fs.existsSync(userSkill))
|
|
88
|
+
fail(`Pass 1: update removed user file "${path.relative(dest, userSkill)}" — must be preserved`);
|
|
89
|
+
|
|
90
|
+
console.log("✓ Pass 1 passed — template files restored, user skill preserved");
|
|
91
|
+
|
|
92
|
+
// ── PASS 2 — seeds missing user-space files ───────────────────────────────
|
|
93
|
+
const agentsMd = path.join(dest, "AGENTS.md");
|
|
94
|
+
const claudeMd = path.join(dest, "CLAUDE.md");
|
|
95
|
+
const dockerfile = path.join(dest, "Dockerfile");
|
|
96
|
+
const packageJson = path.join(dest, "package.json");
|
|
97
|
+
const srcDir = path.join(dest, "src");
|
|
98
|
+
|
|
99
|
+
// Record AGENTS.md content before update — it must not change.
|
|
100
|
+
const agentsContentBefore = fs.readFileSync(agentsMd, "utf-8");
|
|
101
|
+
|
|
102
|
+
fs.rmSync(packageJson);
|
|
103
|
+
fs.rmSync(srcDir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
execFileSync(process.execPath, [BUNDLE, "update"], {
|
|
107
|
+
cwd: dest,
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
|
|
113
|
+
fail(`Pass 2: \`sailor update\` exited non-zero.\n ${out || err.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!fs.existsSync(packageJson))
|
|
117
|
+
fail(`Pass 2: update did not re-add missing "package.json"`);
|
|
118
|
+
if (!fs.existsSync(srcDir))
|
|
119
|
+
fail(`Pass 2: update did not re-add missing "src/" directory`);
|
|
120
|
+
|
|
121
|
+
// AGENTS.md, CLAUDE.md, Dockerfile must not be overwritten.
|
|
122
|
+
const agentsContentAfter = fs.readFileSync(agentsMd, "utf-8");
|
|
123
|
+
if (agentsContentAfter !== agentsContentBefore)
|
|
124
|
+
fail(`Pass 2: update overwrote "AGENTS.md" — user-space files must never be overwritten`);
|
|
125
|
+
if (!fs.existsSync(claudeMd))
|
|
126
|
+
fail(`Pass 2: "CLAUDE.md" is missing after update`);
|
|
127
|
+
if (!fs.existsSync(dockerfile))
|
|
128
|
+
fail(`Pass 2: "Dockerfile" is missing after update`);
|
|
129
|
+
|
|
130
|
+
console.log("✓ Pass 2 passed — missing user-space files seeded, existing ones untouched");
|
|
131
|
+
|
|
132
|
+
// ── PASS 3 — stale path pruning ───────────────────────────────────────────
|
|
133
|
+
const staleSkill = path.join(dest, ".agents/skills/sail-ci/SKILL.md");
|
|
134
|
+
fs.mkdirSync(path.dirname(staleSkill), { recursive: true });
|
|
135
|
+
fs.writeFileSync(staleSkill, "# old sail-ci skill\n", "utf-8");
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
execFileSync(process.execPath, [BUNDLE, "update"], {
|
|
139
|
+
cwd: dest,
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
|
|
145
|
+
fail(`Pass 3: \`sailor update\` exited non-zero.\n ${out || err.message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (fs.existsSync(path.join(dest, ".agents/skills/sail-ci")))
|
|
149
|
+
fail(`Pass 3: stale ".agents/skills/sail-ci" was not removed`);
|
|
150
|
+
if (!fs.existsSync(path.join(dest, ".agents/skills/sail-automation/SKILL.md")))
|
|
151
|
+
fail(`Pass 3: ".agents/skills/sail-automation/SKILL.md" missing after update`);
|
|
152
|
+
|
|
153
|
+
console.log("✓ Pass 3 passed — stale sail-ci skill pruned, sail-automation present");
|
|
154
|
+
|
|
155
|
+
// ── PASS 4 — init-on-existing errors ──────────────────────────────────────
|
|
156
|
+
let initRejected = false;
|
|
157
|
+
let initOutput = "";
|
|
158
|
+
try {
|
|
159
|
+
initOutput = execFileSync(process.execPath, [BUNDLE, "init"], {
|
|
160
|
+
cwd: dest,
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
initRejected = true;
|
|
166
|
+
initOutput = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!initRejected)
|
|
170
|
+
fail(`Pass 4: \`sailor init\` on existing project did not exit non-zero — should refuse`);
|
|
171
|
+
if (!/already initialized/i.test(initOutput))
|
|
172
|
+
fail(`Pass 4: error message did not mention "already initialized".\n output: ${initOutput}`);
|
|
173
|
+
|
|
174
|
+
console.log("✓ Pass 4 passed — init on existing project correctly refused");
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
177
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-automation
|
|
3
|
+
description: Run the agent unattended — four options by reliability and infra overhead: (1) GitHub Actions cloud runner (zero infra, cron drifts), (2) self-hosted runner (reliable timing, user-managed machine), (3) Docker image — run locally or on any cloud VM via a registry, (4) local daemon on the project machine (no Docker, simplest). See references/ for each option. Use after sailor run --once works.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sail automation — running the agent unattended
|
|
7
|
+
|
|
8
|
+
Confirm `sailor run --once` works first. Four options; pick by latency, infra comfort, and uptime needs:
|
|
9
|
+
|
|
10
|
+
| Option | Who it's for | Timing | Infra needed |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| [GitHub Actions](references/github-actions.md) | Any user, no infra knowledge | Low — cron drifts | None |
|
|
13
|
+
| [Self-hosted runner](references/self-hosted-runner.md) | Users who need time-precise execution | High — dedicated runner | Dedicated machine |
|
|
14
|
+
| [Docker](references/docker-vm.md) | Users with basic container knowledge | High — persistent process | Docker + any machine |
|
|
15
|
+
| [Local daemon](references/local-daemon.md) | Any user, runs on the project machine | Medium — depends on uptime | None |
|
|
16
|
+
|
|
17
|
+
## Which option fits you?
|
|
18
|
+
|
|
19
|
+
**1. Do you want to run the agent in the cloud or on a local / dedicated machine?**
|
|
20
|
+
|
|
21
|
+
→ **Local / dedicated machine**
|
|
22
|
+
The machine must be running 24/7 and connected to the internet — missed runs are silent.
|
|
23
|
+
Two options (no further questions needed):
|
|
24
|
+
|
|
25
|
+
- **[Local daemon](references/local-daemon.md)** — `sailor service install`. Simplest option: no extra tools, no extra steps. Runs directly in the project environment on the same machine.
|
|
26
|
+
|
|
27
|
+
- **[Docker](references/docker-vm.md)** — build the image and run it as a container on any machine that has Docker installed. Good if you want a portable setup or plan to move to a cloud deployment later — same image, no code changes.
|
|
28
|
+
|
|
29
|
+
→ **Cloud**
|
|
30
|
+
Three options depending on timing requirements:
|
|
31
|
+
|
|
32
|
+
- **[GitHub Actions](references/github-actions.md)** — zero additional setup. Simplest cloud path. Execution timing is not guaranteed: cron drifts on GitHub's shared runners. Fine for daily DCA, hourly rebalances, treasury strategies.
|
|
33
|
+
|
|
34
|
+
- **Timing-sensitive (LP, liquidations, perps)?** Two options:
|
|
35
|
+
|
|
36
|
+
- **[Self-hosted runner](references/self-hosted-runner.md)** — install GitHub's runner on a machine you control (physical or cloud VM). Same workflow file as GitHub Actions, one line change. The runner picks up jobs immediately with no shared queue.
|
|
37
|
+
|
|
38
|
+
- **[Docker on a cloud VM](references/docker-vm.md)** — provision a VM on any provider, build the image locally, push to a registry, pull and run on the VM. Works with AWS, GCP, Azure, Oracle, Fly.io, and any other provider.
|
|
39
|
+
|
|
40
|
+
## Cadence
|
|
41
|
+
|
|
42
|
+
Match the interval to volatility: **LP / perp → minutes; DCA / rebalance → daily; treasury → hourly–daily.** GitHub Actions cron is a *heartbeat/backstop* that drifts and skips under load — not low-latency; for that, use the self-hosted runner or local execution options.
|
|
43
|
+
|
|
44
|
+
## Keys & trust
|
|
45
|
+
|
|
46
|
+
Cloud options commit only the **encrypted** keystore (`ci-keystore.json`); `SAIL_PASSPHRASE` is a secret, never committed. Local options (daemon, Docker) use keys already on disk — no export needed.
|
|
47
|
+
|
|
48
|
+
Regardless of host, the on-chain **mandate is the backstop** — it bounds the manager's permissions no matter where or how the agent runs.
|
|
49
|
+
|
|
50
|
+
A failing run's logs show the same stderr as the local runner (`reverted: <txHash>`, `skipped: no registered permission…`) — debug with the sail-transactions skill.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Docker — run locally or deploy to any machine
|
|
2
|
+
|
|
3
|
+
**Who this is for:** users with basic Docker knowledge. Works on your local machine, a cloud VM, or any managed container service.
|
|
4
|
+
|
|
5
|
+
**Best for:** users who want a portable, self-contained execution environment — build once, run anywhere. Also the right choice if you want to test locally and then move to a cloud VM or container service without changing anything.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Docker installed on the machine where the agent will run: https://docs.docker.com/get-docker/
|
|
12
|
+
- `ci-keystore.json` present in the project root — generate it with `sailor keys export-ci`
|
|
13
|
+
- `RPC_URL` and `SAIL_PASSPHRASE` available as environment variables (never baked into the image)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Build the image
|
|
18
|
+
|
|
19
|
+
From the project root (where `Dockerfile` lives):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
docker build -t sailor-agent .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Run locally
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker run -d --restart=always \
|
|
31
|
+
-e RPC_URL=<your-rpc-url> \
|
|
32
|
+
-e SAIL_PASSPHRASE=<your-passphrase> \
|
|
33
|
+
-e CHAIN_ID=8453 \
|
|
34
|
+
-e AGENT_INTERVAL=300 \
|
|
35
|
+
--name sailor-agent \
|
|
36
|
+
sailor-agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `--restart=always` — Docker restarts the container automatically on crash or machine reboot (requires Docker daemon set to start on boot)
|
|
40
|
+
- `AGENT_INTERVAL` — seconds between runs; default 300 (5 min). Set to `60` for per-minute, `3600` for hourly, `86400` for daily
|
|
41
|
+
- Logs: `docker logs -f sailor-agent`
|
|
42
|
+
- Stop: `docker stop sailor-agent && docker rm sailor-agent`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Push to a registry (to deploy elsewhere)
|
|
47
|
+
|
|
48
|
+
Build the image once, push to a registry, pull it on any machine.
|
|
49
|
+
|
|
50
|
+
**Docker Hub**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
docker tag sailor-agent <dockerhub-username>/sailor-agent:latest
|
|
54
|
+
docker push <dockerhub-username>/sailor-agent:latest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**GitHub Container Registry (GHCR)**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
echo $GITHUB_TOKEN | docker login ghcr.io -u <github-username> --password-stdin
|
|
61
|
+
docker tag sailor-agent ghcr.io/<github-username>/sailor-agent:latest
|
|
62
|
+
docker push ghcr.io/<github-username>/sailor-agent:latest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Pull and run on any other machine
|
|
68
|
+
|
|
69
|
+
On the target machine (cloud VM, Raspberry Pi, VPS, etc.):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
docker pull <registry>/<image>:latest
|
|
73
|
+
|
|
74
|
+
docker run -d --restart=always \
|
|
75
|
+
-e RPC_URL=<your-rpc-url> \
|
|
76
|
+
-e SAIL_PASSPHRASE=<your-passphrase> \
|
|
77
|
+
-e CHAIN_ID=8453 \
|
|
78
|
+
-e AGENT_INTERVAL=300 \
|
|
79
|
+
<registry>/<image>:latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No code changes — the same image runs everywhere that has Docker.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Use with managed container services
|
|
87
|
+
|
|
88
|
+
The same image works with any managed container service. Pass env vars through the service's UI — no code changes needed.
|
|
89
|
+
|
|
90
|
+
| Service | Notes |
|
|
91
|
+
|---|---|
|
|
92
|
+
| AWS ECS / Fargate | Task definition with env vars; Fargate = no VM to manage |
|
|
93
|
+
| Google Cloud Run | Trigger on schedule via Cloud Scheduler |
|
|
94
|
+
| Azure Container Instances | Simple one-off or always-on container |
|
|
95
|
+
| Fly.io | `fly launch` + set secrets via `fly secrets set` |
|
|
96
|
+
| Railway | Point to image in registry, set env vars in dashboard |
|
|
97
|
+
| Render | Background worker from Docker image |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Updating the agent
|
|
102
|
+
|
|
103
|
+
When you change your strategy code:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
docker build -t sailor-agent .
|
|
107
|
+
docker tag sailor-agent <registry>/<image>:latest
|
|
108
|
+
docker push <registry>/<image>:latest
|
|
109
|
+
# on the target machine:
|
|
110
|
+
docker pull <registry>/<image>:latest
|
|
111
|
+
docker stop sailor-agent && docker rm sailor-agent
|
|
112
|
+
docker run -d --restart=always -e ... <registry>/<image>:latest
|
|
113
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# GitHub Actions — cloud-managed runner
|
|
2
|
+
|
|
3
|
+
**Who this is for:** any user. No infrastructure setup beyond a GitHub account.
|
|
4
|
+
|
|
5
|
+
**Best for:** daily DCA, slow rebalances, treasury strategies — anything where execution does not need to happen at a precise minute. If your strategy requires guaranteed timing (LP, perps, liquidations), use the [self-hosted runner](self-hosted-runner.md) or [Docker](docker-vm.md) options instead.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Your repo contains `.github/workflows/agent-tick.yml`. GitHub runs this on a cron schedule using their shared runner pool and via `workflow_dispatch` for on-demand or externally triggered runs.
|
|
12
|
+
|
|
13
|
+
## Timing limitation
|
|
14
|
+
|
|
15
|
+
GitHub's cron queue is shared across all users. Under load, scheduled jobs drift 5–30 minutes or are skipped entirely. This is a platform constraint — there is no workaround on GitHub's shared runners.
|
|
16
|
+
|
|
17
|
+
**Use `workflow_dispatch` as your primary trigger** and treat cron as a heartbeat/backstop. Fire `workflow_dispatch` from an external event (price alert, on-chain event, keeper) via:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sailor trigger github
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Full setup walkthrough is in the main `sail-automation` skill. Summary:
|
|
26
|
+
|
|
27
|
+
1. `sailor keys export-ci` — generates `ci-keystore.json` (encrypted; safe to commit)
|
|
28
|
+
2. Commit state files and push
|
|
29
|
+
3. Set `SAIL_PASSPHRASE` and `RPC_URL` as GitHub Actions secrets
|
|
30
|
+
4. The workflow runs on the next cron tick or on `workflow_dispatch`
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gh secret set SAIL_PASSPHRASE
|
|
34
|
+
gh secret set RPC_URL
|
|
35
|
+
gh workflow run agent-tick.yml # trigger a manual run to verify
|
|
36
|
+
gh run view --log # check output
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Cron cadence
|
|
40
|
+
|
|
41
|
+
Edit the `cron:` line in `.github/workflows/agent-tick.yml`:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
schedule:
|
|
45
|
+
- cron: "0 * * * *" # hourly (default placeholder — change this)
|
|
46
|
+
# - cron: "*/5 * * * *" # every 5 min (only reliable with a self-hosted runner)
|
|
47
|
+
# - cron: "0 0 * * *" # daily
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For sub-hourly cadence on GitHub's shared runners, the drift makes the schedule unreliable. Use the self-hosted runner option for that.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Local daemon — run on the project machine
|
|
2
|
+
|
|
3
|
+
**Who this is for:** any user who wants simple automation without cloud accounts or containers. The agent runs directly on the machine where the project lives.
|
|
4
|
+
|
|
5
|
+
**Best for:** development, testing, daily/slow strategies, or users who keep their machine on 24/7. Not suitable for strategies that require guaranteed execution at a precise time — if the machine is off or disconnected, the run is silently skipped.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Installs a system service that runs `sailor run --once` on a fixed interval. No keystore export or CI variable setup needed — the keys are already on disk in the project environment.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
sailor service install --interval 300 # every 5 min; tune to your strategy
|
|
15
|
+
sailor service status # check running state
|
|
16
|
+
sailor service logs -f # tail .sail/agent.log
|
|
17
|
+
sailor service stop # pause without uninstalling
|
|
18
|
+
sailor service uninstall # remove the service entirely
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The service is installed as:
|
|
22
|
+
- **macOS** — a `launchd` plist in `~/Library/LaunchAgents/`
|
|
23
|
+
- **Linux** — a `systemd` user unit
|
|
24
|
+
- **Windows** — a Task Scheduler entry
|
|
25
|
+
|
|
26
|
+
`--project` and `--chain` scope the service to a specific project or chain. `--force` overrides a TCC path error or unresolved passphrase prompt.
|
|
27
|
+
|
|
28
|
+
## Limitations
|
|
29
|
+
|
|
30
|
+
- The machine must be powered on and internet-connected when the job fires
|
|
31
|
+
- Missed runs are silent — there is no retry
|
|
32
|
+
- Not suitable for 24/7 strategies unless the machine is always on
|
|
33
|
+
|
|
34
|
+
If you want a portable setup that can move to a cloud deployment later, use [Docker](docker-vm.md) instead — same machine, same result, but the image runs anywhere.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Self-hosted runner — reliable timing on a dedicated machine
|
|
2
|
+
|
|
3
|
+
**Who this is for:** users who need time-precise execution. Requires a dedicated always-on machine that you manage.
|
|
4
|
+
|
|
5
|
+
**Best for:** LP strategies, perps, liquidations, or any strategy where the agent must run within seconds of the scheduled time. A self-hosted runner polls GitHub directly — it picks up jobs immediately, with no shared queue and no drift.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Same `agent-tick.yml` workflow as the GitHub Actions option. One change: `runs-on: ubuntu-latest` becomes `runs-on: [self-hosted, linux]`. The job then runs on your machine instead of GitHub's shared pool.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- A machine that is **always powered on** and **always connected to the internet**
|
|
16
|
+
- Do not use your personal computer — missed runs happen silently whenever it sleeps, restarts, or loses connectivity
|
|
17
|
+
- Recommended hardware: Raspberry Pi 4+, a dedicated mini PC (NUC, Intel BRIX, etc.), or a cloud VM on any provider
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
### 1. Register the runner on GitHub
|
|
22
|
+
|
|
23
|
+
Go to your repo: **Settings → Actions → Runners → New self-hosted runner**
|
|
24
|
+
|
|
25
|
+
Follow the official GitHub guide for your OS:
|
|
26
|
+
https://docs.github.com/es/actions/how-tos/manage-runners/self-hosted-runners/add-runners
|
|
27
|
+
|
|
28
|
+
The guide walks you through downloading the runner application, configuring it, and starting it as a service so it restarts automatically on reboot.
|
|
29
|
+
|
|
30
|
+
### 2. Update the workflow
|
|
31
|
+
|
|
32
|
+
In your local copy of `.github/workflows/agent-tick.yml`, change:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
jobs:
|
|
36
|
+
tick:
|
|
37
|
+
runs-on: ubuntu-latest # ← change this
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
to:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
jobs:
|
|
44
|
+
tick:
|
|
45
|
+
runs-on: [self-hosted, linux] # ← your runner label
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Commit and push. The next run will be picked up by your runner.
|
|
49
|
+
|
|
50
|
+
### 3. Verify
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
gh run list --workflow agent-tick.yml # confirm runs show "self-hosted" runner
|
|
54
|
+
gh run view --log # check for errors
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Machine options
|
|
58
|
+
|
|
59
|
+
| Machine | Cost | Notes |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Raspberry Pi 4 (2 GB+) | ~$45 one-time | Runs 24/7 on ~3W; plug into router via ethernet |
|
|
62
|
+
| Mini PC (NUC, BRIX) | ~$100–200 | More headroom; good if you run other services too |
|
|
63
|
+
| Cloud VM (Oracle Always Free) | Free | 2 AMD VMs permanently free; requires basic Linux knowledge |
|
|
64
|
+
| Cloud VM (any provider) | ~$4–10/month | AWS t3.micro, GCP e2-micro, Hetzner CX11, etc. |
|
|
65
|
+
|
|
66
|
+
## Your responsibility
|
|
67
|
+
|
|
68
|
+
Sail does not manage this machine. You are responsible for:
|
|
69
|
+
- Keeping it powered on and connected
|
|
70
|
+
- OS updates and security patches
|
|
71
|
+
- Restarting the runner service if it stops (configure it to start on boot during setup)
|
|
72
|
+
- Monitoring that runs are completing as expected
|
|
@@ -9,6 +9,8 @@ description: Walks the agent through setting up a new Sailor project or resuming
|
|
|
9
9
|
|
|
10
10
|
The project does not install `sailor` as a dependency, so invoke it with **`npx sailor <command>`** unless it is installed globally (`npm i -g @sail.money/sailor`, then `sailor` works bare). Every `sailor …` command in these skills assumes one of those. Confirm the toolchain up front and pin a recent version — `npx sailor@latest --version` — because an old cached `npx` build can be missing newer commands (e.g. `mandate simulate`); if a documented command reports "unknown command", you are on a stale version, not hitting a missing feature.
|
|
11
11
|
|
|
12
|
+
After upgrading the CLI, run `sailor update` from the project root to pull in updated skills, `AGENTS.md`, `Dockerfile`, and other tooling files. User files (`src/`, `mandates/`, `.sail/`, `package.json`) are never touched.
|
|
13
|
+
|
|
12
14
|
Stage machine keyed off `.sail/`. Read the state, enter at the right stage, never re-run completed stages.
|
|
13
15
|
|
|
14
16
|
## Determine where the user is
|
|
@@ -64,7 +64,7 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
|
|
|
64
64
|
| sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
|
|
65
65
|
| sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
|
|
66
66
|
| sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts | `.agents/skills/sail-mandates/SKILL.md` |
|
|
67
|
-
| sail-
|
|
67
|
+
| sail-automation | Automating the agent — GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
|
|
68
68
|
| sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
|
|
69
69
|
|
|
70
70
|
## Invariants — apply to every turn
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM node:20-slim
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
|
|
4
|
+
COPY package*.json ./
|
|
5
|
+
RUN npm install
|
|
6
|
+
|
|
7
|
+
COPY . .
|
|
8
|
+
|
|
9
|
+
# Seconds between runs. Tune to your strategy: 60 = per-minute, 300 = 5 min, 86400 = daily.
|
|
10
|
+
ENV AGENT_INTERVAL=300
|
|
11
|
+
|
|
12
|
+
CMD ["sh", "-c", "\
|
|
13
|
+
mkdir -p .sail/keys && \
|
|
14
|
+
cp ci-keystore.json .sail/keys/manager.json && \
|
|
15
|
+
while true; do \
|
|
16
|
+
npx sailor run --once; \
|
|
17
|
+
sleep ${AGENT_INTERVAL}; \
|
|
18
|
+
done"]
|