mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 15:01:15 +00:00
feat(hands): add infisical-sync Hand
- Implement create-with-PATCH-on-conflict push pattern (POST 409 → PATCH) - Migrate push and delete endpoints from deprecated v3 to v4 API - Replace workspaceId with projectId in push/delete API calls
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
id = "infisical-sync"
|
||||
name = "Infisical Sync Hand"
|
||||
description = "Autonomous secrets synchronisation between a self-hosted Infisical instance and the agent's local credential vault. Keeps every agent in the Sodality fleet in sync with a single source of truth, and lets agents push new secrets back to Infisical."
|
||||
category = "security"
|
||||
icon = "🔐"
|
||||
tools = [
|
||||
# Core Einstein tools (schedule + memory + knowledge graph + event bus)
|
||||
"schedule_create", "schedule_list", "schedule_delete",
|
||||
"memory_store", "memory_recall",
|
||||
"knowledge_add_entity", "knowledge_add_relation", "knowledge_query",
|
||||
"event_publish",
|
||||
# Infisical-specific tools
|
||||
"shell_exec",
|
||||
"file_read", "file_write",
|
||||
"vault_set", "vault_get", "vault_list", "vault_delete",
|
||||
]
|
||||
|
||||
# ─── Requirements ─────────────────────────────────────────────────────────────
|
||||
|
||||
[[requires]]
|
||||
key = "INFISICAL_URL"
|
||||
label = "Infisical Instance URL"
|
||||
requirement_type = "env_var"
|
||||
check_value = "INFISICAL_URL"
|
||||
description = "Base URL of the self-hosted Infisical instance, e.g. https://infisical.example.com"
|
||||
|
||||
[[requires]]
|
||||
key = "INFISICAL_CLIENT_ID"
|
||||
label = "Infisical Machine Identity Client ID"
|
||||
requirement_type = "env_var"
|
||||
check_value = "INFISICAL_CLIENT_ID"
|
||||
description = "Machine identity Client ID for this agent. Created in Infisical under Access Control → Machine Identities."
|
||||
|
||||
[[requires]]
|
||||
key = "INFISICAL_CLIENT_SECRET"
|
||||
label = "Infisical Machine Identity Client Secret"
|
||||
requirement_type = "env_var"
|
||||
check_value = "INFISICAL_CLIENT_SECRET"
|
||||
description = "Machine identity Client Secret for this agent."
|
||||
|
||||
# ─── Settings ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[[settings]]
|
||||
key = "sync_interval_minutes"
|
||||
label = "Sync Interval (minutes)"
|
||||
description = "How often to pull secrets from Infisical into the local vault. Overridden by the INFISICAL_SYNC_INTERVAL env var when present."
|
||||
setting_type = "select"
|
||||
default = "15"
|
||||
|
||||
[[settings.options]]
|
||||
value = "5"
|
||||
label = "Every 5 minutes (high-frequency)"
|
||||
|
||||
[[settings.options]]
|
||||
value = "15"
|
||||
label = "Every 15 minutes (default)"
|
||||
|
||||
[[settings.options]]
|
||||
value = "30"
|
||||
label = "Every 30 minutes"
|
||||
|
||||
[[settings.options]]
|
||||
value = "60"
|
||||
label = "Every hour"
|
||||
|
||||
[[settings]]
|
||||
key = "environment"
|
||||
label = "Infisical Environment"
|
||||
description = "The environment slug to sync from (e.g. prod, staging, dev). Can also be set via INFISICAL_ENVIRONMENT env var."
|
||||
setting_type = "select"
|
||||
default = "prod"
|
||||
|
||||
[[settings.options]]
|
||||
value = "prod"
|
||||
label = "Production"
|
||||
|
||||
[[settings.options]]
|
||||
value = "staging"
|
||||
label = "Staging"
|
||||
|
||||
[[settings.options]]
|
||||
value = "dev"
|
||||
label = "Development"
|
||||
|
||||
[[settings]]
|
||||
key = "push_on_vault_write"
|
||||
label = "Push on Vault Write"
|
||||
description = "When the agent writes a new secret to the local vault, automatically push it to Infisical as well."
|
||||
setting_type = "toggle"
|
||||
default = "true"
|
||||
|
||||
[[settings]]
|
||||
key = "delete_orphans"
|
||||
label = "Delete Orphaned Local Secrets"
|
||||
description = "Remove local vault entries that no longer exist in Infisical after a sync."
|
||||
setting_type = "toggle"
|
||||
default = "false"
|
||||
|
||||
# ─── Agent configuration ──────────────────────────────────────────────────────
|
||||
|
||||
[agent]
|
||||
name = "infisical-sync-hand"
|
||||
description = "Autonomous secrets sync agent — authenticates with Infisical, pulls secrets into the local vault, and pushes local secrets back to Infisical on demand."
|
||||
module = "builtin:chat"
|
||||
provider = "default"
|
||||
model = "default"
|
||||
max_tokens = 8192
|
||||
temperature = 0.1
|
||||
max_iterations = 40
|
||||
system_prompt = """You are Infisical Sync Hand — an autonomous secrets synchronisation agent.
|
||||
|
||||
Your single purpose: keep the local credential vault in sync with a self-hosted Infisical instance and make Infisical the shared source of truth for every secret in the Sodality agent fleet.
|
||||
|
||||
You are security-critical. Never log secret values. Never expose credentials in error messages. Always authenticate before any Infisical API call.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0 — Startup & State Recovery
|
||||
|
||||
Run this every time you are activated (scheduled or on-demand).
|
||||
|
||||
1. **Read configuration** from User Configuration:
|
||||
- `sync_interval_minutes` — how often to schedule syncs
|
||||
- `environment` — Infisical environment slug
|
||||
- `push_on_vault_write` — whether to push on local write
|
||||
- `delete_orphans` — whether to delete orphaned local entries
|
||||
|
||||
2. **Read environment variables** (env vars override settings):
|
||||
```
|
||||
INFISICAL_URL — base URL (required)
|
||||
INFISICAL_CLIENT_ID — machine identity client ID (required)
|
||||
INFISICAL_CLIENT_SECRET — machine identity client secret (required)
|
||||
INFISICAL_PROJECT_ID — project ID (optional; if absent, list all)
|
||||
INFISICAL_ENVIRONMENT — environment slug (default: "prod", overrides setting)
|
||||
INFISICAL_SYNC_INTERVAL — sync interval in minutes (overrides setting)
|
||||
```
|
||||
Read them with shell_exec:
|
||||
```bash
|
||||
echo "URL=$INFISICAL_URL ENV=$INFISICAL_ENVIRONMENT PROJECT=$INFISICAL_PROJECT_ID"
|
||||
```
|
||||
|
||||
3. **Recover state** from memory:
|
||||
```
|
||||
memory_recall "infisical_sync_state"
|
||||
memory_recall "infisical_sync_last_token_expiry"
|
||||
```
|
||||
|
||||
4. **Check for existing schedule**:
|
||||
```
|
||||
schedule_list
|
||||
```
|
||||
If no `infisical-sync` schedule exists, create one (see Phase 1).
|
||||
|
||||
5. **Read sync state file** if present:
|
||||
```
|
||||
file_read "infisical_sync_state.json"
|
||||
```
|
||||
This file holds the last known secret hashes so we can skip unchanged values.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — Schedule Bootstrap (first run only)
|
||||
|
||||
If no schedule for this hand exists:
|
||||
|
||||
1. Determine interval: check `INFISICAL_SYNC_INTERVAL` env var; fall back to `sync_interval_minutes` setting.
|
||||
|
||||
2. Create schedule:
|
||||
```
|
||||
schedule_create
|
||||
name: "infisical-sync"
|
||||
interval_minutes: <interval>
|
||||
description: "Pull secrets from Infisical into local vault"
|
||||
```
|
||||
|
||||
3. Log to memory:
|
||||
```
|
||||
memory_store "infisical_sync_schedule_created" "<ISO timestamp>"
|
||||
```
|
||||
|
||||
4. Add Infisical instance to knowledge graph:
|
||||
```
|
||||
knowledge_add_entity
|
||||
type: "service"
|
||||
name: "Infisical"
|
||||
properties: { url: "<INFISICAL_URL>", environment: "<env>", project_id: "<project_id>" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — Authentication
|
||||
|
||||
Obtain a short-lived access token using Universal Auth.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}"
|
||||
```
|
||||
|
||||
Parse the response and extract `accessToken`. If the call fails:
|
||||
- Log to memory: `memory_store "infisical_sync_last_error" "auth_failed: <timestamp>"`
|
||||
- Increment error counter in state
|
||||
- `event_publish` an alert: "Infisical Sync: authentication failed — check INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET"
|
||||
- STOP. Do not proceed. Do not crash.
|
||||
|
||||
Store the token in a local variable for use in subsequent API calls. **Never store the raw token in memory or the vault** — it is ephemeral.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — Resolve Project
|
||||
|
||||
If `INFISICAL_PROJECT_ID` is set, use it directly.
|
||||
|
||||
If not set, list accessible projects:
|
||||
```bash
|
||||
curl -s -X GET "$INFISICAL_URL/api/v1/workspace" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
If multiple projects are returned, sync all of them. Store each project ID in the knowledge graph as a `secret_project` entity.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4 — Pull Secrets from Infisical
|
||||
|
||||
For each project ID, fetch all secrets:
|
||||
```bash
|
||||
curl -s -X GET \
|
||||
"$INFISICAL_URL/api/v4/secrets?workspaceId=<PROJECT_ID>&environment=<ENV>&secretPath=/" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
The response contains an array of secrets. Each secret has:
|
||||
- `secretKey` — the name
|
||||
- `secretValue` — the value
|
||||
- `id` — internal Infisical ID
|
||||
- `version` — version number
|
||||
|
||||
**For each secret returned:**
|
||||
|
||||
1. Compute a hash of `secretKey + secretValue` to detect changes.
|
||||
2. Compare with stored hash in `infisical_sync_state.json`.
|
||||
3. If unchanged, skip.
|
||||
4. If new or changed, write to local vault:
|
||||
```
|
||||
vault_set key=<secretKey> value=<secretValue>
|
||||
```
|
||||
5. Record entity in knowledge graph:
|
||||
```
|
||||
knowledge_add_entity
|
||||
type: "secret"
|
||||
name: <secretKey>
|
||||
properties: { project_id: <id>, environment: <env>, version: <version>, last_synced: <ISO timestamp> }
|
||||
```
|
||||
|
||||
**Orphan handling** (if `delete_orphans` setting is "true"):
|
||||
1. Collect the set of all secret keys returned by Infisical.
|
||||
2. Call `vault_list` to get all local keys.
|
||||
3. For any local key NOT in the Infisical set, call `vault_delete`.
|
||||
4. Log each deletion to memory.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5 — Push Secrets to Infisical (on-demand)
|
||||
|
||||
When the user (or another agent) asks you to share a secret with the fleet:
|
||||
|
||||
1. Read the secret from the local vault:
|
||||
```
|
||||
vault_get key=<secretName>
|
||||
```
|
||||
|
||||
2. Push to Infisical using a create-then-update pattern:
|
||||
|
||||
**Step 1 — Try to create (POST):**
|
||||
```bash
|
||||
HTTP_STATUS=$(curl -s -o /tmp/infisical_push_response.json -w "%{http_code}" \
|
||||
-X POST "$INFISICAL_URL/api/v4/secrets/<SECRET_NAME>" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"projectId\":\"<PROJECT_ID>\",\"environment\":\"<ENV>\",\"secretValue\":\"<VALUE>\",\"secretPath\":\"/\"}")
|
||||
```
|
||||
|
||||
**Step 2 — If 409 (secret already exists), update via PATCH:**
|
||||
```bash
|
||||
if [ "$HTTP_STATUS" = "409" ]; then
|
||||
HTTP_STATUS=$(curl -s -o /tmp/infisical_push_response.json -w "%{http_code}" \
|
||||
-X PATCH "$INFISICAL_URL/api/v4/secrets/<SECRET_NAME>" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"projectId\":\"<PROJECT_ID>\",\"environment\":\"<ENV>\",\"secretValue\":\"<VALUE>\",\"secretPath\":\"/\"}")
|
||||
fi
|
||||
```
|
||||
|
||||
POST returns 201 on success. PATCH returns 200 on success. Any other status is an error — log it and notify via `event_publish`.
|
||||
|
||||
3. On success, log to knowledge graph and memory. On failure, log the error and notify via `event_publish`.
|
||||
|
||||
**Never log secret values.** Use placeholders like `<redacted>` in all log messages and memory entries.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 6 — Delete a Secret from Infisical (on-demand)
|
||||
|
||||
When asked to remove a secret from the shared fleet store:
|
||||
|
||||
1. Confirm with the user before deleting (event_publish a confirmation request).
|
||||
2. On confirmation, call:
|
||||
```bash
|
||||
curl -s -X DELETE \
|
||||
"$INFISICAL_URL/api/v4/secrets/<SECRET_NAME>?projectId=<PROJECT_ID>&environment=<ENV>&secretPath=/" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
3. Delete the local vault entry: `vault_delete key=<secretName>`
|
||||
4. Update knowledge graph: remove the `secret` entity.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 7 — State Persistence & Metrics
|
||||
|
||||
After every sync cycle:
|
||||
|
||||
1. **Update sync state file**:
|
||||
```
|
||||
file_write "infisical_sync_state.json" <JSON with secret key→hash map and sync timestamp>
|
||||
```
|
||||
|
||||
2. **Update dashboard metrics via memory_store**:
|
||||
- `infisical_sync_secrets_count` — integer: number of secrets currently in vault
|
||||
- `infisical_sync_last_sync` — string: "YYYY-MM-DD HH:MM UTC"
|
||||
- `infisical_sync_last_error` — string: last error message (or "none")
|
||||
- `infisical_sync_projects_count` — integer: number of Infisical projects synced
|
||||
- `infisical_sync_push_count` — integer: cumulative secrets pushed to Infisical
|
||||
- `infisical_sync_pull_count` — integer: cumulative secrets pulled from Infisical
|
||||
|
||||
3. **Persist state summary**:
|
||||
```
|
||||
memory_store "infisical_sync_state" <JSON summary>
|
||||
```
|
||||
|
||||
4. **Publish sync complete event**:
|
||||
```
|
||||
event_publish "infisical_sync_complete" { secrets_synced: N, project_ids: [...], timestamp: "..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Never crash.** Always catch errors gracefully:
|
||||
|
||||
- Authentication failure → log + notify + stop cycle
|
||||
- API rate limit (HTTP 429) → wait 60 seconds, retry once, then log + stop
|
||||
- Network timeout → log + retry with 10-second delay once, then log + stop
|
||||
- Partial sync failure → log which keys failed, continue with remaining keys
|
||||
- vault_set failure → log + notify, do not mark as synced
|
||||
|
||||
All errors go to memory:
|
||||
```
|
||||
memory_store "infisical_sync_last_error" "<type>: <message> at <ISO timestamp>"
|
||||
```
|
||||
|
||||
And if severity is high (auth failure, total API unreachable):
|
||||
```
|
||||
event_publish "infisical_sync_error" { error: "<type>", message: "<message>", timestamp: "..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Rules
|
||||
|
||||
1. **Never** log, print, or store secret values — use `<redacted>` in all messages.
|
||||
2. **Never** expose secret values in `event_publish` payloads.
|
||||
3. **Never** store the Infisical access token in memory or the vault — it is session-local only.
|
||||
4. Only sync secrets to/from the environment and project configured for this agent.
|
||||
5. When unsure whether to push a secret, ask the user first via `event_publish`.
|
||||
"""
|
||||
|
||||
# ─── Dashboard metrics ────────────────────────────────────────────────────────
|
||||
|
||||
[dashboard]
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Secrets in Vault"
|
||||
memory_key = "infisical_sync_secrets_count"
|
||||
format = "number"
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Last Sync"
|
||||
memory_key = "infisical_sync_last_sync"
|
||||
format = "text"
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Last Error"
|
||||
memory_key = "infisical_sync_last_error"
|
||||
format = "text"
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Projects Synced"
|
||||
memory_key = "infisical_sync_projects_count"
|
||||
format = "number"
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Secrets Pushed"
|
||||
memory_key = "infisical_sync_push_count"
|
||||
format = "number"
|
||||
|
||||
[[dashboard.metrics]]
|
||||
label = "Secrets Pulled"
|
||||
memory_key = "infisical_sync_pull_count"
|
||||
format = "number"
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: infisical-sync-skill
|
||||
version: "1.0.0"
|
||||
description: "Expert knowledge for the Infisical Sync Hand — Infisical API reference, vault operations, error patterns, security guidance"
|
||||
author: OpenFang
|
||||
tags: [secrets, infisical, vault, security, sync]
|
||||
tools: [shell_exec, vault_set, vault_get, vault_list, vault_delete, memory_store, memory_recall]
|
||||
runtime: prompt_only
|
||||
---
|
||||
|
||||
# Infisical Sync Expert Knowledge
|
||||
|
||||
## 1. Infisical API Reference
|
||||
|
||||
### Base URL
|
||||
All requests go to `$INFISICAL_URL`. This is the self-hosted instance base URL, e.g. `https://infisical.example.com`.
|
||||
|
||||
### Authentication — Universal Auth
|
||||
Infisical uses Machine Identities with Universal Auth for agent-to-agent communication.
|
||||
|
||||
**Endpoint**: `POST /api/v1/auth/universal-auth/login`
|
||||
|
||||
**Request**:
|
||||
```json
|
||||
{
|
||||
"clientId": "<INFISICAL_CLIENT_ID>",
|
||||
"clientSecret": "<INFISICAL_CLIENT_SECRET>"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (success):
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJ...",
|
||||
"expiresIn": 7200,
|
||||
"accessTokenMaxTTL": 43200,
|
||||
"tokenType": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
**curl example**:
|
||||
```bash
|
||||
RESPONSE=$(curl -s -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}")
|
||||
|
||||
ACCESS_TOKEN=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
|
||||
```
|
||||
|
||||
Token lifetime: `expiresIn` seconds (usually 7200 = 2 hours). Re-authenticate when expired.
|
||||
|
||||
---
|
||||
|
||||
### List Secrets
|
||||
**Endpoint**: `GET /api/v4/secrets`
|
||||
|
||||
**Query parameters**:
|
||||
| Param | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `workspaceId` | Yes | Infisical project ID |
|
||||
| `environment` | Yes | Environment slug (e.g. `prod`, `staging`, `dev`) |
|
||||
| `secretPath` | No | Path prefix, default `/` |
|
||||
| `includeImports` | No | Include imported secrets, default `false` |
|
||||
| `recursive` | No | Include secrets in sub-paths, default `false` |
|
||||
|
||||
**curl example**:
|
||||
```bash
|
||||
curl -s -X GET \
|
||||
"$INFISICAL_URL/api/v4/secrets?workspaceId=$PROJECT_ID&environment=$ENVIRONMENT&secretPath=/" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
**Response shape**:
|
||||
```json
|
||||
{
|
||||
"secrets": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"version": 1,
|
||||
"secretKey": "DATABASE_URL",
|
||||
"secretValue": "postgres://...",
|
||||
"secretComment": "",
|
||||
"environment": "prod",
|
||||
"workspace": "uuid"
|
||||
}
|
||||
],
|
||||
"imports": []
|
||||
}
|
||||
```
|
||||
|
||||
Parse with:
|
||||
```bash
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for s in data.get('secrets', []):
|
||||
print(s['secretKey'])
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create or Update a Secret
|
||||
|
||||
The API does **not** provide a single upsert endpoint. `POST` creates only (returns 409 if the secret already exists); `PATCH` updates only (returns 404 if missing). Use the create-then-update pattern:
|
||||
|
||||
**Step 1 — Try to create (POST)**
|
||||
**Endpoint**: `POST /api/v4/secrets/{secretName}`
|
||||
|
||||
**Request body**:
|
||||
```json
|
||||
{
|
||||
"projectId": "<PROJECT_ID>",
|
||||
"environment": "<ENV>",
|
||||
"secretValue": "<VALUE>",
|
||||
"secretPath": "/"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" \
|
||||
-X POST "$INFISICAL_URL/api/v4/secrets/$SECRET_NAME" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"projectId\":\"$PROJECT_ID\",\"environment\":\"$ENVIRONMENT\",\"secretValue\":\"$SECRET_VALUE\",\"secretPath\":\"/\"}")
|
||||
```
|
||||
|
||||
Returns `201` on success.
|
||||
|
||||
**Step 2 — If 409, update via PATCH**
|
||||
**Endpoint**: `PATCH /api/v4/secrets/{secretName}`
|
||||
|
||||
```bash
|
||||
if [ "$HTTP_STATUS" = "409" ]; then
|
||||
HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" \
|
||||
-X PATCH "$INFISICAL_URL/api/v4/secrets/$SECRET_NAME" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"projectId\":\"$PROJECT_ID\",\"environment\":\"$ENVIRONMENT\",\"secretValue\":\"$SECRET_VALUE\",\"secretPath\":\"/\"}")
|
||||
fi
|
||||
```
|
||||
|
||||
Returns `200` on success. Any other status code is an error.
|
||||
|
||||
**Important**: URL-encode the secret name if it contains special characters.
|
||||
|
||||
---
|
||||
|
||||
### Delete a Secret
|
||||
**Endpoint**: `DELETE /api/v4/secrets/{secretName}`
|
||||
|
||||
**Query parameters**: `projectId`, `environment`, `secretPath` (default `/`)
|
||||
|
||||
**curl example**:
|
||||
```bash
|
||||
curl -s -X DELETE \
|
||||
"$INFISICAL_URL/api/v4/secrets/$SECRET_NAME?projectId=$PROJECT_ID&environment=$ENVIRONMENT&secretPath=/" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### List Accessible Projects (Workspaces)
|
||||
**Endpoint**: `GET /api/v1/workspace`
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$INFISICAL_URL/api/v1/workspace" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
Response: `{ "workspaces": [{ "id": "uuid", "name": "...", "environments": [...] }] }`
|
||||
|
||||
---
|
||||
|
||||
## 2. HTTP Error Codes
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 200/201 | Success | Continue |
|
||||
| 400 | Bad Request | Log the response body — likely malformed JSON or missing field |
|
||||
| 401 | Unauthorized | Re-authenticate; token may have expired |
|
||||
| 403 | Forbidden | Machine identity lacks permissions — check Infisical Access Control |
|
||||
| 404 | Not Found | Secret or project doesn't exist |
|
||||
| 429 | Rate Limited | Wait 60 seconds, retry once |
|
||||
| 500/503 | Server Error | Log + retry once after 30 seconds; notify if still failing |
|
||||
|
||||
Always check HTTP status before trusting response body:
|
||||
```bash
|
||||
HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" ...)
|
||||
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "201" ]; then
|
||||
# handle error
|
||||
fi
|
||||
RESPONSE=$(cat /tmp/infisical_response.json)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sync State File Format
|
||||
|
||||
Stored at `infisical_sync_state.json`:
|
||||
```json
|
||||
{
|
||||
"last_sync": "2025-01-15T10:30:00Z",
|
||||
"project_ids": ["uuid1", "uuid2"],
|
||||
"environment": "prod",
|
||||
"secrets": {
|
||||
"DATABASE_URL": {
|
||||
"hash": "sha256_of_key_plus_value",
|
||||
"version": 3,
|
||||
"last_synced": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"error_count": 0,
|
||||
"push_count": 12,
|
||||
"pull_count": 47
|
||||
}
|
||||
```
|
||||
|
||||
**Hash computation** (to detect changes without storing values):
|
||||
```bash
|
||||
echo -n "DATABASE_URL:postgres://..." | sha256sum | awk '{print $1}'
|
||||
```
|
||||
|
||||
Or with Python:
|
||||
```python
|
||||
import hashlib
|
||||
h = hashlib.sha256(f"{key}:{value}".encode()).hexdigest()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Vault Operations Reference
|
||||
|
||||
The local vault provides encrypted key-value storage. All secrets synced from Infisical go here.
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| `vault_set key=K value=V` | Write or overwrite secret K |
|
||||
| `vault_get key=K` | Read secret K |
|
||||
| `vault_list` | List all keys (values not returned) |
|
||||
| `vault_delete key=K` | Delete secret K |
|
||||
|
||||
**Bulk sync pattern**:
|
||||
```
|
||||
// Pull from Infisical → vault
|
||||
for each (key, value) in infisical_secrets:
|
||||
vault_set key=<key> value=<value>
|
||||
|
||||
// Optionally remove orphans
|
||||
vault_list → local_keys
|
||||
infisical_keys = set of keys returned by Infisical
|
||||
for key in local_keys - infisical_keys:
|
||||
vault_delete key=<key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Checklist
|
||||
|
||||
Before every sync cycle, verify:
|
||||
- [ ] `INFISICAL_URL` is set and non-empty
|
||||
- [ ] `INFISICAL_CLIENT_ID` is set and non-empty
|
||||
- [ ] `INFISICAL_CLIENT_SECRET` is set and non-empty
|
||||
- [ ] The access token was freshly obtained this cycle (never reuse across cycles)
|
||||
- [ ] No secret values appear in curl command echo output (use variables, not inline values)
|
||||
- [ ] Response body is never logged verbatim (strip `secretValue` fields before logging)
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Failure Modes
|
||||
|
||||
### "Failed to fetch secrets: 403 Forbidden"
|
||||
The Machine Identity exists but lacks permissions. In Infisical:
|
||||
1. Go to Access Control → Machine Identities
|
||||
2. Find this agent's identity
|
||||
3. Assign it the `member` role (or `viewer` for read-only) on the project
|
||||
|
||||
### "Connection refused / Could not connect to server"
|
||||
`INFISICAL_URL` is wrong or the instance is down. Verify the URL is reachable:
|
||||
```bash
|
||||
curl -s "$INFISICAL_URL/api/status" | python3 -c "import sys,json; print(json.load(sys.stdin))"
|
||||
```
|
||||
|
||||
### "invalid character in secret name"
|
||||
Secret names in Infisical must match `[A-Z0-9_]`. If the vault has mixed-case keys, normalise before pushing:
|
||||
```bash
|
||||
echo "my_secret_key" | tr '[:lower:]' '[:upper:]'
|
||||
```
|
||||
|
||||
### "accessToken undefined in response"
|
||||
Authentication failed. The response body will contain an error message. Check:
|
||||
1. `INFISICAL_CLIENT_ID` and `INFISICAL_CLIENT_SECRET` are correct
|
||||
2. The Machine Identity is not disabled in Infisical
|
||||
3. The Machine Identity's token TTL hasn't been set to 0
|
||||
|
||||
---
|
||||
|
||||
## 7. Knowledge Graph Entities
|
||||
|
||||
Track fleet-wide secrets metadata without exposing values.
|
||||
|
||||
### Entity types
|
||||
- `service` — the Infisical instance itself
|
||||
- `secret_project` — an Infisical workspace/project
|
||||
- `secret` — a named secret (key only, never value)
|
||||
|
||||
### Relation types
|
||||
- `secret` → `belongs_to` → `secret_project`
|
||||
- `secret_project` → `hosted_by` → `service`
|
||||
- `secret` → `synced_to` → `agent_vault`
|
||||
|
||||
### Query examples
|
||||
```
|
||||
knowledge_query type=secret // list all known secrets
|
||||
knowledge_query type=secret_project // list all projects
|
||||
knowledge_query relation=belongs_to target=<project_id> // secrets in a project
|
||||
```
|
||||
@@ -45,6 +45,11 @@ pub fn bundled_hands() -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
include_str!("../bundled/trader/HAND.toml"),
|
||||
include_str!("../bundled/trader/SKILL.md"),
|
||||
),
|
||||
(
|
||||
"infisical-sync",
|
||||
include_str!("../bundled/infisical-sync/HAND.toml"),
|
||||
include_str!("../bundled/infisical-sync/SKILL.md"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ mod tests {
|
||||
#[test]
|
||||
fn bundled_hands_count() {
|
||||
let hands = bundled_hands();
|
||||
assert_eq!(hands.len(), 8);
|
||||
assert_eq!(hands.len(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -298,6 +303,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_infisical_sync_hand() {
|
||||
let (id, toml_content, skill_content) = bundled_hands()
|
||||
.into_iter()
|
||||
.find(|(id, _, _)| *id == "infisical-sync")
|
||||
.expect("infisical-sync hand must be in bundled_hands()");
|
||||
let def = parse_bundled(id, toml_content, skill_content).unwrap();
|
||||
assert_eq!(def.id, "infisical-sync");
|
||||
assert_eq!(def.name, "Infisical Sync Hand");
|
||||
assert_eq!(def.category, crate::HandCategory::Security);
|
||||
assert!(def.skill_content.is_some());
|
||||
// Required env vars
|
||||
assert!(!def.requires.is_empty(), "infisical-sync must declare env var requirements");
|
||||
let req_keys: Vec<&str> = def.requires.iter().map(|r| r.key.as_str()).collect();
|
||||
assert!(req_keys.contains(&"INFISICAL_URL"), "must require INFISICAL_URL");
|
||||
assert!(req_keys.contains(&"INFISICAL_CLIENT_ID"), "must require INFISICAL_CLIENT_ID");
|
||||
assert!(req_keys.contains(&"INFISICAL_CLIENT_SECRET"), "must require INFISICAL_CLIENT_SECRET");
|
||||
// Einstein scheduling tools
|
||||
assert!(def.tools.contains(&"schedule_create".to_string()), "must have schedule_create");
|
||||
assert!(def.tools.contains(&"schedule_list".to_string()), "must have schedule_list");
|
||||
assert!(def.tools.contains(&"schedule_delete".to_string()), "must have schedule_delete");
|
||||
// Memory tools
|
||||
assert!(def.tools.contains(&"memory_store".to_string()), "must have memory_store");
|
||||
assert!(def.tools.contains(&"memory_recall".to_string()), "must have memory_recall");
|
||||
// Knowledge graph tools
|
||||
assert!(def.tools.contains(&"knowledge_add_entity".to_string()), "must have knowledge_add_entity");
|
||||
assert!(def.tools.contains(&"knowledge_add_relation".to_string()), "must have knowledge_add_relation");
|
||||
assert!(def.tools.contains(&"knowledge_query".to_string()), "must have knowledge_query");
|
||||
// Event bus
|
||||
assert!(def.tools.contains(&"event_publish".to_string()), "must have event_publish");
|
||||
// Infisical-specific tools
|
||||
assert!(def.tools.contains(&"shell_exec".to_string()), "must have shell_exec");
|
||||
assert!(def.tools.contains(&"vault_set".to_string()), "must have vault_set");
|
||||
assert!(def.tools.contains(&"vault_get".to_string()), "must have vault_get");
|
||||
assert!(def.tools.contains(&"vault_list".to_string()), "must have vault_list");
|
||||
assert!(def.tools.contains(&"vault_delete".to_string()), "must have vault_delete");
|
||||
// Dashboard
|
||||
assert!(!def.dashboard.metrics.is_empty(), "must have dashboard metrics");
|
||||
let metric_keys: Vec<&str> = def.dashboard.metrics.iter().map(|m| m.memory_key.as_str()).collect();
|
||||
assert!(metric_keys.contains(&"infisical_sync_secrets_count"), "must have secrets_count metric");
|
||||
assert!(metric_keys.contains(&"infisical_sync_last_sync"), "must have last_sync metric");
|
||||
// Agent config
|
||||
assert!(!def.agent.system_prompt.is_empty(), "must have system_prompt");
|
||||
assert!(def.agent.temperature < 0.2, "security hand should use low temperature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_einstein_hands_have_knowledge_graph() {
|
||||
let einstein_ids = [
|
||||
|
||||
@@ -642,7 +642,7 @@ mod tests {
|
||||
fn load_bundled_hands() {
|
||||
let reg = HandRegistry::new();
|
||||
let count = reg.load_bundled();
|
||||
assert_eq!(count, 8);
|
||||
assert_eq!(count, 9);
|
||||
assert!(!reg.list_definitions().is_empty());
|
||||
|
||||
// Clip hand should be loaded
|
||||
|
||||
Reference in New Issue
Block a user