Source: projects/identity-management/sandbox-access/README.md

> Source: projects/identity-management/sandbox-access/README.md

Sandbox Access — Project Hub

> Status: v1 — reconciled from Codex round-2 critique 2026-04-25T19:10Z. Build phase begins next.

> History: Claude v0.1 draft (round 1) → Codex critique (round 2) → Claude v1 reconcile (round 3, this doc) → helper build (round 4, in progress).

Defines the canonical access methods to the OIM sandbox VM (im.sandbox.local / 192.168.178.146) so Claude, Codex, and Viktor work through the same documented channels for every OIM project that targets this environment.

Environment definition lives in ../environments/sandbox.md. This project consumes that and adds the access plan + tooling.

Goals

1. Cheapest reliable channel per task. No agent rediscovers the connection pattern each session.

2. Both agents use identical patterns. A SQL helper call looks the same whether Claude or Codex invokes it.

3. No GUI from agents. No Designer / Manager / Sync Editor / RDP. WinRM, SQL, REST, HTTP only.

4. Secrets never on disk in plaintext. Provider chain: SecretStore → DPAPI-encrypted CLIXML → interactive prompt.

5. JSON is the stable contract between helpers and agent tooling; PowerShell objects are a convenience.

6. Lock only on mutation. Reads run free; writes serialize through a workspace lock.

Scope

In scope

Out of scope

Connection channels

ChannelPortBest forAvoid for
WinRM HTTPS5986OS-level ops, PowerShell remoting, AD cmdlets, file read/write, log tailing, installersBulk file transfer (use SMB)
WinRM HTTP5985Fallback when HTTPS cert is fussyAnything where HTTPS works
MS SQL TCP1433Direct queries from local Microsoft.Data.SqlClient / sqlcmd if installedHeavy ad-hoc work — wrap in helpers
OIM REST API443 (HTTPS)OIM business operations under /AppServer/api/... (preferred over direct DML)UI screen-scraping
OIM Web Portal (QER)443 (HTTPS)Visual verification by Viktor only — /ApiServer/html/qer-app-portal/...Agent automation
Mailpit HTTP API18025/api/v1/messages — verify notificationsSending mail (use OIM)
Grafana HTTP API3000Dashboard JSON, alert stateMutating dashboards
Prometheus HTTP API9090/api/v1/query — VM and OIM metricsLong-term storage queries
windows_exporter9182/metrics raw if Prometheus is unreachableDirect querying — go via Prometheus
SMB admin share445\\im.sandbox.local\C$\... — bulk file transfer, large readsTiny one-line reads (WinRM is fine)
OIM Job Server1880TCP reachability ping onlyDirect calls — go via OIM API
OIM Sync Engine2880TCP reachability ping onlyDirect calls — go via OIM API
OpenDJ LDAP(verify on first connect)LDAP bind via ldapsearch over WinRM, System.DirectoryServices.Protocols locallyAnything before ports are confirmed
RDP3389Viktor only. Last-resort GUI tools (Designer, Manager, Sync Editor)All agent work

> Endpoint discovery: Test-SandboxConnectivity.ps1 probes each channel on first run, records the working base URL(s), auth module, and OpenDJ ports to scripts/sandbox/.discovered.json (gitignored). Subsequent helpers consult that file rather than re-probing.

Access matrix — by data class

OIM database (read)

OIM database (write / DML)

HRSystem database

Active Directory

OpenDJ LDAP

Files on the VM (read / review)

Files on the VM (push)

Windows event log

OIM logs (NLog files)

Mailpit messages

Prometheus metrics / Grafana dashboards

OIM job/process state

OIM admin tools on the host (CLI / headless)

Package / transport handling (OIM deployments)

Service / process control

Local .NET fallbacks (when WinRM/local-tool combinations fail)

Decision framework

For any sandbox task, walk this list top-to-bottom and stop at the first match:

