From 1806112955cfaa02fff83d548c4424ba049aae82 Mon Sep 17 00:00:00 2001 From: Robbert Vermeulen Date: Mon, 17 Aug 2026 18:59:41 +0200 Subject: [PATCH] feat(sdk): send install-licence material with every chat turn Consumer gains licence(): the install's licence key + activation id, included as the licence body field on every chat turn so the hub can verify it server-side (hub PR #58). Consumers without a licence system return null. Co-Authored-By: Claude Fable 5 --- src/Config.php | 2 +- src/Consumer.php | 12 ++++++++++++ src/HubClient.php | 14 ++++++++++++++ tests/ConfigTest.php | 2 +- tests/HubClientTest.php | 23 +++++++++++++++++++++++ tests/Rest/GateTest.php | 1 + tests/RoundtableMountTest.php | 1 + tests/SessionControllerTest.php | 1 + tests/Support/FakeConsumer.php | 4 ++++ 9 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Config.php b/src/Config.php index 75fbc65..2d4f79d 100644 --- a/src/Config.php +++ b/src/Config.php @@ -20,7 +20,7 @@ final class Config { public function __construct( public readonly string $projectApiKey, public readonly Consumer $consumer, - public readonly string $agentName = 'Roundtable', + public readonly string $agentName = 'Clara', public readonly ?string $agentAvatarUrl = null, public readonly int $timeoutSeconds = 30, public readonly ?string $hubBaseUrl = null, diff --git a/src/Consumer.php b/src/Consumer.php index e4c5cb1..ae0d1ed 100644 --- a/src/Consumer.php +++ b/src/Consumer.php @@ -46,4 +46,16 @@ public function metadata(): ?string; * @return string|null The version string, or null. */ public function clientVersion(): ?string; + + /** + * The install's licence material, verified server-side by the hub. + * + * Sent with every chat turn as the `licence` body field. Hubs with licence + * verification enabled refuse chat turns without a valid payload; return null + * when the consumer has no licence system (the hub project must then run with + * verification disabled). + * + * @return array{key: string, activation_id: string}|null The licence material, or null. + */ + public function licence(): ?array; } diff --git a/src/HubClient.php b/src/HubClient.php index 39ff18a..ec612f6 100644 --- a/src/HubClient.php +++ b/src/HubClient.php @@ -624,6 +624,7 @@ private function buildBody( string $message, bool $isFirstTurn, ?string $trigger 'subject_id' => $this->config->consumer->subjectId(), ), $this->triggerField( $trigger ), + $this->licenceField(), ); if ( $isFirstTurn ) { $metadata = $this->config->consumer->metadata(); @@ -653,4 +654,17 @@ private function triggerField( ?string $trigger ): array { } return array( 'trigger' => $trigger ); } + + /** + * Optional `licence` field for a chat-turn body, when the consumer provides one. + * + * @return array Zero or one keyed entry. + */ + private function licenceField(): array { + $licence = $this->config->consumer->licence(); + if ( null === $licence ) { + return array(); + } + return array( 'licence' => $licence ); + } } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index e753767..40d373f 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -16,7 +16,7 @@ public function test_exposes_its_values_with_defaults(): void self::assertSame('pk_live_123', $config->projectApiKey); self::assertSame($consumer, $config->consumer); - self::assertSame('Roundtable', $config->agentName); + self::assertSame('Clara', $config->agentName); self::assertNull($config->agentAvatarUrl); self::assertSame(30, $config->timeoutSeconds); self::assertNull($config->hubBaseUrl); diff --git a/tests/HubClientTest.php b/tests/HubClientTest.php index 7147d79..78cdd55 100644 --- a/tests/HubClientTest.php +++ b/tests/HubClientTest.php @@ -45,6 +45,29 @@ public function test_first_turn_body_carries_metadata_and_client_version(): void self::assertSame('1.2.3', $body['client_version']); } + public function test_body_carries_licence_on_every_turn_when_consumer_provides_one(): void + { + $transport = new FakeTransport(200, ''); + $licence = array('key' => 'lic-1', 'activation_id' => 'act-1'); + $client = new HubClient($this->config(new FakeConsumer(true, 'subj', null, null, $licence)), $transport); + + $client->postMessage('chat-1', 'hello', false); + + $body = \json_decode((string) $transport->lastBody, true); + self::assertSame($licence, $body['licence']); + } + + public function test_body_omits_licence_when_consumer_has_none(): void + { + $transport = new FakeTransport(200, ''); + $client = new HubClient($this->config(new FakeConsumer()), $transport); + + $client->postMessage('chat-1', 'hello', false); + + $body = \json_decode((string) $transport->lastBody, true); + self::assertArrayNotHasKey('licence', $body); + } + public function test_later_turn_body_omits_metadata_and_client_version(): void { $transport = new FakeTransport(200, ''); diff --git a/tests/Rest/GateTest.php b/tests/Rest/GateTest.php index adc2653..c3b88f6 100644 --- a/tests/Rest/GateTest.php +++ b/tests/Rest/GateTest.php @@ -21,6 +21,7 @@ public function isUserAllowed(): bool { return $this->allowed; } public function subjectId(): string { return 's'; } public function metadata(): ?string { return null; } public function clientVersion(): ?string { return null; } + public function licence(): ?array { return null; } }; return new Config( 'pk_secret', $consumer ); } diff --git a/tests/RoundtableMountTest.php b/tests/RoundtableMountTest.php index 268b674..ec5930d 100644 --- a/tests/RoundtableMountTest.php +++ b/tests/RoundtableMountTest.php @@ -21,6 +21,7 @@ public function isUserAllowed(): bool { return true; } public function subjectId(): string { return 's'; } public function metadata(): ?string { return null; } public function clientVersion(): ?string { return null; } + public function licence(): ?array { return null; } }; $added = array(); Functions\when( 'add_action' )->alias( diff --git a/tests/SessionControllerTest.php b/tests/SessionControllerTest.php index 15aa2f9..dc58c83 100644 --- a/tests/SessionControllerTest.php +++ b/tests/SessionControllerTest.php @@ -20,6 +20,7 @@ public function isUserAllowed(): bool { return true; } public function subjectId(): string { return 's'; } public function metadata(): ?string { return null; } public function clientVersion(): ?string { return null; } + public function licence(): ?array { return null; } }; return new SessionController( new Config( 'pk_secret', $consumer ) ); } diff --git a/tests/Support/FakeConsumer.php b/tests/Support/FakeConsumer.php index 41d6ca7..8605b59 100644 --- a/tests/Support/FakeConsumer.php +++ b/tests/Support/FakeConsumer.php @@ -8,15 +8,19 @@ /** A configurable Consumer test double. */ final class FakeConsumer implements Consumer { + /** @param array{key: string, activation_id: string}|null $licence */ public function __construct( private bool $allowed = true, private string $subjectId = 'subject-1', private ?string $metadata = null, private ?string $clientVersion = null, + private ?array $licence = null, ) {} public function isUserAllowed(): bool { return $this->allowed; } public function subjectId(): string { return $this->subjectId; } public function metadata(): ?string { return $this->metadata; } public function clientVersion(): ?string { return $this->clientVersion; } + /** @return array{key: string, activation_id: string}|null */ + public function licence(): ?array { return $this->licence; } }