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
- Connection-channel inventory
- Access matrix per data class (OIM DB, HRSystem, AD, OpenDJ LDAP, files, Windows event log, OIM logs, Mailpit, Prometheus/Grafana, OIM job state, OIM admin tools, packages, services, installations)
- Decision framework + standard troubleshooting triage order
- Secret provider abstraction (
Get-SandboxCredential) - Output contract (
-Jsonflag, stderr for diagnostics) - Concurrency / lock model (
Invoke-WithSandboxLock) - Helper script interfaces and build order
- "Viktor action request" template for GUI-only tasks
- Conventions both agents must follow
Out of scope
- OIM-internal customization design (separate projects)
- Sandbox VM administration (Viktor)
- Production hardening (sandbox only)
- v2 refactor into a
.psm1module — only do this if helper count > ~10
Connection channels
| Channel | Port | Best for | Avoid for |
|---|---|---|---|
| WinRM HTTPS | 5986 | OS-level ops, PowerShell remoting, AD cmdlets, file read/write, log tailing, installers | Bulk file transfer (use SMB) |
| WinRM HTTP | 5985 | Fallback when HTTPS cert is fussy | Anything where HTTPS works |
| MS SQL TCP | 1433 | Direct queries from local Microsoft.Data.SqlClient / sqlcmd if installed | Heavy ad-hoc work — wrap in helpers |
| OIM REST API | 443 (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 API | 18025 | /api/v1/messages — verify notifications | Sending mail (use OIM) |
| Grafana HTTP API | 3000 | Dashboard JSON, alert state | Mutating dashboards |
| Prometheus HTTP API | 9090 | /api/v1/query — VM and OIM metrics | Long-term storage queries |
| windows_exporter | 9182 | /metrics raw if Prometheus is unreachable | Direct querying — go via Prometheus |
| SMB admin share | 445 | \\im.sandbox.local\C$\... — bulk file transfer, large reads | Tiny one-line reads (WinRM is fine) |
| OIM Job Server | 1880 | TCP reachability ping only | Direct calls — go via OIM API |
| OIM Sync Engine | 2880 | TCP reachability ping only | Direct calls — go via OIM API |
| OpenDJ LDAP | (verify on first connect) | LDAP bind via ldapsearch over WinRM, System.DirectoryServices.Protocols locally | Anything before ports are confirmed |
| RDP | 3389 | Viktor 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)
- Primary:
Invoke-SandboxSql -Database OIM -Query <sql>(WinRM →Invoke-Sqlcmdon the VM, read-only by default) - Fallback: local
Microsoft.Data.SqlClient/sqlcmdagainst1433 - Notes: Object IDs, DBQueue depth, process state, audit history all live here.
OIM database (write / DML)
- Primary: OIM REST API (
/AppServer/api/...) — preserves business logic, triggers events - Fallback:
Invoke-SandboxSql -Database OIM -DmlMode -IUnderstandDirectOimDml -Reason "<text>" -Query <sql>— sandbox-only escape hatch with required reason; helper wraps in transaction, captures pre- and post-change snapshot, and journals the event automatically - Rule: Direct DML is the rare exception, not the default. If you reach for it twice in one session, ask why no API path exists.
HRSystem database
- Primary:
Invoke-SandboxSql -Database HRSystem -Query <sql>; DML allowed by default but still wrapped in a transaction - Notes: This is the simulated source-of-truth for HR sync tests; mutating it is the point.
Active Directory
- Primary:
Invoke-Commandover WinRM runningGet-ADUser/Set-ADUser/New-ADUser(RSAT on the VM) - Fallback: local
System.DirectoryServices.Protocols.LdapConnectionagainst the DC - Notes: AD changes are visible to OIM target-system sync — useful for testing inbound flow.
OpenDJ LDAP
- Primary:
ldapsearch/ldapmodifyfrom OpenDJ install on the VM, invoked over WinRM - Fallback: local
System.DirectoryServices.Protocols.LdapConnection - Notes: Bind DN
cn=Directory Manager. Ports verified on first connect byTest-SandboxConnectivity.ps1and recorded in.discovered.json.
Files on the VM (read / review)
- Primary:
Invoke-Command { Get-Content -Path <p> -Tail <N> }over WinRM for small/recent reads - Bulk:
\\im.sandbox.local\C$\<path>UNC for entire files (paging viaReadworks) - Common review targets:
C:\Dev\OneIdentityManager.10.0\— install media, SDKC:\Dev\OneIM10.0.0-MDK\MDK\— module developer kit + Developer Guide PDFC:\Program Files\One Identity\— installed components (exact path discovered on first connect)- OIM service log directories — discovered on first connect, recorded here once known
C:\inetpub\logs\LogFiles\— IIS logs
Files on the VM (push)
- Primary:
Copy-Item -ToSession <ps> -Path <local> -Destination <remote> - Bulk:
robocopy <local> \\im.sandbox.local\C$\<remote>for large trees - Rule: Always
Test-Paththe remote dir first; create withInvoke-Commandif missing. Pushes are mutations — wrap inInvoke-WithSandboxLock.
Windows event log
- Primary:
Invoke-Command { Get-WinEvent -LogName <log> -MaxEvents <N> -FilterHashtable @{...} } - Notes: Filter by event id and provider; never dump full logs over WinRM.
OIM logs (NLog files)
- Primary:
Get-OimLogTail -Service <jobservice|appserver|syncservice> -Lines <N>(WinRMGet-Content -Tail) - Discovery: first run enumerates likely paths and pins them in
.discovered.json - Notes: OIM uses NLog; layouts vary per service. Default tail = 200 lines.
Mailpit messages
- Primary:
Get-MailpitMessages -Last <N>→ HTTPGET http://im.sandbox.local:18025/api/v1/messages - Notes: Verify that OIM notification processes actually rendered + sent mail.
Prometheus metrics / Grafana dashboards
- Primary:
Invoke-RestMethod 'http://im.sandbox.local:9090/api/v1/query?query=<promql>' - Dashboard view: Grafana URL handed to Viktor — agents don't render images
- Notes: Default first-stop for VM health questions.
OIM job/process state
- Primary: SQL —
DBQueueTask,JobQueue,Process,JobHistorytables (read) - Helper:
Get-OimHealthaggregates queue depth + service status + recent log pointers in one structured object - Mutations: never directly poke these tables — go through the OIM API
OIM admin tools on the host (CLI / headless)
- Primary: invoke headless tools over WinRM where they support it — DBCompiler, Software Loader, Transporter (verify each on first connect; some are GUI-only)
- Inventory: record CLI-capable tools and their invocation patterns in
scripts/sandbox/.discovered.jsonafter first inspection - Rule: never invoke a GUI-only tool from an agent. If the operation requires Designer/Manager/Sync Editor, file a Viktor Action Request.
Package / transport handling (OIM deployments)
- Primary:
Sync-FilesToSandbox→ push package to a known staging path → run silent installer / Software Loader headless via WinRM → verify with the relevant API/SQL check - Fallback: Viktor Action Request when no headless path exists for a given package
- Rule: every package install gets a journal entry with the package name, target component, and verification result.
Service / process control
- Primary:
Invoke-Command { Get-Service <name> }for status;Restart-Service <name>only when the task explicitly requires it - Rule: every service restart is journaled (service name, reason, pre/post status). Never restart a service speculatively.
Local .NET fallbacks (when WinRM/local-tool combinations fail)
- SQL:
Microsoft.Data.SqlClient.SqlConnectionagainst1433 - LDAP:
System.DirectoryServices.Protocols.LdapConnectionagainst AD or OpenDJ - HTTP:
Invoke-RestMethodis fine; for cookie/auth flows the API helper handles session reuse
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-SandboxConnectivity → Get-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 queue — DBQueueTask 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 fallback — Export-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 prompt — Get-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:
- Reads a temporary local
.local.psd1with role → password mappings (or prompts interactively per role) - Writes each role into the chosen provider
- Deletes the bootstrap file on success
- Verifies by reading each role back
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:
| Flag | Behavior |
|---|---|
| (default) | PowerShell objects on the success stream — convenient for piping in PS |
-Json | Compact JSON on stdout — the stable contract for agents |
-Raw | Whatever the underlying tool emits (e.g., Get-Content text), passed through |
- Diagnostics, progress, and warnings always go to stderr (
Write-Error/Write-Warning), never to stdout. - JSON shape per helper is documented inline in the helper and stable across versions; breaking changes bump a
_vfield. - Agents calling helpers from non-PowerShell contexts pass
-Jsonand parse stdout.
Concurrency model
Both agents may hit the sandbox simultaneously. Lock granularity: mutation only.
Free (no lock)
- Connectivity probes
- SQL SELECT
- HTTP API GET (Mailpit, Grafana, Prometheus)
- WinRM
Get-*, log tails - SMB read
Locked
- SQL DML on either database
- OIM REST mutations
- AD / LDAP modifications
- File pushes
- Service restarts
- Package installations
Mechanism
Invoke-WithSandboxLock -Operation <label> -ScriptBlock { ... }:
- Creates
<workspace-root>\.sandbox-lock(derived from script location or-WorkspaceRootparam) with{ agent, pid, started, operation, target, expiresAt }JSON - Waits up to 5 minutes for an existing lock to clear; on timeout, prints the holder info and exits non-zero (no force-release)
- Stale lock detection: lock file with
expiresAtin the past is auto-released with a warning logged - Releases on script-block exit (success or failure)
SQL DML safety
Direct DML against the OIM database is the riskiest sandbox operation an agent can take. Guardrails:
| Guardrail | Behavior |
|---|---|
-IUnderstandDirectOimDml switch | Required 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 snapshot | Helper runs the user-supplied -VerifyQuery (or auto-derives a SELECT against the affected table) and stores the result. |
| Transaction | All DML wrapped in BEGIN TRAN ... COMMIT (or rollback on error). |
| Post-change verification | Same -VerifyQuery runs after commit; pre/post diff included in the helper output. |
| Auto-journal | One-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:
- WinRM HTTPS reachable;
sandbox\administrator,sandbox\ser_oimcredentials bind - SQL
master.sys.databasesquery succeeds usingsalogin against SQL Server (separate auth from WinRM) - OIM REST auth:
viadminagainst discovered REST base URL - LDAP bind:
cn=Directory Manageragainst discovered OpenDJ port - SQL
master.sys.databasesquery succeeds - HTTP HEAD on AppServer / Web Portal / Mailpit / Grafana / Prometheus
- AD
Get-ADDomainworks through WinRM - OpenDJ LDAP bind succeeds (and records the working port)
- TCP reachability for Job Server (1880) and Sync Engine (2880)
- OIM REST base URL discovery: probes
/AppServer/api/...paths and records the working one
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:
| # | Script | Purpose |
|---|---|---|
| 1 | Get-SandboxCredential.ps1 | Provider-chain credential resolver (SecretStore → DPAPI CLIXML → prompt) + Initialize-SandboxSecrets.ps1 bootstrap |
| 2 | Test-SandboxConnectivity.ps1 | Sanity-check + endpoint discovery |
| 3 | Invoke-SandboxSql.ps1 | SQL helper: read-only default, transactions, JSON output, DML guardrails |
| 4 | Invoke-WithSandboxLock.ps1 | Mutation lock wrapper |
| 5 | Get-OimHealth.ps1 | DBQueue/JobQueue depth + service health + log pointers |
| 6 | Get-MailpitMessages.ps1 | HTTP API wrapper |
| 7 | Invoke-PrometheusQuery.ps1 | PromQL wrapper |
Then as needed:
Get-OimLogTail.ps1(WinRM tail per service)Sync-FilesToSandbox.ps1(file push viaCopy-Item -ToSession/ robocopy, locked)Invoke-OimApi.ps1(REST wrapper with cookie-jar auth)Connect-Sandbox.ps1(PSSession factory used by other helpers)
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
- One Identity Manager 10.0 REST API Reference Guide — https://support-public.cfm.quest.com/82112_one-identity-manager_rest-api-reference_10.0.pdf
- One Identity Manager 10.0 API Development Guide — https://support-public.cfm.quest.com/82061_one-identity-manager_api-development-guide_10.0.pdf
- OIM MDK Developer Guide on the VM —
C:\Dev\OneIM10.0.0-MDK\MDK\OneIM_MDK_DeveloperGuide.pdf - Sandbox environment doc —
../environments/sandbox.md - Credentials inventory (gitignored) —
../environments/sandbox-credentials.local.md
Round-3 status (this round)
- [x] v1 reconciliation incorporates all 10 of Codex's points
- [x] Password literal removed from schema example (
<placeholder>); v0.1 leak verified never committed (sandbox-access/was untracked) - [x] Endpoint discovery, secret provider chain, output contract, lock model, SQL DML guardrails, Viktor Action Request template, triage order all defined
- [x] Build order agreed with Codex
- [ ] Helpers — start with
Get-SandboxCredential+Initialize-SandboxSecrets(round 4)
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.