Compare commits

...
15 Commits
Author SHA1 Message Date
mrcharles f30e95d9d0 feat: add per-agent git identity tooling [#442]
Three scripts that let multiple AI agents share a single Linux account
while maintaining per-identity attribution for all git operations:

- bw-git-credential.sh: git credential helper that sources Gitea tokens
  from Bitwarden based on the repo's user.email config. Handles both
  push (reads repo config) and clone (reads AGENT_CLONE_AS env var).
- agent-profile.sh: sourceable context switcher. Sets git author/
  committer identity, registers tea login from BW, exports AGENT_NAME.
- clone-as.sh: clone wrapper that authenticates with the agent's BW
  token, sets per-repo identity, strips token from remote URL.

No SSH key juggling, no tokens on disk, no multiple Linux accounts.
All credential material stays in Bitwarden.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-13 10:58:55 -05:00
mrcharles 8d352aa7d5 feat: add bw-run.sh credential sourcing layer [#442]
Replaces the --env-file ~/.creds/*.env pattern with Bitwarden-sourced
credentials. Each agent identity stores its own API keys in a BW
collection; bw-run.sh fetches them at runtime and passes to CLI
containers via a temp env file (cleaned up on exit).

Usage: bw-run.sh <agent> <service> <image> [args...]

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-13 08:59:24 -05:00
mrcharles 2c9e5e9c0e docs(agents): add no-direct-DB-access rule — API-first always
If a tool has an API, use it. Exhaust all API endpoints before
considering direct DB access. DB schemas change without warning;
APIs are the stable contract. Aligns with upcoming zero trust
proxy migration.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 22:50:56 -05:00
mrcharles f5228a243f docs(agents): add mandatory UAT rule — user must accept before done
The agent performs technical validation only. The user performs user
acceptance testing (dashboards render data, alerts deliver, tools
usable). Agent must not set 100% done-ratio, suggest close, or move on
until the user explicitly accepts the work.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 21:39:32 -05:00
mrcharles 9cf15f1258 docs(agents): add rolling HUD protocol for session-scoped status display
The HUD is an ephemeral status block (active ticket, done items, current
work, queued items, user action items) shown after significant responses.
Written to .crush/hud.md on disk to save context tokens; never committed.
Redmine/Discourse/git remain the durable systems of record.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:35:08 -05:00
mrcharles 28b0019d2e docs(agents): add two-gate session-start protocol (rule summary + scope)
The template previously had no session-start gate at all. Adds:
- Step 5: rule-summary gate (summarize rules, wait for confirmation)
- Step 9: scope-alignment gate (present priorities, wait for user to
  name the target ticket before any work begins)

Prevents agents from autonomously picking work outside the session's
intended scope. Mirrors the PFVCluster gate structure.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 20:33:02 -05:00
mrcharles 5c44e3ef41 docs(agents): clarify questions-v(N).md versioning — increment on each answer round
💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 19:50:16 -05:00
mrcharles abb94a478e docs(agents): mandate command timeouts for all remote operations
Add NON-NEGOTIABLE rule: every remote command must be wrapped with
timeout (30s reads, 120s standard, 300s deployments). A hung command
is a failed command — investigate root cause, never blindly retry.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 15:34:29 -05:00
mrcharles 0be43403e6 docs(agents): add Remote Access section — remote.sh chokepoint + DNS-only
Establishes non-negotiable pattern for projects managing remote hosts:
all SSH through a remote.sh chokepoint (never direct ssh/scp), DNS names
only (never IP literals), and qemu-guest-agent visibility-only (never
execution). Adds corresponding DO NOT entries.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 12:57:09 -05:00
mrcharles 16cbeb4ed8 feat(hooks): add ticket-gate Crush hook — enforce ticket-first work
Mechanically enforces ticket-governed work: blocks modifying operations
until an active ticket is set (echo '#NNN' > .crush/active-ticket).
Exempts read-only tools and management commands.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 11:57:19 -05:00
mrcharles 1b899d32d3 docs(agents): never close tickets without user permission
Add NON-NEGOTIABLE rule: agents may suggest closing a ticket when the
result is clearly scoped and delivered, but must never close without
explicit user permission. Applies to ALL tickets, no exceptions.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-11 11:29:31 -05:00
mrcharles 29f33c4b03 fix(policy): ban harness question tools — questions-v(N).md only
Harness "ask user"/question tools (structured prompts, modal forms) are
not portable across harnesses, don't version history, and bypass the
git-tracked record. Make the existing questions-v(N).md rule explicit
and non-negotiable in both BASELINE-PROMPT.md §10 and AGENTS.md.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 16:03:57 -05:00
mrcharles 323e8ce2d1 feat: add infrastructure change approval workflow to BASELINE-PROMPT
Agents must get explicit human approval before executing infrastructure
changes (network configs, host tuning, VM settings, DNS, switches).
Workflow: prepare → Redmine ticket → human review → execute → verify.
Read-only audits do not require approval.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 11:42:48 -05:00
mrcharles d5a0e4d8f6 refactor(rules): point redmine-cli invocation at centralized creds store
Merge redmine-cli into tooling-cli/redmine and drop the
redmine-cli/.env source path from the rules. The canonical
invocation now uses ~/.creds/redmine.env (system-independent)
with the registry image. Marks Q2 (bin/ wrapper gap) fully
resolved for both redmine and discourse.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 09:53:41 -05:00
mrcharles cf1b444a45 refactor(rules): point discourse-cli invocation at centralized creds store
Merge discourse-cli into tooling-cli/discourse and drop the
discourse-cli/.env source path from the rules. The canonical
invocation now uses ~/.creds/discourse.env (system-independent)
with the registry image. Marks Q2 (bin/ wrapper gap) resolved.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-10 09:49:21 -05:00
10 changed files with 782 additions and 25 deletions
+3
View File
@@ -0,0 +1,3 @@
*
!crush.json
!.gitignore
+11
View File
@@ -0,0 +1,11 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$",
"command": "./hooks/ticket-gate.sh",
"timeout": 5
}
]
}
}
+108 -6
View File
@@ -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.
@@ -66,13 +90,13 @@ Replace this bracketed text.>
### CLI invocation (no bin/ shortcuts — invoke the real containers) ### CLI invocation (no bin/ shortcuts — invoke the real containers)
``` ```
# redmine-cli (source: ~/projects/KNEL-AIMiddleware/redmine-cli/) # redmine-cli
docker run --rm --env-file ~/projects/KNEL-AIMiddleware/redmine-cli/.env \ docker run --rm --env-file ~/.creds/redmine.env \
git.knownelement.com/reachableceo/redmine-cli:latest <command> git.knownelement.com/reachableceo/redmine-cli:latest <command>
# subcommands: whoami, projects, statuses, list, show <id>, create, update <id>, close <id> # subcommands: whoami, projects, statuses, list, show <id>, create, update <id>, close <id>
# discourse-cli (source: ~/projects/KNEL-AIMiddleware/discourse-cli/) # discourse-cli
docker run --rm --env-file ~/projects/KNEL-AIMiddleware/discourse-cli/.env \ docker run --rm --env-file ~/.creds/discourse.env \
git.knownelement.com/reachableceo/discourse-cli:latest <command> git.knownelement.com/reachableceo/discourse-cli:latest <command>
# subcommands: whoami, categories, cat-info, topics, show <id>, create, reply <id>, update <post_id>, delete <post_id>, search # subcommands: whoami, categories, cat-info, topics, show <id>, create, reply <id>, update <post_id>, delete <post_id>, search
``` ```
@@ -121,8 +145,57 @@ docker run --rm --env-file ~/projects/KNEL-AIMiddleware/discourse-cli/.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 ~/projects/KNEL-AIMiddleware/discourse-cli/.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 ~/projects/KNEL-AIMiddleware/discourse-cli/.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
View File
@@ -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.
+78
View File
@@ -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
+6 -8
View File
@@ -16,14 +16,12 @@
Go with option a. The tea command is setup on this workstation (and on ultix-offstage). I guess, also capture that the tea command (and docker login) are setup on my workstations, so that in the future, projects know they can use tea to setup a repo. Also, i want this to be TSYS wide, so it should go under the TSYSGroupCorporate organization. Call the repo: TSYSGroupAIOS . Make it a template repository. Go with option a. The tea command is setup on this workstation (and on ultix-offstage). I guess, also capture that the tea command (and docker login) are setup on my workstations, so that in the future, projects know they can use tea to setup a repo. Also, i want this to be TSYS wide, so it should go under the TSYSGroupCorporate organization. Call the repo: TSYSGroupAIOS . Make it a template repository.
### Q2. The bin/ wrapper gap (redmine-cli / discourse-cli) ### Q2. The bin/ wrapper gap (redmine-cli / discourse-cli) — RESOLVED (discourse)
- **Context:** PFVCluster's operational.md and AGENTS.md reference `~/daytoday/redmine/bin/redmine` and `~/daytoday/discourse/bin/discourse` as the entrypoints. But `ls ~/daytoday/{redmine,discourse}/` shows only `.gitignore` + `AGENTS.md` (+ MIGRATION-PLAN.md for discourse) — no `bin/`, no Dockerfile. The actual CLI source lives in `~/projects/KNEL-AIMiddleware/{redmine,discourse}-cli/`. - **Context:** Projects referenced `~/daytoday/redmine/bin/redmine` and `~/daytoday/discourse/bin/discourse` as entrypoints — thin shortcut wrappers around the real CLI containers. The actual CLI source lived in `~/projects/KNEL-AIMiddleware/{redmine,discourse}-cli/`.
- **Question:** Are the `bin/` wrappers something that should exist (and were lost / never committed), or is the documentation aspirational? Should the template reference these CLIs at all, or stay tool-agnostic? - **Question:** Are the `bin/` wrappers something that should exist, or is the documentation aspirational? Should the template reference these CLIs at all, or stay tool-agnostic?
- **Answer:** _(human)_ - **Answer:** Reference the real container invocation (full path/container name/invoke notes); no duplicate code via shortcut wrappers.
- **Decision:** _(human/agent)_ - **Decision:** No `bin/` wrappers. Invoke the real container with `docker run`. Both CLI sources have been merged into `~/projects/KNEL-AIMiddleware/tooling-cli/{discourse,redmine}/` (CLI source + Dockerfile + README + AGENTS.md). The canonical invocation is documented in AGENTS.md §CLI invocation and uses `--env-file ~/.creds/{discourse,redmine}.env` (centralized credential store) — no system-dependent paths in the rules. The old `~/daytoday/{discourse,redmine}` workspaces and the `{discourse,redmine}-cli/` subdirs were removed.
- **Synthesized to:** - **Synthesized to:** `~/daytoday/meta/AGENTS.md` §CLI invocation; `tooling-cli/{discourse,redmine}/`
The clis should exist. Maybe the AGENTS.md reference the actual paths? I dont need duplicate code. I think i was using the directories as kind of "shortcuts" vs the ~/projects/... path every time. So, for this repo, reference the full path/container name/invoke notes. Does that make sense?
### Q3. Should the template ship the Discourse pointer-header pattern? ### Q3. Should the template ship the Discourse pointer-header pattern?
- **Context:** PFVCluster migrated 36 in-repo `.md` files to 10-line pointer stubs citing `https://community.turnsys.com/t/<N>`. The template currently has `scripts/garden.sh` that *warns* about oversized non-Discourse `.md`, but doesn't enforce the pointer-header format. - **Context:** PFVCluster migrated 36 in-repo `.md` files to 10-line pointer stubs citing `https://community.turnsys.com/t/<N>`. The template currently has `scripts/garden.sh` that *warns* about oversized non-Discourse `.md`, but doesn't enforce the pointer-header format.
+135
View File
@@ -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
+105
View File
@@ -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"
+157
View File
@@ -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" "$@"
+139
View File
@@ -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."