Skip to content

Commit a9898ac

Browse files
committed
Implement ARCP v1.1 PHP features
Add model.use leases, provisioned credential APIs and lifecycle wiring, feature negotiation, redaction, docs, samples, and coverage for the v1.1 PHP SDK issue set.
1 parent 3f31bb8 commit a9898ac

44 files changed

Lines changed: 1804 additions & 34 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- ARCP v1.1 feature coverage for `session.list_jobs` / `session.jobs`,
1313
versioned `name@version` tool resolution, `job.result_chunk`, and
14-
`cost.budget` enforcement.
14+
`cost.budget`, `model.use`, and provisioned credential enforcement.
1515
- `Arcp\Errors\BudgetExhaustedException` and
1616
`Arcp\Errors\AgentVersionNotAvailableException` mapped to their v1.1
1717
canonical wire codes.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# `arcp/arcp` — ARCP PHP SDK
22

33
PHP 8.4 reference implementation of the [Agent Runtime Control Protocol
4-
(ARCP) v1.0](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
4+
(ARCP) v1.1](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
55

6-
> **Status:** v0.1 complete. The conformance matrix is in
7-
> [CONFORMANCE.md](CONFORMANCE.md).
6+
> **Status:** v1.1 feature coverage. The conformance matrix is in
7+
> [docs/conformance.md](docs/conformance.md).
88
99
## Quick start
1010

@@ -158,9 +158,9 @@ PHP 8.4 and newer. Older minors are not supported.
158158
See [CHANGELOG.md](CHANGELOG.md). Major-version migration notes live
159159
in [UPGRADE.md](UPGRADE.md).
160160

161-
## What's not implemented in v0.1
161+
## Conformance
162162

163-
See [CONFORMANCE.md](CONFORMANCE.md) for the full list. Calls into
163+
See [docs/conformance.md](docs/conformance.md) for the full list. Calls into
164164
out-of-scope surfaces throw `UnimplementedException` with the relevant
165165
RFC section.
166166

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "arcp/arcp",
3-
"description": "PHP reference implementation of the Agent Runtime Control Protocol (ARCP) v1.0",
3+
"description": "PHP reference implementation of the Agent Runtime Control Protocol (ARCP) v1.1",
44
"type": "library",
55
"license": "Apache-2.0",
66
"keywords": ["arcp", "agent", "runtime", "protocol", "websocket", "stdio"],

docs/conformance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ The PHP SDK targets ARCP v1.1.
1515
| Progress, streams, and `job.result_chunk` | implemented |
1616
| Permissions and leases | implemented |
1717
| `cost.budget` counters | implemented |
18+
| `model.use` leases | implemented |
19+
| Provisioned credentials | implemented |
20+
| `LEASE_SUBSET_VIOLATION` | implemented |
1821
| Artifacts | implemented |
1922
| Subscriptions and backfill | implemented |
2023
| Vendor extensions | implemented |
@@ -25,6 +28,8 @@ The v1.1 PHP-specific additions are covered by unit and integration
2528
tests in:
2629

2730
- `tests/Unit/Runtime/V11FeaturesTest.php`
31+
- `tests/Unit/Runtime/ModelUseTest.php`
32+
- `tests/Integration/CredentialLifecycleTest.php`
2833
- `tests/Integration/JobLifecycleTest.php`
2934
- `tests/Unit/MessageCatalogRoundTripTest.php`
3035
- `tests/Unit/ErrorsTest.php`

docs/guides/errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Canonical codes live in `Arcp\Errors\ErrorCode`. v1.1 additions include:
1010
| Code | Exception |
1111
| --- | --- |
1212
| `LEASE_EXPIRED` | `LeaseExpiredException` |
13+
| `LEASE_SUBSET_VIOLATION` | `LeaseSubsetViolationException` |
1314
| `BUDGET_EXHAUSTED` | `BudgetExhaustedException` |
1415
| `AGENT_VERSION_NOT_AVAILABLE` | `AgentVersionNotAvailableException` |
1516

@@ -60,6 +61,10 @@ Direct tool failures are surfaced as typed exceptions by `ErrorMapper`.
6061

6162
Permission and lease failures inside a tool become terminal tool errors.
6263

64+
`LEASE_SUBSET_VIOLATION` is raised when a delegated or child lease
65+
expands `model.use` or `cost.budget` beyond its parent. Its details are
66+
`parent_lease_id`, `child_lease_id`, and `field`.
67+
6368
## Retry guidance
6469

6570
Retry only when `ARCPExceptionInterface::isRetryable()` is true, and use

docs/guides/leases.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,47 @@ Expired leases raise `LeaseExpiredException`.
4646
`CostBudget` parses `currency:decimal` strings and decrements counters
4747
from `cost.*` metrics.
4848

49+
## Model-use leases (v1.1, §9.7)
50+
51+
`ModelUse` carries allow-list patterns such as `openai/gpt-4o`,
52+
`anthropic/*`, or `*`. Tool code can enforce the active job lease before
53+
calling an upstream model:
54+
55+
```php
56+
$ctx->assertModelAllowed('anthropic/claude-3-5-sonnet');
57+
```
58+
59+
Child leases must stay within the parent model set. `LeaseManager`
60+
raises `LeaseSubsetViolationException` when a child expands either
61+
`model.use` or `cost.budget`.
62+
63+
## Provisioned credentials (v1.1, §9.8)
64+
65+
Configure `ARCPRuntime` with a `CredentialProvisioner` to mint
66+
short-lived upstream credentials after the job lease is finalized:
67+
68+
```php
69+
$runtime = new ARCPRuntime(
70+
authRouter: $auth,
71+
credentialProvisioner: $provisioner,
72+
);
73+
```
74+
75+
Clients opt in during the handshake with:
76+
77+
```php
78+
new Capabilities(
79+
anonymous: true,
80+
features: ['provisioned_credentials', 'model.use'],
81+
);
82+
```
83+
84+
When the tool invocation carries `lease.model.use` or
85+
`lease.cost.budget`, the runtime includes a `credentials` array in the
86+
direct `job.accepted` payload. Credential values are redacted in the
87+
event log and subscription delivery, and the runtime revokes outstanding
88+
credentials on success, failure, or cancellation.
89+
4990
## Hand-written validation
5091

5192
Use typed exceptions from `Arcp\Errors` so clients can branch on
@@ -54,4 +95,4 @@ canonical error codes.
5495
## Runnable examples
5596

5697
See `samples/leases/`, `samples/lease_revocation/`, and
57-
`samples/cost_budget/`.
98+
`samples/cost_budget/`, and `samples/provisioned_credentials/`.

samples/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ARCP PHP samples
22

3-
Eighteen single-purpose sample programs, each named for the protocol
3+
Nineteen single-purpose sample programs, each named for the protocol
44
primitive it demonstrates. Mirrors the Python tree at
55
`python-sdk/examples/` and the cross-language brief in
66
`python-sdk/examples/SUBWORKER_BRIEF.md`.
@@ -12,7 +12,7 @@ primitive it demonstrates. Mirrors the Python tree at
1212
> stub modules (`agents.php`, `steps.php`, `cheap.php`, …) so the
1313
> protocol code in `main.php` is what you read.
1414
15-
## The eighteen
15+
## The nineteen
1616

1717
| Directory | Demonstrates | Spec |
1818
|---|---|---|
@@ -34,6 +34,7 @@ primitive it demonstrates. Mirrors the Python tree at
3434
| [`agent_versions/`](./agent_versions) | `name@version` resolution and missing-version errors. | §7.5, §12 |
3535
| [`result_chunk/`](./result_chunk) | Chunked terminal results assembled by the client. | §8.4 |
3636
| [`cost_budget/`](./cost_budget) | `cost.budget` counters decremented by `cost.*` metrics. | §9.6, §12 |
37+
| [`provisioned_credentials/`](./provisioned_credentials) | Lease-bound upstream credentials for model and budget enforcement. | §9.7–§9.8 |
3738

3839
## Conventions
3940

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Arcp\Samples\ProvisionedCredentials;
6+
7+
use Arcp\Messages\Permissions\LeaseGranted;
8+
use Arcp\Runtime\Credentials\Credential;
9+
use Arcp\Runtime\Credentials\CredentialProvisioner;
10+
use Arcp\Runtime\JobContext;
11+
12+
/**
13+
* Reference plug-in sketch for LiteLLM proxy virtual keys.
14+
*
15+
* A production implementation would POST to `/key/generate` with
16+
* max_budget, allowed_models, and expires, then POST `/key/delete` in
17+
* revoke(). This sample keeps the HTTP client out of core.
18+
*/
19+
final readonly class LiteLLMProvisioner implements CredentialProvisioner
20+
{
21+
public function __construct(
22+
private string $endpoint,
23+
private string $adminToken,
24+
) {
25+
}
26+
27+
#[\Override]
28+
public function issue(LeaseGranted $lease, JobContext $ctx): array
29+
{
30+
$id = 'litellm_' . substr(hash('sha256', (string) $ctx->jobId), 0, 12);
31+
return [
32+
Credential::withLeaseConstraints(
33+
$id,
34+
'replace-with-virtual-key-from-' . $this->adminToken,
35+
rtrim($this->endpoint, '/') . '/v1',
36+
$lease,
37+
profile: 'litellm',
38+
),
39+
];
40+
}
41+
42+
#[\Override]
43+
public function revoke(string $credentialId): void
44+
{
45+
// POST /key/delete with the virtual key id.
46+
}
47+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Provisioned Credentials
2+
3+
Demonstrates ARCP v1.1 `model.use` and `provisioned_credentials`.
4+
5+
The runtime is configured with `InMemoryCredentialProvisioner`. The
6+
client requests both feature flags during the handshake, invokes a tool
7+
with a lease carrying `model.use` and `cost.budget`, receives the
8+
credential in `job.accepted`, and the runtime revokes it when the job
9+
completes.
10+
11+
Run:
12+
13+
```sh
14+
php samples/provisioned_credentials/main.php
15+
```
16+
17+
`LiteLLMProvisioner.php` shows the shape of a plug-in provisioner without
18+
coupling core SDK code to LiteLLM.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require __DIR__ . '/../../vendor/autoload.php';
6+
7+
use Amp\Cancellation;
8+
use Arcp\Auth\AuthRouter;
9+
use Arcp\Auth\NoneAuth;
10+
use Arcp\Client\ARCPClient;
11+
use Arcp\Envelope\Envelope;
12+
use Arcp\Messages\Execution\JobAccepted;
13+
use Arcp\Messages\Session\Auth;
14+
use Arcp\Messages\Session\Capabilities;
15+
use Arcp\Messages\Session\PeerInfo;
16+
use Arcp\Runtime\ARCPRuntime;
17+
use Arcp\Runtime\Credentials\InMemoryCredentialProvisioner;
18+
use Arcp\Runtime\JobContext;
19+
use Arcp\Runtime\ToolHandler;
20+
use Arcp\Transport\MemoryTransport;
21+
use Arcp\Transport\Transport;
22+
23+
$provisioner = new InMemoryCredentialProvisioner();
24+
$runtime = new ARCPRuntime(
25+
authRouter: new AuthRouter([new NoneAuth()]),
26+
credentialProvisioner: $provisioner,
27+
);
28+
$runtime->registerTool('planner', new class () implements ToolHandler {
29+
#[\Override]
30+
public function invoke(array $arguments, JobContext $ctx, ?Cancellation $cancellation = null): mixed
31+
{
32+
$ctx->assertModelAllowed('anthropic/claude-3-5-sonnet');
33+
return ['planned' => true];
34+
}
35+
});
36+
37+
[$serverT, $clientT] = MemoryTransport::pair();
38+
$recording = new class ($clientT) implements Transport {
39+
/** @var list<Envelope> */
40+
public array $received = [];
41+
42+
public function __construct(private readonly Transport $inner)
43+
{
44+
}
45+
46+
#[\Override]
47+
public function send(Envelope $env, ?Cancellation $cancellation = null): void
48+
{
49+
$this->inner->send($env, $cancellation);
50+
}
51+
52+
#[\Override]
53+
public function receive(?Cancellation $cancellation = null): ?Envelope
54+
{
55+
$env = $this->inner->receive($cancellation);
56+
if ($env instanceof Envelope) {
57+
$this->received[] = $env;
58+
}
59+
return $env;
60+
}
61+
62+
#[\Override]
63+
public function close(): void
64+
{
65+
$this->inner->close();
66+
}
67+
68+
#[\Override]
69+
public function isClosed(): bool
70+
{
71+
return $this->inner->isClosed();
72+
}
73+
};
74+
$serverFuture = $runtime->serveAsync($serverT);
75+
$client = new ARCPClient($recording);
76+
$client->open(Auth::none(), new PeerInfo('provisioned-demo', '0.1'), new Capabilities(
77+
anonymous: true,
78+
features: ['provisioned_credentials', 'model.use'],
79+
));
80+
$result = $client->invokeTool('planner', [
81+
'lease' => [
82+
'model.use' => ['anthropic/*'],
83+
'cost.budget' => ['USD:1.00'],
84+
],
85+
]);
86+
87+
foreach ($recording->received as $env) {
88+
if ($env->payload instanceof JobAccepted) {
89+
print_r($env->payload->credentials);
90+
break;
91+
}
92+
}
93+
print_r($result->value);
94+
print_r(['revoked' => $provisioner->revoked]);
95+
96+
$client->close();
97+
$serverFuture->await();

0 commit comments

Comments
 (0)