1. Read-only inspection → HTTP API (Mailpit/Grafana/Prometheus) → SQL SELECT → WinRM Get-* → SMB read

2. State change with an OIM API path → OIM REST (/AppServer/api/...)

3. State change with no API path → SQL DML helper with -IUnderstandDirectOimDml -Reason "<text>" (auto-journals)

4. Bulk file transfer → SMB UNC under Invoke-WithSandboxLock

5. One-shot file read/edit → WinRM Get-Content / Set-Content (write under lock)

6. Diagnostics / troubleshooting → standard triage order below

7. Service restart → only if the task requires it; journal pre/post

8. Installation / package import → headless path → Viktor Action Request if GUI-only

9. GUI-only operation → Viktor Action Request; never RDP from agent

Standard troubleshooting triage order

Test-SandboxConnectivityGet-OimHealth together emit a compact health object that other helpers reuse and that gets attached to journal summaries:

1. Connectivity — every channel green? (Test-SandboxConnectivity)

2. Credentials / auth — does each role's PSCredential still bind?

3. Service health — OIM Job Service, App Server, Sync Service, IIS, MS SQL all running?

4. DB / Job queueDBQueueTask and JobQueue depth and oldest entry age

5. Windows event log — recent errors in Application + System

6. OIM logs — recent NLog errors per service

7. Mailpit / Prometheus evidence — did the expected mail/metric actually appear?

Stop at the first failing layer and address it before moving on.

Secret provider abstraction

Scripts never read passwords from Markdown and never embed them in .ps1 files. All credential access goes through Get-SandboxCredential -Role <name>.

Provider chain (in order)

1. PowerShell SecretManagement / SecretStore (Microsoft.PowerShell.SecretManagement + Microsoft.PowerShell.SecretStore) — preferred; encrypted-at-rest with master password unlocked once per session.

2. DPAPI-encrypted CLIXML fallbackExport-Clixml of PSCredential objects writes blobs encrypted to the current Windows user. Path: $env:USERPROFILE\.oim-sandbox\credentials\<role>.clixml (gitignored, machine + user scoped).

3. Interactive promptGet-Credential as last resort when neither store has the role yet; prompts to persist into store on success.

Bootstrap

Initialize-SandboxSecrets.ps1 is run once by Viktor:

The .local.psd1 is bootstrap-only — never the runtime source.

Schema (bootstrap file, not committed)

@{
    Host    = 'im.sandbox.local'
    Domain  = 'sandbox.local'
    Roles = @{
        AdAdmin              = @{ Username = 'sandbox\administrator'; Password = '<placeholder>' }
        AdServiceOim         = @{ Username = 'sandbox\ser_oim';       Password = '<placeholder>' }
        SqlSa                = @{ Username = 'sa';                    Password = '<placeholder>' }
        OimViadmin           = @{ Username = 'viadmin';               Password = '<placeholder>' }
        LdapDirectoryManager = @{ Username = 'cn=Directory Manager';  Password = '<placeholder>' }
        GrafanaAdmin         = @{ Username = 'admin';                 Password = '<placeholder>' }
    }
}

Real values live in ../environments/sandbox-credentials.local.md (gitignored, human-only inventory) and are pulled into the bootstrap psd1 by Viktor when initializing or rotating secrets.

Output contract

Every helper supports a uniform output mode:

FlagBehavior
(default)PowerShell objects on the success stream — convenient for piping in PS
-JsonCompact JSON on stdout — the stable contract for agents
-RawWhatever the underlying tool emits (e.g., Get-Content text), passed through

Concurrency model

Both agents may hit the sandbox simultaneously. Lock granularity: mutation only.

Free (no lock)

Locked

Mechanism

Invoke-WithSandboxLock -Operation <label> -ScriptBlock { ... }:

SQL DML safety

Direct DML against the OIM database is the riskiest sandbox operation an agent can take. Guardrails:

