Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 393 additions & 0 deletions .agents/skills/launch-openshell-gator/SKILL.md

Large diffs are not rendered by default.

22 changes: 19 additions & 3 deletions .agents/skills/triage-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,29 @@ Assess one specific issue. Proceed to Step 1 with the given issue number.
triage issues
```

Query all open issues with the `state:triage-needed` label and process them in sequence:
Batch mode requires a confirmation gate before processing. This prevents accidental mass-commenting on a public repository.

**Step 1: Preview.** Query all matching issues and display a summary:

```bash
gh issue list --label "state:triage-needed" --state open --json number,title --jq '.[].number'
gh issue list --label "state:triage-needed" --state open --json number,title --jq '.[] | "#\(.number) \(.title)"'
```

Present the results to the user:

```
Found N issues with state:triage-needed:

#250 Bug: sandbox fails to start with VM driver
#312 Feature: add --output yaml to sandbox list
... (show up to 10, then "and N more")

This will post a triage comment on each issue.
```

**Step 2: Confirm.** Ask the user for explicit confirmation before proceeding. Use `AskUserQuestion` with options "Proceed with all N issues", "Let me pick specific issues", and let them provide custom input. Do **not** proceed without confirmation.

For each issue returned, run the full triage workflow (Steps 1-7). Report a summary at the end listing each issue and its classification.
**Step 3: Process.** Only after confirmation, run the full triage workflow (Steps 1-7 below) for each issue. Report a summary at the end listing each issue and its classification.

## Step 1: Fetch the Issue

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Skills live in `.agents/skills/`. Your agent's harness can discover and load the
| Reviewing | `review-github-pr` | Summarize PR diffs and key design decisions |
| Reviewing | `review-security-issue` | Assess security issues for severity and remediation |
| Reviewing | `watch-github-actions` | Monitor CI pipeline status and logs |
| Reviewing | `launch-openshell-gator` | Launch and supervise OpenShell gator agents for issue and PR monitoring |
| Reviewing | `test-release-canary` | Dispatch and iterate on the Release Canary workflow that smoke-tests published artifacts |
| Triage | `triage-issue` | Assess, classify, and route community-filed issues |
| Platform | `generate-sandbox-policy` | Generate YAML sandbox policies from requirements or API docs |
Expand Down
46 changes: 22 additions & 24 deletions crates/openshell-server/src/certgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,11 @@ fn read_pem(path: &Path) -> Result<String> {
}

fn write_local_bundle(dir: &Path, bundle: &PkiBundle, paths: &LocalPaths) -> Result<()> {
// Stage to a sibling tmp dir so individual renames into the final layout
// are atomic on the same filesystem.
let temp = sibling_temp_dir(dir);
// Ensure output dir exists before staging inside it.
create_dir_restricted(dir)?;

// Stage inside the output dir so individual renames are atomic on the same filesystem.
let temp = staging_temp_dir(dir);
if temp.exists() {
std::fs::remove_dir_all(&temp)
.into_diagnostic()
Expand Down Expand Up @@ -659,8 +661,7 @@ fn write_local_bundle(dir: &Path, bundle: &PkiBundle, paths: &LocalPaths) -> Res
)?;
write_pem(&temp_jwt.join("kid"), &bundle.jwt_key_id, false)?;

// Final destination (might not exist yet on first run).
create_dir_restricted(dir)?;
// Final destination subdirs (might not exist yet on first run).
create_dir_restricted(&paths.server_dir)?;
create_dir_restricted(&paths.client_dir)?;
create_dir_restricted(&paths.jwt_dir)?;
Expand All @@ -687,7 +688,10 @@ fn write_local_bundle(dir: &Path, bundle: &PkiBundle, paths: &LocalPaths) -> Res
}

fn write_local_tls_bundle(bundle: &PkiBundle, paths: &LocalPaths) -> Result<()> {
let temp = sibling_temp_dir(&paths.server_dir);
create_dir_restricted(&paths.server_dir)?;
create_dir_restricted(&paths.client_dir)?;

let temp = staging_temp_dir(&paths.server_dir);
if temp.exists() {
std::fs::remove_dir_all(&temp)
.into_diagnostic()
Expand All @@ -707,8 +711,6 @@ fn write_local_tls_bundle(bundle: &PkiBundle, paths: &LocalPaths) -> Result<()>
write_pem(&temp_client.join("tls.crt"), &bundle.client_cert_pem, false)?;
write_pem(&temp_client.join("tls.key"), &bundle.client_key_pem, true)?;

create_dir_restricted(&paths.server_dir)?;
create_dir_restricted(&paths.client_dir)?;
let renames: [(PathBuf, &Path); 6] = [
(temp.join("ca.crt"), paths.ca_crt.as_path()),
(temp.join("ca.key"), paths.ca_key.as_path()),
Expand All @@ -728,7 +730,9 @@ fn write_local_tls_bundle(bundle: &PkiBundle, paths: &LocalPaths) -> Result<()>
}

fn write_local_jwt_bundle(bundle: &PkiBundle, paths: &LocalPaths) -> Result<()> {
let temp = sibling_temp_dir(&paths.jwt_dir);
create_dir_restricted(&paths.jwt_dir)?;

let temp = staging_temp_dir(&paths.jwt_dir);
if temp.exists() {
std::fs::remove_dir_all(&temp)
.into_diagnostic()
Expand All @@ -740,7 +744,6 @@ fn write_local_jwt_bundle(bundle: &PkiBundle, paths: &LocalPaths) -> Result<()>
write_pem(&temp.join("public.pem"), &bundle.jwt_public_key_pem, false)?;
write_pem(&temp.join("kid"), &bundle.jwt_key_id, false)?;

create_dir_restricted(&paths.jwt_dir)?;
let renames: [(PathBuf, &Path); 3] = [
(temp.join("signing.pem"), paths.jwt_signing.as_path()),
(temp.join("public.pem"), paths.jwt_public.as_path()),
Expand All @@ -766,14 +769,9 @@ fn write_pem(path: &Path, contents: &str, owner_only: bool) -> Result<()> {
Ok(())
}

fn sibling_temp_dir(dir: &Path) -> PathBuf {
// Use a sibling so std::fs::rename succeeds (same filesystem).
let mut name = dir
.file_name()
.map(std::ffi::OsStr::to_os_string)
.unwrap_or_default();
name.push(".certgen.tmp");
dir.with_file_name(name)
fn staging_temp_dir(dir: &Path) -> PathBuf {
// Place temp inside dir so rename stays on the same filesystem as the destination.
dir.join(".certgen.tmp")
}

// ────────────────────────────── Shared utility ─────────────────────────────
Expand All @@ -790,7 +788,7 @@ fn print_bundle(bundle: &PkiBundle) {
mod tests {
use super::{
CertSan, K8sAction, LocalAction, LocalPaths, decide_k8s, decide_local, jwt_signing_secret,
missing_required_server_sans, read_local_bundle, sibling_temp_dir, tls_secret,
missing_required_server_sans, read_local_bundle, staging_temp_dir, tls_secret,
write_local_bundle, write_local_jwt_bundle, write_local_tls_bundle,
};
use openshell_bootstrap::pki::generate_pki;
Expand Down Expand Up @@ -902,10 +900,10 @@ mod tests {
}

#[test]
fn sibling_temp_dir_is_adjacent_to_target() {
fn staging_temp_dir_is_inside_target() {
assert_eq!(
sibling_temp_dir(Path::new("/var/lib/openshell/tls")),
Path::new("/var/lib/openshell/tls.certgen.tmp")
staging_temp_dir(Path::new("/var/lib/openshell/tls")),
Path::new("/var/lib/openshell/tls/.certgen.tmp")
);
}

Expand All @@ -922,7 +920,7 @@ mod tests {
assert!(f.is_file(), "missing {}", f.display());
}
assert!(
!sibling_temp_dir(&dir).exists(),
!staging_temp_dir(&dir).exists(),
"temp dir should be cleaned up"
);

Expand Down Expand Up @@ -1032,7 +1030,7 @@ mod tests {
fn write_local_bundle_recovers_from_stale_temp_dir() {
let parent = tempfile::tempdir().expect("tempdir");
let dir = parent.path().join("tls");
let stale = sibling_temp_dir(&dir);
let stale = staging_temp_dir(&dir);
std::fs::create_dir_all(&stale).unwrap();
std::fs::write(stale.join("garbage"), "stale").unwrap();

Expand Down
Loading