mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
refactor(skills): retire nc:env-sync — the data/env/env mirror is dead
Upstream removed every setup-side writer of data/env/env (c82f062d) because
nothing has read it since the container mount was dropped — before this
branch even forked. Our engine codified the dead pattern as a directive:
every apply copied the full .env (live tokens included) into a file nothing
consumes.
- engine: env-sync handler removed from selfStatus + applyOne
- grammar: dropped from KNOWN; a new RETIRED table gives a targeted lint
error ("delete the fence, the adapter reads .env directly") instead of a
generic unknown-directive message
- skills: fence stripped from the 14 converted skills; orphaned "sync to the
container" prose cleaned (incl. add-deltachat/add-wechat old-format prose
upstream's sweep missed); teams troubleshooting entry rewritten
- policy/spec doc lists updated
Suite 827 passed | 1 skipped; all nc: skills lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,6 @@ DC_SMTP_SECURITY=2 # 2=STARTTLS (default), 1=SSL/TLS, 3=plain
|
||||
|
||||
Security settings are applied on every startup, so changing them in `.env` and restarting takes effect without wiping the account.
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
### Optional settings
|
||||
|
||||
|
||||
@@ -96,17 +96,13 @@ curl -sf https://discord.com/api/v10/oauth2/applications/@me -H "Authorization:
|
||||
|
||||
Store the token and the two derived credentials — the adapter reads them from
|
||||
`.env` and fails to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID`
|
||||
(set-if-absent, so a value you've already filled in is never overwritten) — and
|
||||
sync them to the container:
|
||||
(set-if-absent, so a value you've already filled in is never overwritten):
|
||||
|
||||
```nc:env-set
|
||||
DISCORD_BOT_TOKEN={{bot_token}}
|
||||
DISCORD_APPLICATION_ID={{application_id}}
|
||||
DISCORD_PUBLIC_KEY={{public_key}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
## Restart
|
||||
|
||||
Restart the service so it loads the Discord adapter and the credentials you just
|
||||
|
||||
@@ -86,7 +86,7 @@ Capture the service account JSON, then write it. `prompt` only *asks* and binds
|
||||
the answer to a name; a separate directive consumes it — so the same prompt
|
||||
could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the
|
||||
consumer. Here it goes to `.env` (set-if-absent — a value you've already filled
|
||||
in is never overwritten) as a single-line string, then syncs to the container:
|
||||
in is never overwritten) as a single-line string:
|
||||
|
||||
```nc:prompt gchat_credentials secret
|
||||
Paste the service account JSON as a single line — the key file you downloaded, e.g. `{"type":"service_account","project_id":"...","private_key":"...","client_email":"..."}`.
|
||||
@@ -94,9 +94,6 @@ Paste the service account JSON as a single line — the key file you downloaded,
|
||||
```nc:env-set
|
||||
GCHAT_CREDENTIALS={{gchat_credentials}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
### Webhook server
|
||||
|
||||
The Chat SDK bridge automatically starts a shared webhook server on port 3000
|
||||
|
||||
@@ -96,7 +96,7 @@ Capture the three values, then write them. `prompt` only *asks* and binds the
|
||||
answer to a name; a separate directive consumes it — so the same prompts could
|
||||
feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer.
|
||||
Here they go to `.env` (set-if-absent — a value you've already filled in is
|
||||
never overwritten) and sync to the container:
|
||||
never overwritten):
|
||||
|
||||
```nc:prompt github_token secret
|
||||
Paste the Fine-grained Personal Access Token for the bot account — starts with `github_pat_`.
|
||||
@@ -112,9 +112,6 @@ GITHUB_TOKEN={{github_token}}
|
||||
GITHUB_WEBHOOK_SECRET={{webhook_secret}}
|
||||
GITHUB_BOT_USERNAME={{bot_username}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
`GITHUB_BOT_USERNAME` must match the bot account's GitHub username exactly. This is used for @-mention detection — the agent responds when someone writes `@your-bot-username` in a PR or issue comment.
|
||||
|
||||
## Wiring
|
||||
|
||||
@@ -159,12 +159,6 @@ bash setup/channels/imessage-configure.sh local
|
||||
bash setup/channels/imessage-configure.sh remote "{{server_url}}" "{{api_key}}"
|
||||
```
|
||||
|
||||
Once the keys for your mode are written, sync `.env` to the container (the host
|
||||
mounts `data/env/env`):
|
||||
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
## Restart
|
||||
|
||||
Restart the service so it loads the iMessage adapter and the credentials you
|
||||
|
||||
@@ -135,9 +135,6 @@ LINEAR_WEBHOOK_SECRET={{linear_webhook_secret}}
|
||||
LINEAR_TEAM_KEY={{linear_team_key}}
|
||||
LINEAR_BOT_USERNAME={{linear_bot_username}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
If you went the Personal API key route, add this line to `.env` instead of the
|
||||
OAuth pair (agent posts as you, your own comments are filtered):
|
||||
|
||||
|
||||
@@ -180,11 +180,6 @@ Option B only — the access token from Element Settings > Help & About, or from
|
||||
MATRIX_ACCESS_TOKEN={{access_token}}
|
||||
```
|
||||
|
||||
Then sync `.env` into the container:
|
||||
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're in the middle of `/setup`, return to the setup flow now.
|
||||
|
||||
@@ -84,7 +84,7 @@ Capture the secrets, then write them. `prompt` only *asks* and binds the answer
|
||||
to a name; a separate directive consumes it — so the same prompts could feed
|
||||
`ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here
|
||||
they go to `.env` (set-if-absent — a value you've already filled in is never
|
||||
overwritten) and sync to the container:
|
||||
overwritten):
|
||||
|
||||
```nc:prompt api_key secret
|
||||
Paste the Resend API key — API Keys, starts with `re_`.
|
||||
@@ -104,9 +104,6 @@ RESEND_FROM_ADDRESS={{from_address}}
|
||||
RESEND_FROM_NAME={{from_name}}
|
||||
RESEND_WEBHOOK_SECRET={{webhook_secret}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
## Connect yourself
|
||||
|
||||
Because email is direct-addressable, the bot can write to you first — so wire
|
||||
|
||||
@@ -118,9 +118,6 @@ it into the container env:
|
||||
```nc:env-set
|
||||
SIGNAL_ACCOUNT={{platform_id}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
## Restart
|
||||
|
||||
Restart the service so it loads the Signal adapter and binds the account you just
|
||||
|
||||
@@ -87,9 +87,6 @@ Paste the Signing Secret — Basic Information.
|
||||
SLACK_BOT_TOKEN={{bot_token}}
|
||||
SLACK_SIGNING_SECRET={{signing_secret}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
The bridge serves the webhook on port 3000 at `/webhook/slack` automatically; to
|
||||
receive replies, that port must be reachable from the internet and registered
|
||||
with Slack. Tell the user:
|
||||
|
||||
@@ -136,7 +136,7 @@ Paste the client secret Value — Certificates & secrets (shown only once, at le
|
||||
### Store the credentials
|
||||
|
||||
The adapter reads these from `.env` (set-if-absent, so a value you've already
|
||||
filled in is never overwritten) and syncs them to the container.
|
||||
filled in is never overwritten).
|
||||
`TEAMS_APP_TENANT_ID` is written only for a Single Tenant app; Multi Tenant
|
||||
doesn't need it.
|
||||
|
||||
@@ -148,9 +148,6 @@ TEAMS_APP_TYPE={{app_type}}
|
||||
```nc:env-set when:app_type=SingleTenant
|
||||
TEAMS_APP_TENANT_ID={{app_tenant_id}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
### Create the Azure Bot resource
|
||||
|
||||
Tell the user:
|
||||
@@ -339,7 +336,7 @@ Business / EDU / developer tenant.
|
||||
|
||||
1. The tunnel is up and the messaging endpoint matches it: Azure Bot → **Configuration** → **Messaging endpoint** must be `https://<your-domain>/api/webhooks/teams`, and your tunnel (e.g. `ngrok http 3000`) must be forwarding to this machine's port 3000.
|
||||
2. The adapter started: `grep -i teams logs/nanoclaw.log | tail`.
|
||||
3. The credentials are in `.env` and synced to `data/env/env` (re-run the env-sync step if not).
|
||||
3. The credentials are in `.env` (`TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_APP_TYPE`).
|
||||
|
||||
### `Unauthorized` / 401 from Azure Bot Service
|
||||
|
||||
|
||||
@@ -102,9 +102,6 @@ Paste the bot token from BotFather (looks like `123456:ABC-DEF...`).
|
||||
```nc:env-set
|
||||
TELEGRAM_BOT_TOKEN={{bot_token}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
Confirm the token works and capture the bot's handle — `getMe` returns the bot
|
||||
account and fails here if the token is bad. You'll use the handle to open the
|
||||
right chat just before pairing:
|
||||
|
||||
@@ -85,7 +85,7 @@ Capture the two values, then write them. `prompt` only *asks* and binds the
|
||||
answer to a name; a separate directive consumes it — so the same prompts could
|
||||
feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer.
|
||||
Here they go to `.env` (set-if-absent — a value you've already filled in is
|
||||
never overwritten) and sync to the container:
|
||||
never overwritten):
|
||||
|
||||
```nc:prompt bot_token secret
|
||||
Paste the Bot Access Token — from the Webex bot you created.
|
||||
@@ -97,9 +97,6 @@ Paste the webhook secret you set for signature verification.
|
||||
WEBEX_BOT_TOKEN={{bot_token}}
|
||||
WEBEX_WEBHOOK_SECRET={{webhook_secret}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
### Webhook server
|
||||
|
||||
The Chat SDK bridge automatically starts a shared webhook server on port 3000
|
||||
|
||||
@@ -85,7 +85,6 @@ Add to `.env`:
|
||||
WECHAT_ENABLED=true
|
||||
```
|
||||
|
||||
Sync to container: `mkdir -p data/env && cp .env data/env/env`
|
||||
|
||||
### 2. Start the service and scan the QR
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ Capture the four values, then write them. `prompt` only *asks* and binds the
|
||||
answer to a name; a separate directive consumes it — so the same prompts could
|
||||
feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer.
|
||||
Here they go to `.env` (set-if-absent — a value you've already filled in is
|
||||
never overwritten) and sync to the container:
|
||||
never overwritten):
|
||||
|
||||
```nc:prompt access_token secret
|
||||
Paste the System User access token — WhatsApp > API Setup, with `whatsapp_business_messaging` permission.
|
||||
@@ -108,9 +108,6 @@ WHATSAPP_PHONE_NUMBER_ID={{phone_number_id}}
|
||||
WHATSAPP_APP_SECRET={{app_secret}}
|
||||
WHATSAPP_VERIFY_TOKEN={{verify_token}}
|
||||
```
|
||||
```nc:env-sync
|
||||
```
|
||||
|
||||
### Webhook server
|
||||
|
||||
The Chat SDK bridge automatically starts a shared webhook server on port 3000
|
||||
|
||||
@@ -169,9 +169,6 @@ shouldn't be prefixed with its name. Record that (skipped for a shared number):
|
||||
```nc:env-set when:number_kind=dedicated
|
||||
ASSISTANT_HAS_OWN_NUMBER=true
|
||||
```
|
||||
```nc:env-sync when:number_kind=dedicated
|
||||
```
|
||||
|
||||
Resolve the conversation address as the WhatsApp JID for the number you chat
|
||||
from — the linked number for a shared account, or the dedicated number you just
|
||||
gave. Run the one matching the choice above:
|
||||
|
||||
@@ -315,7 +315,7 @@ For each `nc:operator` directive at line L, `gatePolicy` computes
|
||||
and double-confirm — the exact bug the teams parity table below forbids.)
|
||||
3. Next compatible directive is a `prompt` → **no confirm** (the prompt is the barrier).
|
||||
4. No such directive (end of document) → **no confirm** (a final handoff block, e.g. teams `:228`).
|
||||
5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `env-sync`, `json-merge`) →
|
||||
5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `json-merge`) →
|
||||
**confirm** after rendering. Confirm wording is derived from the barrier's
|
||||
*flavor* — the next compatible directive's effect: `effect:step` →
|
||||
readiness phrasing (`"Ready? The next step starts immediately."` — the block
|
||||
|
||||
@@ -142,8 +142,6 @@ function selfStatus(d: Directive, root: string): { status: StepStatus; detail: s
|
||||
? { status: 'apply', detail: `set ${missing.join(', ')} in .env` }
|
||||
: { status: 'skip', detail: `${keys.join(', ')} already set` };
|
||||
}
|
||||
case 'env-sync':
|
||||
return { status: 'apply', detail: 'sync .env → data/env/env' };
|
||||
case 'json-merge': {
|
||||
const into = String(d.attrs.into ?? '');
|
||||
const key = String(d.attrs.key ?? '');
|
||||
@@ -668,10 +666,6 @@ async function applyOne(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'env-sync':
|
||||
mkdirSync(join(root, 'data/env'), { recursive: true });
|
||||
copyFileSync(join(root, '.env'), join(root, 'data/env/env'));
|
||||
break;
|
||||
case 'json-merge': {
|
||||
const into = String(d.attrs.into);
|
||||
const key = String(d.attrs.key);
|
||||
|
||||
@@ -19,7 +19,6 @@ describe('skill-directives parser, on the converted add-slack', () => {
|
||||
'prompt', // credentials: capture bot token
|
||||
'prompt', // credentials: capture signing secret
|
||||
'env-set', // credentials: write captured values to .env
|
||||
'env-sync', // credentials: sync to container
|
||||
'operator', // credentials: event-delivery walkthrough
|
||||
'prompt', // resolve: owner member id (owner_handle)
|
||||
'run', // resolve: validate token (auth.test) — fast-fail before the restart
|
||||
@@ -187,6 +186,15 @@ describe('append at:<marker> attribute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('retired directives', () => {
|
||||
it('flags nc:env-sync with a targeted retirement error, not a generic unknown', () => {
|
||||
const probs = validate(parseDirectives(['```nc:env-sync', '```'].join('\n')));
|
||||
expect(probs).toHaveLength(1);
|
||||
expect(probs[0].message).toMatch(/retired/);
|
||||
expect(probs[0].message).toMatch(/data\/env\/env/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when: guard + multi-field capture', () => {
|
||||
it('parses when: into attrs and lints a guard whose var an earlier prompt defined', () => {
|
||||
const md = ['```nc:prompt mode', 'local or remote', '```', '```nc:prompt server_url when:mode=remote', 'url', '```'].join('\n');
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
// pauses for confirmation before the next side effect is derived from
|
||||
// document structure (scripts/skill-policy.ts), never authored here.
|
||||
// env-set body: `KEY=value` ({{var}} allowed) set-if-absent
|
||||
// env-sync (no body) `.env` → data/env/env idempotent copy
|
||||
// json-merge into:<file> key:<field> body: a JSON object push-if-absent
|
||||
//
|
||||
// `append` without `at:` adds to EOF; with `at:<marker>` it inserts before the
|
||||
@@ -113,7 +112,12 @@ export interface Problem {
|
||||
const FENCE = /^```(\S.*)?$/;
|
||||
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
||||
const VAR_REF = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
||||
const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'env-sync', 'json-merge']);
|
||||
const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'json-merge']);
|
||||
// Retired directives get a targeted lint error (not just "unknown") so an
|
||||
// author knows the removal was deliberate and what to do instead.
|
||||
const RETIRED: Record<string, string> = {
|
||||
'env-sync': 'nc:env-sync was retired — nothing reads the data/env/env mirror (and it copied live tokens); delete the fence, the adapter reads .env directly',
|
||||
};
|
||||
const PROMPT_FLAGS = new Set(['secret']);
|
||||
|
||||
export function parseDirectives(markdown: string): Directive[] {
|
||||
@@ -203,7 +207,8 @@ export function validate(directives: Directive[], ctx?: { chatVersion?: string }
|
||||
const defined = new Set<string>();
|
||||
const flag = (d: Directive, message: string) => problems.push({ line: d.line, kind: d.kind, message });
|
||||
for (const d of directives) {
|
||||
if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`);
|
||||
if (RETIRED[d.kind]) flag(d, RETIRED[d.kind]);
|
||||
else if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`);
|
||||
switch (d.kind) {
|
||||
case 'dep':
|
||||
for (const spec of d.body) {
|
||||
|
||||
@@ -65,7 +65,7 @@ function guardOf(d: Directive): { v: string; value: string } | undefined {
|
||||
* 3. Next compatible directive is a `prompt` → no confirm (the prompt is the
|
||||
* barrier — the human can't paste a token before doing the steps).
|
||||
* 4. No such directive (end of document) → no confirm (a final handoff block).
|
||||
* 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `env-sync`,
|
||||
* 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`,
|
||||
* `json-merge`) → confirm, with the flavor derived from that barrier
|
||||
* directive's effect (`effect:step` → readiness, else completed).
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user