GuardrailBehavior
-IUnderstandDirectOimDml switchRequired for any non-SELECT against the OIM DB. Deliberately verbose so it can't be typed accidentally.
-Reason "<text>"Required string; logged to journal alongside the SQL.
Pre-change snapshotHelper runs the user-supplied -VerifyQuery (or auto-derives a SELECT against the affected table) and stores the result.
TransactionAll DML wrapped in BEGIN TRAN ... COMMIT (or rollback on error).
Post-change verificationSame -VerifyQuery runs after commit; pre/post diff included in the helper output.
Auto-journalOne-line journal entry written automatically: agent, timestamp, table(s) touched, reason, row counts.

HRSystem DML uses transactions but skips the -IUnderstand* switch — that database exists to be mutated.

Viktor Action Request template

When a task genuinely needs RDP / a GUI tool / an interactive step the agent can't perform, file a structured request rather than handing off ad-hoc. Append to ../../../coordination/viktor-action-requests.md (file created on first request):

## YYYY-MM-DDTHH:MMZ · from <agent>

**Task:** <one-line goal>
**Why GUI is required:** <which tool, what step, why no headless path exists>
**Host / tool:** <e.g., im.sandbox.local → Synchronization Editor>
**Steps:** <numbered list of the operations Viktor will perform>
**Expected outcome:** <what should be true after>
**Verification command:** `pwsh -NoProfile -File scripts/sandbox/<helper>.ps1 ...` — agent runs this after Viktor confirms done
**Status:** open

Agent waits for a Status: done line from Viktor, then runs the verification command and journals.

Conventions for both agents

1. Read before write. Always SELECT / Get-* before any DML / Set-*.

2. Use OIM REST for state changes when an API path exists. Direct SQL DML only via the guarded helper.

3. No RDP. GUI work goes through Viktor Action Request.

4. Keep the PSSession warm within a task, tear down at end. Don't pool across sessions.

5. No hardcoded credentials. All scripts go through Get-SandboxCredential.

6. No new tool without updating this doc. If you reach for psql, wmic, or a fresh utility, add a row to the matrix first.

7. JSON for inter-tool calls. Pass -Json when consuming helper output programmatically.

8. Lock mutations. Wrap every state-changing call in Invoke-WithSandboxLock.

9. Journal sandbox-touching commits with sandbox in the Scope: line so the other agent notices.

10. Invocation pattern: pwsh -NoProfile -File scripts/sandbox/<helper>.ps1 .... No Bash wrappers — single uniform path.

Sanity check (Test-SandboxConnectivity.ps1)

First helper to build. Verifies and records:

Output: structured health object (-Json for agent consumption); writes scripts/sandbox/.discovered.json (gitignored) with discovered URLs / ports / paths. Exit non-zero if any required channel fails — agents stop and ask Viktor.

Planned helpers (build order)

Codex's recommended order, accepted:

#ScriptPurpose
1Get-SandboxCredential.ps1Provider-chain credential resolver (SecretStore → DPAPI CLIXML → prompt) + Initialize-SandboxSecrets.ps1 bootstrap
2Test-SandboxConnectivity.ps1Sanity-check + endpoint discovery
3Invoke-SandboxSql.ps1SQL helper: read-only default, transactions, JSON output, DML guardrails
4Invoke-WithSandboxLock.ps1Mutation lock wrapper
5Get-OimHealth.ps1DBQueue/JobQueue depth + service health + log pointers
6Get-MailpitMessages.ps1HTTP API wrapper
7Invoke-PrometheusQuery.ps1PromQL wrapper

Then as needed:

Location: scripts/sandbox/ at workspace root. Each .ps1 is a thin command wrapper; if shared internals grow past ~200 LOC they get promoted to scripts/sandbox/SandboxAccess.psm1 (v2 refactor).

References

Round-3 status (this round)

Round-4 plan

Build helpers in the order above. After each, run Test-SandboxConnectivity (or the relevant subset) and journal results. Update this README when the helper's interface stabilizes.

Cross-references