Skip to content

PROD-250: feat(sandbox): add governed command integration - #22

Open
salamisandwich77 wants to merge 126 commits into
mainfrom
feat/PROD-250-sandbox-sdk-integration
Open

PROD-250: feat(sandbox): add governed command integration#22
salamisandwich77 wants to merge 126 commits into
mainfrom
feat/PROD-250-sandbox-sdk-integration

Conversation

@salamisandwich77

Copy link
Copy Markdown

Plugin-only OpenBox integration for Temporal. CONSTRAIN verdicts route the user's activity into the sandbox transparently.

dependabot Bot and others added 30 commits February 12, 2026 17:23
Bumps [black](https://github.com/psf/black) from 23.12.1 to 24.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](psf/black@23.12.1...24.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 24.3.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.2 to 6.33.5.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 6.33.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump black from 23.12.1 to 24.3.0
chore(deps): bump protobuf from 6.33.2 to 6.33.5
Bumps [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 5.3.0 to 6.0.0.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](SonarSource/sonarqube-scan-action@v5.3.0...v6.0.0)

---
updated-dependencies:
- dependency-name: SonarSource/sonarqube-scan-action
  dependency-version: 6.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
…ithub/workflows/SonarSource/sonarqube-scan-action-6.0.0

chore(deps): bump SonarSource/sonarqube-scan-action from 5.3.0 to 6.0.0 in /.github/workflows
Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](pygments/pygments@2.19.2...2.20.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-version: 2.20.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
- constructor: state + span_processor wiring restored (main's completed-hook
  machinery), the governed-command branch preserved from the PR branch
- _run_activity signature carries activity_input/session_id; the session id
  is read from the multi-agent header before the run
- _handle_completion resolves the completed stop from the shared state and
  enforces HALT/patch before the completed event
- _serialize_value handles pydantic-style model_dump before the fallback
- tests: constructor calls + state attribute aligned with the merged API
…factory

The moat shape: OpenBox integrates with Temporal through exactly one
official surface — the native Worker plugins=[OpenBoxPlugin(...)] slot.
User code never imports or calls any other OpenBox symbol.

- remove openbox/worker.py (the create_openbox_worker factory and its
  internal runtime/telemetry plugins) and tests/test_worker.py
- strip the factory export from openbox/__init__.py; OpenBoxPlugin is
  the sole public entry point
- GovernedCommandDeployment builds OpenBoxPlugin(...) and returns the
  native Worker(..., plugins=[openbox_plugin])
- clean every SDK-internal reference to the factory (docstrings, the
  deployment seam, the public-API pin tests)

Also lands the merge reconciliation accumulated on top: config/client/
activities/interceptor fixes aligned with the merged base SDK, plus the
interceptor test expectations (state, session_id, completed_stop,
pydantic serialization).
- _check_pending_verdicts reads the run-scoped signal verdict from the
  shared TemporalGovernanceState (stale-run safe), not the span buffer
- REQUIRE_APPROVAL marks the state's pending-approval marker; retry
  polling checks and clears it through the state
- tests: direct _ActivityInterceptor constructions pass the
  span_processor (the merged constructor requires it)
The moat shape allows exactly one public OpenBox surface: the
OpenBoxPlugin initializer. GovernedCommandDeployment, its error type,
and the manifest loader leave the public namespaces (openbox and
openbox.sandbox lazy exports). They remain importable at their module
path as internal machinery only.
…erged base SDK

- _TemporalEvaluationClient delegates to the wrapped EvaluationClient
  (self._base) instead of super() — the direct GovernanceClient constructor
  path crashed on every evaluate/poll with a real base client
- _run_activity binds the shared core ActivityContext (core_activity_scope)
  around user code; ActivityStarted/ActivityCompleted payloads carry the
  multi_agent_session_id from the header again
- plugin creates one TemporalGovernanceState before the runtime and passes
  it to create_temporal_runtime and BOTH interceptors (signal-verdict /
  HITL-pending / completed-stop bridges)
- create_temporal_runtime wires the shared state into the rich
  TemporalFrameworkAdapter (completed-hook stops + approval markers), using
  the shared core ContextStore
- tests: httpx transport mocks support the async context-manager seam;
  plugin tests pin the merged create_temporal_runtime / run_worker /
  setup_opentelemetry_for_governance contract
…rence

The moat shape: OpenBox appears only as OpenBoxPlugin in the native
Worker plugins slot. The factory sections, parameters, and examples are
replaced with the plugin shape.
create_sandbox_worker (openbox/sandbox/worker.py), OpenBoxSandboxPlugin
(openbox/sandbox/plugin.py), GovernedCommandInterceptor
(openbox/sandbox/interceptor.py), and the resolver module were defined
but never imported, never tested, and never called. The plugin is the
only surface; dead alternate shapes only confuse. Removed.
The plugin intercepts the user's own activity. ALLOW runs it on the
host; CONSTRAIN routes it into the sandbox. The workflow stays clean.
The pre-authorized and receipt compatibility modes are removed from the
docs — they are stale manual paths.
Drop the disconnected command-only worker mode, the receipt-bearing
request path, and the pre-authorized trust model:

- TemporalSandboxConfig loses receipt_verifier and trust_application_agent
  (and their validation); SandboxConfig (create_openbox_worker-era dead
  config carrying the same trust fields) is deleted.
- The activity interceptor always builds the connected governance client;
  the disconnected early-return branch is gone. _execute_governed_command
  no longer verifies receipts or dispatches via trusted/authorized paths.
- OpenBoxPlugin always builds the connected composition; the command-only
  worker branch is deleted.
- openbox/sandbox/receipts.py is deleted; GovernedCommandReceipt and the
  receipt field on GovernedCommandRequest are removed from the wire types.
- README/CHANGELOG drop the compatibility-mode language.
…ntly

The plugin's sandbox configuration now makes a CONSTRAIN verdict on any
user activity route that activity into the sandbox:

- execute_activity intercepts a CONSTRAIN ActivityStarted verdict before
  verdict enforcement when a sandbox is configured, and returns the
  sandbox result to the caller without running the activity on the host.
- The routed path derives argv from the activity's single structured
  input (profile_id + named arguments) through the sandbox profile
  bundle, builds the GovernedCommand from genuine activity.info()
  identity, maps the verdict onto the dispatcher decision shape, and
  executes dispatch_with_decision(command, decision) with the shared
  heartbeat/cancellation/result-mapping machinery.
- A CONSTRAIN verdict without a sandbox config keeps the existing
  unsupported error; a completed CONSTRAIN verdict after execution is a
  no-op when a sandbox is configured.
- The registered governed-command Activity, its workflow helper, the
  activity-type constant, and the plugin registration are deleted — the
  interception path replaces them entirely.
- docs/governed-commands.md is rewritten for the transparent routing
  design; README/CHANGELOG reference the new behavior.
- Four routing tests cover sandbox dispatch, the no-sandbox error
  contract, host execution on ALLOW, and the completed-verdict no-op.
@salamisandwich77
salamisandwich77 marked this pull request as ready for review August 11, 2026 07:03
The SandboxConfig(registry=...) class is the plugin's one-stop sandbox
configuration. The plugin builds the dispatcher, profile bundles,
heartbeat sink, output limits, policy, and asset bundle internally from
it. The previous cleanup classified it as dead and deleted it; the POC
re-added it in the vendored copy, so the standalone SDK diverged.
- the TemporalSandboxConfig NAME_ERROR in plugin.py (TYPE_CHECKING-only import)
- the absolute-path requirement in the sandbox config
- the _deadlines() defaults exceeding the core SDK caps
The ExecutionMetadata carries sandbox_id but the completion event
output was not including it. The FE and Core governance span
attributes need the sandbox identifier for traceability.
The SDK lazily imports every database driver (governance hooks only
activate when the app uses that database), but the install pulled
psycopg2, asyncpg, mysql-connector, pymysql, pymongo, redis, and
sqlalchemy plus their OTel instrumentation unconditionally — native
builds made installation slow for apps that never touch a database.

Base install now covers Temporal + HTTP instrumentation only.
DB governance users install openbox-temporal-sdk-python[db].
The natural sandbox path where the wrapper owns the completion event
previously dropped the typed result. It now carries schema_name and
values alongside the execution metadata.
docs/governed-commands.md imports TemporalCommandProfileBundle from
openbox.sandbox, and that import fails: the package builds its public
surface from a curated map, and the bundle is not in it.

The bundle is the declared type of TemporalSandboxConfig.profiles and a
caller has to construct one, so it belongs in the public surface.
TemporalHeartbeatSink is in the same position for the heartbeat_sink field.
Both are exported now, which makes the documented import work.

Also closes the Worker example in the README: plugins=[OpenBoxPlugin( was
never closed, and the Worker options that follow sit inside the plugin call,
so the snippet could not run. Three blocks showing a class hierarchy and
elided signatures are marked text rather than python.

701 tests pass.
The interceptor records "sandbox_id": execution.sandbox_id when it emits a
completion event, and the fake execution in this test did not carry that
attribute, so the branch failed with AttributeError on SimpleNamespace
while 684 other tests passed.

The interceptor line is identical on the behavioral CONSTRAIN branch, which
already carries this fixture value; only the fixture was missing here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants