Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .agents/schemas/evidence.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
"definitions": {
"probeRecord": {
"type": "object",
"description": "A single probe step. Field shapes for 'observation' and 'destructiveDetails' are placeholders pending a real probing run; populated structures land in Phase 3 / Phase 5.",
"description": "A single probe step. Field shapes for 'observation' and 'destructiveDetails' are placeholders pending a real probing run; populated structures will land once the SSH and HTTP playbooks are authored against real targets.",
"required": ["id", "kind", "command", "result"],
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -191,7 +191,7 @@
"enum": ["ok", "failed", "skipped", "halted"]
},
"observation": {
"description": "TODO: structured observation payload. Shape locked down in Phase 3 once the SSH and HTTP playbooks are authored against real targets. Free-form for now.",
"description": "TODO: structured observation payload. Shape will be locked down once the SSH and HTTP playbooks are authored against real targets. Free-form for now.",
"type": "object",
"additionalProperties": true
},
Expand Down
12 changes: 6 additions & 6 deletions .agents/skills/safeguard-ps-operations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ It is the **only** skill that directly calls `safeguard-ps`. Other skills reques

Every cmdlet, parameter name, and parameter-set described to the operator MUST come from `Get-Help <Cmdlet> -Full` against the **installed** `safeguard-ps` module. Do not paraphrase from memory, vendor docs, or prior conversations.

Before invoking any cmdlet you have not used in the current voyage, the agent runs `Get-Help` itself — do not ask the operator to run it and paste output back:
Before invoking any cmdlet you have not used in the current session, the agent runs `Get-Help` itself — do not ask the operator to run it and paste output back:

```powershell
Get-Help <Cmdlet> -Full | Out-String -Width 200
Expand All @@ -50,7 +50,7 @@ Get-Help <Cmdlet> -Full | Out-String -Width 200

If a cmdlet's parameter is a `[switch]` and the value comes from a variable, use the colon form (`-Insecure:$ins`, not `-Insecure $ins`). PowerShell silently swallows the latter on switches and the cmdlet ends up with the parameter's default — usually `$false` — which is rarely what the agent wanted. The value bound to the colon must be a `[bool]`.

Sibling cmdlets are not symmetric. `Test-` and `Invoke-` pairs diverge in parameter names; `New-SafeguardCustomPlatform` accepts `-ScriptFile` directly with no separate `Import-` step; `Get-SafeguardTaskLog` with no arguments returns a flat GUID array and only returns `{Recorded, Level, Event}` records when given `-TaskId <guid>`. Run `Get-Help` on every cmdlet's first use in the voyage, even when a sibling was just used.
Sibling cmdlets are not symmetric. `Test-` and `Invoke-` pairs diverge in parameter names; `New-SafeguardCustomPlatform` accepts `-ScriptFile` directly with no separate `Import-` step; `Get-SafeguardTaskLog` with no arguments returns a flat GUID array and only returns `{Recorded, Level, Event}` records when given `-TaskId <guid>`. Run `Get-Help` on every cmdlet's first use in the session, even when a sibling was just used.

## Authentication

Expand Down Expand Up @@ -110,11 +110,11 @@ Do not pre-ask whether the appliance has a valid certificate. Try secure; the er

### Persist the session across iterations — serialize the token, never keep a long-running shell

**Login budget = 1 per voyage.** Each `Connect-Safeguard -DeviceCode` (or `-Browser`) costs the operator real time and attention. Connect exactly once.
**Login budget = 1 per session.** Each `Connect-Safeguard -DeviceCode` (or `-Browser`) costs the operator real time and attention. Connect exactly once.

**Long-running interactive PowerShell sessions are banned in agent flows.** They wedge on PSReadLine prediction, swallow `$ConfirmPreference` prompts, return stale back-buffer through `read_powershell`, and routinely cost a re-login when the agent has to kill them. Do not start a persistent shell to hold `$Global:SafeguardSession`. Do not invoke `Connect-Safeguard` as an async command kept alive across iterations.

The only correct shape is short-lived sync `powershell -Command { ... }` calls. `$Global:SafeguardSession` holds **a short-lived bearer token, not a permanent credential** — valid for the rest of the voyage (typically several hours), safe to serialize to the gitignored per-session state directory, expires on its own.
The only correct shape is short-lived sync `powershell -Command { ... }` calls. `$Global:SafeguardSession` holds **a short-lived bearer token, not a permanent credential** — valid for the rest of the session (typically several hours), safe to serialize to the gitignored per-session state directory, expires on its own.

**Step 1 — connect once and serialize, in the same process.** The connect call and the serialization step **must run in the same PowerShell process**: `$Global:SafeguardSession` dies with the process that called `Connect-Safeguard`, so a "connect in shell A, serialize in shell B" split silently throws the token away and burns a login.

Expand Down Expand Up @@ -153,9 +153,9 @@ Two superficially-simpler shortcuts do **not** work. Documented here so the next

If a future cmdlet is found that **only** reads the session variable and refuses `-AccessToken`, the correct response is to file a defect against safeguard-ps to add the parameter set — not to spin up a long-running shell to host the cmdlet.

Treat `sg-session.json` like any other secret: write it only under the per-session state directory, never commit it, never paste it into chat or task-log output. Delete it at the end of the voyage. The bearer token redacts itself naturally on expiry; a stale file cannot be used to attack the appliance later. Never log `$Global:SafeguardSession`, the access token, or any password parameter to operator-visible output.
Treat `sg-session.json` like any other secret: write it only under the per-session state directory, never commit it, never paste it into chat or task-log output. Delete it at the end of the session. The bearer token redacts itself naturally on expiry; a stale file cannot be used to attack the appliance later. Never log `$Global:SafeguardSession`, the access token, or any password parameter to operator-visible output.

If the agent finds itself about to call `Connect-Safeguard` a second time in the same voyage, **stop**. The token in `sg-session.json` is still good unless the operator rebooted the appliance or several hours have passed; re-read it. A second login is a defect, not a workaround.
If the agent finds itself about to call `Connect-Safeguard` a second time in the same session, **stop**. The token in `sg-session.json` is still good unless the operator rebooted the appliance or several hours have passed; re-read it. A second login is a defect, not a workaround.

This pattern is verified in [`tools/README.md`](../../../tools/README.md) ("Authentication" section). `tools/Invoke-PlatformDevLoop.ps1` itself does not call `Connect-Safeguard`; the operator connects once and the wrapper picks up `$Global:SafeguardSession` (when invoked from a session that has it cached) or `-AccessToken` plumbed through.

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/script-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ The script shape is the same regardless of auth scheme: `BaseAddress` → `NewHt

#### Pre-flight: rules every `Request` block must satisfy

These four rules each cost a real iteration on a prior voyage when violated. Check every `Request` block — including token-refresh and login calls inside helper functions — against all four before treating the draft as ready for local schema validation:
These four rules each cost a real iteration when violated in prior testing. Check every `Request` block — including token-refresh and login calls inside helper functions — against all four before treating the draft as ready for local schema validation:

1. **TLS skip is per-`Request`, not per-platform.** Declare the **reserved parameter** `SkipServerCertValidation` (`Type: Boolean`, `DefaultValue: false`) in every operation's `Parameters` and in any function `Parameters` array that issues a `Request`, then set `"IgnoreServerCertAuthentication": "%{SkipServerCertValidation}%"` on **every** `Request` block. SPP auto-sources the value from the asset's `VerifySslCertificate` flag — no `-CustomScriptParameters` plumbing at onboarding. Missing it on even one block (token refresh is the common miss) re-introduces TLS failure on that call only.
2. **Form bodies use `SetFormValue` + `Content.ContentObjectName`.** Never `Content.Value`. `Content.Value` is undocumented and the engine re-encodes — `%40` becomes `%2540`, `+` and `=` may be dropped — producing 400 BadRequest from targets that accept the identical body when sent manually. Mirror [`samples/http/twitter/CustomTwitter.json`](../../../samples/http/twitter/CustomTwitter.json) lines 133–148.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The agent-facing index of every sample and template lives at [`docs/agent-refere
Use this workflow when the operator's request is to build a custom platform that does not yet exist in the appliance.

1. **Gather requirements.** Classify intent (new vs enhance — this workflow is *new*), then collect what is missing:
- **Platform display name** the operator wants the new platform to use on the appliance (e.g., `My Custom Linux`). **Required pre-condition: do not hand off to `strategy-selection` or draft any JSON until the operator has supplied this verbatim.** If unspecified, ask before proceeding — never invent a name from the protocol, vendor, or strategy choice. Renaming a platform mid-voyage costs a re-import.
- **Platform display name** the operator wants the new platform to use on the appliance (e.g., `My Custom Linux`). **Required pre-condition: do not hand off to `strategy-selection` or draft any JSON until the operator has supplied this verbatim.** If unspecified, ask before proceeding — never invent a name from the protocol, vendor, or strategy choice. Renaming a platform mid-development costs a re-import.
- Target system (vendor, product, version) and protocol (SSH or HTTP — telnet is out of scope).
- **Operations needed.** SPP defines parallel operation families for each credential type: password (`CheckSystem`, `CheckPassword`, `ChangePassword`), SSH key (`CheckSshKey`, `ChangeSshKey`), API key (`CheckApiKey`, `ChangeApiKey`, `DiscoverApiKeys`), and file (`CheckFile`, `ChangeFile`), plus discovery operations (`DiscoverAccounts`, `DiscoverServices`, `DiscoverAssets`, `DiscoverSshHostKey`, `DiscoverAuthorizedKeys`). **Required pre-condition: do not hand off to `strategy-selection` or draft any JSON until the operator has confirmed the operation set verbatim.** Pick from the family that matches the credential intent — an API-key-only platform implements `CheckApiKey`/`ChangeApiKey` and nothing from the password family. Within a family, do not assume the full set: some platforms ship `Check*`-only at first, some skip `CheckSystem`, and every discovery operation requires an explicit yes. Each operation drafted but not requested is wasted iteration budget.
- **Credential intent** — self-managed (the managed account rotates its own password) vs service-account (a separate account rotates the managed one).
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Human-facing documentation lives in `docs/concepts/`, `docs/guides/`, `docs/tuto
| --- | --- | --- |
| [`samples-index.md`](samples-index.md) | Normalized index of every sample and template (protocol, auth-scheme, operations, OS-family, file-path, README). | **Generated** by `tools/Build-SamplesIndex.ps1`. CI runs the same script with `-CheckOnly` and fails if the committed copy is stale. |
| [`strategy-decision-tree.md`](strategy-decision-tree.md) | Decision table that backs the `strategy-selection` skill (SSH and HTTP only). | Hand-maintained from `docs/guides/`. SSH and HTTP only. |
| [`failure-patterns.md`](failure-patterns.md) | Error-signature → likely cause → fix catalog used by `task-log-analysis`. | **Empty in Phase 1.** Rows are populated from real extended task logs in Phase 5/F. Invented rows are not acceptable. |
| [`failure-patterns.md`](failure-patterns.md) | Error-signature → likely cause → fix catalog used by `task-log-analysis`. | **Initially empty.** Rows are populated from real extended task logs as failures are encountered. Invented rows are not acceptable. |
| [`vendor-doc-search-recipes.md`](vendor-doc-search-recipes.md) | Query templates for fetching vendor docs and a normalization recipe for pasted vendor-doc excerpts. | Hand-maintained. |

## Related contracts
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-reference/samples-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ CI runs the same script with `-CheckOnly` and fails the build if the committed c
- **protocol** — derived from the directory (`samples/ssh/`, `samples/http/`) or the template filename (`Pattern-GenericLinux*` / `Pattern-WindowsSsh*` / `TemplateSsh*` → ssh; `Pattern-GenericHttp*` / `Pattern-GenericRestApi*` / `TemplateHttp*` → http).
- **auth-scheme** — best-effort from JSON content. HTTP: from `HttpAuth.Type`, an `Authorization: Bearer` header, an API-key-shaped custom header, or `ExtractFormData`. SSH: `Interactive` (Send/Receive), `Batch` (ExecuteCommand), or `Mixed`. Blank when undetermined.
- **operations** — intersection of top-level keys with the canonical operation list from `schema/custom-platform-script.schema.json`. Imports and user-defined functions never appear here.
- **OS-family** — intentionally blank. Phase 1 prefers blank over guessed values; revisit in a later phase if needed.
- **OS-family** — intentionally blank. The build script prefers blank over guessed values; revisit once a reliable detection heuristic exists.
- **file-path** and **README** — filesystem facts. `—` means the field could not be determined.
- `samples/telnet/` is excluded — telnet is out of scope for the agent skill system. The samples remain in the repo for human reference.

Expand Down
2 changes: 1 addition & 1 deletion samples/http/proxmox-ve-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ No custom parameters. The full credential set comes from reserved parameters aut
## Limitations

- **`@pve` realm only.** Users in `@pam` (the host's OS PAM stack) cannot be rotated via the Proxmox API regardless of API-level privileges — the Proxmox API call returns an error directing the caller to use `passwd` on the host. Rotating `@pam` users requires SSH-to-the-host + `sudo passwd <user>`, which is a separate platform (not yet shipped).
- **Both self-managed and service-account modes are voyage-tested.** In service-account mode a privileged user (e.g. holding `PVEUserAdmin` on `/access/realm/pve`) rotates another `@pve` user's password. In self-managed mode the account rotates its own password; Proxmox always allows users to change their own password, so the `Realm.AllocateUser` privilege requirement does not apply. The same script handles both — SPP supplies the credentials such that `%FuncUserName%`/`%FuncPassword%` and `%AccountUserName%`/`%AccountPassword%` resolve to the same identity in self-managed mode, and the API accepts the `confirmation-password` field as benign on a same-user change.
- **Both self-managed and service-account modes are tested live against an appliance.** In service-account mode a privileged user (e.g. holding `PVEUserAdmin` on `/access/realm/pve`) rotates another `@pve` user's password. In self-managed mode the account rotates its own password; Proxmox always allows users to change their own password, so the `Realm.AllocateUser` privilege requirement does not apply. The same script handles both — SPP supplies the credentials such that `%FuncUserName%`/`%FuncPassword%` and `%AccountUserName%`/`%AccountPassword%` resolve to the same identity in self-managed mode, and the API accepts the `confirmation-password` field as benign on a same-user change.
- **Ticket lifetime is ~2 hours.** Each operation fetches a fresh ticket; the script does not persist tickets across SPP operations. Self-rotation invalidates the ticket held during the change, but since the next operation re-authenticates, this is not observable.
- **`401 Unauthorized` responses do not carry `WWW-Authenticate`.** Proxmox does not advertise the auth scheme on failure, which means generic HTTP debugging tools may misclassify the failure mode. The script does not rely on the header.
- **No support for `DiscoverAccounts`.** The script does not enumerate Proxmox users; the operator adds managed accounts explicitly.
Expand Down
4 changes: 2 additions & 2 deletions tools/Build-SamplesIndex.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
SSH: "Interactive" if Send/Receive appear, "Batch" if
ExecuteCommand appears without Send/Receive, "Mixed" if
both, else blank.
- OS-family is left blank by the build script. Phase 1 prefers
- OS-family is left blank by the build script. The script prefers
"blank" over "guess"; OS family is hard to ground from JSON alone
and the sample README's heading text isn't structured. Future
phases may revisit.
Expand Down Expand Up @@ -284,7 +284,7 @@ CI runs the same script with `-CheckOnly` and fails the build if the committed c
- **protocol** — derived from the directory (`samples/ssh/`, `samples/http/`) or the template filename (`Pattern-GenericLinux*` / `Pattern-WindowsSsh*` / `TemplateSsh*` → ssh; `Pattern-GenericHttp*` / `Pattern-GenericRestApi*` / `TemplateHttp*` → http).
- **auth-scheme** — best-effort from JSON content. HTTP: from `HttpAuth.Type`, an `Authorization: Bearer` header, an API-key-shaped custom header, or `ExtractFormData`. SSH: `Interactive` (Send/Receive), `Batch` (ExecuteCommand), or `Mixed`. Blank when undetermined.
- **operations** — intersection of top-level keys with the canonical operation list from `schema/custom-platform-script.schema.json`. Imports and user-defined functions never appear here.
- **OS-family** — intentionally blank. Phase 1 prefers blank over guessed values; revisit in a later phase if needed.
- **OS-family** — intentionally blank. The build script prefers blank over guessed values; revisit once a reliable detection heuristic exists.
- **file-path** and **README** — filesystem facts. `—` means the field could not be determined.
- `samples/telnet/` is excluded — telnet is out of scope for the agent skill system. The samples remain in the repo for human reference.

Expand Down
2 changes: 1 addition & 1 deletion tools/Invoke-PlatformDevLoop.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Run the custom-platform dev loop: validate -> import -> trigger -> fetch task lo
.DESCRIPTION
Wraps the agent-facing iterative loop into a single call that emits a single
structured JSON document on stdout and exits with a phase-indexed exit code.
Designed to be called by the safeguard-ps-operations agent skill (Phase 3) and
Designed to be called by the safeguard-ps-operations agent skill and
by humans during day-to-day authoring.

Four modes, selected by mutually-exclusive switches:
Expand Down
2 changes: 1 addition & 1 deletion tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Tooling for the SafeguardCustomPlatform repo.
| `TestTool.ps1` | Original human-facing upload + trigger script. Edit-in-place script with hard-coded variables. | Humans |
| `Build-SamplesIndex.ps1` | Regenerates `docs/agent-reference/samples-index.md` from `samples/` and `templates/`. | CI + agents |
| `Test-AgentLinks.ps1` | Validates relative links in `AGENTS.md` and `.agents/skills/*/SKILL.md` against `docs/agent-reference/`. | CI |
| `Invoke-PlatformDevLoop.ps1` | Structured dev-loop wrapper: validate → import → trigger → fetch task log. JSON output, phase-indexed exit codes. | Agents (Phase 3 `safeguard-ps-operations` skill) and humans |
| `Invoke-PlatformDevLoop.ps1` | Structured dev-loop wrapper: validate → import → trigger → fetch task log. JSON output, phase-indexed exit codes. | Agents (the `safeguard-ps-operations` skill) and humans |

The remainder of this document covers `Invoke-PlatformDevLoop.ps1`.

Expand Down
2 changes: 1 addition & 1 deletion tools/Test-AgentLinks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- Paths are resolved relative to the markdown file that contains
the link.
- In-page anchors (links starting with '#') are skipped — anchor
validity is not in scope for Phase 1.
validity is not currently in scope.

The script exits 0 on success, 1 on any broken link.

Expand Down
Loading