@meffecta/agent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +197 -0
- package/IMPLEMENTATION.md +292 -0
- package/README.md +64 -0
- package/bin/meffecta-agent.js +120 -0
- package/deployment.env.example +54 -0
- package/engine.json +4 -0
- package/lib/commands.js +407 -0
- package/lib/config.js +56 -0
- package/lib/gcloud.js +109 -0
- package/package.json +38 -0
- package/scripts/artifact-cleanup-policy.json +17 -0
- package/scripts/create-project.sh +106 -0
- package/scripts/deploy.sh +169 -0
- package/scripts/lib/deployment.sh +109 -0
- package/scripts/link-billing.sh +143 -0
- package/scripts/mint-gmail-token.mjs +126 -0
- package/scripts/mint-graph-token.mjs +152 -0
- package/scripts/set-artifact-cleanup.sh +108 -0
- package/scripts/set-env.sh +92 -0
- package/scripts/set-secret.sh +86 -0
- package/scripts/setup-infrastructure.sh +263 -0
- package/scripts/setup-scheduler.sh +287 -0
- package/scripts/update-tooling.sh +117 -0
- package/scripts/verify-credentials.mjs +557 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# How the agent is put together
|
|
2
|
+
|
|
3
|
+
Orientation for someone about to run a deployment. The set-up steps themselves are in
|
|
4
|
+
[IMPLEMENTATION.md](IMPLEMENTATION.md); this is what those steps are building and why.
|
|
5
|
+
|
|
6
|
+
## The shape
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
this engine repo ──CI──▶ ghcr.io/meffecta/agent (the image; never company-specific)
|
|
10
|
+
│
|
|
11
|
+
│ scripts/deploy.sh
|
|
12
|
+
▼
|
|
13
|
+
your content repo ──cloned every run──▶ Cloud Run service (your project, your creds)
|
|
14
|
+
jobs/ worlds/ SYSTEM.md ↑
|
|
15
|
+
└─ credentials in Secret Manager
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The engine is a job runner: it wakes on a schedule (or a webhook, or new mail), clones the
|
|
19
|
+
content repo, and hands one job's prompt to the `claude` CLI with a working directory, a
|
|
20
|
+
system prompt, a set of skills and a memory directory. What the agent *does* is entirely
|
|
21
|
+
in the content repo and the credentials; the engine knows nothing about any company.
|
|
22
|
+
|
|
23
|
+
## Two rules that explain most of the design
|
|
24
|
+
|
|
25
|
+
**The engine is generic; the content repo is yours.** Every run re-clones the content repo,
|
|
26
|
+
so a pushed prompt is live on the next run — no build, no deploy. Only moving to a new
|
|
27
|
+
*engine version* is a deploy. This is also why the engine repo's CI publishes an image and
|
|
28
|
+
stops: when a deployment takes a new version is that deployment's call.
|
|
29
|
+
|
|
30
|
+
**The agent's power is exactly its credentials.** There is no other lever. Give it a GCP
|
|
31
|
+
project of its own, and add one credential at a time — every skill degrades gracefully
|
|
32
|
+
when its variables are unset, so a half-configured agent is a working agent with fewer
|
|
33
|
+
abilities, not a broken one. Reviewing what an agent can do means reviewing what its
|
|
34
|
+
service account is invited to and what tokens sit in its Secret Manager.
|
|
35
|
+
|
|
36
|
+
## What needs what
|
|
37
|
+
|
|
38
|
+
| Change | Effect |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Job prompts, run settings, `SYSTEM.md`, worlds, content-repo skills | **Push** — live on the next run |
|
|
41
|
+
| Adding, renaming, or retriggering a job (`cron`/`webhook`/`inbox` frontmatter) | **Restart** — triggers register from a boot-time clone, plus `scripts/setup-scheduler.sh` on Cloud Run |
|
|
42
|
+
| A new engine version | `scripts/deploy.sh` (which re-syncs the triggers too) |
|
|
43
|
+
|
|
44
|
+
Renaming a job also moves its identity: memory lives at `<MEMORY_DIR>/jobs/<name>/`, so
|
|
45
|
+
copy that directory across or the job wakes up amnesiac, and pending spawns for the old
|
|
46
|
+
name die with it.
|
|
47
|
+
|
|
48
|
+
## Operating a deployment
|
|
49
|
+
|
|
50
|
+
Operating a deployment is one command-line tool, **`npx @meffecta/agent`**, published to
|
|
51
|
+
npm alongside the engine image it deploys. Installing it is all the access an operator
|
|
52
|
+
needs; the engine's source is not involved. Each release of the tool records the engine
|
|
53
|
+
build it was published with, so upgrading the tool is how you move to a new engine —
|
|
54
|
+
deliberately, rather than by surprise.
|
|
55
|
+
|
|
56
|
+
It reads this deployment's identity from a `deployment.env` in your content repo, committed
|
|
57
|
+
beside `jobs/`. Which GCP project a set of jobs runs as is a fact about that deployment,
|
|
58
|
+
and none of it is secret.
|
|
59
|
+
|
|
60
|
+
That makes the working shape: stand in the content repo and run the tool.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd /path/to/acme-agent-home
|
|
64
|
+
npx @meffecta/agent status
|
|
65
|
+
npx @meffecta/agent deploy
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Nothing is read from exported shell variables, so a command cannot silently act on the
|
|
69
|
+
wrong deployment; each script prints the project and service it is about to touch, and
|
|
70
|
+
refuses to run when it cannot find them (`--config <file>` overrides the lookup).
|
|
71
|
+
|
|
72
|
+
`deployment.env` also holds the **runtime shape** — `SCALING`, `CPU`, `MEMORY`,
|
|
73
|
+
`MAX_INSTANCES`, `REQUEST_TIMEOUT` — which `deploy.sh` re-asserts on every rollout. So the
|
|
74
|
+
file, not the console, is the truth about how the service runs, and `SCALING` in particular
|
|
75
|
+
has to stay honest: it and the triggers are one decision made in two places.
|
|
76
|
+
|
|
77
|
+
Every command is one of the scripts below with a name in front of it — the CLI is the
|
|
78
|
+
interface, the scripts are the implementation, and they are published together so the pair
|
|
79
|
+
cannot drift. A deployment without npm can extract the scripts from the public image
|
|
80
|
+
instead and run them directly.
|
|
81
|
+
|
|
82
|
+
| Command / script | Does |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `setup-infrastructure.sh` | one-time GCP provisioning; idempotent |
|
|
85
|
+
| `setup-scheduler.sh` | the deployment's external triggers — Cloud Scheduler + Tasks; idempotent |
|
|
86
|
+
| `set-artifact-cleanup.sh` | expire mirrored images in the deployment's registry |
|
|
87
|
+
| `update-tooling.sh` | refresh these scripts from a published engine image |
|
|
88
|
+
| `set-secret.sh` | store a value in Secret Manager and bind it; re-run to rotate |
|
|
89
|
+
| `set-env.sh` | non-secret service settings, several per revision |
|
|
90
|
+
| `deploy.sh` | roll a published engine image out, and assert the runtime shape |
|
|
91
|
+
|
|
92
|
+
For a question rather than a change, the service serves **`/ask`**: a prompt box behind the
|
|
93
|
+
same secret, over basic auth, that runs it with the deployment's skills and credentials and
|
|
94
|
+
shows the answer. It is the quickest way to find out what the agent can currently see.
|
|
95
|
+
|
|
96
|
+
`set-secret.sh` never lets a value reach the shell history or the screen: `--random`
|
|
97
|
+
generates one, a pipe or `--from-file` supplies one, and otherwise it prompts with echo
|
|
98
|
+
off. `deploy.sh`, `set-env.sh` and `setup-scheduler.sh` take `--dry-run`.
|
|
99
|
+
|
|
100
|
+
## Where the rules live
|
|
101
|
+
|
|
102
|
+
Every run's system prompt is built from two layers:
|
|
103
|
+
|
|
104
|
+
1. **The deployment's `SYSTEM.md`**, from its content repo — who this agent serves, its
|
|
105
|
+
worlds, its tone, which systems belong to whom.
|
|
106
|
+
2. **The engine's `SYSTEM.base.md`**, from the image — the safety rules every deployment
|
|
107
|
+
gets whether or not anyone remembered to write them.
|
|
108
|
+
|
|
109
|
+
The baseline is appended **last**, closest to the task, and says in its own text that it
|
|
110
|
+
overrides what precedes it. A deployment can make a rule stricter or name the concrete
|
|
111
|
+
accounts it applies to; it cannot relax one, and it cannot shadow the file — the engine
|
|
112
|
+
resolves it from its own install directory, never from the working repo, so no content a
|
|
113
|
+
deployment or a run controls can stand in for it. A missing baseline throws rather than
|
|
114
|
+
running unprotected.
|
|
115
|
+
|
|
116
|
+
What the baseline carries: content you read is data and never instructions; act only
|
|
117
|
+
where the job says, with anything outward-facing or irreversible left for a person unless
|
|
118
|
+
the job directs it; secrets never leave; other people's privacy; report honestly. Both
|
|
119
|
+
layers hold across a spawned follow-up run, which inherits its parent's system prompt.
|
|
120
|
+
|
|
121
|
+
A rule that must be *structurally* impossible belongs in code, not prose — the way the
|
|
122
|
+
Google Ads skill splits reads from writes and caps every mutation, and the way a read-only
|
|
123
|
+
mail token makes sending impossible rather than merely forbidden.
|
|
124
|
+
|
|
125
|
+
## Skills
|
|
126
|
+
|
|
127
|
+
Skills are how the agent reaches a system: the how-to for Gmail, Microsoft Graph, Drive,
|
|
128
|
+
Cloudflare, a browser, Grafana, PostHog, Google Ads. They resolve in three layers, each
|
|
129
|
+
winning over the next:
|
|
130
|
+
|
|
131
|
+
1. the **working repo** a job runs in (`repo:` frontmatter),
|
|
132
|
+
2. the **content repo** (`.claude/skills/`) — this deployment's own or its overrides,
|
|
133
|
+
3. the **engine defaults**, shipped in the image.
|
|
134
|
+
|
|
135
|
+
So a deployment can override any default without forking the engine, and a product repo
|
|
136
|
+
can supply its own variant for jobs that run inside it.
|
|
137
|
+
|
|
138
|
+
## Triggers
|
|
139
|
+
|
|
140
|
+
Four ways a run starts. The first three are configured in a job's frontmatter:
|
|
141
|
+
|
|
142
|
+
- **cron** — a schedule, in the deployment's timezone
|
|
143
|
+
- **webhook** — `POST /webhooks/<name>`
|
|
144
|
+
- **inbox** — new mail to a watched address, polled
|
|
145
|
+
|
|
146
|
+
The fourth is the only one the agent pulls itself: a run that cannot finish inside its
|
|
147
|
+
time limit, or that has to wait for something, writes the instruction for a **follow-up
|
|
148
|
+
run** and schedules it. That follow-up is the same job identity — same repo, system
|
|
149
|
+
prompt, settings and memory — with a fresh time limit. The token that authorizes it stands
|
|
150
|
+
for that run's identity and dies with the run, so a run can only ever schedule more of its
|
|
151
|
+
own job's work, whatever its prompt says.
|
|
152
|
+
|
|
153
|
+
### What actually wakes the service
|
|
154
|
+
|
|
155
|
+
On Cloud Run the service **scales to zero and is billed only while a request is open**,
|
|
156
|
+
which is why an agent that is busy half an hour a day does not cost a machine's worth a
|
|
157
|
+
month. The price of that is a service with no CPU of its own: it cannot keep a clock, and
|
|
158
|
+
it cannot finish work in the background of a response. So all four triggers are delivered
|
|
159
|
+
to it as HTTP requests, and the handler holds each request open until the run settles.
|
|
160
|
+
|
|
161
|
+
`scripts/setup-scheduler.sh` creates the senders: **Cloud Scheduler** for the crons and for
|
|
162
|
+
an hourly (or poll-interval) housekeeping sweep, **Cloud Tasks** for everything that has to
|
|
163
|
+
be acknowledged immediately — webhooks, manual runs, follow-ups due later. Both cap one
|
|
164
|
+
attempt at 30 minutes, so a run that lasts longer spans several: the handler answers 503,
|
|
165
|
+
and the retry re-attaches to the same run instead of starting another. Nothing about
|
|
166
|
+
durability rests on this — the queue journal and the spawn spool on the memory bucket stay
|
|
167
|
+
the record, and the sweep re-drives anything a delivery lost.
|
|
168
|
+
|
|
169
|
+
The alternative shape is a machine that is always awake — a Mac mini, or a service pinned
|
|
170
|
+
to `min-instances=1`. There the process keeps its own timers and needs none of the above;
|
|
171
|
+
leaving `AGENT_TASKS_QUEUE` unset is what selects it. Do not do half of each: a
|
|
172
|
+
scaled-to-zero service without Scheduler jobs looks healthy and runs nothing, and an
|
|
173
|
+
always-on service with them fires every cron twice.
|
|
174
|
+
|
|
175
|
+
## Memory and audit
|
|
176
|
+
|
|
177
|
+
Each job has a memory directory it reads at the start of a run and updates at the end —
|
|
178
|
+
working state, not bulk content. It lives on a mounted bucket, so it survives restarts and
|
|
179
|
+
deploys.
|
|
180
|
+
|
|
181
|
+
Separately, every run writes to an **audit bucket** that is never mounted and never
|
|
182
|
+
readable by the agent: the prompt, the trigger, the result, and the full transcript of
|
|
183
|
+
every tool call. That is where you look when a run surprises you, and it is deliberately
|
|
184
|
+
out of the agent's own reach.
|
|
185
|
+
|
|
186
|
+
## Where it runs
|
|
187
|
+
|
|
188
|
+
Cloud Run is the default and the only place where service-account authentication works for
|
|
189
|
+
the metadata-server surfaces (domain-wide delegation, GA4, Search Console, Ads, deployment
|
|
190
|
+
listing). The same image runs anywhere docker does, and the service also runs natively on
|
|
191
|
+
a Mac or Linux box with Node 24+, `MEMORY_DIR` pointed at a real directory and
|
|
192
|
+
`NODE_ENV=production` to enable crons — keep it alive with launchd or systemd. Off Cloud
|
|
193
|
+
Run, the metadata-server surfaces degrade to their fallbacks.
|
|
194
|
+
|
|
195
|
+
**Run it in exactly one place at a time.** Two instances mean every cron fires twice and
|
|
196
|
+
both send email — and on Cloud Run, scaling to zero is what makes the second copy tempting,
|
|
197
|
+
so check that the first one is actually stopped rather than merely idle.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Setting up a new agent
|
|
2
|
+
|
|
3
|
+
Every step from nothing to a first run, in order. For how the pieces fit and why, see
|
|
4
|
+
[ARCHITECTURE.md](ARCHITECTURE.md).
|
|
5
|
+
|
|
6
|
+
Everything here is one command-line tool, `npx @meffecta/agent`, run from **your content
|
|
7
|
+
repo** — the directory step 3 creates, which holds this deployment's jobs and its settings.
|
|
8
|
+
You never need the engine's source: the tool and the engine image it deploys are published
|
|
9
|
+
together, and installing the tool is all the access you need.
|
|
10
|
+
|
|
11
|
+
npx @meffecta/agent steps # this guide, condensed to the command for each step
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Create a GCP project
|
|
16
|
+
|
|
17
|
+
Give the agent a project of its own, separate from any product project it will read: this
|
|
18
|
+
project's service account gets project-wide secret access, and the mail tokens stored here
|
|
19
|
+
must not be readable by another system's machinery.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gcloud auth login
|
|
23
|
+
npx @meffecta/agent create-project
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It asks for a display name, suggests a project ID from it, validates the ID against
|
|
27
|
+
Google's rules before spending a round trip, and creates the project. Pass
|
|
28
|
+
`--name`/`--id` to skip the prompts, or `ORGANIZATION=…` / `FOLDER=…` to place it.
|
|
29
|
+
|
|
30
|
+
- [ ] Note the **project ID** it prints. Project IDs are globally unique, so a plain name
|
|
31
|
+
is often taken and you end up with a suffixed one (`acme-agent-506513`). The id, not
|
|
32
|
+
the display name, is what every later command needs.
|
|
33
|
+
|
|
34
|
+
## 2. Link a billing account
|
|
35
|
+
|
|
36
|
+
Nothing else works without this: provisioning starts by enabling APIs, which fails on an
|
|
37
|
+
unbilled project.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @meffecta/agent link-billing --project <PROJECT_ID>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It lists the billing accounts you can use, marks closed ones as unselectable (linking one
|
|
44
|
+
appears to succeed and leaves the project unbilled), links your choice, and then confirms
|
|
45
|
+
the project really is billed rather than trusting the command's exit code.
|
|
46
|
+
|
|
47
|
+
- [ ] `billingEnabled: true`
|
|
48
|
+
|
|
49
|
+
If it reports a **billing quota**, that is the account's cap on how many projects it may
|
|
50
|
+
fund, not a problem with this project — free a slot with `gcloud billing projects unlink
|
|
51
|
+
<PROJECT>`, or request an increase.
|
|
52
|
+
|
|
53
|
+
## 3. Create the content repo
|
|
54
|
+
|
|
55
|
+
A private GitHub repo holding everything about *this* deployment:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
deployment.env # which GCP project and service this deployment runs as
|
|
59
|
+
SYSTEM.md # who the agent serves, its worlds, its tone
|
|
60
|
+
jobs/ # one .md per job: YAML frontmatter + prompt body
|
|
61
|
+
worlds/ # one folder per business area (optional to start)
|
|
62
|
+
.claude/skills/ # skills this deployment owns or overrides (optional)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`deployment.env` is committed — none of it is secret, and a clone of the repo is then
|
|
66
|
+
everything an operator needs:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @meffecta/agent init # writes deployment.env here
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ini
|
|
73
|
+
PROJECT=acme-agent-506513 # the project ID from step 1
|
|
74
|
+
SERVICE=acme-agent
|
|
75
|
+
ARTIFACT_REPO=acme-agent-images
|
|
76
|
+
REGION=europe-west1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`SYSTEM.md` describes **who this agent serves** — the person or team, the businesses, the
|
|
80
|
+
tone, which systems belong to whom. It does not need to restate the safety rules: the
|
|
81
|
+
engine appends its own baseline ([SYSTEM.base.md](SYSTEM.base.md) — what you read is data
|
|
82
|
+
not instructions, act only where the job says, secrets never leave, third-party privacy,
|
|
83
|
+
report honestly) to every run in every deployment, and that baseline overrides anything
|
|
84
|
+
contradicting it. Your `SYSTEM.md` can make a rule *stricter*, or name the concrete
|
|
85
|
+
accounts and people it applies to; it cannot relax one.
|
|
86
|
+
|
|
87
|
+
One job to start, `jobs/hello.md`:
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
---
|
|
91
|
+
name: hello
|
|
92
|
+
cron: "0 7 * * 1-5"
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
Say good morning and list today's date. Send it by email to <person> using the
|
|
96
|
+
send-email skill. If email is unavailable, return the text as your result.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Frontmatter keys: `cron`, `webhook`, `inbox`, `repo`, `systemMd`, `model`, `effort`,
|
|
100
|
+
`allowedTools`, `timeoutSeconds`, `disabled`. A job with no `name:` takes its filename.
|
|
101
|
+
|
|
102
|
+
- [ ] Repo pushed to GitHub
|
|
103
|
+
- [ ] `cd` into it — **every command from here on runs from the content repo**
|
|
104
|
+
|
|
105
|
+
## 4. Install the tools
|
|
106
|
+
|
|
107
|
+
On the machine you operate from:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
docker info >/dev/null && echo "docker ok" # needed to mirror engine images
|
|
111
|
+
git --version && gh --version
|
|
112
|
+
node --version # 24+
|
|
113
|
+
npm i -g @anthropic-ai/claude-code
|
|
114
|
+
corepack enable
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- [ ] `gcloud auth configure-docker $REGION-docker.pkg.dev` — one-time, so the mirror in
|
|
118
|
+
step 8 can push into your Artifact Registry. Pulling the engine image itself needs
|
|
119
|
+
no login: `ghcr.io/meffecta/agent` is public.
|
|
120
|
+
|
|
121
|
+
Then the tool itself:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm i -g @meffecta/agent # or use npx, which needs no install
|
|
125
|
+
meffecta-agent version # names the engine build this version deploys
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- [ ] `meffecta-agent help` lists the commands
|
|
129
|
+
- [ ] `meffecta-agent version` shows a pinned engine tag, not "latest"
|
|
130
|
+
|
|
131
|
+
Pin the version you install. Each release of the tool deploys the engine image published
|
|
132
|
+
alongside it, so upgrading the tool is how you move to a new engine — deliberately, not by
|
|
133
|
+
surprise.
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>No npm? The same tooling is inside the public image.</summary>
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cid=$(docker create ghcr.io/meffecta/agent:latest) \
|
|
140
|
+
&& docker cp "$cid:/app/scripts" . \
|
|
141
|
+
&& docker rm "$cid" >/dev/null
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
That drops `scripts/` into your content repo; run `scripts/deploy.sh` and friends directly,
|
|
145
|
+
and `scripts/update-tooling.sh --tag <the tag you deploy>` to refresh them. Every CLI
|
|
146
|
+
command is one of these scripts with a name in front of it.
|
|
147
|
+
</details>
|
|
148
|
+
|
|
149
|
+
## 5. Mint the credentials
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
claude setup-token # → CLAUDE_CODE_OAUTH_TOKEN, pasted in step 7
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- [ ] `CLAUDE_CODE_OAUTH_TOKEN` — this deployment bills its own runs
|
|
156
|
+
- [ ] `GITHUB_TOKEN` — fine-grained PAT with **contents read on the content repo** (the
|
|
157
|
+
service cannot start without it), plus write on any repo whose jobs open PRs
|
|
158
|
+
- [ ] `AGENT_WEBHOOK_SECRET` needs nothing here — step 7 generates it
|
|
159
|
+
|
|
160
|
+
**Gmail + Calendar** (skip if the agent needs no mailbox). In the agent's GCP project:
|
|
161
|
+
enable the Gmail and Calendar APIs; OAuth consent screen → **External**, then **publish to
|
|
162
|
+
production** — an app left in "Testing" expires refresh tokens after 7 days; create an
|
|
163
|
+
OAuth client of type **Desktop app**. Then per account:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npx @meffecta/agent mint-gmail \
|
|
167
|
+
--client-id <id> --client-secret <secret> [--account <NAME>] [--readonly]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Sign in as the account the token is for. Default → `GMAIL_REFRESH_TOKEN`; named →
|
|
171
|
+
`GMAIL_<NAME>_REFRESH_TOKEN`. Use `--readonly` for any mailbox this company does not
|
|
172
|
+
administer alone.
|
|
173
|
+
|
|
174
|
+
**Microsoft 365** (only if a mailbox lives there): one Entra app registration, then
|
|
175
|
+
`npx @meffecta/agent mint-graph --account <NAME> …`.
|
|
176
|
+
|
|
177
|
+
## 6. Provision the infrastructure
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx @meffecta/agent setup-infra
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Idempotent — re-run it after any change. It prints the project and service it is about to
|
|
184
|
+
touch, then creates: the APIs, an Artifact Registry repo, the runtime service account, a
|
|
185
|
+
memory bucket (mounted at `/memory`), an audit bucket (365-day retention, write-once,
|
|
186
|
+
never mounted), and the Cloud Run service shell with a placeholder image and CPU always
|
|
187
|
+
allocated.
|
|
188
|
+
|
|
189
|
+
- [ ] The banner names the project from your `deployment.env` — if not, you are in the
|
|
190
|
+
wrong directory
|
|
191
|
+
|
|
192
|
+
## 7. Wire up the configuration
|
|
193
|
+
|
|
194
|
+
`set-secret.sh` stores a value in Secret Manager and binds it to the service in one step;
|
|
195
|
+
the value never reaches your shell history or the screen. Re-running it rotates a value.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx @meffecta/agent set-secret AGENT_WEBHOOK_SECRET --random # generated for you
|
|
199
|
+
npx @meffecta/agent set-secret CLAUDE_CODE_OAUTH_TOKEN # prompts, hidden
|
|
200
|
+
npx @meffecta/agent set-secret GITHUB_TOKEN
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- [ ] `AGENT_WEBHOOK_SECRET`
|
|
204
|
+
- [ ] `CLAUDE_CODE_OAUTH_TOKEN`
|
|
205
|
+
- [ ] `GITHUB_TOKEN`
|
|
206
|
+
|
|
207
|
+
Non-secret settings go through `set-env` (several at once become one revision; add
|
|
208
|
+
`--dry-run` to see what it would do). **The service will not boot without `GIT_REPO_URL`:**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npx @meffecta/agent set-env GIT_REPO_URL=https://github.com/<owner>/<repo>.git
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- [ ] `GIT_REPO_URL` points at the content repo from step 3
|
|
215
|
+
|
|
216
|
+
Optional, one ability each: `GMAIL_*`, `MSGRAPH_*`, `GRAFANA_*`, `CLOUDFLARE_*`,
|
|
217
|
+
`POSTHOG_*`, `GOOGLE_ADS_*`, `AHREFS_API_KEY`, `<WORLD>_GA4_PROPERTY_ID`. Full list with
|
|
218
|
+
comments in the engine repo's `.env.example`. Tokens and keys go through `set-secret`;
|
|
219
|
+
addresses, ids and URLs through `set-env`. `meffecta-agent env` and `meffecta-agent secrets`
|
|
220
|
+
show what is set — the second never prints a value.
|
|
221
|
+
|
|
222
|
+
Surfaces that authenticate as the **runtime service account** rather than a token — GA4,
|
|
223
|
+
Search Console, Google Ads, Cloud Run deployment listing, and delegated Gmail/Drive/
|
|
224
|
+
Calendar — additionally need that service account invited on each property, and
|
|
225
|
+
domain-wide delegation granted in the Workspace admin console. Leave these for last; each
|
|
226
|
+
degrades gracefully until granted.
|
|
227
|
+
|
|
228
|
+
## 8. Deploy the engine
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npx @meffecta/agent deploy # --dry-run first to see the target
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Mirrors the engine image this tool version was published with into your Artifact Registry,
|
|
235
|
+
rolls it out, and asserts the runtime shape from `deployment.env`. `--tag <other>` pins or
|
|
236
|
+
rolls back to a different build.
|
|
237
|
+
|
|
238
|
+
## 9. Create the triggers
|
|
239
|
+
|
|
240
|
+
The service scales to zero and has CPU only while it is answering a request, so nothing in
|
|
241
|
+
it fires on its own. This is what sends it work:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npx @meffecta/agent setup-scheduler # --dry-run first to see what it would create
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
It creates a Cloud Tasks queue, points the service at it, and creates one Cloud Scheduler
|
|
248
|
+
job per `cron:` in your content repo plus a housekeeping sweep. It reads the job list from
|
|
249
|
+
the running service, so step 8 has to have succeeded first.
|
|
250
|
+
|
|
251
|
+
- [ ] It reports one trigger per cron job, plus `<service>-sweep`
|
|
252
|
+
- [ ] `npx @meffecta/agent triggers` agrees, and shows the task queue running
|
|
253
|
+
|
|
254
|
+
Re-run it whenever a job's `cron:` changes or a job is added or renamed — `deploy` does it
|
|
255
|
+
for you on every rollout from here on. Check it any time with `meffecta-agent triggers`.
|
|
256
|
+
|
|
257
|
+
## 10. Verify
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npx @meffecta/agent status
|
|
261
|
+
npx @meffecta/agent jobs
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- [ ] `status` shows a ready revision, your triggers, and an idle queue
|
|
265
|
+
- [ ] `jobs` lists your jobs — this proves the boot clone worked. An empty list means
|
|
266
|
+
`GIT_REPO_URL` is wrong or `GITHUB_TOKEN` cannot clone it.
|
|
267
|
+
|
|
268
|
+
Then exercise every configured credential from inside the deployment:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npx @meffecta/agent ask "run node /app/scripts/verify-credentials.mjs and return its output"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- [ ] Each surface reports ok or skip; no FAIL (`skip` = not configured, which is fine)
|
|
275
|
+
|
|
276
|
+
The same thing has a web page, for anyone who would rather not use a terminal: open
|
|
277
|
+
`/ask` on the service URL that `status` printed. Any username, the secret as the password.
|
|
278
|
+
|
|
279
|
+
- [ ] The page loads and an answer comes back
|
|
280
|
+
|
|
281
|
+
## 11. First real run
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npx @meffecta/agent run hello
|
|
285
|
+
npx @meffecta/agent logs --limit 50
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- [ ] Read what it produced before letting the cron take over
|
|
289
|
+
|
|
290
|
+
Then add jobs one at a time — each push is live on the next run. `disabled: true` in
|
|
291
|
+
frontmatter is the kill switch, effective on push. Every prompt, result and full
|
|
292
|
+
transcript lands in the audit bucket.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @meffecta/agent
|
|
2
|
+
|
|
3
|
+
Set up and operate a **Meffecta Agent** deployment — a self-hosted Claude Code job runner
|
|
4
|
+
that wakes on a schedule, a webhook, or new mail, and runs a job against a fresh clone of
|
|
5
|
+
your own content repo.
|
|
6
|
+
|
|
7
|
+
The agent itself is a container image (`ghcr.io/meffecta/agent`). This package is the tool
|
|
8
|
+
that stands one up and runs it, and it is all you need: the engine's source is private,
|
|
9
|
+
and nothing here depends on it.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @meffecta/agent steps # the whole set-up, in order
|
|
13
|
+
npx @meffecta/agent help # every command
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
Run it from your **content repo** — the directory holding `deployment.env`, `jobs/`,
|
|
19
|
+
`worlds/` and `SYSTEM.md`. That file says which GCP project and Cloud Run service this
|
|
20
|
+
deployment is, so a command can never quietly act on the wrong one.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @meffecta/agent create-project # a GCP project
|
|
24
|
+
npx @meffecta/agent link-billing # attach billing
|
|
25
|
+
npx @meffecta/agent setup-infra # registry, buckets, service account, service
|
|
26
|
+
npx @meffecta/agent set-secret AGENT_WEBHOOK_SECRET --random
|
|
27
|
+
npx @meffecta/agent set-env GIT_REPO_URL=https://github.com/you/your-content-repo.git
|
|
28
|
+
npx @meffecta/agent deploy # roll out the engine
|
|
29
|
+
npx @meffecta/agent setup-scheduler # give it its triggers
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then, day to day:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @meffecta/agent status # revision, shape, triggers, queue
|
|
36
|
+
npx @meffecta/agent jobs # what is registered, and what triggers it
|
|
37
|
+
npx @meffecta/agent env # every setting (secret values never printed)
|
|
38
|
+
npx @meffecta/agent secrets # what is stored, and what the service reads
|
|
39
|
+
npx @meffecta/agent triggers # the Cloud Scheduler jobs and task queue
|
|
40
|
+
npx @meffecta/agent logs --limit 100
|
|
41
|
+
npx @meffecta/agent run morning-brief
|
|
42
|
+
npx @meffecta/agent ask "how did search do last week"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Two things worth knowing
|
|
46
|
+
|
|
47
|
+
**The version you install is the engine you deploy.** Each release records the engine build
|
|
48
|
+
published alongside it, and `deploy` defaults to that image. Tooling and engine move
|
|
49
|
+
together, so a script can never provision the wrong shape for the engine it is aimed at.
|
|
50
|
+
`meffecta-agent version` tells you which pair you have.
|
|
51
|
+
|
|
52
|
+
**Secret values are never printed.** `secrets` lists what exists and whether the service
|
|
53
|
+
reads it; `env` names secret-backed settings without resolving them. Setting one takes the
|
|
54
|
+
value from a pipe, a file, or a hidden prompt — never from your shell history.
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
`gcloud` (authenticated), `docker` (to mirror engine images into your registry), `git`,
|
|
59
|
+
and Node 20+. macOS or Linux.
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
`IMPLEMENTATION.md` and `ARCHITECTURE.md` ship inside this package — the full set-up, and
|
|
64
|
+
how the pieces fit.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { COMMANDS, cliVersion, engineTag, GROUPS } from "../lib/commands.js";
|
|
3
|
+
import { loadDeployment, UserError } from "../lib/config.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One entry point for everything an operator does to a Meffecta Agent deployment.
|
|
7
|
+
*
|
|
8
|
+
* The engine itself is a container image; this is the tool that stands one up and runs it.
|
|
9
|
+
* Most commands are the shipped shell scripts with a name in front of them — the CLI is
|
|
10
|
+
* the interface, those scripts are the implementation, and they travel together in this
|
|
11
|
+
* package so the pair cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* No runtime dependencies, on purpose: `npx @meffecta/agent` should be one download, and
|
|
14
|
+
* a tool that holds someone's cloud credentials is a poor place for a supply chain.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const STEPS = [
|
|
18
|
+
["create-project", "A new GCP project."],
|
|
19
|
+
["link-billing", "Attach a billing account, or nothing can run."],
|
|
20
|
+
["", "Create your content repo — jobs/, worlds/, SYSTEM.md, deployment.env."],
|
|
21
|
+
["", "Install gcloud, docker, git, node 24+, and the claude CLI."],
|
|
22
|
+
["mint-gmail", "Credentials: claude setup-token, then a token per mail account."],
|
|
23
|
+
["setup-infra", "Provision GCP and the Cloud Run service shell."],
|
|
24
|
+
["set-secret", "AGENT_WEBHOOK_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN."],
|
|
25
|
+
["set-env", "GIT_REPO_URL=… — the service will not boot without it."],
|
|
26
|
+
["deploy", "Roll out the engine image."],
|
|
27
|
+
["setup-scheduler", "Give it its triggers. Scaled to zero, it fires nothing without them."],
|
|
28
|
+
["status", 'Check it. Then: jobs, run <job>, ask "…".'],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function help() {
|
|
32
|
+
const { tag, pinned } = engineTag();
|
|
33
|
+
console.log("meffecta-agent — set up and operate a Meffecta Agent deployment\n");
|
|
34
|
+
console.log("Usage: meffecta-agent <command> [options]");
|
|
35
|
+
console.log(" npx @meffecta/agent <command> [options]\n");
|
|
36
|
+
console.log("Run it from your content repo — the directory holding deployment.env and jobs/.\n");
|
|
37
|
+
for (const group of GROUPS) {
|
|
38
|
+
console.log(`${group.title}:`);
|
|
39
|
+
const width = Math.max(...group.commands.map(([name]) => name.length));
|
|
40
|
+
for (const [name, summary] of group.commands) {
|
|
41
|
+
console.log(` ${name.padEnd(width)} ${summary}`);
|
|
42
|
+
}
|
|
43
|
+
console.log("");
|
|
44
|
+
}
|
|
45
|
+
console.log(" steps The whole set-up, in order, with the command for each");
|
|
46
|
+
console.log(" version This CLI and the engine build it deploys\n");
|
|
47
|
+
console.log("Every command takes --help, and passes its options through to the underlying");
|
|
48
|
+
console.log("script — so `meffecta-agent deploy --dry-run` and `--tag <sha>` work as documented.\n");
|
|
49
|
+
console.log(`Engine: ghcr.io/meffecta/agent:${tag}${pinned ? " (pinned to this release)" : ""}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function steps() {
|
|
53
|
+
console.log("Setting up a deployment, in order. The full text is IMPLEMENTATION.md.\n");
|
|
54
|
+
STEPS.forEach(([command, note], index) => {
|
|
55
|
+
const label = `${String(index + 1).padStart(2)}.`;
|
|
56
|
+
console.log(`${label} ${note}`);
|
|
57
|
+
if (command) {
|
|
58
|
+
console.log(` meffecta-agent ${command}`);
|
|
59
|
+
}
|
|
60
|
+
console.log("");
|
|
61
|
+
});
|
|
62
|
+
console.log("Each step is checkable: `meffecta-agent status` after the last one should show");
|
|
63
|
+
console.log("your triggers, and `meffecta-agent jobs` your jobs.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function version() {
|
|
67
|
+
const { tag, pinned } = engineTag();
|
|
68
|
+
const deployment = loadDeployment();
|
|
69
|
+
console.log(`@meffecta/agent ${cliVersion()}`);
|
|
70
|
+
console.log(
|
|
71
|
+
`engine ghcr.io/meffecta/agent:${tag}${pinned ? "" : " (not pinned — a checkout, not a release)"}`,
|
|
72
|
+
);
|
|
73
|
+
if (deployment.PROJECT) {
|
|
74
|
+
console.log(`deployment ${deployment.SERVICE} in ${deployment.PROJECT} (${deployment.REGION})`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [name, ...args] = process.argv.slice(2);
|
|
79
|
+
|
|
80
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
81
|
+
help();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
if (name === "steps") {
|
|
85
|
+
steps();
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
if (name === "version" || name === "--version" || name === "-v") {
|
|
89
|
+
version();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const command = COMMANDS.get(name);
|
|
94
|
+
if (!command) {
|
|
95
|
+
const near = [...COMMANDS.keys()].filter((candidate) => candidate.startsWith(name.slice(0, 3)));
|
|
96
|
+
console.error(`Unknown command: ${name}`);
|
|
97
|
+
if (near.length > 0) {
|
|
98
|
+
console.error(`Did you mean: ${near.join(", ")}?`);
|
|
99
|
+
}
|
|
100
|
+
console.error("Run `meffecta-agent help` for the list.");
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// deploy defaults to the engine built alongside this release, so tooling and engine
|
|
106
|
+
// match unless the operator deliberately asks for something else.
|
|
107
|
+
if (name === "deploy" && !args.includes("--tag") && !args.includes("--image")) {
|
|
108
|
+
const { tag, pinned } = engineTag();
|
|
109
|
+
if (pinned) {
|
|
110
|
+
args.push("--tag", tag);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
process.exit((await command.handler(args)) ?? 0);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err instanceof UserError) {
|
|
116
|
+
console.error(err.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|