Template
Compare commits
13
Commits
d5a0e4d8f6
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f30e95d9d0 | ||
|
|
8d352aa7d5 | ||
|
|
2c9e5e9c0e | ||
|
|
f5228a243f | ||
|
|
9cf15f1258 | ||
|
|
28b0019d2e | ||
|
|
5c44e3ef41 | ||
|
|
abb94a478e | ||
|
|
0be43403e6 | ||
|
|
16cbeb4ed8 | ||
|
|
1b899d32d3 | ||
|
|
29f33c4b03 | ||
|
|
323e8ce2d1 |
@@ -0,0 +1,3 @@
|
|||||||
|
*
|
||||||
|
!crush.json
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$",
|
||||||
|
"command": "./hooks/ticket-gate.sh",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,32 @@ enforce the mechanical rules below; this document states the policy and intent.
|
|||||||
|
|
||||||
**You are an AI agent working on this project. Your first actions, in order:**
|
**You are an AI agent working on this project. Your first actions, in order:**
|
||||||
|
|
||||||
|
> **SESSION-START GATE (NON-NEGOTIABLE):** Steps 1-4 orient you. Step 5 is the
|
||||||
|
> check-for-understanding gate (rule summary). Steps 6-8 gather state. Step 9 is
|
||||||
|
> the scope-alignment gate. You MUST NOT begin any task work until the user
|
||||||
|
> (a) confirms your rule summary AND (b) names the ticket to work on. This runs
|
||||||
|
> **every session, automatically** — the user should never have to ask for it.
|
||||||
|
|
||||||
1. **Set up the environment:** `bash scripts/setup-hooks.sh` (installs git hooks — idempotent).
|
1. **Set up the environment:** `bash scripts/setup-hooks.sh` (installs git hooks — idempotent).
|
||||||
2. **Read [STATUS.md](STATUS.md)** — current state, inbox, blockers, tactical notes.
|
2. **Read [STATUS.md](STATUS.md)** — current state, inbox, blockers, tactical notes.
|
||||||
3. **Read [`.crush/memory/operational.md`](.crush/memory/operational.md)** — access details, key IDs, gotchas.
|
3. **Read [`.crush/memory/operational.md`](.crush/memory/operational.md)** — access details, key IDs, gotchas.
|
||||||
4. **Read [questions-v1.md](questions-v1.md)** — open questions awaiting human input.
|
4. **Read [questions-v1.md](questions-v1.md)** — open questions awaiting human input.
|
||||||
5. **Check Redmine** — `redmine list --assigned-to-me -p <project-id>` for active work.
|
5. **Check for understanding — GATE.** In your own words, summarize the rules
|
||||||
6. **Check current state:** `git log --oneline -10`.
|
from this file back to the user: Systems of Record, Git Workflow, Questions
|
||||||
|
policy, Remote Access (if applicable), Conventions, and any project-specific
|
||||||
|
rules. Then stop and wait for the user to confirm. This checkpoint
|
||||||
|
guarantees every session starts aligned.
|
||||||
|
6. **Check Redmine** — `redmine list --assigned-to-me -p <project-id>` for active work.
|
||||||
|
7. **Check current state:** `git log --oneline -10`.
|
||||||
|
8. **Run rule audit:** `bash scripts/check-rules.sh --fast`.
|
||||||
|
9. **Scope-alignment — GATE.** Present the session handoff's priority list (or
|
||||||
|
the Redmine queue if no handoff exists). Do NOT scan the full ticket queue
|
||||||
|
and pick work on your own — **the user directs what gets worked on, always.**
|
||||||
|
Note any new or urgent items from the Redmine check, then ask which ticket
|
||||||
|
to work on tonight. **STOP and wait.** Do not set `.crush/active-ticket`,
|
||||||
|
do not read systems, do not run diagnostics, until the user names the
|
||||||
|
target. This gate prevents the agent from burning context on work the user
|
||||||
|
didn't ask for.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
@@ -59,6 +79,10 @@ Replace this bracketed text.>
|
|||||||
## Systems of Record (do not duplicate)
|
## Systems of Record (do not duplicate)
|
||||||
|
|
||||||
- **Redmine** is the single system of record for ALL project work — tickets, schedules, Gantt, dependencies. Use the `redmine-cli`. **Do not use Gitea issues.** Reference tickets as `[#NNN]`.
|
- **Redmine** is the single system of record for ALL project work — tickets, schedules, Gantt, dependencies. Use the `redmine-cli`. **Do not use Gitea issues.** Reference tickets as `[#NNN]`.
|
||||||
|
- **NEVER close a Redmine ticket without explicit user permission.** You may SUGGEST a close when the result is clearly scoped and delivered. If it's ambiguous whether the work is truly complete, don't suggest a close — leave that decision to the user. This applies to ALL tickets, no exceptions.
|
||||||
|
- **User acceptance testing is MANDATORY before declaring work done.** The agent performs implementation and technical validation (services running, configs correct, APIs responding). The user performs UAT — visually confirming dashboards render data, alerts deliver, tools are usable. The agent MUST NOT set done-ratio to 100%, MUST NOT suggest closing, and MUST NOT move to the next ticket until the user explicitly accepts the work. "Technically wired but blank dashboard" is NOT done.
|
||||||
|
- **NEVER access a database directly if an API exists.** APIs are the stable contract; databases change schemas without warning. If a tool has an API, use it — exhaust all API endpoints, check the docs, try alternative methods. Only fall back to direct DB access as a last resort AND with explicit user approval for that specific instance. Soon all DB access will route through a proxy under zero trust; building API-first habits now ensures that transition is clean.
|
||||||
|
- **Ticket-first enforcement (mechanically enforced).** Before starting any work, set the active ticket: `echo '#NNN' > .crush/active-ticket`. The Crush hook (`hooks/ticket-gate.sh`) blocks modifying operations until this file exists. If no ticket exists, CREATE ONE FIRST via redmine-cli, then set it. Clear when done: `> .crush/active-ticket`.
|
||||||
- **Discourse** is the single system of record for documentation. Use the `discourse-cli`. **Do not author long-form docs in gitea.**
|
- **Discourse** is the single system of record for documentation. Use the `discourse-cli`. **Do not author long-form docs in gitea.**
|
||||||
- **Git-tracked `.md` files are stubs** that point to the relevant Discourse topic URL. Operational files that must live next to code (`AGENTS.md`, `STATUS.md`, `questions-v*.md`) are the documented exceptions.
|
- **Git-tracked `.md` files are stubs** that point to the relevant Discourse topic URL. Operational files that must live next to code (`AGENTS.md`, `STATUS.md`, `questions-v*.md`) are the documented exceptions.
|
||||||
- `STATUS.md` is a scratchpad for token efficiency, **not** a system of record.
|
- `STATUS.md` is a scratchpad for token efficiency, **not** a system of record.
|
||||||
@@ -121,8 +145,57 @@ docker run --rm --env-file ~/.creds/discourse.env \
|
|||||||
- Clear WORKING.md (to "all done") before responding to the user.
|
- Clear WORKING.md (to "all done") before responding to the user.
|
||||||
- **The human decides when the work is done and when to deploy.** Never declare "done" unilaterally.
|
- **The human decides when the work is done and when to deploy.** Never declare "done" unilaterally.
|
||||||
|
|
||||||
|
## Rolling HUD (session-scoped, NOT persisted)
|
||||||
|
|
||||||
|
The rolling HUD is a **live status display** the agent maintains throughout
|
||||||
|
the session to help the human follow along. It is NOT a system of record —
|
||||||
|
Redmine, Discourse, and git are the durable systems. The HUD exists purely
|
||||||
|
for the human's situational awareness during the session.
|
||||||
|
|
||||||
|
- **Format:** a compact block shown at the end of each significant response
|
||||||
|
(after completing a step, hitting a blocker, or pivoting). Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ SESSION HUD ────────────────────────────────────────
|
||||||
|
│ Active: #NNN (ticket subject)
|
||||||
|
│ Done: ✓ first completed item this session
|
||||||
|
│ ✓ second completed item
|
||||||
|
│ Now: what the agent is actively doing right now
|
||||||
|
│ Next: → #NNN next queued ticket
|
||||||
|
│ → #NNN another queued item (BLOCKED: what's needed)
|
||||||
|
├─ USER ACTION ITEMS ──────────────────────────────────
|
||||||
|
│ • Action only the user can take (unblocks #NNN)
|
||||||
|
│ • Another user action (unblocks #NNN)
|
||||||
|
└──────────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Placement:** may be written to `.crush/hud.md` on disk to keep context
|
||||||
|
window smaller (re-read and update rather than hold in memory). Never
|
||||||
|
committed to git. Wiped at session end.
|
||||||
|
- **Sections:**
|
||||||
|
- **Active:** current ticket number + one-line description
|
||||||
|
- **Done:** ✓ items completed this session (append as work progresses)
|
||||||
|
- **Now:** what the agent is actively doing
|
||||||
|
- **Next:** the queued items (per handoff priority or user direction)
|
||||||
|
- **User action items:** things ONLY the user can do (populate creds,
|
||||||
|
physical work, manual deploys) with the tickets they unblock
|
||||||
|
- **When to show it:** after each logical unit of work, at blockers, and
|
||||||
|
when pivoting between tickets. Not every trivial response — use judgment.
|
||||||
|
- **NOT a substitute for Redmine/Discourse/git.** The HUD is ephemeral. When
|
||||||
|
work completes, update the durable systems (ticket notes, Discourse wiki,
|
||||||
|
commits). The HUD just tracks the live narrative for the human.
|
||||||
|
|
||||||
## Questions
|
## Questions
|
||||||
|
|
||||||
|
- **NEVER use a harness "question"/"ask user" tool** (structured prompts,
|
||||||
|
modal forms, tabbed questions). Banned across every project, every
|
||||||
|
harness. Not portable, not version-controlled, bypasses the git record.
|
||||||
|
Non-negotiable. (See `BASELINE-PROMPT.md` §10.)
|
||||||
|
- **All questions go in the git-tracked `questions-v(N).md` file.** The
|
||||||
|
human edits the answer inline in the same file. **Version up the filename
|
||||||
|
each time answers land** (v1 → v2 → v3...): create `questions-v2.md`
|
||||||
|
with resolved Q&A marked, new questions appended. This preserves the
|
||||||
|
history of each Q&A round.
|
||||||
- Capture questions for the human in a git-tracked `questions-v(N).md` file.
|
- Capture questions for the human in a git-tracked `questions-v(N).md` file.
|
||||||
- The human reviews and edits it inline. Version up when a round of answers lands.
|
- The human reviews and edits it inline. Version up when a round of answers lands.
|
||||||
- Questions, answers, and the reasoning behind decisions are often more important than the code. Synthesize resolved Q&A into Discourse (decisions) and Redmine (work items).
|
- Questions, answers, and the reasoning behind decisions are often more important than the code. Synthesize resolved Q&A into Discourse (decisions) and Redmine (work items).
|
||||||
@@ -134,6 +207,13 @@ docker run --rm --env-file ~/.creds/discourse.env \
|
|||||||
- **Prefer Unix utilities (awk, sed, grep, cut, tr, etc.) for file editing and text processing** over built-in edit tools when feasible. Harness edit tools can be unreliable with whitespace/indentation; Unix tools are deterministic and exact. Use them for surgical edits, bulk substitutions, and structured text extraction.
|
- **Prefer Unix utilities (awk, sed, grep, cut, tr, etc.) for file editing and text processing** over built-in edit tools when feasible. Harness edit tools can be unreliable with whitespace/indentation; Unix tools are deterministic and exact. Use them for surgical edits, bulk substitutions, and structured text extraction.
|
||||||
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test runners. If an LSP is wired up, use it; otherwise pull a Docker image and lint inside it. Do not parse huge code blocks in context.
|
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test runners. If an LSP is wired up, use it; otherwise pull a Docker image and lint inside it. Do not parse huge code blocks in context.
|
||||||
- **Use sub-agents as subcontractors** (see BASELINE-PROMPT.md §12): scoped spec in, distilled deliverable out. Never read 10+ files sequentially; batch into 2-3 agent calls. Read the 3-4 files you'll edit yourself; delegate the rest.
|
- **Use sub-agents as subcontractors** (see BASELINE-PROMPT.md §12): scoped spec in, distilled deliverable out. Never read 10+ files sequentially; batch into 2-3 agent calls. Read the 3-4 files you'll edit yourself; delegate the rest.
|
||||||
|
- **Command timeouts (NON-NEGOTIABLE):** Every command that touches a remote
|
||||||
|
system MUST be wrapped with `timeout`. Hard limits: 30s for quick reads
|
||||||
|
(status, ps, ls), 120s for standard operations, 300s for deployments/pulls.
|
||||||
|
If a command hits the timeout, STOP and investigate root cause — never
|
||||||
|
blindly retry. A hung command is a failed command. Detect failure fast,
|
||||||
|
diagnose, fix, move on. This applies to ALL tools — bash, docker, CLIs,
|
||||||
|
sub-agents.
|
||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
@@ -156,6 +236,25 @@ docker run --rm --env-file ~/.creds/discourse.env \
|
|||||||
- **Secrets:** NEVER commit secrets. Credentials come from env vars / `.env` (gitignored). Use placeholders in `.env.example`.
|
- **Secrets:** NEVER commit secrets. Credentials come from env vars / `.env` (gitignored). Use placeholders in `.env.example`.
|
||||||
- **IAC testing:** test against the corresponding `sectestbed-` VM (snapshot to base state). `preprod-` VMs for vendor-upgrade testing. Do not require AWX as a prerequisite.
|
- **IAC testing:** test against the corresponding `sectestbed-` VM (snapshot to base state). `preprod-` VMs for vendor-upgrade testing. Do not require AWX as a prerequisite.
|
||||||
|
|
||||||
|
## Remote Access (projects with remote hosts)
|
||||||
|
|
||||||
|
**If this project manages remote hosts** (servers, VMs, network devices),
|
||||||
|
these rules are NON-NEGOTIABLE:
|
||||||
|
|
||||||
|
- **Ship a `remote.sh` chokepoint.** ALL SSH/SCP to any host MUST route
|
||||||
|
through a single chokepoint script (`tests/remote.sh` or equivalent).
|
||||||
|
NEVER call `ssh`/`scp` directly — the harness blocks raw ssh and the
|
||||||
|
command scanner rejects it. One script = one place to configure
|
||||||
|
hosts/users/keys, one place to audit.
|
||||||
|
- **DNS names ONLY.** NEVER use IP address literals in any command, script,
|
||||||
|
or config. ALWAYS use DNS names. If a DNS name does not resolve, fix DNS
|
||||||
|
first — do not fall back to IP literals. This eliminates the per-session
|
||||||
|
discovery tax of finding the right IP for each host.
|
||||||
|
- **If using qemu-guest-agent:** visibility only (`qm guest cmd <id> ping`,
|
||||||
|
`network-get-interfaces`). NEVER use `qm guest exec` or any wrapper as an
|
||||||
|
execution, access, or key-delivery channel. SSH is the only approved
|
||||||
|
remote access path.
|
||||||
|
|
||||||
## Key Commands
|
## Key Commands
|
||||||
|
|
||||||
At a terminal (Mode 1), use `make` or call scripts directly:
|
At a terminal (Mode 1), use `make` or call scripts directly:
|
||||||
@@ -209,6 +308,9 @@ Bypass with `--no-verify` in genuine emergencies only.
|
|||||||
- Install language toolchains on the host.
|
- Install language toolchains on the host.
|
||||||
- Use `:latest` image tags or Docker's default container naming.
|
- Use `:latest` image tags or Docker's default container naming.
|
||||||
- Author long-form docs in gitea — use Discourse.
|
- Author long-form docs in gitea — use Discourse.
|
||||||
|
- Call `ssh`/`scp` directly — always route through the project's `remote.sh` chokepoint.
|
||||||
|
- Use IP address literals for any host — always use DNS names.
|
||||||
|
- Use `qm guest exec` or any guest-agent execution channel — SSH only.
|
||||||
- Commit secrets.
|
- Commit secrets.
|
||||||
- Run destructive git operations without explicit instruction.
|
- Run destructive git operations without explicit instruction.
|
||||||
- Declare the work "done" — the human decides that.
|
- Declare the work "done" — the human decides that.
|
||||||
|
|||||||
+40
-11
@@ -34,7 +34,28 @@
|
|||||||
- **Cross-referencing is mandatory.** Every Redmine ticket links to its Discourse doc; every Discourse doc links to its Redmine ticket(s); every commit references `[#NNN]`; every PR links to both Redmine and Discourse. Keep them in sync at all times.
|
- **Cross-referencing is mandatory.** Every Redmine ticket links to its Discourse doc; every Discourse doc links to its Redmine ticket(s); every commit references `[#NNN]`; every PR links to both Redmine and Discourse. Keep them in sync at all times.
|
||||||
- **Code, docs, and tests must be kept in sync at all times.** When you change code, update the corresponding docs (Discourse) and tests in the same commit. Never leave them out of sync.
|
- **Code, docs, and tests must be kept in sync at all times.** When you change code, update the corresponding docs (Discourse) and tests in the same commit. Never leave them out of sync.
|
||||||
|
|
||||||
## 4. Git workflow
|
## 4. Infrastructure change approval workflow
|
||||||
|
|
||||||
|
For infrastructure (operations) work, agents do NOT execute changes
|
||||||
|
without explicit human approval. The workflow is:
|
||||||
|
|
||||||
|
1. **Prepare:** Agent creates scripts, configs, and a plan. Documents
|
||||||
|
the exact changes, blast radius, and rollback procedure in a
|
||||||
|
Redmine ticket.
|
||||||
|
2. **Review:** Human reviews and approves (or rejects) via Redmine.
|
||||||
|
3. **Execute:** Agent applies the change ONLY after approval.
|
||||||
|
4. **Verify:** Agent verifies the change worked and documents results
|
||||||
|
in Discourse.
|
||||||
|
|
||||||
|
This applies to all production infrastructure: network configs, host
|
||||||
|
tuning, VM settings, DNS records, switch configs, etc. Read-only
|
||||||
|
audits and probes do NOT require approval — only changes that alter
|
||||||
|
system behavior.
|
||||||
|
|
||||||
|
Code projects (software development) follow normal git/PR workflow and
|
||||||
|
do not require per-change Redmine approval.
|
||||||
|
|
||||||
|
## 5. Git workflow
|
||||||
|
|
||||||
- **Use the `tea` CLI for pull requests.**
|
- **Use the `tea` CLI for pull requests.**
|
||||||
- **Work smart off master** generally. Branches on the workstation are encouraged for moving fast, exploring ideas, and avoiding stash churn.
|
- **Work smart off master** generally. Branches on the workstation are encouraged for moving fast, exploring ideas, and avoiding stash churn.
|
||||||
@@ -42,13 +63,13 @@
|
|||||||
- Branching strategy is open to per-project discussion.
|
- Branching strategy is open to per-project discussion.
|
||||||
- **Commits and PRs must cross-link** to Redmine tickets (`[#NNN]` in subject or body) and Discourse docs (in PR body).
|
- **Commits and PRs must cross-link** to Redmine tickets (`[#NNN]` in subject or body) and Discourse docs (in PR body).
|
||||||
|
|
||||||
## 5. CI/CD — shift left, keep in lockstep
|
## 6. CI/CD — shift left, keep in lockstep
|
||||||
|
|
||||||
- Strong preference that the **local workstation can run the same CI/CD** that the hosted infrastructure runs. Maintain them in lockstep across all projects.
|
- Strong preference that the **local workstation can run the same CI/CD** that the hosted infrastructure runs. Maintain them in lockstep across all projects.
|
||||||
- **The further left CI/CD runs, the better.** Catch it before push, before PR, before merge.
|
- **The further left CI/CD runs, the better.** Catch it before push, before PR, before merge.
|
||||||
- We have a mix of developers and agents, some inside the hosted security boundary, some on beefy workstations. CI/CD must work for all of them.
|
- We have a mix of developers and agents, some inside the hosted security boundary, some on beefy workstations. CI/CD must work for all of them.
|
||||||
|
|
||||||
## 6. Docker and Kubernetes for everything
|
## 7. Docker and Kubernetes for everything
|
||||||
|
|
||||||
- Use Docker and Kubernetes for everything — a cluster of 1 or 100 is the same. Don't presume scale. Containers are containers; k8s is k8s.
|
- Use Docker and Kubernetes for everything — a cluster of 1 or 100 is the same. Don't presume scale. Containers are containers; k8s is k8s.
|
||||||
- **All development work happens in containers** — custom, off-the-shelf, or a mix. `docker pull` freely without asking.
|
- **All development work happens in containers** — custom, off-the-shelf, or a mix. `docker pull` freely without asking.
|
||||||
@@ -58,7 +79,7 @@
|
|||||||
- **One-off utility needed?** `docker pull` a pinned image and run it ephemerally. Do not install on the host.
|
- **One-off utility needed?** `docker pull` a pinned image and run it ephemerally. Do not install on the host.
|
||||||
- **Broadly useful tool?** Create a Redmine ticket requesting the human add it via the system package manager. Do not install it yourself.
|
- **Broadly useful tool?** Create a Redmine ticket requesting the human add it via the system package manager. Do not install it yourself.
|
||||||
|
|
||||||
## 7. Infrastructure-as-Code testing
|
## 8. Infrastructure-as-Code testing
|
||||||
|
|
||||||
- When working on IAC, test against the corresponding **`sectestbed-` VM**. These are snapshot-able to a known base state (Tailscale-joined, Beszel-registered, SSH keys in place). The base state evolves; the delta of tested code shrinks over time.
|
- When working on IAC, test against the corresponding **`sectestbed-` VM**. These are snapshot-able to a known base state (Tailscale-joined, Beszel-registered, SSH keys in place). The base state evolves; the delta of tested code shrinks over time.
|
||||||
- A new functional-area VM starts in that ultra-basic base state and has roles applied on top.
|
- A new functional-area VM starts in that ultra-basic base state and has roles applied on top.
|
||||||
@@ -66,7 +87,7 @@
|
|||||||
- Compliance mitigations may need to flow through both `sectestbed-` and `preprod-` testing, in lockstep. Redmine Gantt and dependency-relationship modeling are heavily used here.
|
- Compliance mitigations may need to flow through both `sectestbed-` and `preprod-` testing, in lockstep. Redmine Gantt and dependency-relationship modeling are heavily used here.
|
||||||
- **Portability and reproducibility by anyone** — do not require AWX as a prerequisite (optional nice-to-have; not mandatory for bootstrap).
|
- **Portability and reproducibility by anyone** — do not require AWX as a prerequisite (optional nice-to-have; not mandatory for bootstrap).
|
||||||
|
|
||||||
## 8. STATUS.md — scratchpad, not system of record
|
## 9. STATUS.md — scratchpad, not system of record
|
||||||
|
|
||||||
- **STATUS.md is a durable, git-tracked scratchpad for token efficiency.** It is not the system of record (Redmine is).
|
- **STATUS.md is a durable, git-tracked scratchpad for token efficiency.** It is not the system of record (Redmine is).
|
||||||
- The agent fully owns STATUS.md; the human only consumes it.
|
- The agent fully owns STATUS.md; the human only consumes it.
|
||||||
@@ -74,24 +95,32 @@
|
|||||||
- The harness todo tool is fine for tracking *current* work; STATUS.md is the durable cross-session record.
|
- The harness todo tool is fine for tracking *current* work; STATUS.md is the durable cross-session record.
|
||||||
- **STATUS.md has an Inbox section.** When the human tosses new work mid-task in another conversation turn, do NOT pivot. Log it in the Inbox. If it's materially different, spin up a Redmine ticket.
|
- **STATUS.md has an Inbox section.** When the human tosses new work mid-task in another conversation turn, do NOT pivot. Log it in the Inbox. If it's materially different, spin up a Redmine ticket.
|
||||||
|
|
||||||
## 9. Questions file — `questions-v(N).md`
|
## 10. Questions file — `questions-v(N).md`
|
||||||
|
|
||||||
- Capture questions in a git-tracked, versioned file: `questions-v1.md`, `questions-v2.md`, …
|
- **NEVER use a harness "question"/"ask user" tool** (structured prompts,
|
||||||
|
modal forms, tabbed questions, etc.). Ever. They are banned across every
|
||||||
|
project. They are not portable across harnesses, they don't version
|
||||||
|
history, and they bypass the git-tracked record. This is non-negotiable.
|
||||||
|
- **All questions go in the git-tracked `questions-v(N).md` file.** Write
|
||||||
|
the question; the human edits the answer inline in the same file. This
|
||||||
|
preserves history, works under every harness, and keeps reasoning next
|
||||||
|
to the answer. Version up (`questions-v2.md`, …) when a round lands.
|
||||||
|
- Capture questions in a versioned file: `questions-v1.md`, `questions-v2.md`, …
|
||||||
- The human reviews and edits it inline. Version it when a round of answers goes in.
|
- The human reviews and edits it inline. Version it when a round of answers goes in.
|
||||||
- Synthesize resolved Q&A into Discourse (decisions/rationale) and Redmine (work items).
|
- Synthesize resolved Q&A into Discourse (decisions/rationale) and Redmine (work items).
|
||||||
|
|
||||||
## 10. Belt-and-suspenders protocol enforcement
|
## 11. Belt-and-suspenders protocol enforcement
|
||||||
|
|
||||||
- Enforce the rules in **two layers**: prose policy in `AGENTS.md` and mechanical enforcement in git `pre-commit` / `pre-push` hooks. Harness-specific hooks (e.g. Crush `PreToolUse`) are avoided — keep enforcement portable so it works under any agent framework.
|
- Enforce the rules in **two layers**: prose policy in `AGENTS.md` and mechanical enforcement in git `pre-commit` / `pre-push` hooks. Harness-specific hooks (e.g. Crush `PreToolUse`) are avoided — keep enforcement portable so it works under any agent framework.
|
||||||
- Never rely on memory or prose alone.
|
- Never rely on memory or prose alone.
|
||||||
|
|
||||||
## 11. Gardening — keep docs from sprawling
|
## 12. Gardening — keep docs from sprawling
|
||||||
|
|
||||||
- Run a routine **gardening loop.** Agents are disciplined with code but tend to sprawl `.md` files everywhere.
|
- Run a routine **gardening loop.** Agents are disciplined with code but tend to sprawl `.md` files everywhere.
|
||||||
- Keep docs, code, and tests in sync at all times.
|
- Keep docs, code, and tests in sync at all times.
|
||||||
- `scripts/garden.sh` reports `.md` sprawl and files that should be migrated to Discourse.
|
- `scripts/garden.sh` reports `.md` sprawl and files that should be migrated to Discourse.
|
||||||
|
|
||||||
## 12. Sub-agents as subcontractors
|
## 13. Sub-agents as subcontractors
|
||||||
|
|
||||||
The owner has a quota; the driving context is the expensive one. The biggest
|
The owner has a quota; the driving context is the expensive one. The biggest
|
||||||
token cost is prefix mutation, not per-call work — so keep the driving prefix
|
token cost is prefix mutation, not per-call work — so keep the driving prefix
|
||||||
@@ -105,7 +134,7 @@ stable and push volatility into side-channels (sub-agents, STATUS.md).
|
|||||||
|
|
||||||
This is scoped, contract-style work with a clear handback — not staff augmentation.
|
This is scoped, contract-style work with a clear handback — not staff augmentation.
|
||||||
|
|
||||||
## 13. TDD and linting
|
## 14. TDD and linting
|
||||||
|
|
||||||
- **Red/green TDD for all code.** Write the failing test first.
|
- **Red/green TDD for all code.** Write the failing test first.
|
||||||
- **Linters on all code, as early as possible.** Be token-efficient — let deterministic tools find the issues.
|
- **Linters on all code, as early as possible.** Be token-efficient — let deterministic tools find the issues.
|
||||||
|
|||||||
Executable
+78
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# hooks/ticket-gate.sh — enforce ticket-first work policy
|
||||||
|
#
|
||||||
|
# Blocks modifying operations until an active ticket is established.
|
||||||
|
# The agent sets the active ticket via: echo '#NNN' > .crush/active-ticket
|
||||||
|
# And clears it when done: > .crush/active-ticket
|
||||||
|
#
|
||||||
|
# Exempts read-only and management commands (so you can create tickets,
|
||||||
|
# run audits, check status, etc.).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TICKET_FILE="${CRUSH_PROJECT_DIR}/.crush/active-ticket"
|
||||||
|
TOOL="${CRUSH_TOOL_NAME:-}"
|
||||||
|
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
|
||||||
|
|
||||||
|
# Read-only tools — always allowed
|
||||||
|
case "$TOOL" in
|
||||||
|
view|ls|grep|glob|agent|sourcegraph|fetch|agentic_fetch|download|lsp_diagnostics|lsp_symbols|lsp_definition|lsp_references|lsp_call_hierarchy|crush_info|crush_logs|question|todos)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# For bash tool: exempt read-only and management commands
|
||||||
|
if [ "$TOOL" = "bash" ]; then
|
||||||
|
# Ticket/doc/dns management — always allowed
|
||||||
|
case "$CMD" in
|
||||||
|
*"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
# Read-only git
|
||||||
|
case "$CMD" in
|
||||||
|
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
# Repo hygiene scripts
|
||||||
|
case "$CMD" in
|
||||||
|
*"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
# Monitoring/probe commands
|
||||||
|
case "$CMD" in
|
||||||
|
*"tailscale status"*|*"access-matrix"*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
# Setting/clearing the active ticket
|
||||||
|
case "$CMD" in
|
||||||
|
*active-ticket*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For edit/write: exempt policy/hook files (these ARE the policy)
|
||||||
|
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*/AGENTS.md|*/check-rules.sh|*/crush.json|*/hooks/*)
|
||||||
|
if [ "$TOOL" = "write" ] || [ "$TOOL" = "edit" ] || [ "$TOOL" = "multiedit" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check for active ticket
|
||||||
|
if [ -f "$TICKET_FILE" ] && [ -s "$TICKET_FILE" ]; then
|
||||||
|
TICKET=$(cat "$TICKET_FILE")
|
||||||
|
printf '{"context":"Active ticket: %s"}\n' "$TICKET"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No active ticket — block
|
||||||
|
cat >&2 <<'MSG'
|
||||||
|
TICKET GATE: No active ticket set.
|
||||||
|
|
||||||
|
This project requires ticket-governed work (AGENTS.md Agent Authority).
|
||||||
|
Before modifying systems or code, set the active ticket:
|
||||||
|
|
||||||
|
echo '#NNN' > .crush/active-ticket
|
||||||
|
|
||||||
|
If no ticket exists yet, create one first (redmine-cli create), then set it.
|
||||||
|
Clear the ticket when work is complete:
|
||||||
|
|
||||||
|
> .crush/active-ticket
|
||||||
|
MSG
|
||||||
|
exit 2
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# agent-profile.sh — switch agent identity context.
|
||||||
|
#
|
||||||
|
# Source this script to become a specific agent. Sets git author/committer
|
||||||
|
# identity, registers a tea login (from Bitwarden), and exports the agent
|
||||||
|
# name for other tools (bw-run.sh, clone-as.sh, etc.).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# . agent-profile.sh vp-techops
|
||||||
|
# . agent-profile.sh vp-secops
|
||||||
|
# . agent-profile.sh reachableceo (back to Charles's identity)
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Bitwarden session active (BW_SESSION exported) OR BW_CLIENTID/
|
||||||
|
# BW_CLIENTSECRET in ~/.config/bw/env
|
||||||
|
# - Agent credentials provisioned in Bitwarden (see agent-identity-bootstrap.md)
|
||||||
|
|
||||||
|
# Do NOT set -euo pipefail — this script is sourced, and the caller controls
|
||||||
|
# their own shell options. We guard each operation explicitly.
|
||||||
|
|
||||||
|
AGENT_PROFILE_NAME="${1:-}"
|
||||||
|
|
||||||
|
if [ -z "$AGENT_PROFILE_NAME" ]; then
|
||||||
|
echo "Usage: . agent-profile.sh <agent-name>" >&2
|
||||||
|
echo " e.g. . agent-profile.sh vp-techops" >&2
|
||||||
|
echo " . agent-profile.sh reachableceo" >&2
|
||||||
|
# shellcheck disable=SC2317 # reachable when executed (not sourced)
|
||||||
|
return 1 2>/dev/null || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Display name mapping (for git author name)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
case "$AGENT_PROFILE_NAME" in
|
||||||
|
vp-techops) _display="VP TechOps" ;;
|
||||||
|
vp-secops) _display="VP SecOps" ;;
|
||||||
|
vp-techcompliance) _display="VP TechCompliance" ;;
|
||||||
|
coo) _display="Chief Operating Officer" ;;
|
||||||
|
svp-knel) _display="SVP KNEL" ;;
|
||||||
|
svp-tctc) _display="SVP TCTC" ;;
|
||||||
|
reachableceo) _display="Charles N Wyble" ;;
|
||||||
|
*)
|
||||||
|
_display="$AGENT_PROFILE_NAME"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Domain mapping
|
||||||
|
_domain="turnsys.com"
|
||||||
|
if [ "$AGENT_PROFILE_NAME" = "reachableceo" ]; then
|
||||||
|
_email="reachableceo@turnsys.com"
|
||||||
|
else
|
||||||
|
_email="${AGENT_PROFILE_NAME}@${_domain}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Export agent identity environment variables
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export AGENT_NAME="$AGENT_PROFILE_NAME"
|
||||||
|
export AGENT_DISPLAY="$_display"
|
||||||
|
export AGENT_EMAIL="$_email"
|
||||||
|
export GIT_AUTHOR_NAME="$_display"
|
||||||
|
export GIT_COMMITTER_NAME="$_display"
|
||||||
|
export GIT_AUTHOR_EMAIL="$_email"
|
||||||
|
export GIT_COMMITTER_EMAIL="$_email"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ensure Bitwarden session is active
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_bw_session_setup() {
|
||||||
|
if [ -n "${BW_SESSION:-}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local bw_env="${HOME}/.config/bw/env"
|
||||||
|
if [ -f "$bw_env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$bw_env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||||
|
bw login --apikey >/dev/null 2>&1 || true
|
||||||
|
BW_SESSION=$(BW_PASSWORD="${BW_PASSWORD:-}" bw unlock --raw 2>/dev/null || true)
|
||||||
|
if [ -n "$BW_SESSION" ]; then
|
||||||
|
export BW_SESSION
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "agent-profile: WARNING — no BW session, tea login will not be configured" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Register tea login for this agent (if credentials exist in BW)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_tea_setup() {
|
||||||
|
# Only set up tea for agents that have Gitea credentials in BW
|
||||||
|
local token
|
||||||
|
token=$(bw get password "${AGENT_NAME} Gitea" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
# Not an error — some agents may not have Gitea access yet
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if login already exists
|
||||||
|
if tea login list --output csv 2>/dev/null | grep -q "^${AGENT_NAME},"; then
|
||||||
|
# Already registered — just set as default
|
||||||
|
tea login default "$AGENT_NAME" >/dev/null 2>&1 || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Register the login
|
||||||
|
tea login add \
|
||||||
|
--name "$AGENT_NAME" \
|
||||||
|
--url "https://git.knownelement.com" \
|
||||||
|
--token "$token" \
|
||||||
|
--ssh-host "git.knownelement.com" \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
tea login default "$AGENT_NAME" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _bw_session_setup; then
|
||||||
|
_tea_setup
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up helper functions from the environment
|
||||||
|
unset -f _bw_session_setup _tea_setup 2>/dev/null || true
|
||||||
|
unset _display _email _domain 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "agent-profile: now operating as ${AGENT_PROFILE_NAME} (${AGENT_DISPLAY})" >&2
|
||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bw-git-credential.sh — Git credential helper that sources Gitea tokens from Bitwarden.
|
||||||
|
#
|
||||||
|
# This eliminates the need for per-agent SSH keys or on-disk tokens for git
|
||||||
|
# operations. Each repo has a git identity (user.email) that identifies which
|
||||||
|
# agent is working. This helper reads that identity, fetches the matching
|
||||||
|
# Gitea API token from Bitwarden, and provides it to git.
|
||||||
|
#
|
||||||
|
# Setup (once per environment, in ~/.gitconfig):
|
||||||
|
# [credential "https://git.knownelement.com"]
|
||||||
|
# helper = /path/to/bw-git-credential.sh
|
||||||
|
#
|
||||||
|
# How it works:
|
||||||
|
# - During git push (in an existing repo): reads `git config user.email`
|
||||||
|
# to determine the agent, fetches "<agent> Gitea" from Bitwarden.
|
||||||
|
# - During git clone (no repo yet): reads AGENT_CLONE_AS env var, which
|
||||||
|
# clone-as.sh sets before calling git clone.
|
||||||
|
# - If neither is set, exits silently (lets other credential helpers run).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_HOST="git.knownelement.com"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Read git credential input from stdin
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare -A cred
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
# Empty line terminates the credential block
|
||||||
|
[ -z "$key" ] && break
|
||||||
|
cred["$key"]="$value"
|
||||||
|
done
|
||||||
|
|
||||||
|
host="${cred[protocol]:-}://${cred[host]:-}"
|
||||||
|
|
||||||
|
# Only handle our Gitea instance
|
||||||
|
if [ "${cred[host]:-}" != "$GITEA_HOST" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Determine which agent identity to use
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
agent=""
|
||||||
|
|
||||||
|
# During clone: clone-as.sh sets this env var
|
||||||
|
if [ -n "${AGENT_CLONE_AS:-}" ]; then
|
||||||
|
agent="$AGENT_CLONE_AS"
|
||||||
|
else
|
||||||
|
# During push/fetch: read the repo's configured identity
|
||||||
|
agent_email=$(git config user.email 2>/dev/null || echo "")
|
||||||
|
if [ -n "$agent_email" ]; then
|
||||||
|
agent=$(echo "$agent_email" | cut -d@ -f1)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$agent" ]; then
|
||||||
|
# No agent identity — let other credential helpers handle it
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ensure Bitwarden session is active
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ -z "${BW_SESSION:-}" ]; then
|
||||||
|
# Try to establish from the env file
|
||||||
|
bw_env="${HOME}/.config/bw/env"
|
||||||
|
if [ -f "$bw_env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$bw_env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||||
|
bw login --apikey >/dev/null 2>&1 || true
|
||||||
|
export BW_SESSION
|
||||||
|
BW_SESSION=$(BW_PASSWORD="${BW_PASSWORD:-}" bw unlock --raw 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${BW_SESSION:-}" ]; then
|
||||||
|
echo "bw-git-credential: no BW session — cannot fetch token for $agent" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fetch the Gitea token from Bitwarden
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
item_name="$agent Gitea"
|
||||||
|
token=$(bw get password "$item_name" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
echo "bw-git-credential: no Gitea token in BW for: $item_name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output credentials for git
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "username=$agent"
|
||||||
|
echo "password=$token"
|
||||||
Executable
+157
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bw-run.sh — run a CLI command with credentials sourced from Bitwarden.
|
||||||
|
#
|
||||||
|
# Replaces the --env-file ~/.creds/<service>.env pattern. Each agent identity
|
||||||
|
# stores its own API keys/passwords in a Bitwarden collection. This script
|
||||||
|
# fetches the credential, writes it to a temp env file, runs the CLI, and
|
||||||
|
# cleans up — no credential material persists on disk.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bw-run.sh <agent> <service> <image> [args...]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# bw-run.sh vp-techops redmine git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55
|
||||||
|
# bw-run.sh vp-techops discourse git.knownelement.com/reachableceo/discourse-cli:latest ls -c vp-techops
|
||||||
|
# bw-run.sh reachableceo redmine git.knownelement.com/reachableceo/redmine-cli:latest whoami
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - BW_CLIENTID and BW_CLIENTSECRET exported (or in ~/.config/bw/env)
|
||||||
|
# - Or BW_SESSION exported from a prior `bw unlock`
|
||||||
|
#
|
||||||
|
# Bitwarden item convention:
|
||||||
|
# Collection: <agent-name> (or "shared" for cross-agent creds)
|
||||||
|
# Item name: "<agent-name> <Service>" (e.g., "vp-techops Redmine")
|
||||||
|
# Fields: URL, USERNAME, PASSWORD, API_KEY, and custom fields as needed
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
sed -n '2,26p' "$0"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -lt 3 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
||||||
|
print_usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
AGENT="$1"
|
||||||
|
SERVICE="$2"
|
||||||
|
IMAGE="$3"
|
||||||
|
shift 3
|
||||||
|
|
||||||
|
ITEM_NAME="${AGENT} ${SERVICE}"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Ensure Bitwarden session is active
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
ensure_bw_session() {
|
||||||
|
# If BW_SESSION is already set, trust it
|
||||||
|
if [ -n "${BW_SESSION:-}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try machine-to-machine auth via client credentials
|
||||||
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||||
|
log_info "Authenticating to Bitwarden via API key..."
|
||||||
|
if bw login --apikey >/dev/null 2>&1; then
|
||||||
|
export BW_SESSION
|
||||||
|
BW_SESSION=$(bw unlock --raw 2>/dev/null || true)
|
||||||
|
if [ -z "$BW_SESSION" ]; then
|
||||||
|
die "Bitwarden unlock failed after API-key login."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try reading from the bw env file
|
||||||
|
local bw_env="${HOME}/.config/bw/env"
|
||||||
|
if [ -f "$bw_env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$bw_env"
|
||||||
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||||
|
log_info "Authenticating to Bitwarden via env file..."
|
||||||
|
bw login --apikey >/dev/null 2>&1 || true
|
||||||
|
export BW_SESSION
|
||||||
|
BW_SESSION=$(bw unlock --raw 2>/dev/null || true)
|
||||||
|
if [ -n "$BW_SESSION" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
die "No Bitwarden session. Set BW_SESSION or BW_CLIENTID/BW_CLIENTSECRET."
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
ensure_bw_session
|
||||||
|
|
||||||
|
log_info "Fetching credential: ${ITEM_NAME}"
|
||||||
|
|
||||||
|
# Fetch the Bitwarden item and extract fields into env vars
|
||||||
|
RAW_JSON=$(bw get item "$ITEM_NAME" 2>/dev/null) || die "Bitwarden item not found: ${ITEM_NAME}"
|
||||||
|
|
||||||
|
# Build env file content from the JSON response
|
||||||
|
ENV_CONTENT=$(echo "$RAW_JSON" | jq -r '
|
||||||
|
def safename(f): f | gsub("[^A-Za-z0-9_]"; "_") | ascii_upcase;
|
||||||
|
.login as $login |
|
||||||
|
# Standard fields
|
||||||
|
(
|
||||||
|
if $login.uris[0].uri then "URL=\($login.uris[0].uri)\n" else "" end
|
||||||
|
) +
|
||||||
|
(
|
||||||
|
if $login.username then "USERNAME=\($login.username)\n" else "" end
|
||||||
|
) +
|
||||||
|
(
|
||||||
|
if $login.password then "PASSWORD=\($login.password)\n" else "" end
|
||||||
|
) +
|
||||||
|
# Map known field names to common env var patterns
|
||||||
|
(
|
||||||
|
.fields[]? |
|
||||||
|
if .name == "API_KEY" or .name == "api_key" or .name == "apikey" then
|
||||||
|
"API_KEY=\(.value)\n"
|
||||||
|
elif .name == "token" or .name == "TOKEN" then
|
||||||
|
"TOKEN=\(.value)\n"
|
||||||
|
else
|
||||||
|
"\(safename(.name))=\(.value)\n"
|
||||||
|
end
|
||||||
|
) +
|
||||||
|
# Also emit REDMINE_API_KEY / DISCOURSE_API_KEY etc. from the password field
|
||||||
|
# (many CLI tools expect the key in the password field)
|
||||||
|
""
|
||||||
|
') || die "Failed to parse Bitwarden item JSON."
|
||||||
|
|
||||||
|
if [ -z "$ENV_CONTENT" ]; then
|
||||||
|
die "No credential fields found in Bitwarden item: ${ITEM_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check if the password field IS the API key (common for CLI creds)
|
||||||
|
# and add a service-specific key env var based on the service name
|
||||||
|
SERVICE_UPPER=$(echo "$SERVICE" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
|
||||||
|
API_KEY_VAR="${SERVICE_UPPER}_API_KEY"
|
||||||
|
|
||||||
|
# If the service-specific key var isn't already in ENV_CONTENT, try adding it from password
|
||||||
|
if ! echo "$ENV_CONTENT" | grep -q "^${API_KEY_VAR}="; then
|
||||||
|
PASSWORD_VAL=$(echo "$RAW_JSON" | jq -r '.login.password // empty')
|
||||||
|
if [ -n "$PASSWORD_VAL" ]; then
|
||||||
|
ENV_CONTENT="${API_KEY_VAR}=${PASSWORD_VAL}
|
||||||
|
${ENV_CONTENT}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write to temp file, cleaned up on exit
|
||||||
|
ENVFILE=$(mktemp "/tmp/bw-${AGENT}-${SERVICE}-XXXXXX.env")
|
||||||
|
trap 'rm -f "$ENVFILE"' EXIT
|
||||||
|
chmod 600 "$ENVFILE"
|
||||||
|
printf '%s' "$ENV_CONTENT" > "$ENVFILE"
|
||||||
|
|
||||||
|
log_ok "Credential sourced. Running: ${IMAGE} $*"
|
||||||
|
|
||||||
|
# Run the CLI with BW-sourced credentials
|
||||||
|
docker run --rm --env-file "$ENVFILE" "$IMAGE" "$@"
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# clone-as.sh — clone a git repository as a specific agent identity.
|
||||||
|
#
|
||||||
|
# Wraps `git clone` so the clone authenticates with the agent's Gitea token
|
||||||
|
# (from Bitwarden) and the resulting repo has the correct per-repo git
|
||||||
|
# identity (user.name + user.email) set automatically.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# clone-as.sh <agent> <repo-url> [target-dir]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# clone-as.sh vp-techops https://git.knownelement.com/KNEL/PFVCluster.git
|
||||||
|
# clone-as.sh vp-techops https://git.knownelement.com/KNEL/PFVCluster.git ~/projects/pfv
|
||||||
|
# clone-as.sh reachableceo ssh://git@git.knownelement.com:29418/KNEL/PFVCluster.git
|
||||||
|
#
|
||||||
|
# After cloning, subsequent `git push` uses bw-git-credential.sh (configured
|
||||||
|
# in ~/.gitconfig) which reads the repo's user.email to fetch the token.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
if [ $# -lt 2 ]; then
|
||||||
|
sed -n '2,22p' "$0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLONE_AGENT="$1"
|
||||||
|
CLONE_URL="$2"
|
||||||
|
CLONE_DIR="${3:-}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Display name mapping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
case "$CLONE_AGENT" in
|
||||||
|
vp-techops) display="VP TechOps" ;;
|
||||||
|
vp-secops) display="VP SecOps" ;;
|
||||||
|
vp-techcompliance) display="VP TechCompliance" ;;
|
||||||
|
coo) display="Chief Operating Officer" ;;
|
||||||
|
svp-knel) display="SVP KNEL" ;;
|
||||||
|
svp-tctc) display="SVP TCTC" ;;
|
||||||
|
reachableceo) display="Charles N Wyble" ;;
|
||||||
|
*) display="$CLONE_AGENT" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$CLONE_AGENT" = "reachableceo" ]; then
|
||||||
|
email="reachableceo@turnsys.com"
|
||||||
|
else
|
||||||
|
email="${CLONE_AGENT}@turnsys.com"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# If SSH URL, clone directly (SSH key auth, no credential helper needed)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "$CLONE_URL" == ssh://* ]] || [[ "$CLONE_URL" == git@* ]]; then
|
||||||
|
log_info "Cloning (SSH): $CLONE_URL as $CLONE_AGENT"
|
||||||
|
|
||||||
|
if [ -n "$CLONE_DIR" ]; then
|
||||||
|
git clone "$CLONE_URL" "$CLONE_DIR"
|
||||||
|
cd "$CLONE_DIR"
|
||||||
|
else
|
||||||
|
git clone "$CLONE_URL"
|
||||||
|
# Derive repo dir name from URL
|
||||||
|
repo_base=$(basename "$CLONE_URL" .git)
|
||||||
|
cd "$repo_base"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set per-repo identity
|
||||||
|
git config user.name "$display"
|
||||||
|
git config user.email "$email"
|
||||||
|
log_ok "Cloned to $(pwd) — identity: $display <$email>"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTTPS URL — need credential helper for clone authentication
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Ensure Bitwarden session
|
||||||
|
if [ -z "${BW_SESSION:-}" ]; then
|
||||||
|
bw_env="${HOME}/.config/bw/env"
|
||||||
|
if [ -f "$bw_env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$bw_env"
|
||||||
|
fi
|
||||||
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||||
|
bw login --apikey >/dev/null 2>&1 || true
|
||||||
|
export BW_SESSION
|
||||||
|
BW_SESSION=$(BW_PASSWORD="${BW_PASSWORD:-}" bw unlock --raw 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${BW_SESSION:-}" ]; then
|
||||||
|
die "No BW session — cannot fetch Gitea token for $CLONE_AGENT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch token for clone auth
|
||||||
|
token=$(bw get password "$CLONE_AGENT Gitea" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
die "No Gitea token in BW for: $CLONE_AGENT Gitea"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Cloning (HTTPS): $CLONE_URL as $CLONE_AGENT"
|
||||||
|
|
||||||
|
# Set AGENT_CLONE_AS so bw-git-credential.sh knows which agent this is
|
||||||
|
# (needed only if the credential helper is wired; we also inject directly)
|
||||||
|
export AGENT_CLONE_AS="$CLONE_AGENT"
|
||||||
|
|
||||||
|
# Inject token into URL for clone auth, then strip after clone
|
||||||
|
# (avoids storing the token in .git/config)
|
||||||
|
url_path="${CLONE_URL#https://}"
|
||||||
|
injected_url="https://${CLONE_AGENT}:${token}@${url_path}"
|
||||||
|
|
||||||
|
if [ -n "$CLONE_DIR" ]; then
|
||||||
|
git clone "$injected_url" "$CLONE_DIR"
|
||||||
|
cd "$CLONE_DIR"
|
||||||
|
else
|
||||||
|
git clone "$injected_url"
|
||||||
|
repo_base=$(basename "$CLONE_URL" .git)
|
||||||
|
cd "$repo_base"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip the token from the remote URL (so it's not stored in .git/config)
|
||||||
|
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
|
||||||
|
clean_url="https://${remote_url#https://*@}"
|
||||||
|
git remote set-url origin "$clean_url"
|
||||||
|
|
||||||
|
# Set per-repo identity
|
||||||
|
git config user.name "$display"
|
||||||
|
git config user.email "$email"
|
||||||
|
|
||||||
|
log_ok "Cloned to $(pwd) — identity: $display <$email>"
|
||||||
|
log_info "Remote URL sanitized (token stripped). Push will use bw-git-credential.sh."
|
||||||
Reference in New Issue
Block a user